@@ -71,6 +71,8 @@ export interface Character {
7171 proportions : Proportions ;
7272 /** Copy the driver's current pose onto the character skeleton. */
7373 sync ( driver : Mannequin ) : void ;
74+ /** Restore solved terminal contacts after a mocap layer has overwritten them. */
75+ correctContacts ( driver : Mannequin , boneIds : readonly string [ ] ) : void ;
7476 /**
7577 * The character's first skinned mesh, the retarget target for mocap clips
7678 * (see clips.ts). Null on bare skeletons, which then can't play clips.
@@ -283,6 +285,7 @@ export function rigCharacter(charScene: THREE.Object3D): Character {
283285 // ---- Capture rest state for the per-frame retarget. ----
284286 const mapped : MappedBone [ ] = [ ] ;
285287 const mappedByNode = new Map < THREE . Object3D , MappedBone > ( ) ;
288+ const mappedById = new Map < string , MappedBone > ( ) ;
286289 for ( const [ driverId ] of Object . entries ( BONE_MAP ) ) {
287290 const node = bone ( driverId ) ;
288291 const mb : MappedBone = {
@@ -293,6 +296,7 @@ export function rigCharacter(charScene: THREE.Object3D): Character {
293296 } ;
294297 mapped . push ( mb ) ;
295298 mappedByNode . set ( node , mb ) ;
299+ mappedById . set ( driverId , mb ) ;
296300 }
297301 // Distal phalanges: capture rest locals + the curl axis expressed in each
298302 // phalanx's rest-local frame (the driver curls fingers as a single bone; the
@@ -379,6 +383,34 @@ export function rigCharacter(charScene: THREE.Object3D): Character {
379383 group . updateMatrixWorld ( true ) ;
380384 }
381385
386+ function correctContacts ( driver : Mannequin , boneIds : readonly string [ ] ) : void {
387+ const ids = [ ...new Set ( boneIds ) ] . filter ( ( id ) => mappedById . has ( id ) && driver . bones . has ( id ) ) ;
388+ if ( ids . length === 0 ) return ;
389+
390+ group . updateMatrixWorld ( true ) ;
391+ const delta = new THREE . Vector3 ( ) ;
392+ const driverPos = new THREE . Vector3 ( ) ;
393+ const charPos = new THREE . Vector3 ( ) ;
394+ for ( const id of ids ) {
395+ driver . bones . get ( id ) ! . getWorldPosition ( driverPos ) ;
396+ mappedById . get ( id ) ! . node . getWorldPosition ( charPos ) ;
397+ delta . add ( driverPos ) . sub ( charPos ) ;
398+ }
399+ group . position . add ( delta . multiplyScalar ( 1 / ids . length ) ) ;
400+ group . updateMatrixWorld ( true ) ;
401+
402+ for ( const id of ids ) {
403+ const mb = mappedById . get ( id ) ! ;
404+ if ( ! mb . node . parent ) continue ;
405+ driver . bones . get ( id ) ! . getWorldQuaternion ( TMP_Q ) ;
406+ const desiredWorld = TMP_Q2 . copy ( TMP_Q ) . multiply ( mb . restWorld ) ;
407+ mb . node . parent . getWorldQuaternion ( TMP_Q ) ;
408+ mb . node . quaternion . copy ( TMP_Q . invert ( ) . multiply ( desiredWorld ) ) ;
409+ mb . node . updateMatrixWorld ( true ) ;
410+ }
411+ group . updateMatrixWorld ( true ) ;
412+ }
413+
382414 // Surface for the optional mocap-clip layer (clips.ts): the retarget target
383415 // mesh and the set of bones sync() rewrites each frame.
384416 let skinnedMesh : THREE . SkinnedMesh | null = null ;
@@ -394,6 +426,7 @@ export function rigCharacter(charScene: THREE.Object3D): Character {
394426 group,
395427 proportions,
396428 sync,
429+ correctContacts,
397430 skinnedMesh,
398431 drivenNodes,
399432 dispose ( ) {
0 commit comments