zen: UB base loop → Swim1_norm (gentle in-place swim, not the static Idle)
Idle is a resting pose (only eye/mouth/fin micro-motion). Swapped the base loop to Swim1_norm — a ~2.5s swim cycle with ZERO root drift (verified via trim-clip.mjs), so UB undulates continuously without traveling off-screen. Generalized the trimmer (tools/glb-split/trim-clip.mjs: extract any [start,end] range, rebased to 0, + drift report). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,8 @@ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
const MODEL_URL = '/models/ub-angelfish.glb';
|
||||
|
||||
// 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 +
|
||||
// covers the caudal tail too), and a single trimmed loop (Swim1_norm — a ~2.5s in-place
|
||||
// swim cycle, zero root drift, so it undulates without traveling). 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 = {
|
||||
@@ -120,7 +121,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 = mixer.clipAction(gltf.animations[0]); // the trimmed Idle loop
|
||||
const baseClip = mixer.clipAction(gltf.animations[0]); // the trimmed swim loop
|
||||
baseClip.play();
|
||||
const baseDuration = baseClip.getClip().duration || 1;
|
||||
mixer.timeScale = reduced ? 0.6 : 1; // calmer when reduced-motion
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
// 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}`);
|
||||
Reference in New Issue
Block a user