@sage-rsc/talking-head-react 1.1.6 → 1.1.8
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 -9
- package/dist/index.js +1070 -1272
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +137 -536
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -5323,532 +5323,6 @@ class TalkingHead {
|
|
|
5323
5323
|
* @param {number} [ndx=0] Index of the clip
|
|
5324
5324
|
* @param {number} [scale=0.01] Position scale factor
|
|
5325
5325
|
*/
|
|
5326
|
-
/**
|
|
5327
|
-
* Get all bone names from the avatar's armature
|
|
5328
|
-
* @returns {Set<string>} Set of bone names
|
|
5329
|
-
*/
|
|
5330
|
-
getAvailableBoneNames() {
|
|
5331
|
-
const boneNames = new Set();
|
|
5332
|
-
if (!this.armature) return boneNames;
|
|
5333
|
-
|
|
5334
|
-
this.armature.traverse((child) => {
|
|
5335
|
-
if (child.isBone || child.type === 'Bone') {
|
|
5336
|
-
boneNames.add(child.name);
|
|
5337
|
-
}
|
|
5338
|
-
});
|
|
5339
|
-
|
|
5340
|
-
return boneNames;
|
|
5341
|
-
}
|
|
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
|
-
'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',
|
|
5419
|
-
|
|
5420
|
-
// Left arm mapping
|
|
5421
|
-
'L_Upperarm': 'LeftArm',
|
|
5422
|
-
'L_Forearm': 'LeftForeArm',
|
|
5423
|
-
'L_Hand': 'LeftHand',
|
|
5424
|
-
'L_Shoulder': 'LeftShoulder',
|
|
5425
|
-
'L_Clavicle': 'LeftShoulder',
|
|
5426
|
-
'L_UpperArm': 'LeftArm',
|
|
5427
|
-
'L_ForeArm': 'LeftForeArm',
|
|
5428
|
-
'L_Index1': 'LeftHandIndex1',
|
|
5429
|
-
'L_Index2': 'LeftHandIndex2',
|
|
5430
|
-
'L_Index3': 'LeftHandIndex3',
|
|
5431
|
-
'L_Middle1': 'LeftHandMiddle1',
|
|
5432
|
-
'L_Middle2': 'LeftHandMiddle2',
|
|
5433
|
-
'L_Middle3': 'LeftHandMiddle3',
|
|
5434
|
-
'L_Mid1': 'LeftHandMiddle1',
|
|
5435
|
-
'L_Mid2': 'LeftHandMiddle2',
|
|
5436
|
-
'L_Mid3': 'LeftHandMiddle3',
|
|
5437
|
-
'L_Ring1': 'LeftHandRing1',
|
|
5438
|
-
'L_Ring2': 'LeftHandRing2',
|
|
5439
|
-
'L_Ring3': 'LeftHandRing3',
|
|
5440
|
-
'L_Pinky1': 'LeftHandPinky1',
|
|
5441
|
-
'L_Pinky2': 'LeftHandPinky2',
|
|
5442
|
-
'L_Pinky3': 'LeftHandPinky3',
|
|
5443
|
-
'L_Thumb1': 'LeftHandThumb1',
|
|
5444
|
-
'L_Thumb2': 'LeftHandThumb2',
|
|
5445
|
-
'L_Thumb3': 'LeftHandThumb3',
|
|
5446
|
-
|
|
5447
|
-
// Right arm mapping
|
|
5448
|
-
'R_Upperarm': 'RightArm',
|
|
5449
|
-
'R_Forearm': 'RightForeArm',
|
|
5450
|
-
'R_Hand': 'RightHand',
|
|
5451
|
-
'R_Shoulder': 'RightShoulder',
|
|
5452
|
-
'R_Clavicle': 'RightShoulder',
|
|
5453
|
-
'R_UpperArm': 'RightArm',
|
|
5454
|
-
'R_ForeArm': 'RightForeArm',
|
|
5455
|
-
'R_Index1': 'RightHandIndex1',
|
|
5456
|
-
'R_Index2': 'RightHandIndex2',
|
|
5457
|
-
'R_Index3': 'RightHandIndex3',
|
|
5458
|
-
'R_Middle1': 'RightHandMiddle1',
|
|
5459
|
-
'R_Middle2': 'RightHandMiddle2',
|
|
5460
|
-
'R_Middle3': 'RightHandMiddle3',
|
|
5461
|
-
'R_Mid1': 'RightHandMiddle1',
|
|
5462
|
-
'R_Mid2': 'RightHandMiddle2',
|
|
5463
|
-
'R_Mid3': 'RightHandMiddle3',
|
|
5464
|
-
'R_Ring1': 'RightHandRing1',
|
|
5465
|
-
'R_Ring2': 'RightHandRing2',
|
|
5466
|
-
'R_Ring3': 'RightHandRing3',
|
|
5467
|
-
'R_Pinky1': 'RightHandPinky1',
|
|
5468
|
-
'R_Pinky2': 'RightHandPinky2',
|
|
5469
|
-
'R_Pinky3': 'RightHandPinky3',
|
|
5470
|
-
'R_Thumb1': 'RightHandThumb1',
|
|
5471
|
-
'R_Thumb2': 'RightHandThumb2',
|
|
5472
|
-
'R_Thumb3': 'RightHandThumb3',
|
|
5473
|
-
|
|
5474
|
-
// Leg mapping
|
|
5475
|
-
'L_Thigh': 'LeftUpLeg',
|
|
5476
|
-
'L_Calf': 'LeftLeg',
|
|
5477
|
-
'L_Foot': 'LeftFoot',
|
|
5478
|
-
'L_UpLeg': 'LeftUpLeg',
|
|
5479
|
-
'L_Leg': 'LeftLeg',
|
|
5480
|
-
'R_Thigh': 'RightUpLeg',
|
|
5481
|
-
'R_Calf': 'RightLeg',
|
|
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',
|
|
5490
|
-
};
|
|
5491
|
-
|
|
5492
|
-
// Try mapping
|
|
5493
|
-
if (mappings[normalized]) {
|
|
5494
|
-
const mapped = mappings[normalized];
|
|
5495
|
-
if (availableBones.has(mapped)) {
|
|
5496
|
-
return mapped;
|
|
5497
|
-
}
|
|
5498
|
-
}
|
|
5499
|
-
|
|
5500
|
-
// Pattern-based matching for CC_Base and similar naming conventions
|
|
5501
|
-
const lowerNormalized = normalized.toLowerCase();
|
|
5502
|
-
const upperFirst = normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
|
|
5503
|
-
|
|
5504
|
-
// Pattern: R_Index1/2/3 or r_index1/2/3 -> RightHandIndex1/2/3
|
|
5505
|
-
const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
|
|
5506
|
-
if (indexMatch) {
|
|
5507
|
-
const digit = indexMatch[1];
|
|
5508
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5509
|
-
const candidate = `${side}HandIndex${digit}`;
|
|
5510
|
-
if (availableBones.has(candidate)) {
|
|
5511
|
-
return candidate;
|
|
5512
|
-
}
|
|
5513
|
-
}
|
|
5514
|
-
|
|
5515
|
-
// Pattern: R_Pinky1/2/3 -> RightHandPinky1/2/3
|
|
5516
|
-
const pinkyMatch = lowerNormalized.match(/^[rl]_pinky(\d+)$/);
|
|
5517
|
-
if (pinkyMatch) {
|
|
5518
|
-
const digit = pinkyMatch[1];
|
|
5519
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5520
|
-
const candidate = `${side}HandPinky${digit}`;
|
|
5521
|
-
if (availableBones.has(candidate)) {
|
|
5522
|
-
return candidate;
|
|
5523
|
-
}
|
|
5524
|
-
}
|
|
5525
|
-
|
|
5526
|
-
// Pattern: R_Ring1/2/3 -> RightHandRing1/2/3
|
|
5527
|
-
const ringMatch = lowerNormalized.match(/^[rl]_ring(\d+)$/);
|
|
5528
|
-
if (ringMatch) {
|
|
5529
|
-
const digit = ringMatch[1];
|
|
5530
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5531
|
-
const candidate = `${side}HandRing${digit}`;
|
|
5532
|
-
if (availableBones.has(candidate)) {
|
|
5533
|
-
return candidate;
|
|
5534
|
-
}
|
|
5535
|
-
}
|
|
5536
|
-
|
|
5537
|
-
// Pattern: R_Middle1/2/3 or R_Mid1/2/3 -> RightHandMiddle1/2/3
|
|
5538
|
-
const middleMatch = lowerNormalized.match(/^[rl]_(?:middle|mid)(\d+)$/);
|
|
5539
|
-
if (middleMatch) {
|
|
5540
|
-
const digit = middleMatch[1];
|
|
5541
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5542
|
-
const candidate = `${side}HandMiddle${digit}`;
|
|
5543
|
-
if (availableBones.has(candidate)) {
|
|
5544
|
-
return candidate;
|
|
5545
|
-
}
|
|
5546
|
-
}
|
|
5547
|
-
|
|
5548
|
-
// Pattern: R_Thumb1/2/3 -> RightHandThumb1/2/3
|
|
5549
|
-
const thumbMatch = lowerNormalized.match(/^[rl]_thumb(\d+)$/);
|
|
5550
|
-
if (thumbMatch) {
|
|
5551
|
-
const digit = thumbMatch[1];
|
|
5552
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5553
|
-
const candidate = `${side}HandThumb${digit}`;
|
|
5554
|
-
if (availableBones.has(candidate)) {
|
|
5555
|
-
return candidate;
|
|
5556
|
-
}
|
|
5557
|
-
}
|
|
5558
|
-
|
|
5559
|
-
// Pattern: R_Upperarm -> RightArm (case insensitive)
|
|
5560
|
-
if (lowerNormalized.match(/^[rl]_upperarm/)) {
|
|
5561
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5562
|
-
const candidate = `${side}Arm`;
|
|
5563
|
-
if (availableBones.has(candidate)) {
|
|
5564
|
-
return candidate;
|
|
5565
|
-
}
|
|
5566
|
-
}
|
|
5567
|
-
|
|
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')) {
|
|
5574
|
-
return null;
|
|
5575
|
-
}
|
|
5576
|
-
|
|
5577
|
-
// Pattern: R_Forearm -> RightForeArm
|
|
5578
|
-
if (lowerNormalized.match(/^[rl]_forearm/)) {
|
|
5579
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5580
|
-
const candidate = `${side}ForeArm`;
|
|
5581
|
-
if (availableBones.has(candidate)) {
|
|
5582
|
-
return candidate;
|
|
5583
|
-
}
|
|
5584
|
-
}
|
|
5585
|
-
|
|
5586
|
-
// Pattern: R_Hand -> RightHand
|
|
5587
|
-
if (lowerNormalized.match(/^[rl]_hand$/)) {
|
|
5588
|
-
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5589
|
-
const candidate = `${side}Hand`;
|
|
5590
|
-
if (availableBones.has(candidate)) {
|
|
5591
|
-
return candidate;
|
|
5592
|
-
}
|
|
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
|
-
}
|
|
5637
|
-
|
|
5638
|
-
// Try case-insensitive exact match
|
|
5639
|
-
for (const boneName of availableBones) {
|
|
5640
|
-
if (boneName.toLowerCase() === lowerNormalized) {
|
|
5641
|
-
return boneName;
|
|
5642
|
-
}
|
|
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
|
-
}
|
|
5656
|
-
|
|
5657
|
-
return null; // No mapping found
|
|
5658
|
-
}
|
|
5659
|
-
|
|
5660
|
-
/**
|
|
5661
|
-
* Filter animation tracks to only include bones that exist in the avatar
|
|
5662
|
-
* Maps bone names from different naming conventions to avatar bone names
|
|
5663
|
-
* @param {THREE.AnimationClip} clip - Animation clip to filter
|
|
5664
|
-
* @param {Set<string>} availableBones - Set of available bone names
|
|
5665
|
-
* @returns {THREE.AnimationClip} Filtered animation clip with mapped bone names
|
|
5666
|
-
*/
|
|
5667
|
-
filterAnimationTracks(clip, availableBones) {
|
|
5668
|
-
const validTracks = [];
|
|
5669
|
-
const missingBones = new Set();
|
|
5670
|
-
const mappedBones = new Map(); // Track mappings for logging
|
|
5671
|
-
const fbxBoneNames = new Set(); // Track all unique FBX bone names
|
|
5672
|
-
|
|
5673
|
-
// Debug: Log available bones (first time only)
|
|
5674
|
-
if (!this._loggedAvailableBones) {
|
|
5675
|
-
console.log('Available avatar bones:', Array.from(availableBones).sort().join(', '));
|
|
5676
|
-
this._loggedAvailableBones = true;
|
|
5677
|
-
}
|
|
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
|
-
|
|
5690
|
-
clip.tracks.forEach(track => {
|
|
5691
|
-
// Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
|
|
5692
|
-
const trackNameParts = track.name.split('.');
|
|
5693
|
-
const fbxBoneName = trackNameParts[0];
|
|
5694
|
-
const property = trackNameParts[1]; // position, quaternion, rotation, etc.
|
|
5695
|
-
|
|
5696
|
-
// Try to map the bone name
|
|
5697
|
-
const mappedBoneName = this.mapBoneName(fbxBoneName, availableBones);
|
|
5698
|
-
|
|
5699
|
-
if (mappedBoneName) {
|
|
5700
|
-
// Create a new track with the mapped bone name
|
|
5701
|
-
const newTrackName = `${mappedBoneName}.${property}`;
|
|
5702
|
-
|
|
5703
|
-
// Clone the track and update its name
|
|
5704
|
-
const newTrack = track.clone();
|
|
5705
|
-
newTrack.name = newTrackName;
|
|
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
|
-
|
|
5773
|
-
validTracks.push(newTrack);
|
|
5774
|
-
|
|
5775
|
-
// Track mappings for logging
|
|
5776
|
-
if (fbxBoneName !== mappedBoneName) {
|
|
5777
|
-
mappedBones.set(fbxBoneName, mappedBoneName);
|
|
5778
|
-
}
|
|
5779
|
-
} else {
|
|
5780
|
-
missingBones.add(fbxBoneName);
|
|
5781
|
-
}
|
|
5782
|
-
});
|
|
5783
|
-
|
|
5784
|
-
// Log results with detailed mapping information
|
|
5785
|
-
console.log(`\n=== Mapping Results for "${clip.name}" ===`);
|
|
5786
|
-
|
|
5787
|
-
// Show all mapped bones
|
|
5788
|
-
if (mappedBones.size > 0) {
|
|
5789
|
-
console.info(`✓ Successfully mapped ${mappedBones.size} bone(s):`);
|
|
5790
|
-
Array.from(mappedBones.entries()).forEach(([from, to]) => {
|
|
5791
|
-
console.log(` ${from} → ${to}`);
|
|
5792
|
-
});
|
|
5793
|
-
}
|
|
5794
|
-
|
|
5795
|
-
// Show all missing bones with suggestions
|
|
5796
|
-
if (missingBones.size > 0) {
|
|
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
|
-
});
|
|
5814
|
-
}
|
|
5815
|
-
|
|
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) {
|
|
5842
|
-
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
|
|
5843
|
-
}
|
|
5844
|
-
|
|
5845
|
-
// Create a new clip with only valid tracks
|
|
5846
|
-
if (validTracks.length === 0) {
|
|
5847
|
-
return null;
|
|
5848
|
-
}
|
|
5849
|
-
|
|
5850
|
-
return new THREE.AnimationClip(clip.name, clip.duration, validTracks);
|
|
5851
|
-
}
|
|
5852
5326
|
|
|
5853
5327
|
async playAnimation(url, onprogress=null, dur=10, ndx=0, scale=0.01, disablePositionLock=false) {
|
|
5854
5328
|
if ( !this.armature ) return;
|
|
@@ -5997,19 +5471,146 @@ class TalkingHead {
|
|
|
5997
5471
|
if ( fbx && fbx.animations && fbx.animations[ndx] ) {
|
|
5998
5472
|
let anim = fbx.animations[ndx];
|
|
5999
5473
|
|
|
6000
|
-
// Get available bone names from avatar skeleton
|
|
6001
|
-
const availableBones =
|
|
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 mixamorig prefix if present
|
|
5493
|
+
let normalized = fbxBoneName.replace(/^mixamorig/i, '');
|
|
5494
|
+
if (availableBones.has(normalized)) {
|
|
5495
|
+
return normalized;
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
// Common bone name mappings for Ready Player Me / Mixamo
|
|
5499
|
+
const mappings = {
|
|
5500
|
+
// Arm bones - handle common variations
|
|
5501
|
+
'LeftArm': 'LeftArm',
|
|
5502
|
+
'leftArm': 'LeftArm',
|
|
5503
|
+
'LEFTARM': 'LeftArm',
|
|
5504
|
+
'RightArm': 'RightArm',
|
|
5505
|
+
'rightArm': 'RightArm',
|
|
5506
|
+
'RIGHTARM': 'RightArm',
|
|
5507
|
+
'LeftForeArm': 'LeftForeArm',
|
|
5508
|
+
'leftForeArm': 'LeftForeArm',
|
|
5509
|
+
'leftForearm': 'LeftForeArm',
|
|
5510
|
+
'LeftForearm': 'LeftForeArm',
|
|
5511
|
+
'RightForeArm': 'RightForeArm',
|
|
5512
|
+
'rightForeArm': 'RightForeArm',
|
|
5513
|
+
'rightForearm': 'RightForeArm',
|
|
5514
|
+
'RightForearm': 'RightForeArm',
|
|
5515
|
+
'LeftHand': 'LeftHand',
|
|
5516
|
+
'leftHand': 'LeftHand',
|
|
5517
|
+
'RightHand': 'RightHand',
|
|
5518
|
+
'rightHand': 'RightHand',
|
|
5519
|
+
'LeftShoulder': 'LeftShoulder',
|
|
5520
|
+
'leftShoulder': 'LeftShoulder',
|
|
5521
|
+
'RightShoulder': 'RightShoulder',
|
|
5522
|
+
'rightShoulder': 'RightShoulder',
|
|
5523
|
+
// Spine
|
|
5524
|
+
'Spine': 'Spine1',
|
|
5525
|
+
'spine': 'Spine1',
|
|
5526
|
+
'Spine1': 'Spine1',
|
|
5527
|
+
'Spine2': 'Spine2',
|
|
5528
|
+
// Head/Neck
|
|
5529
|
+
'Head': 'Head',
|
|
5530
|
+
'head': 'Head',
|
|
5531
|
+
'Neck': 'Neck',
|
|
5532
|
+
'neck': 'Neck',
|
|
5533
|
+
// Hips
|
|
5534
|
+
'Hips': 'Hips',
|
|
5535
|
+
'hips': 'Hips',
|
|
5536
|
+
'Root': 'Hips',
|
|
5537
|
+
'root': 'Hips',
|
|
5538
|
+
};
|
|
5539
|
+
|
|
5540
|
+
if (mappings[normalized]) {
|
|
5541
|
+
const mapped = mappings[normalized];
|
|
5542
|
+
if (availableBones.has(mapped)) {
|
|
5543
|
+
return mapped;
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
// Try case-insensitive match
|
|
5548
|
+
for (const boneName of availableBones) {
|
|
5549
|
+
if (boneName.toLowerCase() === normalized.toLowerCase()) {
|
|
5550
|
+
return boneName;
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
return null; // No mapping found
|
|
5555
|
+
};
|
|
5556
|
+
|
|
5557
|
+
// Debug: Log FBX bone names and avatar bone names for comparison
|
|
5558
|
+
const fbxBoneNames = new Set();
|
|
5559
|
+
anim.tracks.forEach(track => {
|
|
5560
|
+
const trackParts = track.name.split('.');
|
|
5561
|
+
fbxBoneNames.add(trackParts[0]);
|
|
5562
|
+
});
|
|
6002
5563
|
|
|
6003
|
-
|
|
6004
|
-
|
|
5564
|
+
console.log('Ready Player Me FBX bone names:', Array.from(fbxBoneNames).sort().join(', '));
|
|
5565
|
+
console.log('Avatar skeleton bone names:', Array.from(availableBones).sort().join(', '));
|
|
5566
|
+
|
|
5567
|
+
// Filter and map animation tracks
|
|
5568
|
+
const mappedTracks = [];
|
|
5569
|
+
const unmappedBones = new Set();
|
|
5570
|
+
anim.tracks.forEach(track => {
|
|
5571
|
+
// Remove mixamorig prefix first
|
|
5572
|
+
let trackName = track.name.replaceAll('mixamorig', '');
|
|
5573
|
+
const trackParts = trackName.split('.');
|
|
5574
|
+
const fbxBoneName = trackParts[0];
|
|
5575
|
+
const property = trackParts[1];
|
|
5576
|
+
|
|
5577
|
+
// Map bone name to avatar skeleton
|
|
5578
|
+
const mappedBoneName = mapBoneName(fbxBoneName);
|
|
5579
|
+
|
|
5580
|
+
if (mappedBoneName && property) {
|
|
5581
|
+
// Create new track with mapped bone name
|
|
5582
|
+
const newTrackName = `${mappedBoneName}.${property}`;
|
|
5583
|
+
const newTrack = track.clone();
|
|
5584
|
+
newTrack.name = newTrackName;
|
|
5585
|
+
mappedTracks.push(newTrack);
|
|
5586
|
+
|
|
5587
|
+
// Store mapping for logging
|
|
5588
|
+
if (fbxBoneName !== mappedBoneName) {
|
|
5589
|
+
boneNameMap.set(fbxBoneName, mappedBoneName);
|
|
5590
|
+
}
|
|
5591
|
+
} else {
|
|
5592
|
+
unmappedBones.add(fbxBoneName);
|
|
5593
|
+
// Log unmapped bones (especially arm bones)
|
|
5594
|
+
if (fbxBoneName.toLowerCase().includes('arm') ||
|
|
5595
|
+
fbxBoneName.toLowerCase().includes('hand') ||
|
|
5596
|
+
fbxBoneName.toLowerCase().includes('shoulder')) {
|
|
5597
|
+
console.warn(`⚠️ Arm bone "${fbxBoneName}" could not be mapped to avatar skeleton`);
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
});
|
|
6005
5601
|
|
|
6006
|
-
if (
|
|
6007
|
-
console.
|
|
6008
|
-
|
|
5602
|
+
if (unmappedBones.size > 0) {
|
|
5603
|
+
console.warn(`⚠️ ${unmappedBones.size} bone(s) could not be mapped:`, Array.from(unmappedBones).sort().join(', '));
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5606
|
+
// Use mapped tracks if we have any, otherwise use original
|
|
5607
|
+
if (mappedTracks.length > 0) {
|
|
5608
|
+
anim = new THREE.AnimationClip(anim.name, anim.duration, mappedTracks);
|
|
5609
|
+
if (boneNameMap.size > 0) {
|
|
5610
|
+
console.log(`Mapped ${boneNameMap.size} bone(s) for Ready Player Me animation:`,
|
|
5611
|
+
Array.from(boneNameMap.entries()).slice(0, 5).map(([from, to]) => `${from}→${to}`).join(', '));
|
|
5612
|
+
}
|
|
6009
5613
|
}
|
|
6010
|
-
|
|
6011
|
-
// Use the filtered animation instead of the original
|
|
6012
|
-
anim = filteredAnim;
|
|
6013
5614
|
|
|
6014
5615
|
// Rename and scale Mixamo tracks, create a pose
|
|
6015
5616
|
const props = {};
|