|
1 | 1 | /** Semantic contact-orientation helpers shared by viewer and eval. */ |
2 | 2 | import * as THREE from "three"; |
3 | | -import type { PinTarget, ReachTarget } from "posecode-parser"; |
| 3 | +import { eulerRomFor, type PinTarget, type ReachTarget } from "posecode-parser"; |
4 | 4 | import type { Mannequin } from "./mannequin.js"; |
5 | 5 |
|
6 | 6 | const DOWN = new THREE.Vector3(0, -1, 0); |
| 7 | +const DEG = Math.PI / 180; |
7 | 8 |
|
8 | 9 | /** Rotate contacting wrists so the palm face normal points into the floor. */ |
9 | 10 | export function alignFloorPalms( |
@@ -35,3 +36,70 @@ export function alignFloorPalms( |
35 | 36 | } |
36 | 37 | if (sides.size > 0) m.root.updateMatrixWorld(true); |
37 | 38 | } |
| 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 | +} |
0 commit comments