@sage-rsc/talking-head-react 1.1.1 → 1.1.3
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 +707 -552
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +278 -14
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
|
|
|
@@ -5340,38 +5340,302 @@ class TalkingHead {
|
|
|
5340
5340
|
return boneNames;
|
|
5341
5341
|
}
|
|
5342
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_Ring1': 'LeftHandRing1',
|
|
5423
|
+
'L_Ring2': 'LeftHandRing2',
|
|
5424
|
+
'L_Ring3': 'LeftHandRing3',
|
|
5425
|
+
'L_Pinky1': 'LeftHandPinky1',
|
|
5426
|
+
'L_Pinky2': 'LeftHandPinky2',
|
|
5427
|
+
'L_Pinky3': 'LeftHandPinky3',
|
|
5428
|
+
'L_Thumb1': 'LeftHandThumb1',
|
|
5429
|
+
'L_Thumb2': 'LeftHandThumb2',
|
|
5430
|
+
'L_Thumb3': 'LeftHandThumb3',
|
|
5431
|
+
|
|
5432
|
+
// Right arm mapping
|
|
5433
|
+
'R_Upperarm': 'RightArm',
|
|
5434
|
+
'R_Forearm': 'RightForeArm',
|
|
5435
|
+
'R_Hand': 'RightHand',
|
|
5436
|
+
'R_Shoulder': 'RightShoulder',
|
|
5437
|
+
'R_Index1': 'RightHandIndex1',
|
|
5438
|
+
'R_Index2': 'RightHandIndex2',
|
|
5439
|
+
'R_Index3': 'RightHandIndex3',
|
|
5440
|
+
'R_Middle1': 'RightHandMiddle1',
|
|
5441
|
+
'R_Middle2': 'RightHandMiddle2',
|
|
5442
|
+
'R_Middle3': 'RightHandMiddle3',
|
|
5443
|
+
'R_Ring1': 'RightHandRing1',
|
|
5444
|
+
'R_Ring2': 'RightHandRing2',
|
|
5445
|
+
'R_Ring3': 'RightHandRing3',
|
|
5446
|
+
'R_Pinky1': 'RightHandPinky1',
|
|
5447
|
+
'R_Pinky2': 'RightHandPinky2',
|
|
5448
|
+
'R_Pinky3': 'RightHandPinky3',
|
|
5449
|
+
'R_Thumb1': 'RightHandThumb1',
|
|
5450
|
+
'R_Thumb2': 'RightHandThumb2',
|
|
5451
|
+
'R_Thumb3': 'RightHandThumb3',
|
|
5452
|
+
|
|
5453
|
+
// Leg mapping
|
|
5454
|
+
'L_Thigh': 'LeftUpLeg',
|
|
5455
|
+
'L_Calf': 'LeftLeg',
|
|
5456
|
+
'L_Foot': 'LeftFoot',
|
|
5457
|
+
'R_Thigh': 'RightUpLeg',
|
|
5458
|
+
'R_Calf': 'RightLeg',
|
|
5459
|
+
'R_Foot': 'RightFoot',
|
|
5460
|
+
};
|
|
5461
|
+
|
|
5462
|
+
// Try mapping
|
|
5463
|
+
if (mappings[normalized]) {
|
|
5464
|
+
const mapped = mappings[normalized];
|
|
5465
|
+
if (availableBones.has(mapped)) {
|
|
5466
|
+
return mapped;
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
// Pattern-based matching for CC_Base and similar naming conventions
|
|
5471
|
+
const lowerNormalized = normalized.toLowerCase();
|
|
5472
|
+
const upperFirst = normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
|
|
5473
|
+
|
|
5474
|
+
// Pattern: R_Index1/2/3 or r_index1/2/3 -> RightHandIndex1/2/3
|
|
5475
|
+
const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
|
|
5476
|
+
if (indexMatch) {
|
|
5477
|
+
const digit = indexMatch[1];
|
|
5478
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5479
|
+
const candidate = `${side}HandIndex${digit}`;
|
|
5480
|
+
if (availableBones.has(candidate)) {
|
|
5481
|
+
return candidate;
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5485
|
+
// Pattern: R_Pinky1/2/3 -> RightHandPinky1/2/3
|
|
5486
|
+
const pinkyMatch = lowerNormalized.match(/^[rl]_pinky(\d+)$/);
|
|
5487
|
+
if (pinkyMatch) {
|
|
5488
|
+
const digit = pinkyMatch[1];
|
|
5489
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5490
|
+
const candidate = `${side}HandPinky${digit}`;
|
|
5491
|
+
if (availableBones.has(candidate)) {
|
|
5492
|
+
return candidate;
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
// Pattern: R_Ring1/2/3 -> RightHandRing1/2/3
|
|
5497
|
+
const ringMatch = lowerNormalized.match(/^[rl]_ring(\d+)$/);
|
|
5498
|
+
if (ringMatch) {
|
|
5499
|
+
const digit = ringMatch[1];
|
|
5500
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5501
|
+
const candidate = `${side}HandRing${digit}`;
|
|
5502
|
+
if (availableBones.has(candidate)) {
|
|
5503
|
+
return candidate;
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
// Pattern: R_Middle1/2/3 -> RightHandMiddle1/2/3
|
|
5508
|
+
const middleMatch = lowerNormalized.match(/^[rl]_middle(\d+)$/);
|
|
5509
|
+
if (middleMatch) {
|
|
5510
|
+
const digit = middleMatch[1];
|
|
5511
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5512
|
+
const candidate = `${side}HandMiddle${digit}`;
|
|
5513
|
+
if (availableBones.has(candidate)) {
|
|
5514
|
+
return candidate;
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
|
|
5518
|
+
// Pattern: R_Thumb1/2/3 -> RightHandThumb1/2/3
|
|
5519
|
+
const thumbMatch = lowerNormalized.match(/^[rl]_thumb(\d+)$/);
|
|
5520
|
+
if (thumbMatch) {
|
|
5521
|
+
const digit = thumbMatch[1];
|
|
5522
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5523
|
+
const candidate = `${side}HandThumb${digit}`;
|
|
5524
|
+
if (availableBones.has(candidate)) {
|
|
5525
|
+
return candidate;
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
// Pattern: R_Upperarm -> RightArm (case insensitive)
|
|
5530
|
+
if (lowerNormalized.match(/^[rl]_upperarm/)) {
|
|
5531
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5532
|
+
const candidate = `${side}Arm`;
|
|
5533
|
+
if (availableBones.has(candidate)) {
|
|
5534
|
+
return candidate;
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
// Pattern: R_UpperarmTwist01/02 -> ignore (twist bones)
|
|
5539
|
+
if (lowerNormalized.includes('upperarmtwist') || lowerNormalized.includes('forearmtwist')) {
|
|
5540
|
+
return null;
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
// Pattern: R_Forearm -> RightForeArm
|
|
5544
|
+
if (lowerNormalized.match(/^[rl]_forearm/)) {
|
|
5545
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5546
|
+
const candidate = `${side}ForeArm`;
|
|
5547
|
+
if (availableBones.has(candidate)) {
|
|
5548
|
+
return candidate;
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
// Pattern: R_Hand -> RightHand
|
|
5553
|
+
if (lowerNormalized.match(/^[rl]_hand$/)) {
|
|
5554
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5555
|
+
const candidate = `${side}Hand`;
|
|
5556
|
+
if (availableBones.has(candidate)) {
|
|
5557
|
+
return candidate;
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
|
|
5561
|
+
// Try case-insensitive exact match
|
|
5562
|
+
for (const boneName of availableBones) {
|
|
5563
|
+
if (boneName.toLowerCase() === lowerNormalized) {
|
|
5564
|
+
return boneName;
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
return null; // No mapping found
|
|
5569
|
+
}
|
|
5570
|
+
|
|
5343
5571
|
/**
|
|
5344
5572
|
* Filter animation tracks to only include bones that exist in the avatar
|
|
5573
|
+
* Maps bone names from different naming conventions to avatar bone names
|
|
5345
5574
|
* @param {THREE.AnimationClip} clip - Animation clip to filter
|
|
5346
5575
|
* @param {Set<string>} availableBones - Set of available bone names
|
|
5347
|
-
* @returns {THREE.AnimationClip} Filtered animation clip
|
|
5576
|
+
* @returns {THREE.AnimationClip} Filtered animation clip with mapped bone names
|
|
5348
5577
|
*/
|
|
5349
5578
|
filterAnimationTracks(clip, availableBones) {
|
|
5350
5579
|
const validTracks = [];
|
|
5351
5580
|
const missingBones = new Set();
|
|
5581
|
+
const mappedBones = new Map(); // Track mappings for logging
|
|
5582
|
+
|
|
5583
|
+
// Debug: Log available bones (first time only)
|
|
5584
|
+
if (!this._loggedAvailableBones) {
|
|
5585
|
+
console.log('Available avatar bones:', Array.from(availableBones).sort().slice(0, 50).join(', '),
|
|
5586
|
+
availableBones.size > 50 ? `... (${availableBones.size} total)` : '');
|
|
5587
|
+
this._loggedAvailableBones = true;
|
|
5588
|
+
}
|
|
5352
5589
|
|
|
5353
5590
|
clip.tracks.forEach(track => {
|
|
5354
5591
|
// Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
|
|
5355
5592
|
const trackNameParts = track.name.split('.');
|
|
5356
|
-
const
|
|
5593
|
+
const fbxBoneName = trackNameParts[0];
|
|
5594
|
+
const property = trackNameParts[1]; // position, quaternion, rotation, etc.
|
|
5595
|
+
|
|
5596
|
+
// Try to map the bone name
|
|
5597
|
+
const mappedBoneName = this.mapBoneName(fbxBoneName, availableBones);
|
|
5357
5598
|
|
|
5358
|
-
if (
|
|
5359
|
-
|
|
5599
|
+
if (mappedBoneName) {
|
|
5600
|
+
// Create a new track with the mapped bone name
|
|
5601
|
+
const newTrackName = `${mappedBoneName}.${property}`;
|
|
5602
|
+
|
|
5603
|
+
// Clone the track and update its name
|
|
5604
|
+
const newTrack = track.clone();
|
|
5605
|
+
newTrack.name = newTrackName;
|
|
5606
|
+
|
|
5607
|
+
validTracks.push(newTrack);
|
|
5608
|
+
|
|
5609
|
+
// Track mappings for logging
|
|
5610
|
+
if (fbxBoneName !== mappedBoneName) {
|
|
5611
|
+
mappedBones.set(fbxBoneName, mappedBoneName);
|
|
5612
|
+
}
|
|
5360
5613
|
} else {
|
|
5361
|
-
missingBones.add(
|
|
5614
|
+
missingBones.add(fbxBoneName);
|
|
5362
5615
|
}
|
|
5363
5616
|
});
|
|
5364
5617
|
|
|
5618
|
+
// Log results
|
|
5619
|
+
if (mappedBones.size > 0) {
|
|
5620
|
+
console.info(`FBX animation "${clip.name}": Mapped ${mappedBones.size} bone(s) to avatar skeleton:`,
|
|
5621
|
+
Array.from(mappedBones.entries()).slice(0, 5).map(([from, to]) => `${from} → ${to}`).join(', '),
|
|
5622
|
+
mappedBones.size > 5 ? '...' : '');
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5365
5625
|
if (missingBones.size > 0) {
|
|
5366
|
-
console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s)
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5626
|
+
console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s) that couldn't be mapped:`,
|
|
5627
|
+
Array.from(missingBones).slice(0, 10).join(', '),
|
|
5628
|
+
missingBones.size > 10 ? '...' : '');
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
if (validTracks.length > 0) {
|
|
5632
|
+
console.info(`Filtered ${clip.tracks.length} tracks down to ${validTracks.length} valid tracks (${mappedBones.size} mapped)`);
|
|
5633
|
+
} else {
|
|
5634
|
+
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
|
|
5370
5635
|
}
|
|
5371
5636
|
|
|
5372
5637
|
// Create a new clip with only valid tracks
|
|
5373
5638
|
if (validTracks.length === 0) {
|
|
5374
|
-
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing from avatar skeleton.`);
|
|
5375
5639
|
return null;
|
|
5376
5640
|
}
|
|
5377
5641
|
|