Skip to content

Commit 71204b3

Browse files
committed
Give the mannequin a face so facing and neck turns are readable
The head was a bare sphere, so neck rotations, turns, and any movement where front-vs-back matters (neck-rotation demo, pirouettes, quarter turns) were unreadable — the figure had no visible facing. Add a minimal low-poly face on the head's front (+Z): a nose wedge and two darker eye studs that poke just past the head surface, parented to the head bone so they follow neck/head rotation. Test asserts the face group exists and every element sits on the front side of the head. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0164eTtmmq6U8GQkAEmQsvV5
1 parent 19d2b2c commit 71204b3

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

packages/movit-render/src/mannequin.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export function buildMannequin(material?: THREE.Material): Mannequin {
103103

104104
// Head sphere + small hand/foot caps for readability.
105105
addBall(bones.get("head")!, 0.12, mat);
106+
addFace(bones.get("head")!);
106107
addBall(bones.get("wrist_left")!, 0.05, mat);
107108
addBall(bones.get("wrist_right")!, 0.05, mat);
108109
addFoot(bones.get("ankle_left")!, mat);
@@ -139,6 +140,32 @@ function addBall(bone: THREE.Object3D, diameter: number, mat: THREE.Material): v
139140
bone.add(new THREE.Mesh(geo, mat));
140141
}
141142

143+
/**
144+
* A minimal face — nose wedge + two eye studs — on the head's front (+Z).
145+
* The bare sphere hid which way the figure faces, making neck rotations and
146+
* turns unreadable; darker studs poke just past the head surface so facing
147+
* reads at a glance from any camera angle.
148+
*/
149+
function addFace(head: THREE.Object3D): void {
150+
const mat = new THREE.MeshStandardMaterial({ color: 0x39424e, roughness: 0.6 });
151+
const face = new THREE.Group();
152+
face.name = "face";
153+
154+
// Head ball radius is 0.06 (see addBall(head, 0.12)).
155+
const nose = new THREE.Mesh(new THREE.ConeGeometry(0.013, 0.035, 10), mat);
156+
nose.rotation.x = Math.PI / 2; // cone +Y → +Z
157+
nose.position.set(0, -0.004, 0.064);
158+
face.add(nose);
159+
160+
for (const sx of [-1, 1]) {
161+
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.0095, 10, 8), mat);
162+
eye.position.set(sx * 0.024, 0.016, 0.054);
163+
face.add(eye);
164+
}
165+
166+
head.add(face);
167+
}
168+
142169
function addFoot(bone: THREE.Object3D, mat: THREE.Material): void {
143170
const geo = new THREE.BoxGeometry(0.07, 0.04, 0.16);
144171
const mesh = new THREE.Mesh(geo, mat);

packages/movit-render/test/render.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,3 +494,16 @@ describe("frontal plane direction", () => {
494494
}
495495
});
496496
});
497+
498+
describe("face marker", () => {
499+
it("marks the head's front (+Z) so facing and neck turns are readable", () => {
500+
const m = buildMannequin();
501+
const face = m.bones.get("head")!.getObjectByName("face");
502+
expect(face).toBeTruthy();
503+
expect(face!.children.length).toBeGreaterThanOrEqual(3); // nose + two eyes
504+
// Every face element sits on the figure's front side of the head ball.
505+
for (const part of face!.children) {
506+
expect(part.position.z).toBeGreaterThan(0.04);
507+
}
508+
});
509+
});

0 commit comments

Comments
 (0)