backup befor new branch to test Blockbenh instead of regex renderer
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
// GeoJSON model viewer for the Models/Textures tab. Uses the shared GeoBuilder
|
||||
// geometry math (bone transforms + box-UV) and renders world-space cubes with
|
||||
// Three.js. Textures are served through the gated pack_asset endpoint.
|
||||
// geometry math (bone transforms + box/per-face UV) and renders world-space
|
||||
// cubes with Three.js. Textures are served through the gated pack_asset and
|
||||
// vanilla_asset endpoints, resolved per cube:
|
||||
// 1. afw_bone_textures (pack member), composited over the vanilla entity
|
||||
// skin when one exists (NoN `*_features` overlays sit on the vanilla base)
|
||||
// 2. the model's default pack skin (server-matched)
|
||||
// 3. the bundled vanilla entity skin
|
||||
// 4. gray
|
||||
(function () {
|
||||
let renderer = null;
|
||||
let scene = null;
|
||||
@@ -9,32 +15,117 @@
|
||||
let rafId = null;
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const materialCache = {};
|
||||
let vanillaIndex = null;
|
||||
let vanillaIndexPromise = null;
|
||||
|
||||
function assetUrl(baseUrl, member) {
|
||||
return baseUrl.replace('ASSET', member);
|
||||
}
|
||||
|
||||
function entityNameFromMember(member) {
|
||||
let base = member.slice(member.lastIndexOf('/') + 1);
|
||||
if (base.endsWith('.geo.json')) base = base.slice(0, -'.geo.json'.length);
|
||||
for (const suffix of ['.mf', '.fm', '.m', '.f', '.g']) {
|
||||
if (base.endsWith(suffix)) { base = base.slice(0, -suffix.length); break; }
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function buildGeometry(size, uv, tw, th) {
|
||||
const g = GeoBuilder.cubeGeometry(size, uv, tw, th);
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(g.positions, 3));
|
||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(g.uvs, 2));
|
||||
geometry.setIndex(g.indices);
|
||||
geometry.computeVertexNormals();
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function materialFor(member, baseUrl) {
|
||||
if (materialCache[member || '__gray__']) return materialCache[member || '__gray__'];
|
||||
function fetchVanillaIndex(opts) {
|
||||
if (vanillaIndexPromise) return vanillaIndexPromise;
|
||||
if (!opts.vanillaBaseUrl) {
|
||||
vanillaIndexPromise = Promise.resolve({});
|
||||
} else {
|
||||
vanillaIndexPromise = fetch(assetUrl(opts.vanillaBaseUrl, 'entity_index.json'))
|
||||
.then(r => r.ok ? r.json() : {})
|
||||
.catch(() => ({}));
|
||||
}
|
||||
return vanillaIndexPromise;
|
||||
}
|
||||
|
||||
function vanillaSkinUrl(opts, index, entity) {
|
||||
const rel = (index && index[entity]) || null;
|
||||
return rel ? assetUrl(opts.vanillaBaseUrl, 'entity/' + rel) : null;
|
||||
}
|
||||
|
||||
// desc: { url, vanillaUrl } — url is the pack/overlay member, vanillaUrl the
|
||||
// base skin to composite it over (when available).
|
||||
function resolveTexture(cube, entity, opts, index) {
|
||||
if (cube.texture) {
|
||||
return {
|
||||
url: assetUrl(opts.baseUrl, cube.texture),
|
||||
vanillaUrl: vanillaSkinUrl(opts, index, entity),
|
||||
};
|
||||
}
|
||||
if (opts.defaultTexture) {
|
||||
return { url: assetUrl(opts.baseUrl, opts.defaultTexture), vanillaUrl: null };
|
||||
}
|
||||
const rel = (index && index[entity]) || null;
|
||||
if (rel) {
|
||||
return { url: null, vanillaUrl: vanillaSkinUrl(opts, index, entity) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadImage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error('img'));
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareMaterial(desc, tw, th) {
|
||||
const key = (desc.url || '') + '|' + (desc.vanillaUrl || '');
|
||||
if (materialCache[key]) return materialCache[key];
|
||||
|
||||
let texture = null;
|
||||
if (desc.url && desc.vanillaUrl) {
|
||||
try {
|
||||
const [overlay, base] = await Promise.all([loadImage(desc.url), loadImage(desc.vanillaUrl)]);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = tw;
|
||||
canvas.height = th;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(base, 0, 0, tw, th);
|
||||
ctx.drawImage(overlay, 0, 0, tw, th);
|
||||
texture = new THREE.CanvasTexture(canvas);
|
||||
} catch (e) {
|
||||
texture = null;
|
||||
}
|
||||
}
|
||||
const singleUrl = texture ? null : (desc.url || desc.vanillaUrl);
|
||||
if (singleUrl) texture = textureLoader.load(singleUrl);
|
||||
|
||||
let material;
|
||||
if (member) {
|
||||
const texture = textureLoader.load(assetUrl(baseUrl, member));
|
||||
if (texture) {
|
||||
texture.wrapS = THREE.ClampToEdgeWrapping;
|
||||
texture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
material = new THREE.MeshStandardMaterial({ map: texture, roughness: 0.9, metalness: 0.0 });
|
||||
if (singleUrl) {
|
||||
textureLoader.load(singleUrl, undefined, undefined, () => {
|
||||
if (material.map) material.map.dispose();
|
||||
material.map = null;
|
||||
material.color.setHex(0x9a9a9a);
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
material = new THREE.MeshStandardMaterial({ color: 0x9a9a9a, roughness: 0.9 });
|
||||
}
|
||||
materialCache[member || '__gray__'] = material;
|
||||
materialCache[key] = material;
|
||||
return material;
|
||||
}
|
||||
|
||||
@@ -49,7 +140,7 @@
|
||||
for (const key in materialCache) delete materialCache[key];
|
||||
}
|
||||
|
||||
function render(member, container, baseUrl) {
|
||||
function render(member, container, opts) {
|
||||
if (renderer) dispose();
|
||||
const width = container.clientWidth || 480;
|
||||
const height = container.clientHeight || 480;
|
||||
@@ -60,7 +151,7 @@
|
||||
camera.position.set(0, 20, 60);
|
||||
camera.lookAt(0, 12, 0);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1);
|
||||
container.appendChild(renderer.domElement);
|
||||
@@ -76,14 +167,29 @@
|
||||
meshRoot = new THREE.Group();
|
||||
scene.add(meshRoot);
|
||||
|
||||
fetch(assetUrl(baseUrl, member), { headers: { 'Accept': 'application/json' } })
|
||||
const entity = entityNameFromMember(member);
|
||||
fetch(assetUrl(opts.baseUrl, member), { headers: { 'Accept': 'application/json' } })
|
||||
.then(r => { if (!r.ok) throw new Error('http'); return r.json(); })
|
||||
.then(geo => {
|
||||
.then(geo => Promise.all([geo, fetchVanillaIndex(opts)]))
|
||||
.then(([geo, index]) => {
|
||||
const built = GeoBuilder.build(geo);
|
||||
if (!built.cubes.length) throw new Error('no cubes');
|
||||
for (const cube of built.cubes) {
|
||||
const tw = built.texture_width, th = built.texture_height;
|
||||
const jobs = built.cubes.map((cube) => {
|
||||
const desc = resolveTexture(cube, entity, opts, index);
|
||||
return desc ? prepareMaterial(desc, tw, th) : Promise.resolve(null);
|
||||
});
|
||||
return Promise.all(jobs).then((materials) => ({ built, materials }));
|
||||
})
|
||||
.then(({ built, materials }) => {
|
||||
for (let i = 0; i < built.cubes.length; i++) {
|
||||
const cube = built.cubes[i];
|
||||
const geometry = buildGeometry(cube.size, cube.uv, built.texture_width, built.texture_height);
|
||||
const mesh = new THREE.Mesh(geometry, materialFor(cube.texture, baseUrl));
|
||||
let material = materials[i];
|
||||
if (!material) {
|
||||
material = new THREE.MeshStandardMaterial({ color: 0x9a9a9a, roughness: 0.9 });
|
||||
}
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.matrix = new THREE.Matrix4().fromArray(cube.matrix);
|
||||
mesh.matrixAutoUpdate = false;
|
||||
meshRoot.add(mesh);
|
||||
|
||||
Reference in New Issue
Block a user