Files
upbeatBytes/tools/glb-split/inspect.mjs
T
thejayman77 ce69b8cd18 zen: UB is now the Queen angelfish (real model) + fix admin lockout
- 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>
2026-07-01 18:10:38 -04:00

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(', ')}`);