// Extract ONE clip [tStart,tEnd] (seconds) from a GLB's baked Take, rebased to start at 0, // and report the root joint's net translation over the clip (so we know if a swim cycle // drifts the fish through space vs. swims in place). Usage: // node trim-clip.mjs in.glb out.glb 3.667 6.167 import { NodeIO } from '@gltf-transform/core'; import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; import { prune } from '@gltf-transform/functions'; const [, , inPath, outPath, s0, s1] = process.argv; const tStart = parseFloat(s0), tEnd = parseFloat(s1), EPS = 1e-4; const io = new NodeIO().registerExtensions(ALL_EXTENSIONS); const doc = await io.read(inPath); const root = doc.getRoot(); // Pass 1: per unique input, find the kept index window + rebase times to 0. const win = new Map(); // input -> {a,b} for (const anim of root.listAnimations()) for (const sm of anim.listSamplers()) { const input = sm.getInput(); if (win.has(input)) continue; const t = input.getArray(); let a = 0; while (a < t.length - 1 && t[a] < tStart - EPS) a++; let b = t.length - 1; while (b > a && t[b] > tEnd + EPS) b--; win.set(input, { a, b }); const base = t[a]; input.setArray(t.slice(a, b + 1).map((x) => x - base)); } // Pass 2: slice each output to its input's window. for (const anim of root.listAnimations()) for (const sm of anim.listSamplers()) { const { a, b } = win.get(sm.getInput()); const out = sm.getOutput(); const c = out.getElementSize(); out.setArray(out.getArray().slice(a * c, (b + 1) * c)); } // Report root-joint translation drift over the clip (first joint's translation track). const skin = root.listSkins()[0]; const rootJoint = skin?.listJoints()[0]; let drift = 'n/a'; if (rootJoint) { for (const anim of root.listAnimations()) for (const ch of anim.listChannels()) { if (ch.getTargetNode() === rootJoint && ch.getTargetPath() === 'translation') { const o = ch.getSampler().getOutput().getArray(); const n = o.length / 3; const dx = o[(n - 1) * 3] - o[0], dy = o[(n - 1) * 3 + 1] - o[1], dz = o[(n - 1) * 3 + 2] - o[2]; drift = `Δ[${dx.toFixed(4)}, ${dy.toFixed(4)}, ${dz.toFixed(4)}] (model units)`; } } } await doc.transform(prune()); await io.write(outPath, doc); const a = root.listAnimations()[0]; let tmax = 0; for (const ch of a.listChannels()) { const arr = ch.getSampler().getInput().getArray(); tmax = Math.max(tmax, arr[arr.length - 1]); } console.log(`wrote ${outPath} | clip duration ${tmax.toFixed(2)}s | root drift ${drift}`);