// 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);