Smooth hips: no horizontal root motion, tanh-saturated bounce

The hard clamp made out-of-scale clips ride its boundary and jerk the
model left/right. Hips translations now stay pinned horizontally (the
framing compensation cancelled that motion anyway — the moves live in
the legs and torso) and the vertical bounce saturates smoothly via tanh
at 0.8x the rest length. Same-rig baked clips render identically since
their root motion was already being compensated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-31 11:46:22 +02:00
1 parent 3d90c5b248
commit 06ae7bc422
1 file changed
+13 -4
+13 -4
View File
@@ -664,10 +664,19 @@ class ModelSim {
const k = ch._k;
let tx = (V[i3] + (V[j3]-V[i3])*f)*k, ty = (V[i3+1] + (V[j3+1]-V[i3+1])*f)*k,
tz = (V[i3+2] + (V[j3+2]-V[i3+2])*f)*k;
// safety net: the offset from the rest position may never exceed
// 1.2x the bone's rest length, whatever the source clip does — this
// keeps foreign clips from throwing the model out of the frame
if (ch._rl > 1e-6) {
if (ch._hips === undefined) {
const nn = this.skel.nodes[ch.node];
ch._hips = nn.name.split(':').pop().split('.').pop() === 'Hips';
}
if (ch._hips && ch._rl > 1e-6) {
// Hips: kill the horizontal root motion entirely (it was cancelled
// by the framing compensation anyway, and a hard clamp made it
// jerk left/right); the vertical bounce saturates smoothly.
const max = ch._rl*0.8;
tx = ch._rest[0]; tz = ch._rest[2];
ty = ch._rest[1] + max*Math.tanh((ty - ch._rest[1])/max);
} else if (ch._rl > 1e-6) {
// other bones (same-rig clips only): sane hard limit
const dx = tx - ch._rest[0], dy = ty - ch._rest[1], dz = tz - ch._rest[2];
const d = Math.hypot(dx, dy, dz), max = ch._rl*1.2;
if (d > max) {