1010
1111import * as THREE from "three" ;
1212import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" ;
13+ import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js" ;
1314import { eulerRomFor } from "posecode-parser" ;
1415import type { PosecodeIR , ReachTarget , PinTarget } from "posecode-parser" ;
1516import { 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