@sage-rsc/talking-head-react 1.1.2 → 1.1.5
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 +591 -553
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +107 -8
package/package.json
CHANGED
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -2253,11 +2253,11 @@ class TalkingHead {
|
|
|
2253
2253
|
if (this.lockedPosition && this.armature) {
|
|
2254
2254
|
// Enforce the locked position - keep avatar exactly where it was locked
|
|
2255
2255
|
// This prevents FBX animations from moving the avatar
|
|
2256
|
-
|
|
2257
|
-
|
|
2256
|
+
this.armature.position.set(
|
|
2257
|
+
this.lockedPosition.x,
|
|
2258
2258
|
this.lockedPosition.y,
|
|
2259
|
-
|
|
2260
|
-
|
|
2259
|
+
this.lockedPosition.z
|
|
2260
|
+
);
|
|
2261
2261
|
}
|
|
2262
2262
|
}
|
|
2263
2263
|
|
|
@@ -5354,6 +5354,7 @@ class TalkingHead {
|
|
|
5354
5354
|
|
|
5355
5355
|
// Remove common prefixes
|
|
5356
5356
|
let normalized = fbxBoneName;
|
|
5357
|
+
const originalNormalized = normalized;
|
|
5357
5358
|
|
|
5358
5359
|
// Remove CC_Base prefix (Character Creator)
|
|
5359
5360
|
if (normalized.startsWith('CC_Base_')) {
|
|
@@ -5367,6 +5368,15 @@ class TalkingHead {
|
|
|
5367
5368
|
if (availableBones.has(normalized)) {
|
|
5368
5369
|
return normalized;
|
|
5369
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
|
+
}
|
|
5370
5380
|
|
|
5371
5381
|
// Handle numbered bones (e.g., Spine01 -> Spine1)
|
|
5372
5382
|
if (normalized.match(/^Spine\d+$/)) {
|
|
@@ -5409,6 +5419,9 @@ class TalkingHead {
|
|
|
5409
5419
|
'L_Middle1': 'LeftHandMiddle1',
|
|
5410
5420
|
'L_Middle2': 'LeftHandMiddle2',
|
|
5411
5421
|
'L_Middle3': 'LeftHandMiddle3',
|
|
5422
|
+
'L_Mid1': 'LeftHandMiddle1',
|
|
5423
|
+
'L_Mid2': 'LeftHandMiddle2',
|
|
5424
|
+
'L_Mid3': 'LeftHandMiddle3',
|
|
5412
5425
|
'L_Ring1': 'LeftHandRing1',
|
|
5413
5426
|
'L_Ring2': 'LeftHandRing2',
|
|
5414
5427
|
'L_Ring3': 'LeftHandRing3',
|
|
@@ -5430,6 +5443,9 @@ class TalkingHead {
|
|
|
5430
5443
|
'R_Middle1': 'RightHandMiddle1',
|
|
5431
5444
|
'R_Middle2': 'RightHandMiddle2',
|
|
5432
5445
|
'R_Middle3': 'RightHandMiddle3',
|
|
5446
|
+
'R_Mid1': 'RightHandMiddle1',
|
|
5447
|
+
'R_Mid2': 'RightHandMiddle2',
|
|
5448
|
+
'R_Mid3': 'RightHandMiddle3',
|
|
5433
5449
|
'R_Ring1': 'RightHandRing1',
|
|
5434
5450
|
'R_Ring2': 'RightHandRing2',
|
|
5435
5451
|
'R_Ring3': 'RightHandRing3',
|
|
@@ -5459,8 +5475,9 @@ class TalkingHead {
|
|
|
5459
5475
|
|
|
5460
5476
|
// Pattern-based matching for CC_Base and similar naming conventions
|
|
5461
5477
|
const lowerNormalized = normalized.toLowerCase();
|
|
5478
|
+
const upperFirst = normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
|
|
5462
5479
|
|
|
5463
|
-
// Pattern: R_Index1/2/3 -> RightHandIndex1/2/3
|
|
5480
|
+
// Pattern: R_Index1/2/3 or r_index1/2/3 -> RightHandIndex1/2/3
|
|
5464
5481
|
const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
|
|
5465
5482
|
if (indexMatch) {
|
|
5466
5483
|
const digit = indexMatch[1];
|
|
@@ -5493,8 +5510,8 @@ class TalkingHead {
|
|
|
5493
5510
|
}
|
|
5494
5511
|
}
|
|
5495
5512
|
|
|
5496
|
-
// Pattern: R_Middle1/2/3 -> RightHandMiddle1/2/3
|
|
5497
|
-
const middleMatch = lowerNormalized.match(/^[rl]
|
|
5513
|
+
// Pattern: R_Middle1/2/3 or R_Mid1/2/3 -> RightHandMiddle1/2/3
|
|
5514
|
+
const middleMatch = lowerNormalized.match(/^[rl]_(?:middle|mid)(\d+)$/);
|
|
5498
5515
|
if (middleMatch) {
|
|
5499
5516
|
const digit = middleMatch[1];
|
|
5500
5517
|
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
@@ -5515,7 +5532,7 @@ class TalkingHead {
|
|
|
5515
5532
|
}
|
|
5516
5533
|
}
|
|
5517
5534
|
|
|
5518
|
-
// Pattern: R_Upperarm -> RightArm
|
|
5535
|
+
// Pattern: R_Upperarm -> RightArm (case insensitive)
|
|
5519
5536
|
if (lowerNormalized.match(/^[rl]_upperarm/)) {
|
|
5520
5537
|
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5521
5538
|
const candidate = `${side}Arm`;
|
|
@@ -5524,6 +5541,15 @@ class TalkingHead {
|
|
|
5524
5541
|
}
|
|
5525
5542
|
}
|
|
5526
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
|
+
|
|
5527
5553
|
// Pattern: R_Forearm -> RightForeArm
|
|
5528
5554
|
if (lowerNormalized.match(/^[rl]_forearm/)) {
|
|
5529
5555
|
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
@@ -5564,6 +5590,13 @@ class TalkingHead {
|
|
|
5564
5590
|
const missingBones = new Set();
|
|
5565
5591
|
const mappedBones = new Map(); // Track mappings for logging
|
|
5566
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
|
+
|
|
5567
5600
|
clip.tracks.forEach(track => {
|
|
5568
5601
|
// Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
|
|
5569
5602
|
const trackNameParts = track.name.split('.');
|
|
@@ -5581,6 +5614,72 @@ class TalkingHead {
|
|
|
5581
5614
|
const newTrack = track.clone();
|
|
5582
5615
|
newTrack.name = newTrackName;
|
|
5583
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
|
+
|
|
5584
5683
|
validTracks.push(newTrack);
|
|
5585
5684
|
|
|
5586
5685
|
// Track mappings for logging
|