@sage-rsc/talking-head-react 1.4.3 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +7 -1
- package/dist/index.js +3138 -2424
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +3701 -3001
- package/src/lib/talkinghead.mjs.new +25 -749
- package/scripts/merge-talkinghead.js +0 -108
|
@@ -31,25 +31,8 @@ import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
|
31
31
|
import Stats from 'three/addons/libs/stats.module.js';
|
|
32
32
|
|
|
33
33
|
import{ DynamicBones } from './dynamicbones.mjs';
|
|
34
|
-
import { AudioAnalyzer } from './audioAnalyzer.js';
|
|
35
34
|
const workletUrl = new URL('./playback-worklet.js', import.meta.url);
|
|
36
35
|
|
|
37
|
-
// Import lipsync modules statically to ensure they're bundled
|
|
38
|
-
import * as LipsyncEn from './lipsync-en.mjs';
|
|
39
|
-
import * as LipsyncDe from './lipsync-de.mjs';
|
|
40
|
-
import * as LipsyncFr from './lipsync-fr.mjs';
|
|
41
|
-
import * as LipsyncFi from './lipsync-fi.mjs';
|
|
42
|
-
import * as LipsyncLt from './lipsync-lt.mjs';
|
|
43
|
-
|
|
44
|
-
// Lipsync module map for dynamic access
|
|
45
|
-
const LIPSYNC_MODULES = {
|
|
46
|
-
en: LipsyncEn,
|
|
47
|
-
de: LipsyncDe,
|
|
48
|
-
fr: LipsyncFr,
|
|
49
|
-
fi: LipsyncFi,
|
|
50
|
-
lt: LipsyncLt
|
|
51
|
-
};
|
|
52
|
-
|
|
53
36
|
// Temporary objects for animation loop
|
|
54
37
|
const q = new THREE.Quaternion();
|
|
55
38
|
const e = new THREE.Euler();
|
|
@@ -146,8 +129,8 @@ class TalkingHead {
|
|
|
146
129
|
ttsVoice: "fi-FI-Standard-A",
|
|
147
130
|
ttsRate: 1,
|
|
148
131
|
ttsPitch: 0,
|
|
149
|
-
ttsVolume: 0
|
|
150
|
-
mixerGainSpeech:
|
|
132
|
+
ttsVolume: 0,
|
|
133
|
+
mixerGainSpeech: null,
|
|
151
134
|
mixerGainBackground: null,
|
|
152
135
|
lipsyncLang: 'fi',
|
|
153
136
|
lipsyncModules: ['fi','en','lt'],
|
|
@@ -168,9 +151,9 @@ class TalkingHead {
|
|
|
168
151
|
cameraPanEnable: false,
|
|
169
152
|
cameraZoomEnable: false,
|
|
170
153
|
lightAmbientColor: 0xffffff,
|
|
171
|
-
lightAmbientIntensity:
|
|
154
|
+
lightAmbientIntensity: 2,
|
|
172
155
|
lightDirectColor: 0x8888aa,
|
|
173
|
-
lightDirectIntensity:
|
|
156
|
+
lightDirectIntensity: 30,
|
|
174
157
|
lightDirectPhi: 1,
|
|
175
158
|
lightDirectTheta: 2,
|
|
176
159
|
lightSpotIntensity: 0,
|
|
@@ -180,9 +163,9 @@ class TalkingHead {
|
|
|
180
163
|
lightSpotDispersion: 1,
|
|
181
164
|
avatarMood: "neutral",
|
|
182
165
|
avatarMute: false,
|
|
183
|
-
avatarIdleEyeContact: 0.
|
|
166
|
+
avatarIdleEyeContact: 0.2,
|
|
184
167
|
avatarIdleHeadMove: 0.5,
|
|
185
|
-
avatarSpeakingEyeContact: 0.
|
|
168
|
+
avatarSpeakingEyeContact: 0.5,
|
|
186
169
|
avatarSpeakingHeadMove: 0.5,
|
|
187
170
|
avatarIgnoreCamera: false,
|
|
188
171
|
listeningSilenceThresholdLevel: 40,
|
|
@@ -425,20 +408,16 @@ class TalkingHead {
|
|
|
425
408
|
|
|
426
409
|
this.animMoods = {
|
|
427
410
|
'neutral' : {
|
|
428
|
-
baseline: { eyesLookDown: 0 },
|
|
411
|
+
baseline: { eyesLookDown: 0.1 },
|
|
429
412
|
speech: { deltaRate: 0, deltaPitch: 0, deltaVolume: 0 },
|
|
430
413
|
anims: [
|
|
431
414
|
{ name: 'breathing', delay: 1500, dt: [ 1200,500,1000 ], vs: { chestInhale: [0.5,0.5,0] } },
|
|
432
415
|
{ name: 'pose', alt: [
|
|
433
|
-
{ p: 0.5, delay: [5000,30000], vs: { pose: ['side'] },
|
|
434
|
-
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
435
|
-
},
|
|
416
|
+
{ p: 0.5, delay: [5000,30000], vs: { pose: ['side'] } },
|
|
436
417
|
{ p: 0.3, delay: [5000,30000], vs: { pose: ['hip'] },
|
|
437
418
|
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
438
419
|
},
|
|
439
|
-
{ delay: [5000,30000], vs: { pose: ['straight'] }
|
|
440
|
-
'M': { delay: [5000,30000], vs: { pose: ['wide'] } }
|
|
441
|
-
}
|
|
420
|
+
{ delay: [5000,30000], vs: { pose: ['straight'] } }
|
|
442
421
|
]},
|
|
443
422
|
{ name: 'head',
|
|
444
423
|
idle: { delay: [0,1000], dt: [ [200,5000] ], vs: { bodyRotateX: [[-0.04,0.10]], bodyRotateY: [[-0.3,0.3]], bodyRotateZ: [[-0.08,0.08]] } },
|
|
@@ -1237,16 +1216,6 @@ class TalkingHead {
|
|
|
1237
1216
|
|
|
1238
1217
|
this.stop();
|
|
1239
1218
|
this.avatar = avatar;
|
|
1240
|
-
|
|
1241
|
-
// Initialize custom properties
|
|
1242
|
-
this.bodyMovement = avatar.bodyMovement || 'idle';
|
|
1243
|
-
this.movementIntensity = avatar.movementIntensity || 0.5;
|
|
1244
|
-
this.lockedPosition = null;
|
|
1245
|
-
this.originalPosition = null;
|
|
1246
|
-
this.positionWasLocked = false;
|
|
1247
|
-
|
|
1248
|
-
// Initialize FBX animation loader
|
|
1249
|
-
this.fbxAnimationLoader = null;
|
|
1250
1219
|
|
|
1251
1220
|
// Dispose Dynamic Bones
|
|
1252
1221
|
this.dynamicbones.dispose();
|
|
@@ -1420,24 +1389,6 @@ class TalkingHead {
|
|
|
1420
1389
|
// Set pose, view and start animation
|
|
1421
1390
|
if ( !this.viewName ) this.setView( this.opt.cameraView );
|
|
1422
1391
|
this.setMood( this.avatar.avatarMood || this.moodName || this.opt.avatarMood );
|
|
1423
|
-
|
|
1424
|
-
// Set initial gender-appropriate pose for male avatars to avoid feminine appearance
|
|
1425
|
-
// Do this BEFORE starting the animation system to prevent initial female pose
|
|
1426
|
-
if (this.avatar.body === 'M' && this.poseTemplates['wide']) {
|
|
1427
|
-
// Use setPoseFromTemplate which properly handles pose transitions without loops
|
|
1428
|
-
this.poseName = 'wide';
|
|
1429
|
-
this.setPoseFromTemplate(this.poseTemplates['wide'], 0); // 0ms = instant, no transition
|
|
1430
|
-
console.log('Set initial male-appropriate pose: wide');
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// Initialize FBX animation loader
|
|
1434
|
-
this.initializeFBXAnimationLoader();
|
|
1435
|
-
|
|
1436
|
-
// Apply body movement animation if set
|
|
1437
|
-
if (this.bodyMovement && this.bodyMovement !== 'idle') {
|
|
1438
|
-
this.applyBodyMovementAnimation();
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
1392
|
this.start();
|
|
1442
1393
|
|
|
1443
1394
|
}
|
|
@@ -1610,9 +1561,6 @@ class TalkingHead {
|
|
|
1610
1561
|
}
|
|
1611
1562
|
}
|
|
1612
1563
|
}
|
|
1613
|
-
|
|
1614
|
-
// Apply shoulder adjustment to lower shoulders
|
|
1615
|
-
this.applyShoulderAdjustment();
|
|
1616
1564
|
}
|
|
1617
1565
|
|
|
1618
1566
|
/**
|
|
@@ -1631,68 +1579,6 @@ class TalkingHead {
|
|
|
1631
1579
|
}
|
|
1632
1580
|
}
|
|
1633
1581
|
}
|
|
1634
|
-
|
|
1635
|
-
/**
|
|
1636
|
-
* Apply shoulder adjustment to lower shoulders to a more natural position
|
|
1637
|
-
* This is called from updatePoseBase for pose-based animations
|
|
1638
|
-
*/
|
|
1639
|
-
applyShoulderAdjustment() {
|
|
1640
|
-
// Shoulder adjustment: reduce X-axis rotation by ~0.6 radians (34 degrees) to lower shoulders to a relaxed position
|
|
1641
|
-
const shoulderAdjustment = -0.6; // Negative to lower shoulders (increased for more relaxed look)
|
|
1642
|
-
const tempEuler = new THREE.Euler();
|
|
1643
|
-
|
|
1644
|
-
// Adjust left shoulder
|
|
1645
|
-
if (this.poseAvatar.props['LeftShoulder.quaternion']) {
|
|
1646
|
-
const leftShoulder = this.poseAvatar.props['LeftShoulder.quaternion'];
|
|
1647
|
-
tempEuler.setFromQuaternion(leftShoulder, 'XYZ');
|
|
1648
|
-
tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
|
|
1649
|
-
leftShoulder.setFromEuler(tempEuler, 'XYZ');
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
// Adjust right shoulder
|
|
1653
|
-
if (this.poseAvatar.props['RightShoulder.quaternion']) {
|
|
1654
|
-
const rightShoulder = this.poseAvatar.props['RightShoulder.quaternion'];
|
|
1655
|
-
tempEuler.setFromQuaternion(rightShoulder, 'XYZ');
|
|
1656
|
-
tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
|
|
1657
|
-
rightShoulder.setFromEuler(tempEuler, 'XYZ');
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
/**
|
|
1662
|
-
* Apply shoulder adjustment directly to bone objects
|
|
1663
|
-
* This is called AFTER FBX animations update to ensure shoulders stay relaxed
|
|
1664
|
-
* regardless of what the animation sets
|
|
1665
|
-
*/
|
|
1666
|
-
applyShoulderAdjustmentToBones() {
|
|
1667
|
-
if (!this.armature) return;
|
|
1668
|
-
|
|
1669
|
-
// Shoulder adjustment: reduce X-axis rotation by ~0.6 radians (34 degrees) to lower shoulders
|
|
1670
|
-
const shoulderAdjustment = -0.6; // Negative to lower shoulders
|
|
1671
|
-
const tempEuler = new THREE.Euler();
|
|
1672
|
-
const tempQuaternion = new THREE.Quaternion();
|
|
1673
|
-
|
|
1674
|
-
// Get shoulder bones directly from armature
|
|
1675
|
-
const leftShoulderBone = this.armature.getObjectByName('LeftShoulder');
|
|
1676
|
-
const rightShoulderBone = this.armature.getObjectByName('RightShoulder');
|
|
1677
|
-
|
|
1678
|
-
// Adjust left shoulder bone directly
|
|
1679
|
-
if (leftShoulderBone && leftShoulderBone.quaternion) {
|
|
1680
|
-
tempEuler.setFromQuaternion(leftShoulderBone.quaternion, 'XYZ');
|
|
1681
|
-
tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
|
|
1682
|
-
tempQuaternion.setFromEuler(tempEuler, 'XYZ');
|
|
1683
|
-
leftShoulderBone.quaternion.copy(tempQuaternion);
|
|
1684
|
-
leftShoulderBone.updateMatrixWorld(true);
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// Adjust right shoulder bone directly
|
|
1688
|
-
if (rightShoulderBone && rightShoulderBone.quaternion) {
|
|
1689
|
-
tempEuler.setFromQuaternion(rightShoulderBone.quaternion, 'XYZ');
|
|
1690
|
-
tempEuler.x += shoulderAdjustment; // Reduce X rotation to lower shoulder
|
|
1691
|
-
tempQuaternion.setFromEuler(tempEuler, 'XYZ');
|
|
1692
|
-
rightShoulderBone.quaternion.copy(tempQuaternion);
|
|
1693
|
-
rightShoulderBone.updateMatrixWorld(true);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
1582
|
|
|
1697
1583
|
/**
|
|
1698
1584
|
* Update morph target values.
|
|
@@ -2466,9 +2352,6 @@ class TalkingHead {
|
|
|
2466
2352
|
dt = dt / this.animSlowdownRate;
|
|
2467
2353
|
this.animClock += dt;
|
|
2468
2354
|
|
|
2469
|
-
// Maintain locked position if set
|
|
2470
|
-
this.maintainLockedPosition();
|
|
2471
|
-
|
|
2472
2355
|
let i,j,l,k,vol=0;
|
|
2473
2356
|
|
|
2474
2357
|
// Statistics start
|
|
@@ -2787,15 +2670,6 @@ class TalkingHead {
|
|
|
2787
2670
|
|
|
2788
2671
|
// Update Dynamic Bones
|
|
2789
2672
|
this.dynamicbones.update(dt);
|
|
2790
|
-
|
|
2791
|
-
// Update FBX animations
|
|
2792
|
-
if (this.fbxAnimationLoader) {
|
|
2793
|
-
this.fbxAnimationLoader.update();
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
// Apply shoulder adjustment AFTER FBX animations to ensure relaxed shoulders
|
|
2797
|
-
// This overrides any shoulder positions set by animations
|
|
2798
|
-
this.applyShoulderAdjustmentToBones();
|
|
2799
2673
|
|
|
2800
2674
|
// Custom update
|
|
2801
2675
|
if ( this.opt.update ) {
|
|
@@ -4415,20 +4289,9 @@ class TalkingHead {
|
|
|
4415
4289
|
* @param {number} [ndx=0] Index of the clip
|
|
4416
4290
|
* @param {number} [scale=0.01] Position scale factor
|
|
4417
4291
|
*/
|
|
4418
|
-
async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01
|
|
4292
|
+
async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01) {
|
|
4419
4293
|
if ( !this.armature ) return;
|
|
4420
4294
|
|
|
4421
|
-
// Track whether position was locked for this animation
|
|
4422
|
-
this.positionWasLocked = !disablePositionLock;
|
|
4423
|
-
|
|
4424
|
-
// Lock position IMMEDIATELY to prevent any position changes (unless disabled)
|
|
4425
|
-
if (!disablePositionLock) {
|
|
4426
|
-
this.lockAvatarPosition();
|
|
4427
|
-
console.log('Position locked immediately before FBX animation:', url);
|
|
4428
|
-
} else {
|
|
4429
|
-
console.log('Position locking disabled for FBX animation:', url);
|
|
4430
|
-
}
|
|
4431
|
-
|
|
4432
4295
|
let item = this.animClips.find( x => x.url === url+'-'+ndx );
|
|
4433
4296
|
if ( item ) {
|
|
4434
4297
|
|
|
@@ -4446,40 +4309,27 @@ class TalkingHead {
|
|
|
4446
4309
|
this.poseTarget.props[x[0]].d = 1000;
|
|
4447
4310
|
});
|
|
4448
4311
|
|
|
4449
|
-
//
|
|
4450
|
-
if (
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4312
|
+
// Create a new mixer
|
|
4313
|
+
if (this.mixer) {
|
|
4314
|
+
this.mixer.removeEventListener('finished', this._mixerHandler);
|
|
4315
|
+
this.mixer.stopAllAction();
|
|
4316
|
+
this.mixer.uncacheRoot(this.armature);
|
|
4317
|
+
this.mixer = null;
|
|
4318
|
+
this._mixerHandler = null;
|
|
4455
4319
|
}
|
|
4456
|
-
this.mixer
|
|
4320
|
+
this.mixer = new THREE.AnimationMixer(this.armature);
|
|
4321
|
+
this._mixerHandler = () => {
|
|
4322
|
+
this.stopAnimation();
|
|
4323
|
+
this.mixer?.removeEventListener('finished', this._mixerHandler);
|
|
4324
|
+
};
|
|
4325
|
+
this.mixer.addEventListener('finished', this._mixerHandler);
|
|
4457
4326
|
|
|
4458
|
-
// Play action
|
|
4327
|
+
// Play action
|
|
4459
4328
|
const repeat = Math.ceil(dur / item.clip.duration);
|
|
4460
4329
|
const action = this.mixer.clipAction(item.clip);
|
|
4461
4330
|
action.setLoop( THREE.LoopRepeat, repeat );
|
|
4462
4331
|
action.clampWhenFinished = true;
|
|
4463
|
-
|
|
4464
|
-
// Store the current FBX action for proper cleanup
|
|
4465
|
-
this.currentFBXAction = action;
|
|
4466
|
-
|
|
4467
|
-
try {
|
|
4468
4332
|
action.fadeIn(0.5).play();
|
|
4469
|
-
console.log('FBX animation started successfully:', url);
|
|
4470
|
-
} catch (error) {
|
|
4471
|
-
console.warn('FBX animation failed to start:', error);
|
|
4472
|
-
// Stop the animation and unlock position on error
|
|
4473
|
-
this.stopAnimation();
|
|
4474
|
-
return;
|
|
4475
|
-
}
|
|
4476
|
-
|
|
4477
|
-
// Check if the animation actually has valid tracks
|
|
4478
|
-
if (action.getClip().tracks.length === 0) {
|
|
4479
|
-
console.warn('FBX animation has no valid tracks, stopping');
|
|
4480
|
-
this.stopAnimation();
|
|
4481
|
-
return;
|
|
4482
|
-
}
|
|
4483
4333
|
|
|
4484
4334
|
} else {
|
|
4485
4335
|
|
|
@@ -4491,200 +4341,10 @@ class TalkingHead {
|
|
|
4491
4341
|
if ( fbx && fbx.animations && fbx.animations[ndx] ) {
|
|
4492
4342
|
let anim = fbx.animations[ndx];
|
|
4493
4343
|
|
|
4494
|
-
// Get available bone names from avatar skeleton for mapping
|
|
4495
|
-
const availableBones = new Set();
|
|
4496
|
-
if (this.armature) {
|
|
4497
|
-
this.armature.traverse((child) => {
|
|
4498
|
-
if (child.isBone || child.type === 'Bone') {
|
|
4499
|
-
availableBones.add(child.name);
|
|
4500
|
-
}
|
|
4501
|
-
});
|
|
4502
|
-
}
|
|
4503
|
-
|
|
4504
|
-
// Map bone names from FBX to avatar skeleton
|
|
4505
|
-
const boneNameMap = new Map();
|
|
4506
|
-
const mapBoneName = (fbxBoneName) => {
|
|
4507
|
-
// Direct match
|
|
4508
|
-
if (availableBones.has(fbxBoneName)) {
|
|
4509
|
-
return fbxBoneName;
|
|
4510
|
-
}
|
|
4511
|
-
|
|
4512
|
-
// Remove common prefixes (mixamorig, CC_Base_, etc.)
|
|
4513
|
-
let normalized = fbxBoneName
|
|
4514
|
-
.replace(/^mixamorig/i, '')
|
|
4515
|
-
.replace(/^CC_Base_/i, '')
|
|
4516
|
-
.replace(/^RPM_/i, '');
|
|
4517
|
-
|
|
4518
|
-
if (availableBones.has(normalized)) {
|
|
4519
|
-
return normalized;
|
|
4520
|
-
}
|
|
4521
|
-
|
|
4522
|
-
// Pattern-based matching for Ready Player Me / Mixamo
|
|
4523
|
-
const lowerNormalized = normalized.toLowerCase();
|
|
4524
|
-
|
|
4525
|
-
// Arm bones - pattern matching
|
|
4526
|
-
if (lowerNormalized.includes('left') && lowerNormalized.includes('arm')) {
|
|
4527
|
-
if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
|
|
4528
|
-
if (availableBones.has('LeftForeArm')) return 'LeftForeArm';
|
|
4529
|
-
if (availableBones.has('LeftForearm')) return 'LeftForearm';
|
|
4530
|
-
} else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
|
|
4531
|
-
if (availableBones.has('LeftArm')) return 'LeftArm';
|
|
4532
|
-
}
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
|
-
if (lowerNormalized.includes('right') && lowerNormalized.includes('arm')) {
|
|
4536
|
-
if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
|
|
4537
|
-
if (availableBones.has('RightForeArm')) return 'RightForeArm';
|
|
4538
|
-
if (availableBones.has('RightForearm')) return 'RightForearm';
|
|
4539
|
-
} else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
|
|
4540
|
-
if (availableBones.has('RightArm')) return 'RightArm';
|
|
4541
|
-
}
|
|
4542
|
-
}
|
|
4543
|
-
|
|
4544
|
-
// Hand bones
|
|
4545
|
-
if (lowerNormalized.includes('left') && lowerNormalized.includes('hand') &&
|
|
4546
|
-
!lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
|
|
4547
|
-
!lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
|
|
4548
|
-
!lowerNormalized.includes('pinky')) {
|
|
4549
|
-
if (availableBones.has('LeftHand')) return 'LeftHand';
|
|
4550
|
-
}
|
|
4551
|
-
|
|
4552
|
-
if (lowerNormalized.includes('right') && lowerNormalized.includes('hand') &&
|
|
4553
|
-
!lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
|
|
4554
|
-
!lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
|
|
4555
|
-
!lowerNormalized.includes('pinky')) {
|
|
4556
|
-
if (availableBones.has('RightHand')) return 'RightHand';
|
|
4557
|
-
}
|
|
4558
|
-
|
|
4559
|
-
// Shoulder bones
|
|
4560
|
-
if (lowerNormalized.includes('left') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
|
|
4561
|
-
if (availableBones.has('LeftShoulder')) return 'LeftShoulder';
|
|
4562
|
-
}
|
|
4563
|
-
|
|
4564
|
-
if (lowerNormalized.includes('right') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
|
|
4565
|
-
if (availableBones.has('RightShoulder')) return 'RightShoulder';
|
|
4566
|
-
}
|
|
4567
|
-
|
|
4568
|
-
// Common bone name mappings
|
|
4569
|
-
const mappings = {
|
|
4570
|
-
// Arm bones - exact matches
|
|
4571
|
-
'LeftArm': 'LeftArm',
|
|
4572
|
-
'leftArm': 'LeftArm',
|
|
4573
|
-
'LEFTARM': 'LeftArm',
|
|
4574
|
-
'RightArm': 'RightArm',
|
|
4575
|
-
'rightArm': 'RightArm',
|
|
4576
|
-
'RIGHTARM': 'RightArm',
|
|
4577
|
-
'LeftForeArm': 'LeftForeArm',
|
|
4578
|
-
'leftForeArm': 'LeftForeArm',
|
|
4579
|
-
'leftForearm': 'LeftForeArm',
|
|
4580
|
-
'LeftForearm': 'LeftForeArm',
|
|
4581
|
-
'RightForeArm': 'RightForeArm',
|
|
4582
|
-
'rightForeArm': 'RightForeArm',
|
|
4583
|
-
'rightForearm': 'RightForeArm',
|
|
4584
|
-
'RightForearm': 'RightForeArm',
|
|
4585
|
-
'LeftHand': 'LeftHand',
|
|
4586
|
-
'leftHand': 'LeftHand',
|
|
4587
|
-
'RightHand': 'RightHand',
|
|
4588
|
-
'rightHand': 'RightHand',
|
|
4589
|
-
'LeftShoulder': 'LeftShoulder',
|
|
4590
|
-
'leftShoulder': 'LeftShoulder',
|
|
4591
|
-
'RightShoulder': 'RightShoulder',
|
|
4592
|
-
'rightShoulder': 'RightShoulder',
|
|
4593
|
-
// Spine
|
|
4594
|
-
'Spine': 'Spine1',
|
|
4595
|
-
'spine': 'Spine1',
|
|
4596
|
-
'Spine1': 'Spine1',
|
|
4597
|
-
'Spine2': 'Spine2',
|
|
4598
|
-
// Head/Neck
|
|
4599
|
-
'Head': 'Head',
|
|
4600
|
-
'head': 'Head',
|
|
4601
|
-
'Neck': 'Neck',
|
|
4602
|
-
'neck': 'Neck',
|
|
4603
|
-
// Hips
|
|
4604
|
-
'Hips': 'Hips',
|
|
4605
|
-
'hips': 'Hips',
|
|
4606
|
-
'Root': 'Hips',
|
|
4607
|
-
'root': 'Hips',
|
|
4608
|
-
};
|
|
4609
|
-
|
|
4610
|
-
if (mappings[normalized]) {
|
|
4611
|
-
const mapped = mappings[normalized];
|
|
4612
|
-
if (availableBones.has(mapped)) {
|
|
4613
|
-
return mapped;
|
|
4614
|
-
}
|
|
4615
|
-
}
|
|
4616
|
-
|
|
4617
|
-
// Try case-insensitive match
|
|
4618
|
-
for (const boneName of availableBones) {
|
|
4619
|
-
if (boneName.toLowerCase() === lowerNormalized) {
|
|
4620
|
-
return boneName;
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
|
|
4624
|
-
// Try partial match (contains)
|
|
4625
|
-
for (const boneName of availableBones) {
|
|
4626
|
-
const boneLower = boneName.toLowerCase();
|
|
4627
|
-
// Match if normalized contains key parts of bone name
|
|
4628
|
-
if ((lowerNormalized.includes('left') && boneLower.includes('left')) ||
|
|
4629
|
-
(lowerNormalized.includes('right') && boneLower.includes('right'))) {
|
|
4630
|
-
if ((lowerNormalized.includes('arm') && boneLower.includes('arm') && !boneLower.includes('fore')) ||
|
|
4631
|
-
(lowerNormalized.includes('forearm') && boneLower.includes('forearm')) ||
|
|
4632
|
-
(lowerNormalized.includes('hand') && boneLower.includes('hand') && !boneLower.includes('index') && !boneLower.includes('thumb')) ||
|
|
4633
|
-
(lowerNormalized.includes('shoulder') && boneLower.includes('shoulder'))) {
|
|
4634
|
-
return boneName;
|
|
4635
|
-
}
|
|
4636
|
-
}
|
|
4637
|
-
}
|
|
4638
|
-
|
|
4639
|
-
return null; // No mapping found
|
|
4640
|
-
};
|
|
4641
|
-
|
|
4642
|
-
// Filter and map animation tracks
|
|
4643
|
-
const mappedTracks = [];
|
|
4644
|
-
const unmappedBones = new Set();
|
|
4645
|
-
anim.tracks.forEach(track => {
|
|
4646
|
-
// Remove mixamorig prefix first
|
|
4647
|
-
let trackName = track.name.replaceAll('mixamorig', '');
|
|
4648
|
-
const trackParts = trackName.split('.');
|
|
4649
|
-
const fbxBoneName = trackParts[0];
|
|
4650
|
-
const property = trackParts[1];
|
|
4651
|
-
|
|
4652
|
-
// Map bone name to avatar skeleton
|
|
4653
|
-
const mappedBoneName = mapBoneName(fbxBoneName);
|
|
4654
|
-
|
|
4655
|
-
if (mappedBoneName && property) {
|
|
4656
|
-
// Create new track with mapped bone name
|
|
4657
|
-
const newTrackName = `${mappedBoneName}.${property}`;
|
|
4658
|
-
const newTrack = track.clone();
|
|
4659
|
-
newTrack.name = newTrackName;
|
|
4660
|
-
mappedTracks.push(newTrack);
|
|
4661
|
-
|
|
4662
|
-
// Store mapping for logging
|
|
4663
|
-
if (fbxBoneName !== mappedBoneName) {
|
|
4664
|
-
boneNameMap.set(fbxBoneName, mappedBoneName);
|
|
4665
|
-
}
|
|
4666
|
-
} else {
|
|
4667
|
-
unmappedBones.add(fbxBoneName);
|
|
4668
|
-
}
|
|
4669
|
-
});
|
|
4670
|
-
|
|
4671
|
-
if (unmappedBones.size > 0) {
|
|
4672
|
-
console.warn(`⚠️ ${unmappedBones.size} bone(s) could not be mapped:`, Array.from(unmappedBones).sort().join(', '));
|
|
4673
|
-
}
|
|
4674
|
-
|
|
4675
|
-
// Use mapped tracks if we have any, otherwise use original
|
|
4676
|
-
if (mappedTracks.length > 0) {
|
|
4677
|
-
anim = new THREE.AnimationClip(anim.name, anim.duration, mappedTracks);
|
|
4678
|
-
console.log(`✓ Created animation with ${mappedTracks.length} mapped tracks (from ${anim.tracks.length} original tracks)`);
|
|
4679
|
-
if (boneNameMap.size > 0) {
|
|
4680
|
-
console.log(`✓ Mapped ${boneNameMap.size} bone(s):`,
|
|
4681
|
-
Array.from(boneNameMap.entries()).map(([from, to]) => `${from}→${to}`).join(', '));
|
|
4682
|
-
}
|
|
4683
|
-
}
|
|
4684
|
-
|
|
4685
4344
|
// Rename and scale Mixamo tracks, create a pose
|
|
4686
4345
|
const props = {};
|
|
4687
4346
|
anim.tracks.forEach( t => {
|
|
4347
|
+
t.name = t.name.replaceAll('mixamorig','');
|
|
4688
4348
|
const ids = t.name.split('.');
|
|
4689
4349
|
if ( ids[1] === 'position' ) {
|
|
4690
4350
|
for(let i=0; i<t.values.length; i++ ) {
|
|
@@ -4738,14 +4398,6 @@ class TalkingHead {
|
|
|
4738
4398
|
this._mixerHandler = null;
|
|
4739
4399
|
}
|
|
4740
4400
|
|
|
4741
|
-
// Unlock position if it was locked
|
|
4742
|
-
if (this.positionWasLocked) {
|
|
4743
|
-
this.unlockAvatarPosition();
|
|
4744
|
-
console.log('Position unlocked after FBX animation stopped');
|
|
4745
|
-
} else {
|
|
4746
|
-
console.log('Position was not locked, no unlock needed');
|
|
4747
|
-
}
|
|
4748
|
-
|
|
4749
4401
|
// Restart gesture
|
|
4750
4402
|
if ( this.gesture ) {
|
|
4751
4403
|
for( let [p,v] of Object.entries(this.gesture) ) {
|
|
@@ -5096,376 +4748,6 @@ class TalkingHead {
|
|
|
5096
4748
|
}
|
|
5097
4749
|
}
|
|
5098
4750
|
|
|
5099
|
-
/**
|
|
5100
|
-
* Initialize FBX animation loader
|
|
5101
|
-
*/
|
|
5102
|
-
async initializeFBXAnimationLoader() {
|
|
5103
|
-
try {
|
|
5104
|
-
// Dynamic import to avoid loading issues
|
|
5105
|
-
const { FBXAnimationLoader } = await import('./fbxAnimationLoader.js');
|
|
5106
|
-
this.fbxAnimationLoader = new FBXAnimationLoader(this.armature);
|
|
5107
|
-
console.log('FBX Animation Loader initialized');
|
|
5108
|
-
} catch (error) {
|
|
5109
|
-
console.warn('FBX Animation Loader not available:', error);
|
|
5110
|
-
this.fbxAnimationLoader = null;
|
|
5111
|
-
}
|
|
5112
|
-
}
|
|
5113
|
-
|
|
5114
|
-
/**
|
|
5115
|
-
* Set body movement type.
|
|
5116
|
-
* @param {string} movement Movement type (idle, walking, prancing, gesturing, dancing, excited).
|
|
5117
|
-
*/
|
|
5118
|
-
setBodyMovement(movement) {
|
|
5119
|
-
this.bodyMovement = movement;
|
|
5120
|
-
|
|
5121
|
-
// Only set avatar property if avatar exists
|
|
5122
|
-
if (this.avatar) {
|
|
5123
|
-
this.avatar.bodyMovement = movement;
|
|
5124
|
-
}
|
|
5125
|
-
|
|
5126
|
-
console.log('Body movement set to:', movement);
|
|
5127
|
-
|
|
5128
|
-
// Respect the current showFullAvatar setting instead of forcing it to true
|
|
5129
|
-
// Only unlock position when returning to idle
|
|
5130
|
-
if (movement === 'idle') {
|
|
5131
|
-
// Unlock position when returning to idle
|
|
5132
|
-
this.unlockAvatarPosition();
|
|
5133
|
-
}
|
|
5134
|
-
// Note: We no longer force showFullAvatar to true for body movements
|
|
5135
|
-
// The avatar will use whatever showFullAvatar value was set by the user
|
|
5136
|
-
|
|
5137
|
-
// Apply body movement animation
|
|
5138
|
-
this.applyBodyMovementAnimation();
|
|
5139
|
-
}
|
|
5140
|
-
|
|
5141
|
-
/**
|
|
5142
|
-
* Apply body movement animation based on current movement type.
|
|
5143
|
-
*/
|
|
5144
|
-
async applyBodyMovementAnimation() {
|
|
5145
|
-
// Check if avatar is ready
|
|
5146
|
-
if (!this.armature || !this.animQueue) {
|
|
5147
|
-
console.log('Avatar not ready for body movement animations');
|
|
5148
|
-
return;
|
|
5149
|
-
}
|
|
5150
|
-
|
|
5151
|
-
console.log('Avatar is running:', this.isRunning);
|
|
5152
|
-
console.log('Animation queue exists:', !!this.animQueue);
|
|
5153
|
-
|
|
5154
|
-
// Remove existing body movement animations
|
|
5155
|
-
const beforeLength = this.animQueue.length;
|
|
5156
|
-
this.animQueue = this.animQueue.filter(anim => !anim.template.name.startsWith('bodyMovement'));
|
|
5157
|
-
const afterLength = this.animQueue.length;
|
|
5158
|
-
console.log(`Filtered animation queue: ${beforeLength} -> ${afterLength} animations`);
|
|
5159
|
-
|
|
5160
|
-
if (this.bodyMovement === 'idle') {
|
|
5161
|
-
// Stop FBX animations if any
|
|
5162
|
-
if (this.fbxAnimationLoader) {
|
|
5163
|
-
this.fbxAnimationLoader.stopCurrentAnimation();
|
|
5164
|
-
}
|
|
5165
|
-
return; // No body movement for idle
|
|
5166
|
-
}
|
|
5167
|
-
|
|
5168
|
-
// Try to use FBX animations first
|
|
5169
|
-
if (this.fbxAnimationLoader) {
|
|
5170
|
-
try {
|
|
5171
|
-
await this.fbxAnimationLoader.playGestureAnimation(this.bodyMovement, this.movementIntensity);
|
|
5172
|
-
console.log('Applied FBX body movement animation:', this.bodyMovement);
|
|
5173
|
-
return; // Successfully applied FBX animation
|
|
5174
|
-
} catch (error) {
|
|
5175
|
-
console.warn('FBX animation failed, falling back to code animation:', error);
|
|
5176
|
-
}
|
|
5177
|
-
}
|
|
5178
|
-
|
|
5179
|
-
// Fallback to code-based animations
|
|
5180
|
-
const movementAnim = this.createBodyMovementAnimation(this.bodyMovement);
|
|
5181
|
-
console.log('Created movement animation:', movementAnim);
|
|
5182
|
-
if (movementAnim) {
|
|
5183
|
-
try {
|
|
5184
|
-
// Use animFactory to create proper animation object
|
|
5185
|
-
const animObj = this.animFactory(movementAnim, true); // true for looping
|
|
5186
|
-
|
|
5187
|
-
// Validate the animation object before adding
|
|
5188
|
-
if (animObj && animObj.ts && animObj.ts.length > 0) {
|
|
5189
|
-
this.animQueue.push(animObj);
|
|
5190
|
-
console.log('Applied code-based body movement animation:', this.bodyMovement);
|
|
5191
|
-
console.log('Animation queue length:', this.animQueue.length);
|
|
5192
|
-
console.log('Animation object:', animObj);
|
|
5193
|
-
} else {
|
|
5194
|
-
console.error('Invalid animation object created for:', this.bodyMovement);
|
|
5195
|
-
console.error('Animation object:', animObj);
|
|
5196
|
-
}
|
|
5197
|
-
} catch (error) {
|
|
5198
|
-
console.error('Error creating body movement animation:', error);
|
|
5199
|
-
}
|
|
5200
|
-
}
|
|
5201
|
-
}
|
|
5202
|
-
|
|
5203
|
-
/**
|
|
5204
|
-
* Lock avatar position to prevent movement during animations.
|
|
5205
|
-
*/
|
|
5206
|
-
lockAvatarPosition() {
|
|
5207
|
-
if (!this.armature) {
|
|
5208
|
-
console.warn('Cannot lock position: armature not available');
|
|
5209
|
-
return;
|
|
5210
|
-
}
|
|
5211
|
-
|
|
5212
|
-
// Store the original position if not already stored
|
|
5213
|
-
if (!this.originalPosition) {
|
|
5214
|
-
this.originalPosition = {
|
|
5215
|
-
x: this.armature.position.x,
|
|
5216
|
-
y: this.armature.position.y,
|
|
5217
|
-
z: this.armature.position.z
|
|
5218
|
-
};
|
|
5219
|
-
console.log('Original position stored:', this.originalPosition);
|
|
5220
|
-
}
|
|
5221
|
-
|
|
5222
|
-
// Lock the avatar at its CURRENT position (don't move it)
|
|
5223
|
-
this.lockedPosition = {
|
|
5224
|
-
x: this.armature.position.x,
|
|
5225
|
-
y: this.armature.position.y,
|
|
5226
|
-
z: this.armature.position.z
|
|
5227
|
-
};
|
|
5228
|
-
|
|
5229
|
-
console.log('Avatar position locked at current position:', this.lockedPosition);
|
|
5230
|
-
}
|
|
5231
|
-
|
|
5232
|
-
/**
|
|
5233
|
-
* Unlock avatar position and restore original position.
|
|
5234
|
-
*/
|
|
5235
|
-
unlockAvatarPosition() {
|
|
5236
|
-
if (this.armature && this.originalPosition) {
|
|
5237
|
-
// Restore avatar to its original position before locking
|
|
5238
|
-
this.armature.position.set(
|
|
5239
|
-
this.originalPosition.x,
|
|
5240
|
-
this.originalPosition.y,
|
|
5241
|
-
this.originalPosition.z
|
|
5242
|
-
);
|
|
5243
|
-
console.log('Avatar position restored to original:', this.originalPosition);
|
|
5244
|
-
} else if (this.armature) {
|
|
5245
|
-
// Fallback: reset to center if no original position was stored
|
|
5246
|
-
this.armature.position.set(0, 0, 0);
|
|
5247
|
-
console.log('Avatar position reset to center (0,0,0)');
|
|
5248
|
-
}
|
|
5249
|
-
this.lockedPosition = null;
|
|
5250
|
-
this.originalPosition = null; // Clear original position after unlock
|
|
5251
|
-
console.log('Avatar position unlocked');
|
|
5252
|
-
}
|
|
5253
|
-
|
|
5254
|
-
/**
|
|
5255
|
-
* Ensure avatar stays at locked position.
|
|
5256
|
-
*/
|
|
5257
|
-
maintainLockedPosition() {
|
|
5258
|
-
if (this.lockedPosition && this.armature) {
|
|
5259
|
-
// Enforce the locked position - keep avatar exactly where it was locked
|
|
5260
|
-
// This prevents FBX animations from moving the avatar
|
|
5261
|
-
this.armature.position.set(
|
|
5262
|
-
this.lockedPosition.x,
|
|
5263
|
-
this.lockedPosition.y,
|
|
5264
|
-
this.lockedPosition.z
|
|
5265
|
-
);
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
|
|
5269
|
-
/**
|
|
5270
|
-
* Create body movement animation.
|
|
5271
|
-
* @param {string} movementType Movement type.
|
|
5272
|
-
* @returns {Object} Animation object.
|
|
5273
|
-
*/
|
|
5274
|
-
createBodyMovementAnimation(movementType) {
|
|
5275
|
-
const intensity = this.movementIntensity || 0.5;
|
|
5276
|
-
|
|
5277
|
-
const movementAnimations = {
|
|
5278
|
-
walking: {
|
|
5279
|
-
name: 'bodyMovement_walking',
|
|
5280
|
-
delay: [500, 2000],
|
|
5281
|
-
dt: [800, 1200],
|
|
5282
|
-
vs: {
|
|
5283
|
-
bodyRotateY: [-0.1 * intensity, 0.1 * intensity, 0],
|
|
5284
|
-
bodyRotateZ: [-0.05 * intensity, 0.05 * intensity, 0],
|
|
5285
|
-
bodyRotateX: [-0.02 * intensity, 0.02 * intensity, 0]
|
|
5286
|
-
}
|
|
5287
|
-
},
|
|
5288
|
-
prancing: {
|
|
5289
|
-
name: 'bodyMovement_prancing',
|
|
5290
|
-
delay: [300, 1000],
|
|
5291
|
-
dt: [400, 800],
|
|
5292
|
-
vs: {
|
|
5293
|
-
bodyRotateY: [-0.15 * intensity, 0.15 * intensity, 0],
|
|
5294
|
-
bodyRotateZ: [-0.08 * intensity, 0.08 * intensity, 0],
|
|
5295
|
-
bodyRotateX: [-0.05 * intensity, 0.05 * intensity, 0]
|
|
5296
|
-
}
|
|
5297
|
-
},
|
|
5298
|
-
gesturing: {
|
|
5299
|
-
name: 'bodyMovement_gesturing',
|
|
5300
|
-
delay: [400, 1500],
|
|
5301
|
-
dt: [600, 1000],
|
|
5302
|
-
vs: {
|
|
5303
|
-
bodyRotateY: [-0.08 * intensity, 0.08 * intensity, 0],
|
|
5304
|
-
bodyRotateZ: [-0.03 * intensity, 0.03 * intensity, 0]
|
|
5305
|
-
}
|
|
5306
|
-
},
|
|
5307
|
-
dancing: {
|
|
5308
|
-
name: 'bodyMovement_dancing',
|
|
5309
|
-
delay: [200, 600],
|
|
5310
|
-
dt: [400, 800],
|
|
5311
|
-
vs: {
|
|
5312
|
-
bodyRotateY: [-0.25 * intensity, 0.25 * intensity, 0],
|
|
5313
|
-
bodyRotateZ: [-0.15 * intensity, 0.15 * intensity, 0],
|
|
5314
|
-
bodyRotateX: [-0.1 * intensity, 0.1 * intensity, 0]
|
|
5315
|
-
}
|
|
5316
|
-
},
|
|
5317
|
-
dancing2: {
|
|
5318
|
-
name: 'bodyMovement_dancing2',
|
|
5319
|
-
delay: [150, 500],
|
|
5320
|
-
dt: [300, 700],
|
|
5321
|
-
vs: {
|
|
5322
|
-
bodyRotateY: [-0.3 * intensity, 0.3 * intensity, 0],
|
|
5323
|
-
bodyRotateZ: [-0.2 * intensity, 0.2 * intensity, 0],
|
|
5324
|
-
bodyRotateX: [-0.12 * intensity, 0.12 * intensity, 0]
|
|
5325
|
-
}
|
|
5326
|
-
},
|
|
5327
|
-
dancing3: {
|
|
5328
|
-
name: 'bodyMovement_dancing3',
|
|
5329
|
-
delay: [100, 400],
|
|
5330
|
-
dt: [200, 600],
|
|
5331
|
-
vs: {
|
|
5332
|
-
bodyRotateY: [-0.35 * intensity, 0.35 * intensity, 0],
|
|
5333
|
-
bodyRotateZ: [-0.25 * intensity, 0.25 * intensity, 0],
|
|
5334
|
-
bodyRotateX: [-0.15 * intensity, 0.15 * intensity, 0]
|
|
5335
|
-
}
|
|
5336
|
-
},
|
|
5337
|
-
excited: {
|
|
5338
|
-
name: 'bodyMovement_excited',
|
|
5339
|
-
delay: [200, 600],
|
|
5340
|
-
dt: [300, 700],
|
|
5341
|
-
vs: {
|
|
5342
|
-
bodyRotateY: [-0.12 * intensity, 0.12 * intensity, 0],
|
|
5343
|
-
bodyRotateZ: [-0.06 * intensity, 0.06 * intensity, 0],
|
|
5344
|
-
bodyRotateX: [-0.04 * intensity, 0.04 * intensity, 0]
|
|
5345
|
-
}
|
|
5346
|
-
},
|
|
5347
|
-
happy: {
|
|
5348
|
-
name: 'bodyMovement_happy',
|
|
5349
|
-
delay: [300, 800],
|
|
5350
|
-
dt: [500, 1000],
|
|
5351
|
-
vs: {
|
|
5352
|
-
bodyRotateY: [-0.08 * intensity, 0.08 * intensity, 0],
|
|
5353
|
-
bodyRotateZ: [-0.04 * intensity, 0.04 * intensity, 0],
|
|
5354
|
-
bodyRotateX: [-0.02 * intensity, 0.02 * intensity, 0]
|
|
5355
|
-
}
|
|
5356
|
-
},
|
|
5357
|
-
surprised: {
|
|
5358
|
-
name: 'bodyMovement_surprised',
|
|
5359
|
-
delay: [100, 300],
|
|
5360
|
-
dt: [200, 500],
|
|
5361
|
-
vs: {
|
|
5362
|
-
bodyRotateY: [-0.05 * intensity, 0.05 * intensity, 0],
|
|
5363
|
-
bodyRotateZ: [-0.03 * intensity, 0.03 * intensity, 0],
|
|
5364
|
-
bodyRotateX: [-0.01 * intensity, 0.01 * intensity, 0]
|
|
5365
|
-
}
|
|
5366
|
-
},
|
|
5367
|
-
thinking: {
|
|
5368
|
-
name: 'bodyMovement_thinking',
|
|
5369
|
-
delay: [800, 2000],
|
|
5370
|
-
dt: [1000, 1500],
|
|
5371
|
-
vs: {
|
|
5372
|
-
bodyRotateY: [-0.06 * intensity, 0.06 * intensity, 0],
|
|
5373
|
-
bodyRotateZ: [-0.03 * intensity, 0.03 * intensity, 0],
|
|
5374
|
-
bodyRotateX: [-0.02 * intensity, 0.02 * intensity, 0]
|
|
5375
|
-
}
|
|
5376
|
-
},
|
|
5377
|
-
nodding: {
|
|
5378
|
-
name: 'bodyMovement_nodding',
|
|
5379
|
-
delay: [400, 800],
|
|
5380
|
-
dt: [300, 600],
|
|
5381
|
-
vs: {
|
|
5382
|
-
bodyRotateX: [-0.1 * intensity, 0.1 * intensity, 0],
|
|
5383
|
-
bodyRotateY: [-0.02 * intensity, 0.02 * intensity, 0]
|
|
5384
|
-
}
|
|
5385
|
-
},
|
|
5386
|
-
shaking: {
|
|
5387
|
-
name: 'bodyMovement_shaking',
|
|
5388
|
-
delay: [200, 400],
|
|
5389
|
-
dt: [150, 300],
|
|
5390
|
-
vs: {
|
|
5391
|
-
bodyRotateY: [-0.15 * intensity, 0.15 * intensity, 0],
|
|
5392
|
-
bodyRotateZ: [-0.05 * intensity, 0.05 * intensity, 0]
|
|
5393
|
-
}
|
|
5394
|
-
},
|
|
5395
|
-
celebration: {
|
|
5396
|
-
name: 'bodyMovement_celebration',
|
|
5397
|
-
delay: [100, 300],
|
|
5398
|
-
dt: [200, 500],
|
|
5399
|
-
vs: {
|
|
5400
|
-
bodyRotateY: [-0.2 * intensity, 0.2 * intensity, 0],
|
|
5401
|
-
bodyRotateZ: [-0.1 * intensity, 0.1 * intensity, 0],
|
|
5402
|
-
bodyRotateX: [-0.08 * intensity, 0.08 * intensity, 0]
|
|
5403
|
-
}
|
|
5404
|
-
},
|
|
5405
|
-
energetic: {
|
|
5406
|
-
name: 'bodyMovement_energetic',
|
|
5407
|
-
delay: [150, 400],
|
|
5408
|
-
dt: [250, 500],
|
|
5409
|
-
vs: {
|
|
5410
|
-
bodyRotateY: [-0.18 * intensity, 0.18 * intensity, 0],
|
|
5411
|
-
bodyRotateZ: [-0.12 * intensity, 0.12 * intensity, 0],
|
|
5412
|
-
bodyRotateX: [-0.08 * intensity, 0.08 * intensity, 0]
|
|
5413
|
-
}
|
|
5414
|
-
},
|
|
5415
|
-
swaying: {
|
|
5416
|
-
name: 'bodyMovement_swaying',
|
|
5417
|
-
delay: [600, 1200],
|
|
5418
|
-
dt: [800, 1000],
|
|
5419
|
-
vs: {
|
|
5420
|
-
bodyRotateY: [-0.1 * intensity, 0.1 * intensity, 0],
|
|
5421
|
-
bodyRotateZ: [-0.05 * intensity, 0.05 * intensity, 0]
|
|
5422
|
-
}
|
|
5423
|
-
},
|
|
5424
|
-
bouncing: {
|
|
5425
|
-
name: 'bodyMovement_bouncing',
|
|
5426
|
-
delay: [300, 600],
|
|
5427
|
-
dt: [400, 700],
|
|
5428
|
-
vs: {
|
|
5429
|
-
bodyRotateY: [-0.05 * intensity, 0.05 * intensity, 0]
|
|
5430
|
-
}
|
|
5431
|
-
}
|
|
5432
|
-
};
|
|
5433
|
-
|
|
5434
|
-
// Handle dance variations
|
|
5435
|
-
if (movementType === 'dancing') {
|
|
5436
|
-
const danceVariations = ['dancing', 'dancing2', 'dancing3'];
|
|
5437
|
-
const randomDance = danceVariations[Math.floor(Math.random() * danceVariations.length)];
|
|
5438
|
-
return movementAnimations[randomDance] || movementAnimations['dancing'];
|
|
5439
|
-
}
|
|
5440
|
-
|
|
5441
|
-
return movementAnimations[movementType] || null;
|
|
5442
|
-
}
|
|
5443
|
-
|
|
5444
|
-
/**
|
|
5445
|
-
* Set movement intensity.
|
|
5446
|
-
* @param {number} intensity Movement intensity (0-1).
|
|
5447
|
-
*/
|
|
5448
|
-
setMovementIntensity(intensity) {
|
|
5449
|
-
this.movementIntensity = Math.max(0, Math.min(1, intensity));
|
|
5450
|
-
|
|
5451
|
-
// Only set avatar property if avatar exists
|
|
5452
|
-
if (this.avatar) {
|
|
5453
|
-
this.avatar.movementIntensity = this.movementIntensity;
|
|
5454
|
-
}
|
|
5455
|
-
|
|
5456
|
-
console.log('Movement intensity set to:', this.movementIntensity);
|
|
5457
|
-
|
|
5458
|
-
// Update FBX animation intensity if available
|
|
5459
|
-
if (this.fbxAnimationLoader) {
|
|
5460
|
-
this.fbxAnimationLoader.setIntensity(this.movementIntensity);
|
|
5461
|
-
}
|
|
5462
|
-
|
|
5463
|
-
// Reapply body movement animation with new intensity
|
|
5464
|
-
if (this.bodyMovement && this.bodyMovement !== 'idle') {
|
|
5465
|
-
this.applyBodyMovementAnimation();
|
|
5466
|
-
}
|
|
5467
|
-
}
|
|
5468
|
-
|
|
5469
4751
|
/**
|
|
5470
4752
|
* Dispose the instance.
|
|
5471
4753
|
*/
|
|
@@ -5532,12 +4814,6 @@ class TalkingHead {
|
|
|
5532
4814
|
|
|
5533
4815
|
this.clearThree( this.ikMesh );
|
|
5534
4816
|
this.dynamicbones.dispose();
|
|
5535
|
-
|
|
5536
|
-
// Clean up FBX animation loader
|
|
5537
|
-
if (this.fbxAnimationLoader) {
|
|
5538
|
-
this.fbxAnimationLoader.stopCurrentAnimation();
|
|
5539
|
-
this.fbxAnimationLoader = null;
|
|
5540
|
-
}
|
|
5541
4817
|
|
|
5542
4818
|
// DOM
|
|
5543
4819
|
this.nodeAvatar = null;
|