@sage-rsc/talking-head-react 1.1.7 → 1.1.9
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 +2 -2
- package/dist/index.js +891 -758
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +273 -0
package/package.json
CHANGED
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -5471,6 +5471,279 @@ class TalkingHead {
|
|
|
5471
5471
|
if ( fbx && fbx.animations && fbx.animations[ndx] ) {
|
|
5472
5472
|
let anim = fbx.animations[ndx];
|
|
5473
5473
|
|
|
5474
|
+
// Get available bone names from avatar skeleton for mapping
|
|
5475
|
+
const availableBones = new Set();
|
|
5476
|
+
if (this.armature) {
|
|
5477
|
+
this.armature.traverse((child) => {
|
|
5478
|
+
if (child.isBone || child.type === 'Bone') {
|
|
5479
|
+
availableBones.add(child.name);
|
|
5480
|
+
}
|
|
5481
|
+
});
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
// Map bone names from FBX to avatar skeleton
|
|
5485
|
+
const boneNameMap = new Map();
|
|
5486
|
+
const mapBoneName = (fbxBoneName) => {
|
|
5487
|
+
// Direct match
|
|
5488
|
+
if (availableBones.has(fbxBoneName)) {
|
|
5489
|
+
return fbxBoneName;
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
// Remove common prefixes (mixamorig, CC_Base_, etc.)
|
|
5493
|
+
let normalized = fbxBoneName
|
|
5494
|
+
.replace(/^mixamorig/i, '')
|
|
5495
|
+
.replace(/^CC_Base_/i, '')
|
|
5496
|
+
.replace(/^RPM_/i, '');
|
|
5497
|
+
|
|
5498
|
+
if (availableBones.has(normalized)) {
|
|
5499
|
+
return normalized;
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
// Pattern-based matching for Ready Player Me / Mixamo
|
|
5503
|
+
const lowerNormalized = normalized.toLowerCase();
|
|
5504
|
+
|
|
5505
|
+
// Arm bones - pattern matching
|
|
5506
|
+
if (lowerNormalized.includes('left') && lowerNormalized.includes('arm')) {
|
|
5507
|
+
if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
|
|
5508
|
+
if (availableBones.has('LeftForeArm')) return 'LeftForeArm';
|
|
5509
|
+
if (availableBones.has('LeftForearm')) return 'LeftForearm';
|
|
5510
|
+
} else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
|
|
5511
|
+
if (availableBones.has('LeftArm')) return 'LeftArm';
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
if (lowerNormalized.includes('right') && lowerNormalized.includes('arm')) {
|
|
5516
|
+
if (lowerNormalized.includes('fore') || lowerNormalized.includes('lower')) {
|
|
5517
|
+
if (availableBones.has('RightForeArm')) return 'RightForeArm';
|
|
5518
|
+
if (availableBones.has('RightForearm')) return 'RightForearm';
|
|
5519
|
+
} else if (!lowerNormalized.includes('fore') && !lowerNormalized.includes('hand')) {
|
|
5520
|
+
if (availableBones.has('RightArm')) return 'RightArm';
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
// Hand bones
|
|
5525
|
+
if (lowerNormalized.includes('left') && lowerNormalized.includes('hand') &&
|
|
5526
|
+
!lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
|
|
5527
|
+
!lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
|
|
5528
|
+
!lowerNormalized.includes('pinky')) {
|
|
5529
|
+
if (availableBones.has('LeftHand')) return 'LeftHand';
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
if (lowerNormalized.includes('right') && lowerNormalized.includes('hand') &&
|
|
5533
|
+
!lowerNormalized.includes('index') && !lowerNormalized.includes('thumb') &&
|
|
5534
|
+
!lowerNormalized.includes('middle') && !lowerNormalized.includes('ring') &&
|
|
5535
|
+
!lowerNormalized.includes('pinky')) {
|
|
5536
|
+
if (availableBones.has('RightHand')) return 'RightHand';
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
// Shoulder bones
|
|
5540
|
+
if (lowerNormalized.includes('left') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
|
|
5541
|
+
if (availableBones.has('LeftShoulder')) return 'LeftShoulder';
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
if (lowerNormalized.includes('right') && (lowerNormalized.includes('shoulder') || lowerNormalized.includes('clavicle'))) {
|
|
5545
|
+
if (availableBones.has('RightShoulder')) return 'RightShoulder';
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5548
|
+
// Common bone name mappings
|
|
5549
|
+
const mappings = {
|
|
5550
|
+
// Arm bones - exact matches
|
|
5551
|
+
'LeftArm': 'LeftArm',
|
|
5552
|
+
'leftArm': 'LeftArm',
|
|
5553
|
+
'LEFTARM': 'LeftArm',
|
|
5554
|
+
'RightArm': 'RightArm',
|
|
5555
|
+
'rightArm': 'RightArm',
|
|
5556
|
+
'RIGHTARM': 'RightArm',
|
|
5557
|
+
'LeftForeArm': 'LeftForeArm',
|
|
5558
|
+
'leftForeArm': 'LeftForeArm',
|
|
5559
|
+
'leftForearm': 'LeftForeArm',
|
|
5560
|
+
'LeftForearm': 'LeftForeArm',
|
|
5561
|
+
'RightForeArm': 'RightForeArm',
|
|
5562
|
+
'rightForeArm': 'RightForeArm',
|
|
5563
|
+
'rightForearm': 'RightForeArm',
|
|
5564
|
+
'RightForearm': 'RightForeArm',
|
|
5565
|
+
'LeftHand': 'LeftHand',
|
|
5566
|
+
'leftHand': 'LeftHand',
|
|
5567
|
+
'RightHand': 'RightHand',
|
|
5568
|
+
'rightHand': 'RightHand',
|
|
5569
|
+
'LeftShoulder': 'LeftShoulder',
|
|
5570
|
+
'leftShoulder': 'LeftShoulder',
|
|
5571
|
+
'RightShoulder': 'RightShoulder',
|
|
5572
|
+
'rightShoulder': 'RightShoulder',
|
|
5573
|
+
// Spine
|
|
5574
|
+
'Spine': 'Spine1',
|
|
5575
|
+
'spine': 'Spine1',
|
|
5576
|
+
'Spine1': 'Spine1',
|
|
5577
|
+
'Spine2': 'Spine2',
|
|
5578
|
+
// Head/Neck
|
|
5579
|
+
'Head': 'Head',
|
|
5580
|
+
'head': 'Head',
|
|
5581
|
+
'Neck': 'Neck',
|
|
5582
|
+
'neck': 'Neck',
|
|
5583
|
+
// Hips
|
|
5584
|
+
'Hips': 'Hips',
|
|
5585
|
+
'hips': 'Hips',
|
|
5586
|
+
'Root': 'Hips',
|
|
5587
|
+
'root': 'Hips',
|
|
5588
|
+
};
|
|
5589
|
+
|
|
5590
|
+
if (mappings[normalized]) {
|
|
5591
|
+
const mapped = mappings[normalized];
|
|
5592
|
+
if (availableBones.has(mapped)) {
|
|
5593
|
+
return mapped;
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
// Try case-insensitive match
|
|
5598
|
+
for (const boneName of availableBones) {
|
|
5599
|
+
if (boneName.toLowerCase() === lowerNormalized) {
|
|
5600
|
+
return boneName;
|
|
5601
|
+
}
|
|
5602
|
+
}
|
|
5603
|
+
|
|
5604
|
+
// Try partial match (contains)
|
|
5605
|
+
for (const boneName of availableBones) {
|
|
5606
|
+
const boneLower = boneName.toLowerCase();
|
|
5607
|
+
// Match if normalized contains key parts of bone name
|
|
5608
|
+
if ((lowerNormalized.includes('left') && boneLower.includes('left')) ||
|
|
5609
|
+
(lowerNormalized.includes('right') && boneLower.includes('right'))) {
|
|
5610
|
+
if ((lowerNormalized.includes('arm') && boneLower.includes('arm') && !boneLower.includes('fore')) ||
|
|
5611
|
+
(lowerNormalized.includes('forearm') && boneLower.includes('forearm')) ||
|
|
5612
|
+
(lowerNormalized.includes('hand') && boneLower.includes('hand') && !boneLower.includes('index') && !boneLower.includes('thumb')) ||
|
|
5613
|
+
(lowerNormalized.includes('shoulder') && boneLower.includes('shoulder'))) {
|
|
5614
|
+
return boneName;
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
return null; // No mapping found
|
|
5620
|
+
};
|
|
5621
|
+
|
|
5622
|
+
// Debug: Log FBX bone names and avatar bone names for comparison
|
|
5623
|
+
const fbxBoneNames = new Set();
|
|
5624
|
+
anim.tracks.forEach(track => {
|
|
5625
|
+
const trackParts = track.name.split('.');
|
|
5626
|
+
fbxBoneNames.add(trackParts[0]);
|
|
5627
|
+
});
|
|
5628
|
+
|
|
5629
|
+
console.log('=== Ready Player Me Animation Bone Analysis ===');
|
|
5630
|
+
console.log('FBX bone names:', Array.from(fbxBoneNames).sort().join(', '));
|
|
5631
|
+
console.log('Avatar skeleton bone names:', Array.from(availableBones).sort().join(', '));
|
|
5632
|
+
|
|
5633
|
+
// Check for arm bones specifically
|
|
5634
|
+
const fbxArmBones = Array.from(fbxBoneNames).filter(b =>
|
|
5635
|
+
b.toLowerCase().includes('arm') ||
|
|
5636
|
+
b.toLowerCase().includes('hand') ||
|
|
5637
|
+
b.toLowerCase().includes('shoulder')
|
|
5638
|
+
);
|
|
5639
|
+
const avatarArmBones = Array.from(availableBones).filter(b =>
|
|
5640
|
+
b.includes('Arm') ||
|
|
5641
|
+
b.includes('Hand') ||
|
|
5642
|
+
b.includes('Shoulder')
|
|
5643
|
+
);
|
|
5644
|
+
console.log('FBX arm/hand/shoulder bones:', fbxArmBones.sort().join(', '));
|
|
5645
|
+
console.log('Avatar arm/hand/shoulder bones:', avatarArmBones.sort().join(', '));
|
|
5646
|
+
|
|
5647
|
+
// Filter and map animation tracks
|
|
5648
|
+
const mappedTracks = [];
|
|
5649
|
+
const unmappedBones = new Set();
|
|
5650
|
+
anim.tracks.forEach(track => {
|
|
5651
|
+
// Remove mixamorig prefix first
|
|
5652
|
+
let trackName = track.name.replaceAll('mixamorig', '');
|
|
5653
|
+
const trackParts = trackName.split('.');
|
|
5654
|
+
const fbxBoneName = trackParts[0];
|
|
5655
|
+
const property = trackParts[1];
|
|
5656
|
+
|
|
5657
|
+
// Map bone name to avatar skeleton
|
|
5658
|
+
const mappedBoneName = mapBoneName(fbxBoneName);
|
|
5659
|
+
|
|
5660
|
+
if (mappedBoneName && property) {
|
|
5661
|
+
// Create new track with mapped bone name
|
|
5662
|
+
const newTrackName = `${mappedBoneName}.${property}`;
|
|
5663
|
+
const newTrack = track.clone();
|
|
5664
|
+
newTrack.name = newTrackName;
|
|
5665
|
+
|
|
5666
|
+
// Fix rotations for Ready Player Me arm bones (coordinate system correction)
|
|
5667
|
+
const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
|
|
5668
|
+
const isLeftArm = mappedBoneName.includes('Left');
|
|
5669
|
+
|
|
5670
|
+
if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
|
|
5671
|
+
if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
|
|
5672
|
+
// Adjust quaternion rotations for Ready Player Me coordinate system
|
|
5673
|
+
const numKeyframes = newTrack.times.length;
|
|
5674
|
+
for (let i = 0; i < numKeyframes; i++) {
|
|
5675
|
+
const baseIdx = i * 4;
|
|
5676
|
+
if (baseIdx + 3 < newTrack.values.length) {
|
|
5677
|
+
let x = newTrack.values[baseIdx];
|
|
5678
|
+
let y = newTrack.values[baseIdx + 1];
|
|
5679
|
+
let z = newTrack.values[baseIdx + 2];
|
|
5680
|
+
let w = newTrack.values[baseIdx + 3];
|
|
5681
|
+
|
|
5682
|
+
// For Ready Player Me, we may need to flip certain axes
|
|
5683
|
+
// Try rotating 180 degrees around Y axis for left arm
|
|
5684
|
+
if (isLeftArm && mappedBoneName.includes('ForeArm')) {
|
|
5685
|
+
// Left forearm might need Y-axis flip
|
|
5686
|
+
const flipY = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
|
|
5687
|
+
const q = new THREE.Quaternion(x, y, z, w);
|
|
5688
|
+
q.multiply(flipY);
|
|
5689
|
+
x = q.x;
|
|
5690
|
+
y = q.y;
|
|
5691
|
+
z = q.z;
|
|
5692
|
+
w = q.w;
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5695
|
+
newTrack.values[baseIdx] = x;
|
|
5696
|
+
newTrack.values[baseIdx + 1] = y;
|
|
5697
|
+
newTrack.values[baseIdx + 2] = z;
|
|
5698
|
+
newTrack.values[baseIdx + 3] = w;
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
mappedTracks.push(newTrack);
|
|
5705
|
+
|
|
5706
|
+
// Store mapping for logging
|
|
5707
|
+
if (fbxBoneName !== mappedBoneName) {
|
|
5708
|
+
boneNameMap.set(fbxBoneName, mappedBoneName);
|
|
5709
|
+
}
|
|
5710
|
+
} else {
|
|
5711
|
+
unmappedBones.add(fbxBoneName);
|
|
5712
|
+
// Log unmapped bones (especially arm bones)
|
|
5713
|
+
if (fbxBoneName.toLowerCase().includes('arm') ||
|
|
5714
|
+
fbxBoneName.toLowerCase().includes('hand') ||
|
|
5715
|
+
fbxBoneName.toLowerCase().includes('shoulder')) {
|
|
5716
|
+
console.warn(`⚠️ Arm bone "${fbxBoneName}" could not be mapped to avatar skeleton`);
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
});
|
|
5720
|
+
|
|
5721
|
+
if (unmappedBones.size > 0) {
|
|
5722
|
+
console.warn(`⚠️ ${unmappedBones.size} bone(s) could not be mapped:`, Array.from(unmappedBones).sort().join(', '));
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
// Use mapped tracks if we have any, otherwise use original
|
|
5726
|
+
if (mappedTracks.length > 0) {
|
|
5727
|
+
anim = new THREE.AnimationClip(anim.name, anim.duration, mappedTracks);
|
|
5728
|
+
console.log(`✓ Created animation with ${mappedTracks.length} mapped tracks (from ${anim.tracks.length} original tracks)`);
|
|
5729
|
+
if (boneNameMap.size > 0) {
|
|
5730
|
+
console.log(`✓ Mapped ${boneNameMap.size} bone(s):`,
|
|
5731
|
+
Array.from(boneNameMap.entries()).map(([from, to]) => `${from}→${to}`).join(', '));
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
// Check if arm bones were mapped
|
|
5735
|
+
const mappedArmBones = Array.from(boneNameMap.values()).filter(b =>
|
|
5736
|
+
b.includes('Arm') || b.includes('Hand') || b.includes('Shoulder')
|
|
5737
|
+
);
|
|
5738
|
+
if (mappedArmBones.length > 0) {
|
|
5739
|
+
console.log(`✓ Arm bones mapped: ${mappedArmBones.join(', ')}`);
|
|
5740
|
+
} else {
|
|
5741
|
+
console.warn('⚠️ No arm bones were mapped! This may cause arm rigging issues.');
|
|
5742
|
+
}
|
|
5743
|
+
} else {
|
|
5744
|
+
console.error('❌ No tracks could be mapped! Animation may not work correctly.');
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5474
5747
|
// Rename and scale Mixamo tracks, create a pose
|
|
5475
5748
|
const props = {};
|
|
5476
5749
|
anim.tracks.forEach( t => {
|