Verifying the GLBs in three.js (not just Blender) showed four assets reporting inflated bounds: tramp_01 came back 3.29 x 1.27 m against a true 2.96 x 0.78. Box3.setFromObject expands each mesh's LOCAL box by the world matrix, so a node carrying a rotation over-reports — the same trap Blender's obj.bound_box sets, and what three uses for frustum culling. Joined nodes inherited parts[0]'s rotation, which for an arc is half a segment step off-axis. Applying rotation at join time makes every local box axis-aligned, so the default Box3 path is now correct for consumers and culling is tight. World geometry is unchanged; the exported dims are identical. Adds tools/assetcheck/, the three.js harness that caught this. It's the only check that can: a Blender round-trip exports Z-up->Y-up and imports Y-up->Z-up, so a broken export_yup flips back and passes green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
3.4 KiB
HTML
83 lines
3.4 KiB
HTML
<!doctype html>
|
|
<meta charset="utf-8">
|
|
<title>Lane E — GLB check in the real consumer (three.js r175)</title>
|
|
<style>
|
|
body { background:#14161a; color:#dfe3e8; font:13px/1.5 ui-monospace,Menlo,monospace; padding:16px; }
|
|
.pass { color:#7fd67f; } .fail { color:#ff6b6b; } h1 { font-size:15px; color:#9fb4c7; }
|
|
</style>
|
|
<h1>GLB verification — loaded by three.js GLTFLoader, not Blender</h1>
|
|
<pre id="out">loading…</pre>
|
|
|
|
<script type="importmap">
|
|
{ "imports": { "three": "/world/vendor/three.module.js",
|
|
"three/addons/": "/world/vendor/addons/" } }
|
|
</script>
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
|
|
const out = document.getElementById('out');
|
|
const log = [];
|
|
const say = (s, cls) => {
|
|
log.push(s);
|
|
out.innerHTML += `<span class="${cls || ''}">${s}</span>\n`;
|
|
console.log(s);
|
|
};
|
|
|
|
const report = await (await fetch('/asset_report.json')).json();
|
|
const loader = new GLTFLoader();
|
|
let fails = 0;
|
|
|
|
say(`three.js r${THREE.REVISION} | ${report.assets.length} assets\n`);
|
|
|
|
for (const a of report.assets) {
|
|
let gltf = null;
|
|
for (const dir of ['/models/', '/models/debris/']) {
|
|
try { gltf = await loader.loadAsync(`${dir}${a.name}_v1.glb`); break; } catch (e) {}
|
|
}
|
|
if (!gltf) { say(`[FAIL] ${a.name.padEnd(16)} could not load`, 'fail'); fails++; continue; }
|
|
|
|
const size = new THREE.Vector3();
|
|
new THREE.Box3().setFromObject(gltf.scene).getSize(size);
|
|
|
|
// The whole point of this page. Blender is Z-up, glTF is Y-up, so a Blender
|
|
// asset measuring (dx, dy, dz) MUST arrive here as (dx, dz, dy). A Blender
|
|
// re-import can never catch a broken export_yup — it just flips it back.
|
|
const [bx, by, bz] = a.dims;
|
|
const exp = [bx, bz, by];
|
|
const got = [size.x, size.y, size.z];
|
|
const axisOk = got.every((v, i) => Math.abs(v - exp[i]) < 0.02);
|
|
|
|
const names = [];
|
|
gltf.scene.traverse(o => names.push(o.name));
|
|
const missing = a.nodes.filter(n => !names.includes(n));
|
|
|
|
const ok = axisOk && missing.length === 0;
|
|
if (!ok) fails++;
|
|
say(`[${ok ? 'PASS' : 'FAIL'}] ${a.name.padEnd(16)} ` +
|
|
`${got.map(v => v.toFixed(2)).join(' x ')} m (Y-up)`, ok ? 'pass' : 'fail');
|
|
if (!axisOk) say(` axis/scale: expected ${exp.map(v => v.toFixed(2)).join(' x ')}`, 'fail');
|
|
if (missing.length) say(` nodes lost in three.js: ${missing.join(', ')}`, 'fail');
|
|
}
|
|
|
|
// Empties are the risky part: glTF has no "empty", they arrive as bare Object3D
|
|
// nodes, and exporters have been known to prune childless ones. Anchors ARE the
|
|
// contract, so prove one survives with a usable world position.
|
|
const t = await loader.loadAsync('/models/tree_gum_01_v1.glb');
|
|
const anchor = t.scene.getObjectByName('branch_anchor_01');
|
|
if (anchor) {
|
|
t.scene.updateWorldMatrix(true, true);
|
|
const p = new THREE.Vector3().setFromMatrixPosition(anchor.matrixWorld);
|
|
const upright = p.y > 1.0 && p.y < 6.0;
|
|
if (!upright) fails++;
|
|
say(`\n[${upright ? 'PASS' : 'FAIL'}] branch_anchor_01 world pos ` +
|
|
`(${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)}) — ` +
|
|
`${upright ? 'height is on +Y, anchors are usable' : 'height is NOT on +Y!'}`,
|
|
upright ? 'pass' : 'fail');
|
|
} else { fails++; say('\n[FAIL] branch_anchor_01 missing entirely', 'fail'); }
|
|
|
|
say(`\nSUMMARY: ${fails === 0 ? 'ALL PASS IN THREE.JS' : fails + ' FAILURES'}`,
|
|
fails === 0 ? 'pass' : 'fail');
|
|
window.__done = true; window.__fails = fails;
|
|
</script>
|