@sage-rsc/talking-head-react 1.1.5 → 1.1.7
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/README.md +2 -2
- package/dist/index.cjs +2 -2
- package/dist/index.js +1039 -1253
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +0 -408
package/package.json
CHANGED
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -5323,400 +5323,6 @@ class TalkingHead {
|
|
|
5323
5323
|
* @param {number} [ndx=0] Index of the clip
|
|
5324
5324
|
* @param {number} [scale=0.01] Position scale factor
|
|
5325
5325
|
*/
|
|
5326
|
-
/**
|
|
5327
|
-
* Get all bone names from the avatar's armature
|
|
5328
|
-
* @returns {Set<string>} Set of bone names
|
|
5329
|
-
*/
|
|
5330
|
-
getAvailableBoneNames() {
|
|
5331
|
-
const boneNames = new Set();
|
|
5332
|
-
if (!this.armature) return boneNames;
|
|
5333
|
-
|
|
5334
|
-
this.armature.traverse((child) => {
|
|
5335
|
-
if (child.isBone || child.type === 'Bone') {
|
|
5336
|
-
boneNames.add(child.name);
|
|
5337
|
-
}
|
|
5338
|
-
});
|
|
5339
|
-
|
|
5340
|
-
return boneNames;
|
|
5341
|
-
}
|
|
5342
|
-
|
|
5343
|
-
/**
|
|
5344
|
-
* Map bone names from different naming conventions to avatar bone names
|
|
5345
|
-
* @param {string} fbxBoneName - Bone name from FBX animation
|
|
5346
|
-
* @param {Set<string>} availableBones - Set of available bone names in avatar
|
|
5347
|
-
* @returns {string|null} Mapped bone name or null if no match found
|
|
5348
|
-
*/
|
|
5349
|
-
mapBoneName(fbxBoneName, availableBones) {
|
|
5350
|
-
// Direct match first
|
|
5351
|
-
if (availableBones.has(fbxBoneName)) {
|
|
5352
|
-
return fbxBoneName;
|
|
5353
|
-
}
|
|
5354
|
-
|
|
5355
|
-
// Remove common prefixes
|
|
5356
|
-
let normalized = fbxBoneName;
|
|
5357
|
-
const originalNormalized = normalized;
|
|
5358
|
-
|
|
5359
|
-
// Remove CC_Base prefix (Character Creator)
|
|
5360
|
-
if (normalized.startsWith('CC_Base_')) {
|
|
5361
|
-
normalized = normalized.replace('CC_Base_', '');
|
|
5362
|
-
}
|
|
5363
|
-
|
|
5364
|
-
// Remove mixamorig prefix (Mixamo)
|
|
5365
|
-
normalized = normalized.replace(/^mixamorig/i, '');
|
|
5366
|
-
|
|
5367
|
-
// Try direct match after prefix removal
|
|
5368
|
-
if (availableBones.has(normalized)) {
|
|
5369
|
-
return normalized;
|
|
5370
|
-
}
|
|
5371
|
-
|
|
5372
|
-
// Debug: Log first few failed mappings to help identify patterns
|
|
5373
|
-
if (!this._mappingDebugLog) {
|
|
5374
|
-
this._mappingDebugLog = new Set();
|
|
5375
|
-
}
|
|
5376
|
-
if (this._mappingDebugLog.size < 5 && !this._mappingDebugLog.has(fbxBoneName)) {
|
|
5377
|
-
this._mappingDebugLog.add(fbxBoneName);
|
|
5378
|
-
console.debug(`Mapping attempt: "${fbxBoneName}" -> "${normalized}" (not found in available bones)`);
|
|
5379
|
-
}
|
|
5380
|
-
|
|
5381
|
-
// Handle numbered bones (e.g., Spine01 -> Spine1)
|
|
5382
|
-
if (normalized.match(/^Spine\d+$/)) {
|
|
5383
|
-
const num = normalized.match(/\d+/)?.[0];
|
|
5384
|
-
if (num) {
|
|
5385
|
-
const mapped = `Spine${parseInt(num)}`;
|
|
5386
|
-
if (availableBones.has(mapped)) {
|
|
5387
|
-
return mapped;
|
|
5388
|
-
}
|
|
5389
|
-
// Try Spine1 if Spine01, Spine2 if Spine02+
|
|
5390
|
-
if (num === '01' && availableBones.has('Spine1')) {
|
|
5391
|
-
return 'Spine1';
|
|
5392
|
-
}
|
|
5393
|
-
if (parseInt(num) >= 2 && availableBones.has('Spine2')) {
|
|
5394
|
-
return 'Spine2';
|
|
5395
|
-
}
|
|
5396
|
-
}
|
|
5397
|
-
}
|
|
5398
|
-
|
|
5399
|
-
// Handle twist bones (ignore them, they're usually not in standard skeletons)
|
|
5400
|
-
if (normalized.includes('Twist')) {
|
|
5401
|
-
return null;
|
|
5402
|
-
}
|
|
5403
|
-
|
|
5404
|
-
// Mapping rules for common bone name patterns
|
|
5405
|
-
const mappings = {
|
|
5406
|
-
// Spine mapping
|
|
5407
|
-
'Spine01': 'Spine1',
|
|
5408
|
-
'Spine02': 'Spine2',
|
|
5409
|
-
'Spine03': 'Spine2',
|
|
5410
|
-
|
|
5411
|
-
// Left arm mapping
|
|
5412
|
-
'L_Upperarm': 'LeftArm',
|
|
5413
|
-
'L_Forearm': 'LeftForeArm',
|
|
5414
|
-
'L_Hand': 'LeftHand',
|
|
5415
|
-
'L_Shoulder': 'LeftShoulder',
|
|
5416
|
-
'L_Index1': 'LeftHandIndex1',
|
|
5417
|
-
'L_Index2': 'LeftHandIndex2',
|
|
5418
|
-
'L_Index3': 'LeftHandIndex3',
|
|
5419
|
-
'L_Middle1': 'LeftHandMiddle1',
|
|
5420
|
-
'L_Middle2': 'LeftHandMiddle2',
|
|
5421
|
-
'L_Middle3': 'LeftHandMiddle3',
|
|
5422
|
-
'L_Mid1': 'LeftHandMiddle1',
|
|
5423
|
-
'L_Mid2': 'LeftHandMiddle2',
|
|
5424
|
-
'L_Mid3': 'LeftHandMiddle3',
|
|
5425
|
-
'L_Ring1': 'LeftHandRing1',
|
|
5426
|
-
'L_Ring2': 'LeftHandRing2',
|
|
5427
|
-
'L_Ring3': 'LeftHandRing3',
|
|
5428
|
-
'L_Pinky1': 'LeftHandPinky1',
|
|
5429
|
-
'L_Pinky2': 'LeftHandPinky2',
|
|
5430
|
-
'L_Pinky3': 'LeftHandPinky3',
|
|
5431
|
-
'L_Thumb1': 'LeftHandThumb1',
|
|
5432
|
-
'L_Thumb2': 'LeftHandThumb2',
|
|
5433
|
-
'L_Thumb3': 'LeftHandThumb3',
|
|
5434
|
-
|
|
5435
|
-
// Right arm mapping
|
|
5436
|
-
'R_Upperarm': 'RightArm',
|
|
5437
|
-
'R_Forearm': 'RightForeArm',
|
|
5438
|
-
'R_Hand': 'RightHand',
|
|
5439
|
-
'R_Shoulder': 'RightShoulder',
|
|
5440
|
-
'R_Index1': 'RightHandIndex1',
|
|
5441
|
-
'R_Index2': 'RightHandIndex2',
|
|
5442
|
-
'R_Index3': 'RightHandIndex3',
|
|
5443
|
-
'R_Middle1': 'RightHandMiddle1',
|
|
5444
|
-
'R_Middle2': 'RightHandMiddle2',
|
|
5445
|
-
'R_Middle3': 'RightHandMiddle3',
|
|
5446
|
-
'R_Mid1': 'RightHandMiddle1',
|
|
5447
|
-
'R_Mid2': 'RightHandMiddle2',
|
|
5448
|
-
'R_Mid3': 'RightHandMiddle3',
|
|
5449
|
-
'R_Ring1': 'RightHandRing1',
|
|
5450
|
-
'R_Ring2': 'RightHandRing2',
|
|
5451
|
-
'R_Ring3': 'RightHandRing3',
|
|
5452
|
-
'R_Pinky1': 'RightHandPinky1',
|
|
5453
|
-
'R_Pinky2': 'RightHandPinky2',
|
|
5454
|
-
'R_Pinky3': 'RightHandPinky3',
|
|
5455
|
-
'R_Thumb1': 'RightHandThumb1',
|
|
5456
|
-
'R_Thumb2': 'RightHandThumb2',
|
|
5457
|
-
'R_Thumb3': 'RightHandThumb3',
|
|
5458
|
-
|
|
5459
|
-
// Leg mapping
|
|
5460
|
-
'L_Thigh': 'LeftUpLeg',
|
|
5461
|
-
'L_Calf': 'LeftLeg',
|
|
5462
|
-
'L_Foot': 'LeftFoot',
|
|
5463
|
-
'R_Thigh': 'RightUpLeg',
|
|
5464
|
-
'R_Calf': 'RightLeg',
|
|
5465
|
-
'R_Foot': 'RightFoot',
|
|
5466
|
-
};
|
|
5467
|
-
|
|
5468
|
-
// Try mapping
|
|
5469
|
-
if (mappings[normalized]) {
|
|
5470
|
-
const mapped = mappings[normalized];
|
|
5471
|
-
if (availableBones.has(mapped)) {
|
|
5472
|
-
return mapped;
|
|
5473
|
-
}
|
|
5474
|
-
}
|
|
5475
|
-
|
|
5476
|
-
// Pattern-based matching for CC_Base and similar naming conventions
|
|
5477
|
-
const lowerNormalized = normalized.toLowerCase();
|
|
5478
|
-
const upperFirst = normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
|
|
5479
|
-
|
|
5480
|
-
// Pattern: R_Index1/2/3 or r_index1/2/3 -> RightHandIndex1/2/3
|
|
5481
|
-
const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
|
|
5482
|
-
if (indexMatch) {
|
|
5483
|
-
const digit = indexMatch[1];
|
|
5484
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5485
|
-
const candidate = `${side}HandIndex${digit}`;
|
|
5486
|
-
if (availableBones.has(candidate)) {
|
|
5487
|
-
return candidate;
|
|
5488
|
-
}
|
|
5489
|
-
}
|
|
5490
|
-
|
|
5491
|
-
// Pattern: R_Pinky1/2/3 -> RightHandPinky1/2/3
|
|
5492
|
-
const pinkyMatch = lowerNormalized.match(/^[rl]_pinky(\d+)$/);
|
|
5493
|
-
if (pinkyMatch) {
|
|
5494
|
-
const digit = pinkyMatch[1];
|
|
5495
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5496
|
-
const candidate = `${side}HandPinky${digit}`;
|
|
5497
|
-
if (availableBones.has(candidate)) {
|
|
5498
|
-
return candidate;
|
|
5499
|
-
}
|
|
5500
|
-
}
|
|
5501
|
-
|
|
5502
|
-
// Pattern: R_Ring1/2/3 -> RightHandRing1/2/3
|
|
5503
|
-
const ringMatch = lowerNormalized.match(/^[rl]_ring(\d+)$/);
|
|
5504
|
-
if (ringMatch) {
|
|
5505
|
-
const digit = ringMatch[1];
|
|
5506
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5507
|
-
const candidate = `${side}HandRing${digit}`;
|
|
5508
|
-
if (availableBones.has(candidate)) {
|
|
5509
|
-
return candidate;
|
|
5510
|
-
}
|
|
5511
|
-
}
|
|
5512
|
-
|
|
5513
|
-
// Pattern: R_Middle1/2/3 or R_Mid1/2/3 -> RightHandMiddle1/2/3
|
|
5514
|
-
const middleMatch = lowerNormalized.match(/^[rl]_(?:middle|mid)(\d+)$/);
|
|
5515
|
-
if (middleMatch) {
|
|
5516
|
-
const digit = middleMatch[1];
|
|
5517
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5518
|
-
const candidate = `${side}HandMiddle${digit}`;
|
|
5519
|
-
if (availableBones.has(candidate)) {
|
|
5520
|
-
return candidate;
|
|
5521
|
-
}
|
|
5522
|
-
}
|
|
5523
|
-
|
|
5524
|
-
// Pattern: R_Thumb1/2/3 -> RightHandThumb1/2/3
|
|
5525
|
-
const thumbMatch = lowerNormalized.match(/^[rl]_thumb(\d+)$/);
|
|
5526
|
-
if (thumbMatch) {
|
|
5527
|
-
const digit = thumbMatch[1];
|
|
5528
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5529
|
-
const candidate = `${side}HandThumb${digit}`;
|
|
5530
|
-
if (availableBones.has(candidate)) {
|
|
5531
|
-
return candidate;
|
|
5532
|
-
}
|
|
5533
|
-
}
|
|
5534
|
-
|
|
5535
|
-
// Pattern: R_Upperarm -> RightArm (case insensitive)
|
|
5536
|
-
if (lowerNormalized.match(/^[rl]_upperarm/)) {
|
|
5537
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5538
|
-
const candidate = `${side}Arm`;
|
|
5539
|
-
if (availableBones.has(candidate)) {
|
|
5540
|
-
return candidate;
|
|
5541
|
-
}
|
|
5542
|
-
}
|
|
5543
|
-
|
|
5544
|
-
// Pattern: R_UpperarmTwist01/02 -> ignore (twist bones and other extra bones)
|
|
5545
|
-
if (lowerNormalized.includes('upperarmtwist') ||
|
|
5546
|
-
lowerNormalized.includes('forearmtwist') ||
|
|
5547
|
-
lowerNormalized.includes('ribstwist') ||
|
|
5548
|
-
lowerNormalized.includes('breast') ||
|
|
5549
|
-
lowerNormalized.includes('twist')) {
|
|
5550
|
-
return null;
|
|
5551
|
-
}
|
|
5552
|
-
|
|
5553
|
-
// Pattern: R_Forearm -> RightForeArm
|
|
5554
|
-
if (lowerNormalized.match(/^[rl]_forearm/)) {
|
|
5555
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5556
|
-
const candidate = `${side}ForeArm`;
|
|
5557
|
-
if (availableBones.has(candidate)) {
|
|
5558
|
-
return candidate;
|
|
5559
|
-
}
|
|
5560
|
-
}
|
|
5561
|
-
|
|
5562
|
-
// Pattern: R_Hand -> RightHand
|
|
5563
|
-
if (lowerNormalized.match(/^[rl]_hand$/)) {
|
|
5564
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5565
|
-
const candidate = `${side}Hand`;
|
|
5566
|
-
if (availableBones.has(candidate)) {
|
|
5567
|
-
return candidate;
|
|
5568
|
-
}
|
|
5569
|
-
}
|
|
5570
|
-
|
|
5571
|
-
// Try case-insensitive exact match
|
|
5572
|
-
for (const boneName of availableBones) {
|
|
5573
|
-
if (boneName.toLowerCase() === lowerNormalized) {
|
|
5574
|
-
return boneName;
|
|
5575
|
-
}
|
|
5576
|
-
}
|
|
5577
|
-
|
|
5578
|
-
return null; // No mapping found
|
|
5579
|
-
}
|
|
5580
|
-
|
|
5581
|
-
/**
|
|
5582
|
-
* Filter animation tracks to only include bones that exist in the avatar
|
|
5583
|
-
* Maps bone names from different naming conventions to avatar bone names
|
|
5584
|
-
* @param {THREE.AnimationClip} clip - Animation clip to filter
|
|
5585
|
-
* @param {Set<string>} availableBones - Set of available bone names
|
|
5586
|
-
* @returns {THREE.AnimationClip} Filtered animation clip with mapped bone names
|
|
5587
|
-
*/
|
|
5588
|
-
filterAnimationTracks(clip, availableBones) {
|
|
5589
|
-
const validTracks = [];
|
|
5590
|
-
const missingBones = new Set();
|
|
5591
|
-
const mappedBones = new Map(); // Track mappings for logging
|
|
5592
|
-
|
|
5593
|
-
// Debug: Log available bones (first time only)
|
|
5594
|
-
if (!this._loggedAvailableBones) {
|
|
5595
|
-
console.log('Available avatar bones:', Array.from(availableBones).sort().slice(0, 50).join(', '),
|
|
5596
|
-
availableBones.size > 50 ? `... (${availableBones.size} total)` : '');
|
|
5597
|
-
this._loggedAvailableBones = true;
|
|
5598
|
-
}
|
|
5599
|
-
|
|
5600
|
-
clip.tracks.forEach(track => {
|
|
5601
|
-
// Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
|
|
5602
|
-
const trackNameParts = track.name.split('.');
|
|
5603
|
-
const fbxBoneName = trackNameParts[0];
|
|
5604
|
-
const property = trackNameParts[1]; // position, quaternion, rotation, etc.
|
|
5605
|
-
|
|
5606
|
-
// Try to map the bone name
|
|
5607
|
-
const mappedBoneName = this.mapBoneName(fbxBoneName, availableBones);
|
|
5608
|
-
|
|
5609
|
-
if (mappedBoneName) {
|
|
5610
|
-
// Create a new track with the mapped bone name
|
|
5611
|
-
const newTrackName = `${mappedBoneName}.${property}`;
|
|
5612
|
-
|
|
5613
|
-
// Clone the track and update its name
|
|
5614
|
-
const newTrack = track.clone();
|
|
5615
|
-
newTrack.name = newTrackName;
|
|
5616
|
-
|
|
5617
|
-
// Fix rotations for arm/hand bones that might be inverted
|
|
5618
|
-
// If hands are folding behind instead of in front, we need to adjust rotations
|
|
5619
|
-
const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
|
|
5620
|
-
const isForearmBone = mappedBoneName.includes('ForeArm');
|
|
5621
|
-
|
|
5622
|
-
if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
|
|
5623
|
-
// For quaternion tracks, we might need to adjust the rotation
|
|
5624
|
-
// Check if this is a quaternion track
|
|
5625
|
-
if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
|
|
5626
|
-
// Quaternion format: [x, y, z, w] per keyframe
|
|
5627
|
-
// For arm bones, we might need to invert Y or Z rotation
|
|
5628
|
-
// Adjust quaternion values to fix hand position
|
|
5629
|
-
const numKeyframes = newTrack.times.length;
|
|
5630
|
-
for (let i = 0; i < numKeyframes; i++) {
|
|
5631
|
-
const baseIdx = i * 4;
|
|
5632
|
-
if (baseIdx + 3 < newTrack.values.length) {
|
|
5633
|
-
// Get quaternion values
|
|
5634
|
-
let x = newTrack.values[baseIdx];
|
|
5635
|
-
let y = newTrack.values[baseIdx + 1];
|
|
5636
|
-
let z = newTrack.values[baseIdx + 2];
|
|
5637
|
-
let w = newTrack.values[baseIdx + 3];
|
|
5638
|
-
|
|
5639
|
-
// For arms, adjust rotation to flip hands from back to front
|
|
5640
|
-
// This is a common fix for FBX animations with different coordinate systems
|
|
5641
|
-
if (isForearmBone || mappedBoneName.includes('Hand')) {
|
|
5642
|
-
// Rotate 180 degrees around X axis to flip hands from behind to in front
|
|
5643
|
-
// Create a 180-degree rotation around X axis
|
|
5644
|
-
const flipAngle = Math.PI;
|
|
5645
|
-
const flipX = Math.cos(flipAngle / 2); // w component
|
|
5646
|
-
const flipY = Math.sin(flipAngle / 2); // x component (axis X = 1,0,0)
|
|
5647
|
-
|
|
5648
|
-
// Multiply quaternions: q_result = q_flip * q_original
|
|
5649
|
-
// For rotation around X axis: q_flip = (sin(θ/2), 0, 0, cos(θ/2))
|
|
5650
|
-
const qw = flipX * w - flipY * x;
|
|
5651
|
-
const qx = flipX * x + flipY * w;
|
|
5652
|
-
const qy = flipX * y - flipY * z;
|
|
5653
|
-
const qz = flipX * z + flipY * y;
|
|
5654
|
-
|
|
5655
|
-
x = qx;
|
|
5656
|
-
y = qy;
|
|
5657
|
-
z = qz;
|
|
5658
|
-
w = qw;
|
|
5659
|
-
}
|
|
5660
|
-
|
|
5661
|
-
newTrack.values[baseIdx] = x;
|
|
5662
|
-
newTrack.values[baseIdx + 1] = y;
|
|
5663
|
-
newTrack.values[baseIdx + 2] = z;
|
|
5664
|
-
newTrack.values[baseIdx + 3] = w;
|
|
5665
|
-
}
|
|
5666
|
-
}
|
|
5667
|
-
} else if (property === 'rotation' && newTrack.values && newTrack.values.length >= 3) {
|
|
5668
|
-
// Euler rotation format: [x, y, z] per keyframe
|
|
5669
|
-
const numKeyframes = newTrack.times.length;
|
|
5670
|
-
for (let i = 0; i < numKeyframes; i++) {
|
|
5671
|
-
const baseIdx = i * 3;
|
|
5672
|
-
if (baseIdx + 2 < newTrack.values.length) {
|
|
5673
|
-
// For arm bones, adjust Y rotation to flip hands
|
|
5674
|
-
if (isForearmBone || mappedBoneName.includes('Hand')) {
|
|
5675
|
-
// Add 180 degrees (PI radians) to Y rotation
|
|
5676
|
-
newTrack.values[baseIdx + 1] += Math.PI;
|
|
5677
|
-
}
|
|
5678
|
-
}
|
|
5679
|
-
}
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
|
|
5683
|
-
validTracks.push(newTrack);
|
|
5684
|
-
|
|
5685
|
-
// Track mappings for logging
|
|
5686
|
-
if (fbxBoneName !== mappedBoneName) {
|
|
5687
|
-
mappedBones.set(fbxBoneName, mappedBoneName);
|
|
5688
|
-
}
|
|
5689
|
-
} else {
|
|
5690
|
-
missingBones.add(fbxBoneName);
|
|
5691
|
-
}
|
|
5692
|
-
});
|
|
5693
|
-
|
|
5694
|
-
// Log results
|
|
5695
|
-
if (mappedBones.size > 0) {
|
|
5696
|
-
console.info(`FBX animation "${clip.name}": Mapped ${mappedBones.size} bone(s) to avatar skeleton:`,
|
|
5697
|
-
Array.from(mappedBones.entries()).slice(0, 5).map(([from, to]) => `${from} → ${to}`).join(', '),
|
|
5698
|
-
mappedBones.size > 5 ? '...' : '');
|
|
5699
|
-
}
|
|
5700
|
-
|
|
5701
|
-
if (missingBones.size > 0) {
|
|
5702
|
-
console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s) that couldn't be mapped:`,
|
|
5703
|
-
Array.from(missingBones).slice(0, 10).join(', '),
|
|
5704
|
-
missingBones.size > 10 ? '...' : '');
|
|
5705
|
-
}
|
|
5706
|
-
|
|
5707
|
-
if (validTracks.length > 0) {
|
|
5708
|
-
console.info(`Filtered ${clip.tracks.length} tracks down to ${validTracks.length} valid tracks (${mappedBones.size} mapped)`);
|
|
5709
|
-
} else {
|
|
5710
|
-
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
|
|
5711
|
-
}
|
|
5712
|
-
|
|
5713
|
-
// Create a new clip with only valid tracks
|
|
5714
|
-
if (validTracks.length === 0) {
|
|
5715
|
-
return null;
|
|
5716
|
-
}
|
|
5717
|
-
|
|
5718
|
-
return new THREE.AnimationClip(clip.name, clip.duration, validTracks);
|
|
5719
|
-
}
|
|
5720
5326
|
|
|
5721
5327
|
async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01, disablePositionLock=false) {
|
|
5722
5328
|
if ( !this.armature ) return;
|
|
@@ -5865,20 +5471,6 @@ class TalkingHead {
|
|
|
5865
5471
|
if ( fbx && fbx.animations && fbx.animations[ndx] ) {
|
|
5866
5472
|
let anim = fbx.animations[ndx];
|
|
5867
5473
|
|
|
5868
|
-
// Get available bone names from avatar skeleton
|
|
5869
|
-
const availableBones = this.getAvailableBoneNames();
|
|
5870
|
-
|
|
5871
|
-
// Filter animation tracks to only include bones that exist
|
|
5872
|
-
const filteredAnim = this.filterAnimationTracks(anim, availableBones);
|
|
5873
|
-
|
|
5874
|
-
if (!filteredAnim) {
|
|
5875
|
-
console.error(`Cannot play FBX animation "${url}": No compatible bones found.`);
|
|
5876
|
-
return;
|
|
5877
|
-
}
|
|
5878
|
-
|
|
5879
|
-
// Use the filtered animation instead of the original
|
|
5880
|
-
anim = filteredAnim;
|
|
5881
|
-
|
|
5882
5474
|
// Rename and scale Mixamo tracks, create a pose
|
|
5883
5475
|
const props = {};
|
|
5884
5476
|
anim.tracks.forEach( t => {
|