Skip to content

Commit be3354f

Browse files
committed
feat(render): levelPlantedFeet — plantigrade foot-flat correction
1 parent d7bae8e commit be3354f

2 files changed

Lines changed: 122 additions & 1 deletion

File tree

packages/posecode-render/src/contacts.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/** Semantic contact-orientation helpers shared by viewer and eval. */
22
import * as THREE from "three";
3-
import type { PinTarget, ReachTarget } from "posecode-parser";
3+
import { eulerRomFor, type PinTarget, type ReachTarget } from "posecode-parser";
44
import type { Mannequin } from "./mannequin.js";
55

66
const DOWN = new THREE.Vector3(0, -1, 0);
7+
const DEG = Math.PI / 180;
78

89
/** Rotate contacting wrists so the palm face normal points into the floor. */
910
export function alignFloorPalms(
@@ -35,3 +36,70 @@ export function alignFloorPalms(
3536
}
3637
if (sides.size > 0) m.root.updateMatrixWorld(true);
3738
}
39+
40+
const SOLE_LOCAL = new THREE.Vector3(0, -1, 0);
41+
/** Foot mesh-bottom height at/below which the sole is fully leveled (m). */
42+
export const PLANT_FADE = 0.06;
43+
/** Authored plantarflex (ankle local +X) beyond this opts out of leveling (rad). */
44+
export const PLANTARFLEX_SKIP = 15 * DEG;
45+
46+
const FOOT_SIDES: Array<"left" | "right"> = ["left", "right"];
47+
const TMP_EULER = new THREE.Euler();
48+
49+
/**
50+
* Level each ground-locked foot: rotate the ankle so the sole normal points
51+
* world-down (the whole sole rests flat), weighted by how planted the foot is
52+
* and skipped when the ankle is authored into plantarflexion (tiptoe intent).
53+
* The plantigrade analogue of `alignFloorPalms` for feet: it fixes the
54+
* squat/lunge "balancing on the toes" artifact that ground-lock alone leaves,
55+
* where a leg-induced foot tilt makes the ball the lowest mesh point.
56+
*/
57+
export function levelPlantedFeet(m: Mannequin, activeGroundLock: readonly string[]): void {
58+
if (!activeGroundLock.includes("feet")) return;
59+
let changed = false;
60+
for (const side of FOOT_SIDES) {
61+
const ankle = m.bones.get(`ankle_${side}`);
62+
if (!ankle?.parent) continue;
63+
// Tiptoe opt-out: an ankle authored into plantarflexion (local +X) is a
64+
// deliberate relevé / calf-raise / demi-plié — leave it on its toes.
65+
TMP_EULER.setFromQuaternion(ankle.quaternion, "XYZ");
66+
const authoredX = TMP_EULER.x;
67+
const authoredZ = TMP_EULER.z;
68+
if (authoredX > PLANTARFLEX_SKIP) continue;
69+
// Planted-ness weight from the foot mesh bottom height: fully level when the
70+
// sole is on the floor, fading out as a swing foot lifts past PLANT_FADE.
71+
const box = new THREE.Box3().setFromObject(ankle);
72+
const y = Number.isFinite(box.min.y) ? box.min.y : 0;
73+
const weight = THREE.MathUtils.clamp((PLANT_FADE - y) / PLANT_FADE, 0, 1);
74+
if (weight <= 1e-3) continue;
75+
// Minimal rotation aligning the sole normal to world-down (preserves yaw).
76+
const world = ankle.getWorldQuaternion(new THREE.Quaternion());
77+
const current = SOLE_LOCAL.clone().applyQuaternion(world).normalize();
78+
const correction = new THREE.Quaternion().setFromUnitVectors(current, DOWN);
79+
if (weight < 1) correction.slerp(new THREE.Quaternion(), 1 - weight);
80+
const desiredWorld = correction.multiply(world);
81+
const parentWorld = ankle.parent.getWorldQuaternion(new THREE.Quaternion());
82+
const local = parentWorld.invert().multiply(desiredWorld);
83+
// Clamp the corrected ankle to its ROM, widened to admit the authored angle
84+
// so leveling can never push the joint past a healthy range.
85+
const rom = eulerRomFor(`ankle_${side}`);
86+
if (rom) {
87+
TMP_EULER.setFromQuaternion(local, "XYZ");
88+
const cx = THREE.MathUtils.clamp(
89+
TMP_EULER.x,
90+
Math.min(rom.x.min * DEG, authoredX),
91+
Math.max(rom.x.max * DEG, authoredX),
92+
);
93+
const cz = THREE.MathUtils.clamp(
94+
TMP_EULER.z,
95+
Math.min(rom.z.min * DEG, authoredZ),
96+
Math.max(rom.z.max * DEG, authoredZ),
97+
);
98+
TMP_EULER.set(cx, TMP_EULER.y, cz, "XYZ");
99+
local.setFromEuler(TMP_EULER);
100+
}
101+
ankle.quaternion.copy(local);
102+
changed = true;
103+
}
104+
if (changed) m.root.updateMatrixWorld(true);
105+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, it, expect } from "vitest";
2+
import * as THREE from "three";
3+
import { buildMannequin } from "../src/mannequin.js";
4+
import { levelPlantedFeet } from "../src/contacts.js";
5+
import { groundFigure } from "../src/groundlock.js";
6+
7+
const DEG = Math.PI / 180;
8+
9+
/** World-space sole normal (ankle local -Y) for a foot. */
10+
function soleNormal(m: ReturnType<typeof buildMannequin>, side: "left" | "right") {
11+
const ankle = m.bones.get(`ankle_${side}`)!;
12+
const q = ankle.getWorldQuaternion(new THREE.Quaternion());
13+
return new THREE.Vector3(0, -1, 0).applyQuaternion(q).normalize();
14+
}
15+
16+
describe("levelPlantedFeet", () => {
17+
it("levels a tilted planted foot so the sole faces down", () => {
18+
const m = buildMannequin();
19+
// Tilt the leg so the foot pitches within the ankle's dorsiflexion range
20+
// (deeper tilts legitimately lift the heel — capped by ROM), then plant it
21+
// on the floor as the frame loop's ground-lock does before leveling.
22+
m.bones.get("knee_left")!.rotation.x = 12 * DEG;
23+
m.root.updateMatrixWorld(true);
24+
groundFigure(m);
25+
const tiltedDot = soleNormal(m, "left").dot(new THREE.Vector3(0, -1, 0));
26+
levelPlantedFeet(m, ["feet"]);
27+
m.root.updateMatrixWorld(true);
28+
const n = soleNormal(m, "left");
29+
// Leveling makes the sole face world-down (dot ~ 1), flatter than the tilt.
30+
expect(n.dot(new THREE.Vector3(0, -1, 0))).toBeGreaterThan(0.98);
31+
expect(n.dot(new THREE.Vector3(0, -1, 0))).toBeGreaterThan(tiltedDot);
32+
});
33+
34+
it("leaves an authored-plantarflex foot on its toes", () => {
35+
const m = buildMannequin();
36+
m.bones.get("ankle_left")!.rotation.x = 30 * DEG; // plantarflex (toe-down)
37+
m.root.updateMatrixWorld(true);
38+
const before = m.bones.get("ankle_left")!.quaternion.clone();
39+
levelPlantedFeet(m, ["feet"]);
40+
expect(m.bones.get("ankle_left")!.quaternion.angleTo(before)).toBeLessThan(1e-6);
41+
});
42+
43+
it("does not touch a swing foot lifted off the floor", () => {
44+
const m = buildMannequin();
45+
// Lift the foot well above the floor by bending the knee back and raising hip.
46+
m.bones.get("hip_left")!.rotation.x = -60 * DEG;
47+
m.root.position.y = 0.5;
48+
m.root.updateMatrixWorld(true);
49+
const before = m.bones.get("ankle_left")!.quaternion.clone();
50+
levelPlantedFeet(m, ["feet"]);
51+
expect(m.bones.get("ankle_left")!.quaternion.angleTo(before)).toBeLessThan(1e-3);
52+
});
53+
});

0 commit comments

Comments
 (0)