zen: Phase B — UB swims with a behavior engine (wander, cruise/rest/burst, U-turns)
UB is no longer a static in-place loop. New behavior.js owns locomotion: UB wanders a bounded tank, cruises at a chosen speed, drifts to new depths (nose tilts into it), occasionally rests or darts, and banks through smooth U-turns at the edges — the tail beats faster/slower with speed. All clips are in-place, so the engine drives world position + heading and crossfades between the named clips (idle/cruise/burst/turnL/turnR). Multi-clip GLB built via tools/glb-split/build-clips.mjs (5 clips, 8.7MB — orphaned Take accessors explicitly disposed). aquarium.js reworked: clip crossfade + per-frame behavior apply. Tuner (/zen?debug=1) now exposes scale + Behavior (cruise speed / roam width / roam height / liveliness) + the fins section. Reduced-motion calms speed + liveliness. Still admin-gated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// Build a multi-clip GLB from the pack's single baked Take: extract a chosen set of
|
||||
// clips (by frame range from angelfish-clips.json) into SEPARATE named animations, each
|
||||
// rebased to start at 0, then drop the original 75s Take. This is what the Zen Den's
|
||||
// behavior engine crossfades between. Usage: node build-clips.mjs in.glb out.glb
|
||||
import { NodeIO } from '@gltf-transform/core';
|
||||
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
||||
import { prune } from '@gltf-transform/functions';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const [, , inPath, outPath] = process.argv;
|
||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
const doc = await io.read(inPath);
|
||||
const root = doc.getRoot();
|
||||
const buffer = root.listBuffers()[0];
|
||||
const meta = JSON.parse(fs.readFileSync(new URL('./angelfish-clips.json', import.meta.url))).clips;
|
||||
|
||||
// output-clip-name -> source clip in the pack
|
||||
const MAP = {
|
||||
idle: 'Idle',
|
||||
cruise: 'Swim1_norm',
|
||||
burst: 'Swim2_Fast',
|
||||
turnL: 'Turn_L_loop',
|
||||
turnR: 'Turn_R_loop',
|
||||
};
|
||||
|
||||
const src = root.listAnimations()[0];
|
||||
const srcChannels = src.listChannels();
|
||||
const EPS = 1e-4;
|
||||
// The original Take's time/value accessors — captured now, disposed after we've copied
|
||||
// the windows we need (new clips create their own fresh accessors, so these become dead).
|
||||
const oldAccessors = new Set();
|
||||
for (const sm of src.listSamplers()) { oldAccessors.add(sm.getInput()); oldAccessors.add(sm.getOutput()); }
|
||||
|
||||
for (const [outName, srcName] of Object.entries(MAP)) {
|
||||
const [s0, s1] = meta[srcName].seconds;
|
||||
const anim = doc.createAnimation(outName);
|
||||
for (const ch of srcChannels) {
|
||||
const sm = ch.getSampler();
|
||||
const times = sm.getInput().getArray();
|
||||
let a = 0; while (a < times.length - 1 && times[a] < s0 - EPS) a++;
|
||||
let b = times.length - 1; while (b > a && times[b] > s1 + EPS) b--;
|
||||
const base = times[a];
|
||||
const nTimes = Float32Array.from(times.slice(a, b + 1), (x) => x - base);
|
||||
const out = sm.getOutput();
|
||||
const c = out.getElementSize();
|
||||
const nOut = out.getArray().slice(a * c, (b + 1) * c);
|
||||
const inAcc = doc.createAccessor().setType('SCALAR').setArray(nTimes).setBuffer(buffer);
|
||||
const outAcc = doc.createAccessor().setType(out.getType()).setArray(nOut).setBuffer(buffer);
|
||||
const ns = doc.createAnimationSampler().setInput(inAcc).setOutput(outAcc).setInterpolation(sm.getInterpolation());
|
||||
anim.addSampler(ns);
|
||||
anim.addChannel(doc.createAnimationChannel().setTargetNode(ch.getTargetNode()).setTargetPath(ch.getTargetPath()).setSampler(ns));
|
||||
}
|
||||
}
|
||||
src.dispose(); // remove the original 75s Take
|
||||
for (const acc of oldAccessors) acc.dispose(); // free its ~10MB of keyframe data
|
||||
await doc.transform(prune());
|
||||
await io.write(outPath, doc);
|
||||
console.log('clips:', root.listAnimations().map((a) => `${a.getName()}(${a.listChannels().length}ch)`).join(', '));
|
||||
Reference in New Issue
Block a user