Skip to content

Commit ae63c35

Browse files
committed
Fix arm swing accumulation and add stable bar grip orientation
1 parent 07fe312 commit ae63c35

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
@@ -205,13 +205,44 @@ export function swingArms(
205205
HIP_EULER.setFromQuaternion(contraHip.quaternion, "XYZ");
206206
if (Math.abs(HIP_EULER.x) < 1e-3) continue; // legs still → no swing
207207
SWING_EULER.setFromQuaternion(shoulder.quaternion, "XYZ");
208-
SWING_EULER.x += HIP_EULER.x * SWING_GAIN;
208+
// This pass runs every render frame. Assign the procedural channel instead
209+
// of adding to last frame's result, otherwise an unauthored shoulder keeps
210+
// accumulating rotation (most visibly the left arm in a forward lunge).
211+
SWING_EULER.x = HIP_EULER.x * SWING_GAIN;
209212
shoulder.quaternion.setFromEuler(SWING_EULER);
210213
changed = true;
211214
}
212215
if (changed) m.root.updateMatrixWorld(true);
213216
}
214217

218+
const GRIP_FRAME = new THREE.Quaternion().setFromAxisAngle(
219+
new THREE.Vector3(0, 1, 0),
220+
Math.PI,
221+
);
222+
223+
/**
224+
* Give gripping wrists a complete overhand contact frame. Arm IK only
225+
* constrains wrist position, leaving hand orientation underdetermined; without
226+
* this pass the fingers inherit the forearm direction and can point above the
227+
* bar. In root space the palm faces back (-Z), the fingers extend down (-Y),
228+
* and the wrist remains exactly on its solved anchor.
229+
*/
230+
export function orientBarGrips(m: Mannequin, grips: readonly GripTarget[]): void {
231+
let changed = false;
232+
for (const g of grips) {
233+
const side = /_(left|right)$/.exec(g.effector)?.[1];
234+
if (!side) continue;
235+
const wrist = m.bones.get(`wrist_${side}`);
236+
if (!wrist?.parent) continue;
237+
const rootWorld = m.root.getWorldQuaternion(new THREE.Quaternion());
238+
const desiredWorld = rootWorld.multiply(GRIP_FRAME);
239+
const parentWorld = wrist.parent.getWorldQuaternion(new THREE.Quaternion());
240+
wrist.quaternion.copy(parentWorld.invert().multiply(desiredWorld));
241+
changed = true;
242+
}
243+
if (changed) m.root.updateMatrixWorld(true);
244+
}
245+
215246
/** Max head turn toward a look target (radians) so the neck never over-rotates. */
216247
export const MAX_LOOK = 55 * (Math.PI / 180);
217248
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

@@ -536,7 +536,9 @@ export function createViewer(
536536
if (joints.length === 0) continue;
537537
solveCCD({ joints, limits, effector, target }, 12);
538538
}
539-
// 3. Finger wrap.
539+
// 3. Resolve the wrist roll left underdetermined by positional arm IK.
540+
orientBarGrips(mannequin, grips);
541+
// 4. Finger wrap.
540542
wrapGrip(mannequin, grips);
541543
}
542544

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;
@@ -104,6 +104,43 @@ describe("swingArms (L4.2)", () => {
104104
swingArms(m, new Set(), new Set(["left"]));
105105
expect(m.bones.get("shoulder_left")!.rotation.x).toBeCloseTo(before, 5);
106106
});
107+
108+
it("is idempotent across render frames instead of accumulating a spin", () => {
109+
const m = buildMannequin();
110+
m.bones.get("hip_right")!.rotation.x = -0.6;
111+
swingArms(m, new Set(), new Set());
112+
const once = m.bones.get("shoulder_left")!.quaternion.clone();
113+
for (let frame = 0; frame < 120; frame++) swingArms(m, new Set(), new Set());
114+
expect(m.bones.get("shoulder_left")!.quaternion.angleTo(once)).toBeLessThan(1e-6);
115+
});
116+
});
117+
118+
describe("orientBarGrips", () => {
119+
it("resolves wrist roll without moving a hand off its grip point", () => {
120+
const m = buildMannequin();
121+
const wrist = m.bones.get("wrist_left")!;
122+
wrist.rotation.y = 1.7;
123+
m.root.updateMatrixWorld(true);
124+
const position = wrist.getWorldPosition(new THREE.Vector3()).clone();
125+
orientBarGrips(m, [{ effector: "hand_left", anchor: "bar_left" }]);
126+
expect(wrist.getWorldPosition(new THREE.Vector3()).distanceTo(position)).toBeLessThan(1e-6);
127+
128+
const palm = new THREE.Vector3(0, 0, 1)
129+
.applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion()));
130+
const fingers = new THREE.Vector3(0, -1, 0)
131+
.applyQuaternion(wrist.getWorldQuaternion(new THREE.Quaternion()));
132+
expect(palm.dot(new THREE.Vector3(0, 0, -1))).toBeGreaterThan(0.999);
133+
expect(fingers.dot(new THREE.Vector3(0, -1, 0))).toBeGreaterThan(0.999);
134+
});
135+
136+
it("is stable when applied repeatedly", () => {
137+
const m = buildMannequin();
138+
const grips = [{ effector: "hand_right", anchor: "bar_right" }];
139+
orientBarGrips(m, grips);
140+
const once = m.bones.get("wrist_right")!.quaternion.clone();
141+
for (let frame = 0; frame < 60; frame++) orientBarGrips(m, grips);
142+
expect(m.bones.get("wrist_right")!.quaternion.angleTo(once)).toBeLessThan(1e-6);
143+
});
107144
});
108145

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

0 commit comments

Comments
 (0)