@sage-rsc/talking-head-react 1.1.8 → 1.2.0
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 +836 -773
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +149 -8
package/package.json
CHANGED
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -5489,15 +5489,65 @@ class TalkingHead {
|
|
|
5489
5489
|
return fbxBoneName;
|
|
5490
5490
|
}
|
|
5491
5491
|
|
|
5492
|
-
// Remove mixamorig
|
|
5493
|
-
let normalized = fbxBoneName
|
|
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
|
+
|
|
5494
5498
|
if (availableBones.has(normalized)) {
|
|
5495
5499
|
return normalized;
|
|
5496
5500
|
}
|
|
5497
5501
|
|
|
5498
|
-
//
|
|
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
|
|
5499
5549
|
const mappings = {
|
|
5500
|
-
// Arm bones -
|
|
5550
|
+
// Arm bones - exact matches
|
|
5501
5551
|
'LeftArm': 'LeftArm',
|
|
5502
5552
|
'leftArm': 'LeftArm',
|
|
5503
5553
|
'LEFTARM': 'LeftArm',
|
|
@@ -5546,11 +5596,26 @@ class TalkingHead {
|
|
|
5546
5596
|
|
|
5547
5597
|
// Try case-insensitive match
|
|
5548
5598
|
for (const boneName of availableBones) {
|
|
5549
|
-
if (boneName.toLowerCase() ===
|
|
5599
|
+
if (boneName.toLowerCase() === lowerNormalized) {
|
|
5550
5600
|
return boneName;
|
|
5551
5601
|
}
|
|
5552
5602
|
}
|
|
5553
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
|
+
|
|
5554
5619
|
return null; // No mapping found
|
|
5555
5620
|
};
|
|
5556
5621
|
|
|
@@ -5561,8 +5626,23 @@ class TalkingHead {
|
|
|
5561
5626
|
fbxBoneNames.add(trackParts[0]);
|
|
5562
5627
|
});
|
|
5563
5628
|
|
|
5564
|
-
console.log('Ready Player Me
|
|
5629
|
+
console.log('=== Ready Player Me Animation Bone Analysis ===');
|
|
5630
|
+
console.log('FBX bone names:', Array.from(fbxBoneNames).sort().join(', '));
|
|
5565
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(', '));
|
|
5566
5646
|
|
|
5567
5647
|
// Filter and map animation tracks
|
|
5568
5648
|
const mappedTracks = [];
|
|
@@ -5582,6 +5662,54 @@ class TalkingHead {
|
|
|
5582
5662
|
const newTrackName = `${mappedBoneName}.${property}`;
|
|
5583
5663
|
const newTrack = track.clone();
|
|
5584
5664
|
newTrack.name = newTrackName;
|
|
5665
|
+
|
|
5666
|
+
// Fix rotations for Ready Player Me arm bones (coordinate system correction)
|
|
5667
|
+
// Ready Player Me uses a different coordinate system, so we need to adjust rotations
|
|
5668
|
+
const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
|
|
5669
|
+
const isLeftSide = mappedBoneName.includes('Left');
|
|
5670
|
+
const isRightSide = mappedBoneName.includes('Right');
|
|
5671
|
+
const isForearm = mappedBoneName.includes('ForeArm') || mappedBoneName.includes('Forearm');
|
|
5672
|
+
const isHand = mappedBoneName.includes('Hand') && !mappedBoneName.includes('Index') &&
|
|
5673
|
+
!mappedBoneName.includes('Thumb') && !mappedBoneName.includes('Middle') &&
|
|
5674
|
+
!mappedBoneName.includes('Ring') && !mappedBoneName.includes('Pinky');
|
|
5675
|
+
|
|
5676
|
+
if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
|
|
5677
|
+
if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
|
|
5678
|
+
// Adjust quaternion rotations for Ready Player Me coordinate system
|
|
5679
|
+
const numKeyframes = newTrack.times.length;
|
|
5680
|
+
for (let i = 0; i < numKeyframes; i++) {
|
|
5681
|
+
const baseIdx = i * 4;
|
|
5682
|
+
if (baseIdx + 3 < newTrack.values.length) {
|
|
5683
|
+
let x = newTrack.values[baseIdx];
|
|
5684
|
+
let y = newTrack.values[baseIdx + 1];
|
|
5685
|
+
let z = newTrack.values[baseIdx + 2];
|
|
5686
|
+
let w = newTrack.values[baseIdx + 3];
|
|
5687
|
+
|
|
5688
|
+
const q = new THREE.Quaternion(x, y, z, w);
|
|
5689
|
+
|
|
5690
|
+
// For Ready Player Me, hands may need coordinate system correction
|
|
5691
|
+
// Hands folding behind suggests we need to rotate around Y axis
|
|
5692
|
+
if (isHand) {
|
|
5693
|
+
// Rotate 180 degrees around Y axis to fix hands folding behind
|
|
5694
|
+
const flipY = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
|
|
5695
|
+
// Apply rotation: q_result = q_original * q_flip (post-multiply)
|
|
5696
|
+
q.multiply(flipY);
|
|
5697
|
+
} else if (isForearm) {
|
|
5698
|
+
// For forearms, try a smaller correction - rotate 90 degrees around Z axis
|
|
5699
|
+
// This might help with the twisting
|
|
5700
|
+
const adjustZ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);
|
|
5701
|
+
q.multiply(adjustZ);
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
newTrack.values[baseIdx] = q.x;
|
|
5705
|
+
newTrack.values[baseIdx + 1] = q.y;
|
|
5706
|
+
newTrack.values[baseIdx + 2] = q.z;
|
|
5707
|
+
newTrack.values[baseIdx + 3] = q.w;
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5585
5713
|
mappedTracks.push(newTrack);
|
|
5586
5714
|
|
|
5587
5715
|
// Store mapping for logging
|
|
@@ -5606,10 +5734,23 @@ class TalkingHead {
|
|
|
5606
5734
|
// Use mapped tracks if we have any, otherwise use original
|
|
5607
5735
|
if (mappedTracks.length > 0) {
|
|
5608
5736
|
anim = new THREE.AnimationClip(anim.name, anim.duration, mappedTracks);
|
|
5737
|
+
console.log(`✓ Created animation with ${mappedTracks.length} mapped tracks (from ${anim.tracks.length} original tracks)`);
|
|
5609
5738
|
if (boneNameMap.size > 0) {
|
|
5610
|
-
console.log(
|
|
5611
|
-
Array.from(boneNameMap.entries()).
|
|
5739
|
+
console.log(`✓ Mapped ${boneNameMap.size} bone(s):`,
|
|
5740
|
+
Array.from(boneNameMap.entries()).map(([from, to]) => `${from}→${to}`).join(', '));
|
|
5612
5741
|
}
|
|
5742
|
+
|
|
5743
|
+
// Check if arm bones were mapped
|
|
5744
|
+
const mappedArmBones = Array.from(boneNameMap.values()).filter(b =>
|
|
5745
|
+
b.includes('Arm') || b.includes('Hand') || b.includes('Shoulder')
|
|
5746
|
+
);
|
|
5747
|
+
if (mappedArmBones.length > 0) {
|
|
5748
|
+
console.log(`✓ Arm bones mapped: ${mappedArmBones.join(', ')}`);
|
|
5749
|
+
} else {
|
|
5750
|
+
console.warn('⚠️ No arm bones were mapped! This may cause arm rigging issues.');
|
|
5751
|
+
}
|
|
5752
|
+
} else {
|
|
5753
|
+
console.error('❌ No tracks could be mapped! Animation may not work correctly.');
|
|
5613
5754
|
}
|
|
5614
5755
|
|
|
5615
5756
|
// Rename and scale Mixamo tracks, create a pose
|