Rigged models dance to the music (Modello 3D family)
Two paths for skinned GLBs with no live body driving them: - animation clips in the GLB (e.g. Mixamo moves) are parsed (rotation/ translation channels, nlerp sampling) and looped at the effect speed; - without clips, a beat-locked procedural dance drives the FK targets: arms pump on alternate beats, hips sway, head nods, knees bounce, with a free-running metronome keeping the groove between detected beats and bass scaling the amplitude. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
748d036ff6
commit
31bfc96f12
1 file changed
+109
-10
+109
-10
@@ -330,7 +330,26 @@ function parseGLB(buf) {
|
||||
});
|
||||
});
|
||||
if (!prims.length) throw new Error('nessuna mesh triangolare nel GLB');
|
||||
return { prims, min, max, skel: prims.some(p => p.skinned) ? skel : null };
|
||||
const hasSkin = prims.some(p => p.skinned);
|
||||
// animation clips (rotation/translation channels) for skinned models
|
||||
let anims = null;
|
||||
if (hasSkin && json.animations && json.animations.length) {
|
||||
anims = json.animations.map(a => {
|
||||
const channels = a.channels
|
||||
.filter(ch => ch.target.node != null &&
|
||||
(ch.target.path === 'rotation' || ch.target.path === 'translation'))
|
||||
.map(ch => {
|
||||
const s = a.samplers[ch.sampler];
|
||||
return { node: ch.target.node, path: ch.target.path,
|
||||
times: readAccessor(json, bin, s.input).data,
|
||||
vals: readAccessor(json, bin, s.output).data };
|
||||
});
|
||||
let dur = 0;
|
||||
channels.forEach(c => { const e = c.times[c.times.length-1]; if (e > dur) dur = e; });
|
||||
return { name: a.name || 'clip', channels, dur: Math.max(0.1, dur) };
|
||||
});
|
||||
}
|
||||
return { prims, min, max, skel: hasSkin ? skel : null, anims };
|
||||
}
|
||||
|
||||
// Generated fallback: a torus knot, so the family shows something before any
|
||||
@@ -437,7 +456,9 @@ class ModelSim {
|
||||
const gl = this.gl;
|
||||
this._freeMeshes();
|
||||
this.skel = model.skel || null;
|
||||
this.anims = model.anims || null;
|
||||
this.restJoints = null;
|
||||
this._db = null;
|
||||
if (this.skel) {
|
||||
// rest-pose joint matrices, then a CPU-skinned vertex sample for the
|
||||
// bounding box (skinned vertices live in mesh space until deformed)
|
||||
@@ -514,7 +535,74 @@ class ModelSim {
|
||||
// `targets` (base bone name -> world-space direction) an extra local
|
||||
// rotation is solved per bone so that its chain child points along the
|
||||
// target — FK retargeting of the tracked body onto the Mixamo rig.
|
||||
_computeJoints(targets) {
|
||||
// Sample the first animation clip at time tt (looping): node -> {r?, t?}.
|
||||
_sampleAnim(tt) {
|
||||
const a = this.anims[0];
|
||||
const T = tt % a.dur;
|
||||
const out = {};
|
||||
for (const ch of a.channels) {
|
||||
const times = ch.times;
|
||||
let i = 0;
|
||||
while (i < times.length - 2 && times[i+1] < T) i++;
|
||||
const t0 = times[i], t1 = times[i+1] !== undefined ? times[i+1] : t0;
|
||||
const f = t1 > t0 ? Math.min(1, Math.max(0, (T - t0)/(t1 - t0))) : 0;
|
||||
const o = out[ch.node] || (out[ch.node] = {});
|
||||
if (ch.path === 'rotation') {
|
||||
const A = ch.vals, i4 = i*4, j4 = Math.min(i4+4, A.length-4);
|
||||
let d = A[i4]*A[j4] + A[i4+1]*A[j4+1] + A[i4+2]*A[j4+2] + A[i4+3]*A[j4+3];
|
||||
const sg = d < 0 ? -1 : 1;
|
||||
const q = [A[i4] + (A[j4]*sg - A[i4])*f, A[i4+1] + (A[j4+1]*sg - A[i4+1])*f,
|
||||
A[i4+2] + (A[j4+2]*sg - A[i4+2])*f, A[i4+3] + (A[j4+3]*sg - A[i4+3])*f];
|
||||
const l = Math.hypot(q[0], q[1], q[2], q[3]) || 1;
|
||||
o.r = [q[0]/l, q[1]/l, q[2]/l, q[3]/l];
|
||||
} else {
|
||||
const V = ch.vals, i3 = i*3, j3 = Math.min(i3+3, V.length-3);
|
||||
o.t = [V[i3] + (V[j3]-V[i3])*f, V[i3+1] + (V[j3+1]-V[i3+1])*f, V[i3+2] + (V[j3+2]-V[i3+2])*f];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Beat-locked procedural dance for rigged models without animation clips:
|
||||
// arms pump on alternate beats, hips sway, head nods, knees bounce. A
|
||||
// free-running metronome keeps the groove between detected beats.
|
||||
_danceTargets(t, beat, bass) {
|
||||
const db = this._db || (this._db = { n: 0, last: -1, period: 0.5, prevBeat: 0 });
|
||||
if (beat > 0.6 && db.prevBeat <= 0.6) {
|
||||
if (db.last >= 0) {
|
||||
const iv = t - db.last;
|
||||
if (iv > 0.24 && iv < 1.3) db.period = iv;
|
||||
}
|
||||
db.last = t; db.n++;
|
||||
} else if (db.last < 0 || t - db.last > db.period*1.6) {
|
||||
db.last = db.last < 0 ? t : db.last + db.period;
|
||||
db.n++;
|
||||
}
|
||||
db.prevBeat = beat;
|
||||
const p = Math.min(1, Math.max(0, (t - db.last)/db.period));
|
||||
const sm = p*p*(3 - 2*p);
|
||||
const ph = (db.n + sm)*Math.PI;
|
||||
const s = Math.sin(ph);
|
||||
const bounce = Math.abs(s);
|
||||
const amp = 0.65 + 0.6*bass;
|
||||
return {
|
||||
targets: {
|
||||
Spine: v3norm([s*0.16*amp, 1, 0.05]),
|
||||
Neck: v3norm([s*0.22*amp, 1, 0.16]),
|
||||
LeftArm: v3norm([0.75, -0.45 + 1.0*amp*Math.max(0, s), 0.25]),
|
||||
LeftForeArm: v3norm([0.30, 0.40 + 0.6*amp*Math.max(0, s), 0.5]),
|
||||
RightArm: v3norm([-0.75, -0.45 + 1.0*amp*Math.max(0, -s), 0.25]),
|
||||
RightForeArm: v3norm([-0.30, 0.40 + 0.6*amp*Math.max(0, -s), 0.5]),
|
||||
LeftUpLeg: v3norm([0.15, -1, 0.08*bounce]),
|
||||
RightUpLeg: v3norm([-0.15, -1, 0.08*bounce]),
|
||||
LeftLeg: v3norm([0.05, -1, -0.10*bounce]),
|
||||
RightLeg: v3norm([-0.05, -1, -0.10*bounce])
|
||||
},
|
||||
bounceY: (bounce - 0.5)*0.05
|
||||
};
|
||||
}
|
||||
|
||||
_computeJoints(targets, anim) {
|
||||
const sk = this.skel;
|
||||
const worlds = new Array(sk.nodes.length);
|
||||
const base = (name) => name.split(':').pop().split('.').pop();
|
||||
@@ -526,10 +614,12 @@ class ModelSim {
|
||||
const visit = (ni, parentWorld) => {
|
||||
const n = sk.nodes[ni];
|
||||
let local;
|
||||
if (n.matrix) {
|
||||
const ao = anim && anim[ni];
|
||||
if (n.matrix && !ao) {
|
||||
local = new Float32Array(n.matrix);
|
||||
} else {
|
||||
let r3 = m3FromQuat(n.r);
|
||||
let r3 = m3FromQuat(ao && ao.r ? ao.r : n.r);
|
||||
const nt = ao && ao.t ? ao.t : n.t;
|
||||
if (targets) {
|
||||
const tgt = targets[base(n.name)];
|
||||
const childBase = CHAIN[base(n.name)];
|
||||
@@ -547,7 +637,7 @@ class ModelSim {
|
||||
const s = n.s;
|
||||
local = m4FromM3T([r3[0]*s[0], r3[1]*s[0], r3[2]*s[0],
|
||||
r3[3]*s[1], r3[4]*s[1], r3[5]*s[1],
|
||||
r3[6]*s[2], r3[7]*s[2], r3[8]*s[2]], n.t);
|
||||
r3[6]*s[2], r3[7]*s[2], r3[8]*s[2]], nt);
|
||||
}
|
||||
const world = parentWorld ? m4mul(parentWorld, local) : local;
|
||||
worlds[ni] = world;
|
||||
@@ -774,11 +864,20 @@ class ModelSim {
|
||||
// model
|
||||
const asp = canvas.width/Math.max(1, canvas.height);
|
||||
const dist = this.radius*2.6;
|
||||
// live body-driven skinning?
|
||||
// live body-driven skinning? else: animation clip or procedural dance
|
||||
const skinnedLive = this.skel && pose && pose.skinTargets;
|
||||
const curJoints = this.skel
|
||||
? (skinnedLive ? this._computeJoints(pose.skinTargets) : this.restJoints)
|
||||
: null;
|
||||
let curJoints = null, danceY = 0;
|
||||
if (this.skel) {
|
||||
if (skinnedLive) {
|
||||
curJoints = this._computeJoints(pose.skinTargets);
|
||||
} else if (this.anims) {
|
||||
curJoints = this._computeJoints(null, this._sampleAnim(timeSec*speed));
|
||||
} else {
|
||||
const dance = this._danceTargets(timeSec, beat, bass);
|
||||
curJoints = this._computeJoints(dance.targets);
|
||||
danceY = dance.bounceY*this.radius;
|
||||
}
|
||||
}
|
||||
// With a live pose the auto-spin slows right down: the person drives it.
|
||||
const yaw = skinnedLive ? 0
|
||||
: timeSec*0.45*speed*(pose ? 0.12 : 1) + (pose ? pose.yaw : 0);
|
||||
@@ -790,7 +889,7 @@ class ModelSim {
|
||||
const sq = pose && !skinnedLive ? Math.max(0.7, Math.min(1.3, pose.squash || 1)) : 1;
|
||||
const world = skinnedLive
|
||||
? [(pose.track ? pose.track[0] : 0)*this.radius, (pose.track ? pose.track[1] : 0)*this.radius, 0]
|
||||
: (pose ? [pose.leanX*this.radius*1.6, (pose.hopY || 0)*this.radius, 0] : [0, 0, 0]);
|
||||
: (pose ? [pose.leanX*this.radius*1.6, (pose.hopY || 0)*this.radius, 0] : [0, danceY, 0]);
|
||||
const model = m4mul(m4mul(m4mul(m4trans(world), m4rotY(yaw)),
|
||||
m4scale3(scale/Math.sqrt(sq), scale*sq, scale/Math.sqrt(sq))),
|
||||
m4trans([-this.center[0], -this.center[1], -this.center[2]]));
|
||||
|
||||
Reference in new issue
Block a user