Skip to content

Commit 3a1f37a

Browse files
Merge pull request #19 from posecode-dev/feat/humanized-figure
2 parents 90d33f5 + a3a6f9c commit 3a1f37a

6 files changed

Lines changed: 282 additions & 64 deletions

File tree

packages/posecode-render/src/index.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import * as THREE from "three";
1212
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
13+
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
1314
import { eulerRomFor } from "posecode-parser";
1415
import type { PosecodeIR, ReachTarget, PinTarget } from "posecode-parser";
1516
import { buildMannequin, type Mannequin } from "./mannequin.js";
@@ -77,6 +78,13 @@ export function createViewer(
7778
scene.background = new THREE.Color(0x0c0f15);
7879
scene.fog = new THREE.Fog(0x0c0f15, 9, 18);
7980

81+
// Image-based environment light: soft bounced light that gives the matte
82+
// figure materials realistic shading gradients instead of flat CG plastic.
83+
const pmrem = new THREE.PMREMGenerator(renderer);
84+
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
85+
scene.environmentIntensity = 0.35;
86+
pmrem.dispose();
87+
8088
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
8189
camera.position.set(2.6, 1.6, 3.4);
8290

@@ -91,7 +99,8 @@ export function createViewer(
9199
controls.autoRotateSpeed = 0.5;
92100

93101
// --- Studio lighting ---
94-
const hemi = new THREE.HemisphereLight(0xdfe9ff, 0x20242c, 0.85);
102+
// Trimmed from 0.85 to keep exposure level after adding the environment map.
103+
const hemi = new THREE.HemisphereLight(0xdfe9ff, 0x20242c, 0.6);
95104
scene.add(hemi);
96105

97106
const key = new THREE.DirectionalLight(0xfff4e6, 2.0);
@@ -135,6 +144,38 @@ export function createViewer(
135144
enableShadows(mannequin.root);
136145
scene.add(mannequin.root);
137146

147+
// --- Life layer: breathing + blinking so the figure reads as alive even
148+
// when the movement is paused. Breathing is a tiny additive sagittal
149+
// rotation layered onto the sampled pose each frame; it can never drift
150+
// because timeline.sample() rewrites those quaternions every frame.
151+
const BREATH_PERIOD = 3.8; // seconds per breath cycle
152+
const BLINK_DURATION = 0.13;
153+
const LIFE_AXIS = new THREE.Vector3(1, 0, 0);
154+
const LIFE_Q = new THREE.Quaternion();
155+
const eyes = ["eye_left", "eye_right"]
156+
.map((n) => mannequin.root.getObjectByName(n))
157+
.filter((o): o is THREE.Object3D => Boolean(o));
158+
let nextBlink = performance.now() / 1000 + 2;
159+
160+
function breatheBone(boneId: string, angle: number): void {
161+
const bone = mannequin.bones.get(boneId);
162+
if (!bone) return;
163+
LIFE_Q.setFromAxisAngle(LIFE_AXIS, angle);
164+
bone.quaternion.multiply(LIFE_Q);
165+
}
166+
167+
function applyLife(nowSec: number): void {
168+
const breath = Math.sin((nowSec * Math.PI * 2) / BREATH_PERIOD);
169+
breatheBone("chest", breath * 0.022);
170+
breatheBone("spine", breath * 0.012);
171+
breatheBone("neck", breath * -0.014); // counter-rotate: head stays level
172+
if (nowSec >= nextBlink + BLINK_DURATION) {
173+
nextBlink = nowSec + 2.5 + Math.random() * 3;
174+
}
175+
const closed = nowSec >= nextBlink && nowSec < nextBlink + BLINK_DURATION;
176+
for (const eye of eyes) eye.scale.y = closed ? 0.12 : 1;
177+
}
178+
138179
let timeline: BuiltTimeline | null = null;
139180
let groundTargets = new Map<string, THREE.Vector3>();
140181
// World-space anchor points contributed by scene props (chair seat, bar grip,
@@ -347,6 +388,9 @@ export function createViewer(
347388
function frame(): void {
348389
if (timeline) {
349390
const info = timeline.sample(time, mannequin.bones);
391+
// Life layer rides on wall-clock time (not timeline time) so the figure
392+
// keeps breathing and blinking while paused or scrubbing.
393+
applyLife(performance.now() / 1000);
350394
// Recompute root contact from the grounded base each frame (no drift).
351395
mannequin.root.position.copy(baseRootPos);
352396
mannequin.root.quaternion.copy(baseRootQuat);

0 commit comments

Comments
 (0)