@sage-rsc/talking-head-react 1.1.3 → 1.1.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/README.md +2 -2
- package/dist/index.cjs +9 -2
- package/dist/index.js +718 -624
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +224 -16
package/package.json
CHANGED
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -5407,18 +5407,33 @@ class TalkingHead {
|
|
|
5407
5407
|
'Spine01': 'Spine1',
|
|
5408
5408
|
'Spine02': 'Spine2',
|
|
5409
5409
|
'Spine03': 'Spine2',
|
|
5410
|
+
'Spine1': 'Spine1',
|
|
5411
|
+
'Spine2': 'Spine2',
|
|
5412
|
+
'Spine': 'Spine1',
|
|
5413
|
+
|
|
5414
|
+
// Head and neck
|
|
5415
|
+
'Head': 'Head',
|
|
5416
|
+
'Neck': 'Neck',
|
|
5417
|
+
'Neck1': 'Neck',
|
|
5418
|
+
'Neck2': 'Neck',
|
|
5410
5419
|
|
|
5411
5420
|
// Left arm mapping
|
|
5412
5421
|
'L_Upperarm': 'LeftArm',
|
|
5413
5422
|
'L_Forearm': 'LeftForeArm',
|
|
5414
5423
|
'L_Hand': 'LeftHand',
|
|
5415
5424
|
'L_Shoulder': 'LeftShoulder',
|
|
5425
|
+
'L_Clavicle': 'LeftShoulder',
|
|
5426
|
+
'L_UpperArm': 'LeftArm',
|
|
5427
|
+
'L_ForeArm': 'LeftForeArm',
|
|
5416
5428
|
'L_Index1': 'LeftHandIndex1',
|
|
5417
5429
|
'L_Index2': 'LeftHandIndex2',
|
|
5418
5430
|
'L_Index3': 'LeftHandIndex3',
|
|
5419
5431
|
'L_Middle1': 'LeftHandMiddle1',
|
|
5420
5432
|
'L_Middle2': 'LeftHandMiddle2',
|
|
5421
5433
|
'L_Middle3': 'LeftHandMiddle3',
|
|
5434
|
+
'L_Mid1': 'LeftHandMiddle1',
|
|
5435
|
+
'L_Mid2': 'LeftHandMiddle2',
|
|
5436
|
+
'L_Mid3': 'LeftHandMiddle3',
|
|
5422
5437
|
'L_Ring1': 'LeftHandRing1',
|
|
5423
5438
|
'L_Ring2': 'LeftHandRing2',
|
|
5424
5439
|
'L_Ring3': 'LeftHandRing3',
|
|
@@ -5434,12 +5449,18 @@ class TalkingHead {
|
|
|
5434
5449
|
'R_Forearm': 'RightForeArm',
|
|
5435
5450
|
'R_Hand': 'RightHand',
|
|
5436
5451
|
'R_Shoulder': 'RightShoulder',
|
|
5452
|
+
'R_Clavicle': 'RightShoulder',
|
|
5453
|
+
'R_UpperArm': 'RightArm',
|
|
5454
|
+
'R_ForeArm': 'RightForeArm',
|
|
5437
5455
|
'R_Index1': 'RightHandIndex1',
|
|
5438
5456
|
'R_Index2': 'RightHandIndex2',
|
|
5439
5457
|
'R_Index3': 'RightHandIndex3',
|
|
5440
5458
|
'R_Middle1': 'RightHandMiddle1',
|
|
5441
5459
|
'R_Middle2': 'RightHandMiddle2',
|
|
5442
5460
|
'R_Middle3': 'RightHandMiddle3',
|
|
5461
|
+
'R_Mid1': 'RightHandMiddle1',
|
|
5462
|
+
'R_Mid2': 'RightHandMiddle2',
|
|
5463
|
+
'R_Mid3': 'RightHandMiddle3',
|
|
5443
5464
|
'R_Ring1': 'RightHandRing1',
|
|
5444
5465
|
'R_Ring2': 'RightHandRing2',
|
|
5445
5466
|
'R_Ring3': 'RightHandRing3',
|
|
@@ -5454,9 +5475,18 @@ class TalkingHead {
|
|
|
5454
5475
|
'L_Thigh': 'LeftUpLeg',
|
|
5455
5476
|
'L_Calf': 'LeftLeg',
|
|
5456
5477
|
'L_Foot': 'LeftFoot',
|
|
5478
|
+
'L_UpLeg': 'LeftUpLeg',
|
|
5479
|
+
'L_Leg': 'LeftLeg',
|
|
5457
5480
|
'R_Thigh': 'RightUpLeg',
|
|
5458
5481
|
'R_Calf': 'RightLeg',
|
|
5459
5482
|
'R_Foot': 'RightFoot',
|
|
5483
|
+
'R_UpLeg': 'RightUpLeg',
|
|
5484
|
+
'R_Leg': 'RightLeg',
|
|
5485
|
+
|
|
5486
|
+
// Root/Hips
|
|
5487
|
+
'Hips': 'Hips',
|
|
5488
|
+
'Root': 'Hips',
|
|
5489
|
+
'Pelvis': 'Hips',
|
|
5460
5490
|
};
|
|
5461
5491
|
|
|
5462
5492
|
// Try mapping
|
|
@@ -5504,8 +5534,8 @@ class TalkingHead {
|
|
|
5504
5534
|
}
|
|
5505
5535
|
}
|
|
5506
5536
|
|
|
5507
|
-
// Pattern: R_Middle1/2/3 -> RightHandMiddle1/2/3
|
|
5508
|
-
const middleMatch = lowerNormalized.match(/^[rl]
|
|
5537
|
+
// Pattern: R_Middle1/2/3 or R_Mid1/2/3 -> RightHandMiddle1/2/3
|
|
5538
|
+
const middleMatch = lowerNormalized.match(/^[rl]_(?:middle|mid)(\d+)$/);
|
|
5509
5539
|
if (middleMatch) {
|
|
5510
5540
|
const digit = middleMatch[1];
|
|
5511
5541
|
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
@@ -5535,8 +5565,12 @@ class TalkingHead {
|
|
|
5535
5565
|
}
|
|
5536
5566
|
}
|
|
5537
5567
|
|
|
5538
|
-
// Pattern: R_UpperarmTwist01/02 -> ignore (twist bones)
|
|
5539
|
-
if (lowerNormalized.includes('upperarmtwist') ||
|
|
5568
|
+
// Pattern: R_UpperarmTwist01/02 -> ignore (twist bones and other extra bones)
|
|
5569
|
+
if (lowerNormalized.includes('upperarmtwist') ||
|
|
5570
|
+
lowerNormalized.includes('forearmtwist') ||
|
|
5571
|
+
lowerNormalized.includes('ribstwist') ||
|
|
5572
|
+
lowerNormalized.includes('breast') ||
|
|
5573
|
+
lowerNormalized.includes('twist')) {
|
|
5540
5574
|
return null;
|
|
5541
5575
|
}
|
|
5542
5576
|
|
|
@@ -5557,6 +5591,49 @@ class TalkingHead {
|
|
|
5557
5591
|
return candidate;
|
|
5558
5592
|
}
|
|
5559
5593
|
}
|
|
5594
|
+
|
|
5595
|
+
// Pattern: R_Clavicle -> RightShoulder
|
|
5596
|
+
if (lowerNormalized.match(/^[rl]_clavicle/)) {
|
|
5597
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5598
|
+
const candidate = `${side}Shoulder`;
|
|
5599
|
+
if (availableBones.has(candidate)) {
|
|
5600
|
+
return candidate;
|
|
5601
|
+
}
|
|
5602
|
+
}
|
|
5603
|
+
|
|
5604
|
+
// Pattern: R_Shoulder -> RightShoulder
|
|
5605
|
+
if (lowerNormalized.match(/^[rl]_shoulder/)) {
|
|
5606
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5607
|
+
const candidate = `${side}Shoulder`;
|
|
5608
|
+
if (availableBones.has(candidate)) {
|
|
5609
|
+
return candidate;
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5613
|
+
// Pattern: Spine01/02/03 -> Spine1/Spine2
|
|
5614
|
+
if (lowerNormalized.match(/^spine0?(\d+)$/)) {
|
|
5615
|
+
const spineNum = lowerNormalized.match(/^spine0?(\d+)$/)[1];
|
|
5616
|
+
if (spineNum === '1') {
|
|
5617
|
+
if (availableBones.has('Spine1')) return 'Spine1';
|
|
5618
|
+
} else if (spineNum === '2' || spineNum === '3') {
|
|
5619
|
+
if (availableBones.has('Spine2')) return 'Spine2';
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
// Pattern: Neck1/2 -> Neck
|
|
5624
|
+
if (lowerNormalized.match(/^neck\d*$/)) {
|
|
5625
|
+
if (availableBones.has('Neck')) return 'Neck';
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
// Pattern: Head -> Head
|
|
5629
|
+
if (lowerNormalized === 'head') {
|
|
5630
|
+
if (availableBones.has('Head')) return 'Head';
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
// Pattern: Hips/Pelvis/Root -> Hips
|
|
5634
|
+
if (lowerNormalized.match(/^(hips|pelvis|root)$/)) {
|
|
5635
|
+
if (availableBones.has('Hips')) return 'Hips';
|
|
5636
|
+
}
|
|
5560
5637
|
|
|
5561
5638
|
// Try case-insensitive exact match
|
|
5562
5639
|
for (const boneName of availableBones) {
|
|
@@ -5564,6 +5641,18 @@ class TalkingHead {
|
|
|
5564
5641
|
return boneName;
|
|
5565
5642
|
}
|
|
5566
5643
|
}
|
|
5644
|
+
|
|
5645
|
+
// Try partial match (contains)
|
|
5646
|
+
for (const boneName of availableBones) {
|
|
5647
|
+
const boneLower = boneName.toLowerCase();
|
|
5648
|
+
if (boneLower.includes(lowerNormalized) || lowerNormalized.includes(boneLower)) {
|
|
5649
|
+
// Prefer exact matches or very close matches
|
|
5650
|
+
if (boneLower === lowerNormalized ||
|
|
5651
|
+
boneLower.replace(/[^a-z0-9]/g, '') === lowerNormalized.replace(/[^a-z0-9]/g, '')) {
|
|
5652
|
+
return boneName;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5567
5656
|
|
|
5568
5657
|
return null; // No mapping found
|
|
5569
5658
|
}
|
|
@@ -5579,14 +5668,25 @@ class TalkingHead {
|
|
|
5579
5668
|
const validTracks = [];
|
|
5580
5669
|
const missingBones = new Set();
|
|
5581
5670
|
const mappedBones = new Map(); // Track mappings for logging
|
|
5671
|
+
const fbxBoneNames = new Set(); // Track all unique FBX bone names
|
|
5582
5672
|
|
|
5583
5673
|
// Debug: Log available bones (first time only)
|
|
5584
5674
|
if (!this._loggedAvailableBones) {
|
|
5585
|
-
console.log('Available avatar bones:', Array.from(availableBones).sort().
|
|
5586
|
-
availableBones.size > 50 ? `... (${availableBones.size} total)` : '');
|
|
5675
|
+
console.log('Available avatar bones:', Array.from(availableBones).sort().join(', '));
|
|
5587
5676
|
this._loggedAvailableBones = true;
|
|
5588
5677
|
}
|
|
5589
5678
|
|
|
5679
|
+
// First pass: collect all unique FBX bone names
|
|
5680
|
+
clip.tracks.forEach(track => {
|
|
5681
|
+
const trackNameParts = track.name.split('.');
|
|
5682
|
+
const fbxBoneName = trackNameParts[0];
|
|
5683
|
+
fbxBoneNames.add(fbxBoneName);
|
|
5684
|
+
});
|
|
5685
|
+
|
|
5686
|
+
console.log(`\n=== FBX Animation "${clip.name}" Bone Analysis ===`);
|
|
5687
|
+
console.log('FBX bone names in animation:', Array.from(fbxBoneNames).sort().join(', '));
|
|
5688
|
+
console.log(`Total FBX bones: ${fbxBoneNames.size}, Avatar bones: ${availableBones.size}\n`);
|
|
5689
|
+
|
|
5590
5690
|
clip.tracks.forEach(track => {
|
|
5591
5691
|
// Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
|
|
5592
5692
|
const trackNameParts = track.name.split('.');
|
|
@@ -5604,6 +5704,72 @@ class TalkingHead {
|
|
|
5604
5704
|
const newTrack = track.clone();
|
|
5605
5705
|
newTrack.name = newTrackName;
|
|
5606
5706
|
|
|
5707
|
+
// Fix rotations for arm/hand bones that might be inverted
|
|
5708
|
+
// If hands are folding behind instead of in front, we need to adjust rotations
|
|
5709
|
+
const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
|
|
5710
|
+
const isForearmBone = mappedBoneName.includes('ForeArm');
|
|
5711
|
+
|
|
5712
|
+
if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
|
|
5713
|
+
// For quaternion tracks, we might need to adjust the rotation
|
|
5714
|
+
// Check if this is a quaternion track
|
|
5715
|
+
if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
|
|
5716
|
+
// Quaternion format: [x, y, z, w] per keyframe
|
|
5717
|
+
// For arm bones, we might need to invert Y or Z rotation
|
|
5718
|
+
// Adjust quaternion values to fix hand position
|
|
5719
|
+
const numKeyframes = newTrack.times.length;
|
|
5720
|
+
for (let i = 0; i < numKeyframes; i++) {
|
|
5721
|
+
const baseIdx = i * 4;
|
|
5722
|
+
if (baseIdx + 3 < newTrack.values.length) {
|
|
5723
|
+
// Get quaternion values
|
|
5724
|
+
let x = newTrack.values[baseIdx];
|
|
5725
|
+
let y = newTrack.values[baseIdx + 1];
|
|
5726
|
+
let z = newTrack.values[baseIdx + 2];
|
|
5727
|
+
let w = newTrack.values[baseIdx + 3];
|
|
5728
|
+
|
|
5729
|
+
// For arms, adjust rotation to flip hands from back to front
|
|
5730
|
+
// This is a common fix for FBX animations with different coordinate systems
|
|
5731
|
+
if (isForearmBone || mappedBoneName.includes('Hand')) {
|
|
5732
|
+
// Rotate 180 degrees around X axis to flip hands from behind to in front
|
|
5733
|
+
// Create a 180-degree rotation around X axis
|
|
5734
|
+
const flipAngle = Math.PI;
|
|
5735
|
+
const flipX = Math.cos(flipAngle / 2); // w component
|
|
5736
|
+
const flipY = Math.sin(flipAngle / 2); // x component (axis X = 1,0,0)
|
|
5737
|
+
|
|
5738
|
+
// Multiply quaternions: q_result = q_flip * q_original
|
|
5739
|
+
// For rotation around X axis: q_flip = (sin(θ/2), 0, 0, cos(θ/2))
|
|
5740
|
+
const qw = flipX * w - flipY * x;
|
|
5741
|
+
const qx = flipX * x + flipY * w;
|
|
5742
|
+
const qy = flipX * y - flipY * z;
|
|
5743
|
+
const qz = flipX * z + flipY * y;
|
|
5744
|
+
|
|
5745
|
+
x = qx;
|
|
5746
|
+
y = qy;
|
|
5747
|
+
z = qz;
|
|
5748
|
+
w = qw;
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
newTrack.values[baseIdx] = x;
|
|
5752
|
+
newTrack.values[baseIdx + 1] = y;
|
|
5753
|
+
newTrack.values[baseIdx + 2] = z;
|
|
5754
|
+
newTrack.values[baseIdx + 3] = w;
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
} else if (property === 'rotation' && newTrack.values && newTrack.values.length >= 3) {
|
|
5758
|
+
// Euler rotation format: [x, y, z] per keyframe
|
|
5759
|
+
const numKeyframes = newTrack.times.length;
|
|
5760
|
+
for (let i = 0; i < numKeyframes; i++) {
|
|
5761
|
+
const baseIdx = i * 3;
|
|
5762
|
+
if (baseIdx + 2 < newTrack.values.length) {
|
|
5763
|
+
// For arm bones, adjust Y rotation to flip hands
|
|
5764
|
+
if (isForearmBone || mappedBoneName.includes('Hand')) {
|
|
5765
|
+
// Add 180 degrees (PI radians) to Y rotation
|
|
5766
|
+
newTrack.values[baseIdx + 1] += Math.PI;
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5607
5773
|
validTracks.push(newTrack);
|
|
5608
5774
|
|
|
5609
5775
|
// Track mappings for logging
|
|
@@ -5615,22 +5781,64 @@ class TalkingHead {
|
|
|
5615
5781
|
}
|
|
5616
5782
|
});
|
|
5617
5783
|
|
|
5618
|
-
// Log results
|
|
5784
|
+
// Log results with detailed mapping information
|
|
5785
|
+
console.log(`\n=== Mapping Results for "${clip.name}" ===`);
|
|
5786
|
+
|
|
5787
|
+
// Show all mapped bones
|
|
5619
5788
|
if (mappedBones.size > 0) {
|
|
5620
|
-
console.info(
|
|
5621
|
-
|
|
5622
|
-
|
|
5789
|
+
console.info(`✓ Successfully mapped ${mappedBones.size} bone(s):`);
|
|
5790
|
+
Array.from(mappedBones.entries()).forEach(([from, to]) => {
|
|
5791
|
+
console.log(` ${from} → ${to}`);
|
|
5792
|
+
});
|
|
5623
5793
|
}
|
|
5624
5794
|
|
|
5795
|
+
// Show all missing bones with suggestions
|
|
5625
5796
|
if (missingBones.size > 0) {
|
|
5626
|
-
console.warn(
|
|
5627
|
-
|
|
5628
|
-
|
|
5797
|
+
console.warn(`\n✗ Could not map ${missingBones.size} bone(s):`);
|
|
5798
|
+
Array.from(missingBones).sort().forEach(boneName => {
|
|
5799
|
+
console.log(` - ${boneName}`);
|
|
5800
|
+
// Try to suggest a potential mapping
|
|
5801
|
+
const normalized = boneName.replace(/^(CC_Base_|mixamorig)/i, '').replace(/^[RL]_/, (match) => match.toLowerCase());
|
|
5802
|
+
const suggestions = [];
|
|
5803
|
+
for (const avatarBone of availableBones) {
|
|
5804
|
+
const avatarLower = avatarBone.toLowerCase();
|
|
5805
|
+
const normalizedLower = normalized.toLowerCase();
|
|
5806
|
+
if (avatarLower.includes(normalizedLower) || normalizedLower.includes(avatarLower)) {
|
|
5807
|
+
suggestions.push(avatarBone);
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
if (suggestions.length > 0) {
|
|
5811
|
+
console.log(` → Possible matches: ${suggestions.slice(0, 3).join(', ')}`);
|
|
5812
|
+
}
|
|
5813
|
+
});
|
|
5629
5814
|
}
|
|
5630
5815
|
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5816
|
+
// Show bones that matched directly (no mapping needed)
|
|
5817
|
+
const directMatches = new Set();
|
|
5818
|
+
fbxBoneNames.forEach(fbxBone => {
|
|
5819
|
+
if (availableBones.has(fbxBone) && !mappedBones.has(fbxBone)) {
|
|
5820
|
+
directMatches.add(fbxBone);
|
|
5821
|
+
}
|
|
5822
|
+
});
|
|
5823
|
+
if (directMatches.size > 0) {
|
|
5824
|
+
console.info(`\n✓ Direct matches (no mapping needed): ${directMatches.size} bone(s)`);
|
|
5825
|
+
Array.from(directMatches).sort().slice(0, 10).forEach(bone => {
|
|
5826
|
+
console.log(` - ${bone}`);
|
|
5827
|
+
});
|
|
5828
|
+
if (directMatches.size > 10) {
|
|
5829
|
+
console.log(` ... and ${directMatches.size - 10} more`);
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
console.log(`\n=== Summary ===`);
|
|
5834
|
+
console.log(`Total FBX tracks: ${clip.tracks.length}`);
|
|
5835
|
+
console.log(`Valid tracks: ${validTracks.length}`);
|
|
5836
|
+
console.log(`Mapped bones: ${mappedBones.size}`);
|
|
5837
|
+
console.log(`Direct matches: ${directMatches.size}`);
|
|
5838
|
+
console.log(`Missing bones: ${missingBones.size}`);
|
|
5839
|
+
console.log(`========================================\n`);
|
|
5840
|
+
|
|
5841
|
+
if (validTracks.length === 0) {
|
|
5634
5842
|
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
|
|
5635
5843
|
}
|
|
5636
5844
|
|