ce69b8cd18
- Admin lockout: /zen checked blockedForViewer() before auth loaded, so a hard-refresh/
direct-link bounced admins to /play. Now revalidate auth (await refresh if !ready)
BEFORE the gate check.
- UB swap: retired the two-tail koi (ub.glb/ub-split.glb) for the vetted Queen angelfish.
Trimmed the 75.67s baked Take down to just the Idle loop (tools/glb-split/trim-idle.mjs
→ 16MB → 6.9MB) → static/models/ub-angelfish.glb. aquarium.js reworked for the pack's
ONE-mesh/TWO-material layout (…_body opaque single-sided; …_fins opaque alpha-tested,
tunable); animation is the trimmed Idle. Debug tuner (/zen?debug=1) updated: yaw/pitch/
scale + one fins&tail section. Still devgate IN_DEV={'zen'} — admin-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
3.8 KiB
JavaScript
72 lines
3.8 KiB
JavaScript
// Generic GLB structural audit (species-agnostic). For each file: mesh/poly counts,
|
|
// materials with alphaMode + doubleSided (the koi's failure was one BLEND double-sided
|
|
// mesh), textures, skin/joint count, animation clip names (must carry Idle/Swim in the
|
|
// GLB, not just the Maya source), bbox proportions, and a crude "two-tail" check —
|
|
// histogram of the rear-slice vertices along the thin (left/right) axis: a single peak
|
|
// at centre = one tail; two side peaks with a centre gap = a forked / double tail.
|
|
import { NodeIO } from '@gltf-transform/core';
|
|
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
|
|
|
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
const path = process.argv[2];
|
|
const doc = await io.read(path);
|
|
const root = doc.getRoot();
|
|
const base = path.split('/').pop();
|
|
|
|
const meshes = root.listMeshes();
|
|
let prims = 0, tris = 0, verts = 0;
|
|
const bb = { min: [1e9, 1e9, 1e9], max: [-1e9, -1e9, -1e9] };
|
|
const allPos = [];
|
|
for (const m of meshes) for (const p of m.listPrimitives()) {
|
|
prims++;
|
|
const pos = p.getAttribute('POSITION');
|
|
const idx = p.getIndices();
|
|
tris += idx ? idx.getCount() / 3 : pos.getCount() / 3;
|
|
verts += pos.getCount();
|
|
const v = [0, 0, 0];
|
|
for (let i = 0; i < pos.getCount(); i++) {
|
|
pos.getElement(i, v);
|
|
allPos.push(v[0], v[1], v[2]);
|
|
for (let d = 0; d < 3; d++) { bb.min[d] = Math.min(bb.min[d], v[d]); bb.max[d] = Math.max(bb.max[d], v[d]); }
|
|
}
|
|
}
|
|
const size = bb.max.map((m, d) => m - bb.min[d]);
|
|
const mats = root.listMaterials();
|
|
const texs = root.listTextures();
|
|
const skin = root.listSkins()[0];
|
|
const joints = skin ? skin.listJoints() : [];
|
|
const anims = root.listAnimations();
|
|
|
|
// axes: width = thinnest (fish are laterally compressed); length = longest of the other two.
|
|
const widthAxis = size.indexOf(Math.min(...size));
|
|
const other = [0, 1, 2].filter((d) => d !== widthAxis);
|
|
const lenAxis = other[0] === undefined ? 0 : (size[other[0]] >= size[other[1]] ? other[0] : other[1]);
|
|
// rear slice = last 22% along the length axis (either end — pick the end with fewer verts = the tail, not the body)
|
|
const loEnd = bb.min[lenAxis] + 0.22 * size[lenAxis];
|
|
const hiEnd = bb.max[lenAxis] - 0.22 * size[lenAxis];
|
|
let loN = 0, hiN = 0;
|
|
for (let i = 0; i < allPos.length; i += 3) { const L = allPos[i + lenAxis]; if (L <= loEnd) loN++; else if (L >= hiEnd) hiN++; }
|
|
const tailIsLo = loN <= hiN;
|
|
const wc = (bb.min[widthAxis] + bb.max[widthAxis]) / 2, wHalf = size[widthAxis] / 2 || 1;
|
|
const bins = new Array(11).fill(0);
|
|
let tailN = 0;
|
|
for (let i = 0; i < allPos.length; i += 3) {
|
|
const L = allPos[i + lenAxis];
|
|
if (tailIsLo ? L > loEnd : L < hiEnd) continue;
|
|
tailN++;
|
|
const t = (allPos[i + widthAxis] - wc) / wHalf; // -1..1 across width
|
|
bins[Math.max(0, Math.min(10, Math.round((t + 1) * 5)))]++;
|
|
}
|
|
const centreFrac = tailN ? (bins[4] + bins[5] + bins[6]) / tailN : 0; // fraction near centre-plane
|
|
|
|
const AX = ['X', 'Y', 'Z'];
|
|
console.log(`\n== ${base} ==`);
|
|
console.log(` meshes ${meshes.length} prims ${prims} tris ${Math.round(tris)} verts ${verts}`);
|
|
console.log(` materials ${mats.length}: ${mats.map((m) => `${m.getName() || '—'}[${m.getAlphaMode()}${m.getDoubleSided() ? ',2SIDED' : ''}]`).join(' ')}`);
|
|
console.log(` textures ${texs.length} skin joints ${joints.length}`);
|
|
console.log(` anims ${anims.length}: ${anims.map((a) => a.getName()).join(', ') || '(none in GLB!)'}`);
|
|
console.log(` bbox size ${AX.map((a, d) => `${a}=${size[d].toFixed(2)}`).join(' ')} (len axis ${AX[lenAxis]}, width axis ${AX[widthAxis]})`);
|
|
console.log(` tail slice: ${tailN} verts, ${(centreFrac * 100).toFixed(0)}% near centre-plane → ${centreFrac > 0.5 ? 'SINGLE tail (centred)' : 'possible fork/side-lobes — eyeball it'}`);
|
|
const tj = joints.filter((j) => /tail|caudal|fork|fin/i.test(j.getName())).map((j) => j.getName());
|
|
if (tj.length) console.log(` fin/tail joints: ${tj.join(', ')}`);
|