Skip to content

Commit 695678f

Browse files
Merge pull request #45 from posecode-dev/codex/fix-hand-grip-animation
Fix arm swing accumulation and stabilize hand grips
2 parents db7e2a6 + be94fce commit 695678f

3 files changed

Lines changed: 74 additions & 4 deletions

File tree

packages/posecode-render/src/contacts.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,44 @@ export function swingArms(
235235
HIP_EULER.setFromQuaternion(contraHip.quaternion, "XYZ");
236236
if (Math.abs(HIP_EULER.x) < 1e-3) continue; // legs still → no swing
237237
SWING_EULER.setFromQuaternion(shoulder.quaternion, "XYZ");
238-
SWING_EULER.x += HIP_EULER.x * SWING_GAIN;
238+
// This pass runs every render frame. Assign the procedural channel instead
239+
// of adding to last frame's result, otherwise an unauthored shoulder keeps
240+
// accumulating rotation (most visibly the left arm in a forward lunge).
241+
SWING_EULER.x = HIP_EULER.x * SWING_GAIN;
239242
shoulder.quaternion.setFromEuler(SWING_EULER);
240243
changed = true;
241244
}
242245
if (changed) m.root.updateMatrixWorld(true);
243246
}
244247

248+
const GRIP_FRAME = new THREE.Quaternion().setFromAxisAngle(
249+
new THREE.Vector3(0, 1, 0),
250+
Math.PI,
251+
);
252+
253+
/**
254+
* Give gripping wrists a complete overhand contact frame. Arm IK only
255+
* constrains wrist position, leaving hand orientation underdetermined; without
256+
* this pass the fingers inherit the forearm direction and can point above the
257+
* bar. In root space the palm faces back (-Z), the fingers extend down (-Y),
258+
* and the wrist remains exactly on its solved anchor.
259+
*/
260+
export function orientBarGrips(m: Mannequin, grips: readonly GripTarget[]): void {
261+
let changed = false;
262+
for (const g of grips) {
263+
const side = /_(left|right)$/.exec(g.effector)?.[1];
264+
if (!side) continue;
265+
const wrist = m.bones.get(`wrist_${side}`);
266+
if (!wrist?.parent) continue;
267+
const rootWorld = m.root.getWorldQuaternion(new THREE.Quaternion());
268+
const desiredWorld = rootWorld.multiply(GRIP_FRAME);
269+
const parentWorld = wrist.parent.getWorldQuaternion(new THREE.Quaternion());
270+
wrist.quaternion.copy(parentWorld.invert().multiply(desiredWorld));
271+
changed = true;
272+
}
273+
if (changed) m.root.updateMatrixWorld(true);
274+
}
275+
245276
/** Max head turn toward a look target (radians) so the neck never over-rotates. */
246277
export const MAX_LOOK = 55 * (Math.PI / 180);
247278
const LOOK_FWD = new THREE.Vector3(0, 0, 1);

packages/posecode-render/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
type ClipSource,
2828
} from "./clips.js";
2929
import { depenetrate } from "./depenetrate.js";
30-
import { alignFloorPalms, levelPlantedFeet, wrapGrip, relaxHands, swingArms, aimHead } from "./contacts.js";
30+
import { alignFloorPalms, levelPlantedFeet, wrapGrip, relaxHands, swingArms, aimHead, orientBarGrips } from "./contacts.js";
3131

3232
const DEG = Math.PI / 180;
3333

@@ -549,7 +549,9 @@ export function createViewer(
549549
if (joints.length === 0) continue;
550550
solveCCD({ joints, limits, effector, target }, 12);
551551
}
552-
// 3. Finger wrap.
552+
// 3. Resolve the wrist roll left underdetermined by positional arm IK.
553+
orientBarGrips(mannequin, grips);
554+
// 4. Finger wrap.
553555
wrapGrip(mannequin, grips);
554556
}
555557

packages/posecode-render/test/contacts.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect } from "vitest";
22
import * as THREE from "three";
33
import { buildMannequin } from "../src/mannequin.js";
4-
import { levelPlantedFeet, relaxHands, swingArms, aimHead } from "../src/contacts.js";
4+
import { levelPlantedFeet, relaxHands, swingArms, aimHead, orientBarGrips } from "../src/contacts.js";
55
import { groundFigure } from "../src/groundlock.js";
66

77
const DEG = Math.PI / 180;
@@ -116,6 +116,43 @@ describe("swingArms (L4.2)", () => {
116116
swingArms(m, new Set(), new Set(["left"]));
117117
expect(m.bones.get("shoulder_left")!.rotation.x).toBeCloseTo(before, 5);
118118
});
119+
120+
it("is idempotent across render frames instead of accumulating a spin", () => {
121+
const m = buildMannequin();
122+
m.bones.get("hip_right")!.rotation.x = -0.6;
123+
swingArms(m, new Set(), new Set());
124+
const once = m.bones.get("shoulder_left")!.quaternion.clone();
125+
for (let frame = 0; frame < 120; frame++) swingArms(m, new Set(), new Set());
126+
expect(m.bones.get("shoulder_left")!.quaternion.angleTo(once)).toBeLessThan(1e-6);
127+
});
128+
});
129+
130+
describe("orientBarGrips", () => {
131+
it("resolves wrist roll without moving a hand off its grip point", () => {
132+
const m = buildMannequin();
133+
const wrist = m.bones.get("wrist_left")!;
134+
wrist.rotation.y = 1.7;
135+
m.root.updateMatrixWorld(true);
136+
const position = wrist.getWorldPosition(new THREE.Vector3()).clone();
137+
orientBarGrips(m, [{ effector: "hand_left", anchor: "bar_left" }]);
138+
expect(wrist.getWorldPosition(new THREE.Vector3()).distanceTo(position)).toBeLessThan(1e-6);
139+
140+
const palm = new THREE.Vector3(0, 0, 1)
141+
.applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion()));
142+
const fingers = new THREE.Vector3(0, -1, 0)
143+
.applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion()));
144+
expect(palm.dot(new THREE.Vector3(0, 0, -1))).toBeGreaterThan(0.999);
145+
expect(fingers.dot(new THREE.Vector3(0, -1, 0))).toBeGreaterThan(0.999);
146+
});
147+
148+
it("is stable when applied repeatedly", () => {
149+
const m = buildMannequin();
150+
const grips = [{ effector: "hand_right", anchor: "bar_right" }];
151+
orientBarGrips(m, grips);
152+
const once = m.bones.get("wrist_right")!.quaternion.clone();
153+
for (let frame = 0; frame < 60; frame++) orientBarGrips(m, grips);
154+
expect(m.bones.get("wrist_right")!.quaternion.angleTo(once)).toBeLessThan(1e-6);
155+
});
119156
});
120157

121158
describe("aimHead (L4.3 look-at)", () => {

0 commit comments

Comments
 (0)