diff --git a/.gitignore b/.gitignore index a54cf36..94dd417 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ data/geo_audit*.json logs/ data/art_cache/ data/img_cache/ +tools/glb-split/incoming/ diff --git a/frontend/src/lib/zen/aquarium.js b/frontend/src/lib/zen/aquarium.js index 7b4ee25..e7c2eb4 100644 --- a/frontend/src/lib/zen/aquarium.js +++ b/frontend/src/lib/zen/aquarium.js @@ -16,22 +16,20 @@ import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; -const MODEL_URL = '/models/ub-split.glb'; +const MODEL_URL = '/models/ub-angelfish.glb'; -// Converged Phase-A starting point (Claude + Codex): a hair off strict profile so -// the mouth/chin reads naturally; tail OPAQUE alpha-tested (one coherent tail, no -// blend bleed); fins translucent + single-sided (kills the double-sided ghosting -// that read as an off-center dorsal). All live-tunable at /zen?debug=1. +// UB is now a marine angelfish (Queen): ONE mesh, TWO materials (…_body, …_fins — fins +// covers the caudal tail too), and a single trimmed Idle loop. Body renders OPAQUE + +// single-sided; fins default to OPAQUE alpha-tested (clean silhouette, no blend bleed — +// the koi's lesson) but can go translucent. All live-tunable at /zen?debug=1. export const DEFAULTS = { - yaw: Math.PI / 2 + 0.10, // ub.rotation.y — slight 3/4 view + yaw: Math.PI / 2, // ub.rotation.y — side-on-ish (tune live; head faces ±Z) pitch: 0, // ub.rotation.x — nose up/down - tailTranslucent: false, // false = opaque alpha-tested (coherent); true = blended - tailSide: 'front', // front | back | double - tailAlphaTest: 0.035, - tailOpacity: 1.0, // only when translucent - finSide: 'front', // front | back | double - finOpacity: 0.75, - finAlphaTest: 0.02, + scale: 1, // multiplier on the auto-fit size + finTranslucent: false, // false = opaque alpha-tested (coherent); true = blended + finSide: 'double', // front | back | double (fins are thin → double reads fuller) + finOpacity: 0.9, // only when translucent + finAlphaTest: 0.5, // clip the fin-edge alpha paused: false, frame: 0, // 0..1 scrub position when paused }; @@ -76,43 +74,41 @@ export async function createAquarium(canvas, initial = {}) { const center = box.getCenter(new THREE.Vector3()); ub.position.sub(center); const maxDim = Math.max(size.x, size.y, size.z) || 1; - ub.scale.setScalar(2.6 / maxDim); + const baseScale = 2.6 / maxDim; // auto-fit; params.scale fine-tunes it live - // Collect the three named materials/meshes so applyMaterials can retarget them - // live without re-traversing semantics. + // Classify the two meshes by material-name substring (…_body / …_fins) so + // applyMaterials can retarget them live regardless of the species prefix. const part = {}; - ub.traverse((o) => { if (o.isMesh && o.material?.name) part[o.material.name] = o; }); + ub.traverse((o) => { + if (!o.isMesh || !o.material?.name) return; + const n = o.material.name.toLowerCase(); + if (n.includes('body')) part.body = o; else if (n.includes('fin')) part.fins = o; + }); function applyMaterials() { - const body = part.UB_Body, fins = part.UB_Fins, tail = part.UB_Tail; - if (body) { + const body = part.body, fins = part.fins; + if (body) { // body: always OPAQUE, single-sided const m = body.material; m.transparent = false; m.opacity = 1; m.alphaTest = 0; m.depthWrite = true; m.depthTest = true; m.side = THREE.FrontSide; body.renderOrder = 1; m.needsUpdate = true; } - if (fins) { + if (fins) { // fins (+ tail): opaque alpha-tested by default const m = fins.material; - m.transparent = true; m.opacity = params.finOpacity; m.alphaTest = params.finAlphaTest; - m.alphaToCoverage = true; m.depthWrite = false; m.depthTest = true; - m.side = SIDE[params.finSide] ?? THREE.FrontSide; - fins.renderOrder = 3; m.needsUpdate = true; - } - if (tail) { - const m = tail.material; - m.alphaTest = params.tailAlphaTest; m.alphaToCoverage = true; m.depthTest = true; - m.side = SIDE[params.tailSide] ?? THREE.FrontSide; - if (params.tailTranslucent) { - m.transparent = true; m.opacity = params.tailOpacity; m.depthWrite = false; + m.alphaTest = params.finAlphaTest; m.alphaToCoverage = true; m.depthTest = true; + m.side = SIDE[params.finSide] ?? THREE.DoubleSide; + if (params.finTranslucent) { + m.transparent = true; m.opacity = params.finOpacity; m.depthWrite = false; } else { - m.transparent = false; m.opacity = 1; m.depthWrite = true; // opaque, coherent + m.transparent = false; m.opacity = 1; m.depthWrite = true; // clean, coherent } - tail.renderOrder = 2; m.needsUpdate = true; + fins.renderOrder = 2; m.needsUpdate = true; } } function applyTransform() { ub.rotation.set(params.pitch, params.yaw, 0); + ub.scale.setScalar(baseScale * (params.scale ?? 1)); } applyMaterials(); @@ -124,7 +120,7 @@ export async function createAquarium(canvas, initial = {}) { const mixer = new THREE.AnimationMixer(ub); const actions = {}; for (const clip of gltf.animations) actions[clip.name] = mixer.clipAction(clip); - const baseClip = (actions.Idle_swim ? actions.Idle_swim : mixer.clipAction(gltf.animations[0])); + const baseClip = mixer.clipAction(gltf.animations[0]); // the trimmed Idle loop baseClip.play(); const baseDuration = baseClip.getClip().duration || 1; mixer.timeScale = reduced ? 0.6 : 1; // calmer when reduced-motion diff --git a/frontend/src/routes/zen/+page.svelte b/frontend/src/routes/zen/+page.svelte index b55b549..ccfb62d 100644 --- a/frontend/src/routes/zen/+page.svelte +++ b/frontend/src/routes/zen/+page.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import { auth } from '$lib/auth.svelte.js'; + import { auth, refresh as refreshAuth } from '$lib/auth.svelte.js'; import { isDevGated, blockedForViewer } from '$lib/devgate.js'; import Footer from '$lib/components/Footer.svelte'; @@ -25,12 +25,17 @@ } onMount(() => { - // Dev-gated while UB is being ironed out: non-admins (no preview token) bounce. - if (blockedForViewer('zen', auth.user, $page.url)) { goto('/play'); return; } - debug = $page.url.searchParams.get('debug') === '1'; let h; let cancelled = false; // guard the async load against an early unmount (async () => { + // Don't decide the gate until we actually KNOW who's here. A cold load / hard + // refresh / direct link mounts before auth has revalidated, so auth.user is still + // null and an admin would be wrongly bounced. Revalidate first, THEN gate. + if (!auth.ready) { try { await refreshAuth(); } catch { /* offline → treat as gated */ } } + if (cancelled) return; + // Dev-gated while UB is being ironed out: non-admins (no preview token) bounce. + if (blockedForViewer('zen', auth.user, $page.url)) { goto('/play'); return; } + debug = $page.url.searchParams.get('debug') === '1'; try { // WebGL guard — fall back to a warm card rather than a blank canvas. const t = document.createElement('canvas'); @@ -88,27 +93,20 @@ +
-
Tail
- - - - {#if dbg.tailTranslucent} - - {/if} - -
-
Fins
+
Fins & tail
+ - + + {#if dbg.finTranslucent} + + {/if}
diff --git a/frontend/static/models/ub-angelfish.glb b/frontend/static/models/ub-angelfish.glb new file mode 100644 index 0000000..54ba103 Binary files /dev/null and b/frontend/static/models/ub-angelfish.glb differ diff --git a/frontend/static/models/ub-split.glb b/frontend/static/models/ub-split.glb deleted file mode 100644 index 7442cc8..0000000 Binary files a/frontend/static/models/ub-split.glb and /dev/null differ diff --git a/frontend/static/models/ub.glb b/frontend/static/models/ub.glb deleted file mode 100644 index d51050a..0000000 Binary files a/frontend/static/models/ub.glb and /dev/null differ diff --git a/tools/glb-split/angelfish-clips.json b/tools/glb-split/angelfish-clips.json new file mode 100644 index 0000000..523a565 --- /dev/null +++ b/tools/glb-split/angelfish-clips.json @@ -0,0 +1,315 @@ +{ + "note": "Angelfish Animated Pack 10 (CGTrader). One baked 75.67s/2270-frame Take at 30fps in each GLB. GLB frame = Maya frame + 110. Slice with THREE.AnimationUtils.subclip(clip,name,startFrame,endFrame,30).", + "fps": 30, + "total_frames": 2270, + "clips": { + "Swim1_norm": { + "glb_frames": [ + 110, + 185 + ], + "seconds": [ + 3.667, + 6.167 + ], + "maya_frames": [ + 0, + 75 + ] + }, + "Turn_R_in": { + "glb_frames": [ + 186, + 255 + ], + "seconds": [ + 6.2, + 8.5 + ], + "maya_frames": [ + 76, + 145 + ] + }, + "Turn_R_loop": { + "glb_frames": [ + 256, + 330 + ], + "seconds": [ + 8.533, + 11.0 + ], + "maya_frames": [ + 146, + 220 + ] + }, + "Turn_R_out": { + "glb_frames": [ + 331, + 409 + ], + "seconds": [ + 11.033, + 13.633 + ], + "maya_frames": [ + 221, + 299 + ] + }, + "Swim2_Fast": { + "glb_frames": [ + 410, + 450 + ], + "seconds": [ + 13.667, + 15.0 + ], + "maya_frames": [ + 300, + 340 + ] + }, + "Turn_L_in": { + "glb_frames": [ + 486, + 535 + ], + "seconds": [ + 16.2, + 17.833 + ], + "maya_frames": [ + 376, + 425 + ] + }, + "Turn_L_loop": { + "glb_frames": [ + 536, + 610 + ], + "seconds": [ + 17.867, + 20.333 + ], + "maya_frames": [ + 426, + 500 + ] + }, + "Turn_L_out": { + "glb_frames": [ + 611, + 710 + ], + "seconds": [ + 20.367, + 23.667 + ], + "maya_frames": [ + 501, + 600 + ] + }, + "Attack": { + "glb_frames": [ + 711, + 751 + ], + "seconds": [ + 23.7, + 25.033 + ], + "maya_frames": [ + 601, + 641 + ] + }, + "Eat_Swim": { + "glb_frames": [ + 752, + 811 + ], + "seconds": [ + 25.067, + 27.033 + ], + "maya_frames": [ + 642, + 701 + ] + }, + "Eat_Ground": { + "glb_frames": [ + 812, + 852 + ], + "seconds": [ + 27.067, + 28.4 + ], + "maya_frames": [ + 702, + 742 + ] + }, + "Eat_Wall": { + "glb_frames": [ + 853, + 893 + ], + "seconds": [ + 28.433, + 29.767 + ], + "maya_frames": [ + 743, + 783 + ] + }, + "Turn_L_Fast": { + "glb_frames": [ + 894, + 923 + ], + "seconds": [ + 29.8, + 30.767 + ], + "maya_frames": [ + 784, + 813 + ] + }, + "Turn_R_Fast": { + "glb_frames": [ + 924, + 953 + ], + "seconds": [ + 30.8, + 31.767 + ], + "maya_frames": [ + 814, + 843 + ] + }, + "Swim3_Long_Wide": { + "glb_frames": [ + 954, + 1401 + ], + "seconds": [ + 31.8, + 46.7 + ], + "maya_frames": [ + 844, + 1291 + ] + }, + "Swim4_Long_Near": { + "glb_frames": [ + 1402, + 1852 + ], + "seconds": [ + 46.733, + 61.733 + ], + "maya_frames": [ + 1292, + 1742 + ] + }, + "Death1 Sink Start": { + "glb_frames": [ + 1853, + 1930 + ], + "seconds": [ + 61.767, + 64.333 + ], + "maya_frames": [ + 1743, + 1820 + ] + }, + "Death1 Sink Loop": { + "glb_frames": [ + 1930, + 1987 + ], + "seconds": [ + 64.333, + 66.233 + ], + "maya_frames": [ + 1820, + 1877 + ] + }, + "Death2 Float Start": { + "glb_frames": [ + 1988, + 2120 + ], + "seconds": [ + 66.267, + 70.667 + ], + "maya_frames": [ + 1878, + 2010 + ] + }, + "Death2 Float Loop": { + "glb_frames": [ + 2121, + 2210 + ], + "seconds": [ + 70.7, + 73.667 + ], + "maya_frames": [ + 2011, + 2100 + ] + }, + "Jump": { + "glb_frames": [ + 2211, + 2270 + ], + "seconds": [ + 73.7, + 75.667 + ], + "maya_frames": [ + 2101, + 2160 + ] + }, + "Idle": { + "glb_frames": [ + 0, + 109 + ], + "seconds": [ + 0.0, + 3.633 + ], + "maya_frames": [ + -110, + -1 + ] + } + } +} \ No newline at end of file diff --git a/tools/glb-split/inspect.mjs b/tools/glb-split/inspect.mjs new file mode 100644 index 0000000..943b477 --- /dev/null +++ b/tools/glb-split/inspect.mjs @@ -0,0 +1,71 @@ +// 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(', ')}`); diff --git a/tools/glb-split/package-lock.json b/tools/glb-split/package-lock.json index d70f96f..4420527 100644 --- a/tools/glb-split/package-lock.json +++ b/tools/glb-split/package-lock.json @@ -7,7 +7,17 @@ "name": "glb-split", "dependencies": { "@gltf-transform/core": "^4.4.0", - "@gltf-transform/extensions": "^4.4.0" + "@gltf-transform/extensions": "^4.4.0", + "@gltf-transform/functions": "^4.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@gltf-transform/core": { @@ -33,15 +43,604 @@ "url": "https://github.com/sponsors/donmccurdy" } }, + "node_modules/@gltf-transform/functions": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@gltf-transform/functions/-/functions-4.4.0.tgz", + "integrity": "sha512-CaSTAVAd2NXNWsxdgvq090rKHqy7AQlcNWV4ec7xtQyS8WEv3S3gVN27ikWmdB8nWEsXUbOIDhtPMLbXI6xDJg==", + "dependencies": { + "@gltf-transform/core": "^4.4.0", + "@gltf-transform/extensions": "^4.4.0", + "ktx-parse": "^1.1.0", + "ndarray": "^1.0.19", + "ndarray-lanczos": "^0.3.0", + "ndarray-pixels": "^5.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/donmccurdy" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@types/ndarray": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.0.14.tgz", + "integrity": "sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==" + }, + "node_modules/cwise-compiler": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz", + "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==", + "dependencies": { + "uniq": "^1.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/iota-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", + "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/ktx-parse": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz", "integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==" }, + "node_modules/ndarray": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", + "integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==", + "dependencies": { + "iota-array": "^1.0.0", + "is-buffer": "^1.0.2" + } + }, + "node_modules/ndarray-lanczos": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ndarray-lanczos/-/ndarray-lanczos-0.3.0.tgz", + "integrity": "sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg==", + "dependencies": { + "@types/ndarray": "^1.0.11", + "ndarray": "^1.0.19" + } + }, + "node_modules/ndarray-ops": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz", + "integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==", + "dependencies": { + "cwise-compiler": "^1.0.0" + } + }, + "node_modules/ndarray-pixels": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.0.1.tgz", + "integrity": "sha512-IBtrpefpqlI8SPDCGjXk4v5NV5z7r3JSuCbfuEEXaM0vrOJtNGgYUa4C3Lt5H+qWdYF4BCPVFsnXhNC7QvZwkw==", + "dependencies": { + "@types/ndarray": "^1.0.14", + "ndarray": "^1.0.19", + "ndarray-ops": "^1.2.2", + "sharp": "^0.34.0" + } + }, "node_modules/property-graph": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz", "integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "optional": true + }, + "node_modules/uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==" } } } diff --git a/tools/glb-split/package.json b/tools/glb-split/package.json index 11202b9..829cd71 100644 --- a/tools/glb-split/package.json +++ b/tools/glb-split/package.json @@ -1 +1 @@ -{"name":"glb-split","private":true,"type":"module","dependencies":{"@gltf-transform/core":"^4.4.0","@gltf-transform/extensions":"^4.4.0"}} \ No newline at end of file +{"name":"glb-split","private":true,"type":"module","dependencies":{"@gltf-transform/core":"^4.4.0","@gltf-transform/extensions":"^4.4.0","@gltf-transform/functions":"^4.4.0"}} \ No newline at end of file diff --git a/tools/glb-split/trim-idle.mjs b/tools/glb-split/trim-idle.mjs new file mode 100644 index 0000000..e244a27 --- /dev/null +++ b/tools/glb-split/trim-idle.mjs @@ -0,0 +1,39 @@ +// Trim a GLB's baked animation down to a [0, tEnd] window (seconds) — for the angelfish +// pack that's the Idle clip (GLB frames 0–109 ≈ 3.633s), which drops ~95% of the animation +// data (the full Take is 75.67s). Geometry/skin/textures are untouched. Shared input +// accessors are sliced once; outputs per-sampler. Usage: node trim-idle.mjs in.glb out.glb 3.633 +import { NodeIO } from '@gltf-transform/core'; +import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; +import { prune } from '@gltf-transform/functions'; + +const [, , inPath, outPath, tEndStr] = process.argv; +const tEnd = parseFloat(tEndStr); +const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); +const doc = await io.read(inPath); +const root = doc.getRoot(); + +// Pass 1: per UNIQUE input time-track, compute how many keyframes fall in [0, tEnd] +// (BEFORE mutating — all channels share one input), then slice the input once. +const keepByInput = new Map(); +for (const anim of root.listAnimations()) + for (const s of anim.listSamplers()) { + const input = s.getInput(); + if (keepByInput.has(input)) continue; + const times = input.getArray(); + let last = 0; + for (let i = 0; i < times.length; i++) { if (times[i] <= tEnd + 1e-6) last = i; else break; } + const keep = last + 1; + keepByInput.set(input, keep); + if (keep >= 2 && keep < times.length) input.setArray(times.slice(0, keep)); + } +// Pass 2: slice every sampler's OUTPUT to its input's keep count. +for (const anim of root.listAnimations()) + for (const s of anim.listSamplers()) { + const keep = keepByInput.get(s.getInput()); + const output = s.getOutput(); + const comps = output.getElementSize(); + if (keep * comps < output.getArray().length) output.setArray(output.getArray().slice(0, keep * comps)); + } +await doc.transform(prune()); // repack buffers + drop the orphaned (trimmed-away) data +await io.write(outPath, doc); +console.log('wrote', outPath);