@sage-rsc/talking-head-react 1.1.0 → 1.1.2
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 +686 -551
- package/package.json +1 -1
- package/src/lib/talkinghead.mjs +275 -55
package/package.json
CHANGED
package/src/lib/talkinghead.mjs
CHANGED
|
@@ -2214,38 +2214,35 @@ class TalkingHead {
|
|
|
2214
2214
|
console.log('Original position stored:', this.originalPosition);
|
|
2215
2215
|
}
|
|
2216
2216
|
|
|
2217
|
-
//
|
|
2218
|
-
// Set a higher base position so FBX animations land in the center
|
|
2219
|
-
const upwardOffset = 2.0; // Even more upward movement
|
|
2220
|
-
|
|
2217
|
+
// Lock the avatar at its CURRENT position (don't move it)
|
|
2221
2218
|
this.lockedPosition = {
|
|
2222
|
-
x:
|
|
2223
|
-
y:
|
|
2224
|
-
z:
|
|
2219
|
+
x: this.armature.position.x,
|
|
2220
|
+
y: this.armature.position.y,
|
|
2221
|
+
z: this.armature.position.z
|
|
2225
2222
|
};
|
|
2226
2223
|
|
|
2227
|
-
|
|
2228
|
-
this.armature.position.set(
|
|
2229
|
-
this.lockedPosition.x,
|
|
2230
|
-
this.lockedPosition.y,
|
|
2231
|
-
this.lockedPosition.z
|
|
2232
|
-
);
|
|
2233
|
-
|
|
2234
|
-
console.log('BEFORE: Avatar position was:', this.armature.position.x, this.armature.position.y, this.armature.position.z);
|
|
2235
|
-
console.log('AFTER: Avatar position moved up and locked at:', this.lockedPosition);
|
|
2236
|
-
console.log('Current view:', this.viewName);
|
|
2224
|
+
console.log('Avatar position locked at current position:', this.lockedPosition);
|
|
2237
2225
|
}
|
|
2238
2226
|
|
|
2239
2227
|
/**
|
|
2240
|
-
* Unlock avatar position and
|
|
2228
|
+
* Unlock avatar position and restore original position.
|
|
2241
2229
|
*/
|
|
2242
2230
|
unlockAvatarPosition() {
|
|
2243
|
-
if (this.armature) {
|
|
2244
|
-
//
|
|
2231
|
+
if (this.armature && this.originalPosition) {
|
|
2232
|
+
// Restore avatar to its original position before locking
|
|
2233
|
+
this.armature.position.set(
|
|
2234
|
+
this.originalPosition.x,
|
|
2235
|
+
this.originalPosition.y,
|
|
2236
|
+
this.originalPosition.z
|
|
2237
|
+
);
|
|
2238
|
+
console.log('Avatar position restored to original:', this.originalPosition);
|
|
2239
|
+
} else if (this.armature) {
|
|
2240
|
+
// Fallback: reset to center if no original position was stored
|
|
2245
2241
|
this.armature.position.set(0, 0, 0);
|
|
2246
2242
|
console.log('Avatar position reset to center (0,0,0)');
|
|
2247
2243
|
}
|
|
2248
2244
|
this.lockedPosition = null;
|
|
2245
|
+
this.originalPosition = null; // Clear original position after unlock
|
|
2249
2246
|
console.log('Avatar position unlocked');
|
|
2250
2247
|
}
|
|
2251
2248
|
|
|
@@ -2254,31 +2251,13 @@ class TalkingHead {
|
|
|
2254
2251
|
*/
|
|
2255
2252
|
maintainLockedPosition() {
|
|
2256
2253
|
if (this.lockedPosition && this.armature) {
|
|
2257
|
-
//
|
|
2258
|
-
// This
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
// If moved too far down, bring it back up
|
|
2265
|
-
this.armature.position.set(
|
|
2266
|
-
this.lockedPosition.x,
|
|
2267
|
-
minY,
|
|
2268
|
-
this.lockedPosition.z
|
|
2269
|
-
);
|
|
2270
|
-
} else if (currentY > maxY) {
|
|
2271
|
-
// If moved too far up, bring it back down
|
|
2272
|
-
this.armature.position.set(
|
|
2273
|
-
this.lockedPosition.x,
|
|
2274
|
-
maxY,
|
|
2275
|
-
this.lockedPosition.z
|
|
2276
|
-
);
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
// Always maintain X and Z position
|
|
2280
|
-
this.armature.position.x = this.lockedPosition.x;
|
|
2281
|
-
this.armature.position.z = this.lockedPosition.z;
|
|
2254
|
+
// Enforce the locked position - keep avatar exactly where it was locked
|
|
2255
|
+
// This prevents FBX animations from moving the avatar
|
|
2256
|
+
this.armature.position.set(
|
|
2257
|
+
this.lockedPosition.x,
|
|
2258
|
+
this.lockedPosition.y,
|
|
2259
|
+
this.lockedPosition.z
|
|
2260
|
+
);
|
|
2282
2261
|
}
|
|
2283
2262
|
}
|
|
2284
2263
|
|
|
@@ -5361,38 +5340,279 @@ class TalkingHead {
|
|
|
5361
5340
|
return boneNames;
|
|
5362
5341
|
}
|
|
5363
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
|
+
|
|
5358
|
+
// Remove CC_Base prefix (Character Creator)
|
|
5359
|
+
if (normalized.startsWith('CC_Base_')) {
|
|
5360
|
+
normalized = normalized.replace('CC_Base_', '');
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
// Remove mixamorig prefix (Mixamo)
|
|
5364
|
+
normalized = normalized.replace(/^mixamorig/i, '');
|
|
5365
|
+
|
|
5366
|
+
// Try direct match after prefix removal
|
|
5367
|
+
if (availableBones.has(normalized)) {
|
|
5368
|
+
return normalized;
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
// Handle numbered bones (e.g., Spine01 -> Spine1)
|
|
5372
|
+
if (normalized.match(/^Spine\d+$/)) {
|
|
5373
|
+
const num = normalized.match(/\d+/)?.[0];
|
|
5374
|
+
if (num) {
|
|
5375
|
+
const mapped = `Spine${parseInt(num)}`;
|
|
5376
|
+
if (availableBones.has(mapped)) {
|
|
5377
|
+
return mapped;
|
|
5378
|
+
}
|
|
5379
|
+
// Try Spine1 if Spine01, Spine2 if Spine02+
|
|
5380
|
+
if (num === '01' && availableBones.has('Spine1')) {
|
|
5381
|
+
return 'Spine1';
|
|
5382
|
+
}
|
|
5383
|
+
if (parseInt(num) >= 2 && availableBones.has('Spine2')) {
|
|
5384
|
+
return 'Spine2';
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
// Handle twist bones (ignore them, they're usually not in standard skeletons)
|
|
5390
|
+
if (normalized.includes('Twist')) {
|
|
5391
|
+
return null;
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
// Mapping rules for common bone name patterns
|
|
5395
|
+
const mappings = {
|
|
5396
|
+
// Spine mapping
|
|
5397
|
+
'Spine01': 'Spine1',
|
|
5398
|
+
'Spine02': 'Spine2',
|
|
5399
|
+
'Spine03': 'Spine2',
|
|
5400
|
+
|
|
5401
|
+
// Left arm mapping
|
|
5402
|
+
'L_Upperarm': 'LeftArm',
|
|
5403
|
+
'L_Forearm': 'LeftForeArm',
|
|
5404
|
+
'L_Hand': 'LeftHand',
|
|
5405
|
+
'L_Shoulder': 'LeftShoulder',
|
|
5406
|
+
'L_Index1': 'LeftHandIndex1',
|
|
5407
|
+
'L_Index2': 'LeftHandIndex2',
|
|
5408
|
+
'L_Index3': 'LeftHandIndex3',
|
|
5409
|
+
'L_Middle1': 'LeftHandMiddle1',
|
|
5410
|
+
'L_Middle2': 'LeftHandMiddle2',
|
|
5411
|
+
'L_Middle3': 'LeftHandMiddle3',
|
|
5412
|
+
'L_Ring1': 'LeftHandRing1',
|
|
5413
|
+
'L_Ring2': 'LeftHandRing2',
|
|
5414
|
+
'L_Ring3': 'LeftHandRing3',
|
|
5415
|
+
'L_Pinky1': 'LeftHandPinky1',
|
|
5416
|
+
'L_Pinky2': 'LeftHandPinky2',
|
|
5417
|
+
'L_Pinky3': 'LeftHandPinky3',
|
|
5418
|
+
'L_Thumb1': 'LeftHandThumb1',
|
|
5419
|
+
'L_Thumb2': 'LeftHandThumb2',
|
|
5420
|
+
'L_Thumb3': 'LeftHandThumb3',
|
|
5421
|
+
|
|
5422
|
+
// Right arm mapping
|
|
5423
|
+
'R_Upperarm': 'RightArm',
|
|
5424
|
+
'R_Forearm': 'RightForeArm',
|
|
5425
|
+
'R_Hand': 'RightHand',
|
|
5426
|
+
'R_Shoulder': 'RightShoulder',
|
|
5427
|
+
'R_Index1': 'RightHandIndex1',
|
|
5428
|
+
'R_Index2': 'RightHandIndex2',
|
|
5429
|
+
'R_Index3': 'RightHandIndex3',
|
|
5430
|
+
'R_Middle1': 'RightHandMiddle1',
|
|
5431
|
+
'R_Middle2': 'RightHandMiddle2',
|
|
5432
|
+
'R_Middle3': 'RightHandMiddle3',
|
|
5433
|
+
'R_Ring1': 'RightHandRing1',
|
|
5434
|
+
'R_Ring2': 'RightHandRing2',
|
|
5435
|
+
'R_Ring3': 'RightHandRing3',
|
|
5436
|
+
'R_Pinky1': 'RightHandPinky1',
|
|
5437
|
+
'R_Pinky2': 'RightHandPinky2',
|
|
5438
|
+
'R_Pinky3': 'RightHandPinky3',
|
|
5439
|
+
'R_Thumb1': 'RightHandThumb1',
|
|
5440
|
+
'R_Thumb2': 'RightHandThumb2',
|
|
5441
|
+
'R_Thumb3': 'RightHandThumb3',
|
|
5442
|
+
|
|
5443
|
+
// Leg mapping
|
|
5444
|
+
'L_Thigh': 'LeftUpLeg',
|
|
5445
|
+
'L_Calf': 'LeftLeg',
|
|
5446
|
+
'L_Foot': 'LeftFoot',
|
|
5447
|
+
'R_Thigh': 'RightUpLeg',
|
|
5448
|
+
'R_Calf': 'RightLeg',
|
|
5449
|
+
'R_Foot': 'RightFoot',
|
|
5450
|
+
};
|
|
5451
|
+
|
|
5452
|
+
// Try mapping
|
|
5453
|
+
if (mappings[normalized]) {
|
|
5454
|
+
const mapped = mappings[normalized];
|
|
5455
|
+
if (availableBones.has(mapped)) {
|
|
5456
|
+
return mapped;
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
// Pattern-based matching for CC_Base and similar naming conventions
|
|
5461
|
+
const lowerNormalized = normalized.toLowerCase();
|
|
5462
|
+
|
|
5463
|
+
// Pattern: R_Index1/2/3 -> RightHandIndex1/2/3
|
|
5464
|
+
const indexMatch = lowerNormalized.match(/^[rl]_index(\d+)$/);
|
|
5465
|
+
if (indexMatch) {
|
|
5466
|
+
const digit = indexMatch[1];
|
|
5467
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5468
|
+
const candidate = `${side}HandIndex${digit}`;
|
|
5469
|
+
if (availableBones.has(candidate)) {
|
|
5470
|
+
return candidate;
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
|
|
5474
|
+
// Pattern: R_Pinky1/2/3 -> RightHandPinky1/2/3
|
|
5475
|
+
const pinkyMatch = lowerNormalized.match(/^[rl]_pinky(\d+)$/);
|
|
5476
|
+
if (pinkyMatch) {
|
|
5477
|
+
const digit = pinkyMatch[1];
|
|
5478
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5479
|
+
const candidate = `${side}HandPinky${digit}`;
|
|
5480
|
+
if (availableBones.has(candidate)) {
|
|
5481
|
+
return candidate;
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5485
|
+
// Pattern: R_Ring1/2/3 -> RightHandRing1/2/3
|
|
5486
|
+
const ringMatch = lowerNormalized.match(/^[rl]_ring(\d+)$/);
|
|
5487
|
+
if (ringMatch) {
|
|
5488
|
+
const digit = ringMatch[1];
|
|
5489
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5490
|
+
const candidate = `${side}HandRing${digit}`;
|
|
5491
|
+
if (availableBones.has(candidate)) {
|
|
5492
|
+
return candidate;
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
// Pattern: R_Middle1/2/3 -> RightHandMiddle1/2/3
|
|
5497
|
+
const middleMatch = lowerNormalized.match(/^[rl]_middle(\d+)$/);
|
|
5498
|
+
if (middleMatch) {
|
|
5499
|
+
const digit = middleMatch[1];
|
|
5500
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5501
|
+
const candidate = `${side}HandMiddle${digit}`;
|
|
5502
|
+
if (availableBones.has(candidate)) {
|
|
5503
|
+
return candidate;
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
// Pattern: R_Thumb1/2/3 -> RightHandThumb1/2/3
|
|
5508
|
+
const thumbMatch = lowerNormalized.match(/^[rl]_thumb(\d+)$/);
|
|
5509
|
+
if (thumbMatch) {
|
|
5510
|
+
const digit = thumbMatch[1];
|
|
5511
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5512
|
+
const candidate = `${side}HandThumb${digit}`;
|
|
5513
|
+
if (availableBones.has(candidate)) {
|
|
5514
|
+
return candidate;
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
|
|
5518
|
+
// Pattern: R_Upperarm -> RightArm
|
|
5519
|
+
if (lowerNormalized.match(/^[rl]_upperarm/)) {
|
|
5520
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5521
|
+
const candidate = `${side}Arm`;
|
|
5522
|
+
if (availableBones.has(candidate)) {
|
|
5523
|
+
return candidate;
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
// Pattern: R_Forearm -> RightForeArm
|
|
5528
|
+
if (lowerNormalized.match(/^[rl]_forearm/)) {
|
|
5529
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5530
|
+
const candidate = `${side}ForeArm`;
|
|
5531
|
+
if (availableBones.has(candidate)) {
|
|
5532
|
+
return candidate;
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
// Pattern: R_Hand -> RightHand
|
|
5537
|
+
if (lowerNormalized.match(/^[rl]_hand$/)) {
|
|
5538
|
+
const side = lowerNormalized.startsWith('r') ? 'Right' : 'Left';
|
|
5539
|
+
const candidate = `${side}Hand`;
|
|
5540
|
+
if (availableBones.has(candidate)) {
|
|
5541
|
+
return candidate;
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5545
|
+
// Try case-insensitive exact match
|
|
5546
|
+
for (const boneName of availableBones) {
|
|
5547
|
+
if (boneName.toLowerCase() === lowerNormalized) {
|
|
5548
|
+
return boneName;
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
return null; // No mapping found
|
|
5553
|
+
}
|
|
5554
|
+
|
|
5364
5555
|
/**
|
|
5365
5556
|
* Filter animation tracks to only include bones that exist in the avatar
|
|
5557
|
+
* Maps bone names from different naming conventions to avatar bone names
|
|
5366
5558
|
* @param {THREE.AnimationClip} clip - Animation clip to filter
|
|
5367
5559
|
* @param {Set<string>} availableBones - Set of available bone names
|
|
5368
|
-
* @returns {THREE.AnimationClip} Filtered animation clip
|
|
5560
|
+
* @returns {THREE.AnimationClip} Filtered animation clip with mapped bone names
|
|
5369
5561
|
*/
|
|
5370
5562
|
filterAnimationTracks(clip, availableBones) {
|
|
5371
5563
|
const validTracks = [];
|
|
5372
5564
|
const missingBones = new Set();
|
|
5565
|
+
const mappedBones = new Map(); // Track mappings for logging
|
|
5373
5566
|
|
|
5374
5567
|
clip.tracks.forEach(track => {
|
|
5375
5568
|
// Extract bone name from track name (e.g., "CC_Base_R_Index3.position" -> "CC_Base_R_Index3")
|
|
5376
5569
|
const trackNameParts = track.name.split('.');
|
|
5377
|
-
const
|
|
5570
|
+
const fbxBoneName = trackNameParts[0];
|
|
5571
|
+
const property = trackNameParts[1]; // position, quaternion, rotation, etc.
|
|
5572
|
+
|
|
5573
|
+
// Try to map the bone name
|
|
5574
|
+
const mappedBoneName = this.mapBoneName(fbxBoneName, availableBones);
|
|
5378
5575
|
|
|
5379
|
-
if (
|
|
5380
|
-
|
|
5576
|
+
if (mappedBoneName) {
|
|
5577
|
+
// Create a new track with the mapped bone name
|
|
5578
|
+
const newTrackName = `${mappedBoneName}.${property}`;
|
|
5579
|
+
|
|
5580
|
+
// Clone the track and update its name
|
|
5581
|
+
const newTrack = track.clone();
|
|
5582
|
+
newTrack.name = newTrackName;
|
|
5583
|
+
|
|
5584
|
+
validTracks.push(newTrack);
|
|
5585
|
+
|
|
5586
|
+
// Track mappings for logging
|
|
5587
|
+
if (fbxBoneName !== mappedBoneName) {
|
|
5588
|
+
mappedBones.set(fbxBoneName, mappedBoneName);
|
|
5589
|
+
}
|
|
5381
5590
|
} else {
|
|
5382
|
-
missingBones.add(
|
|
5591
|
+
missingBones.add(fbxBoneName);
|
|
5383
5592
|
}
|
|
5384
5593
|
});
|
|
5385
5594
|
|
|
5595
|
+
// Log results
|
|
5596
|
+
if (mappedBones.size > 0) {
|
|
5597
|
+
console.info(`FBX animation "${clip.name}": Mapped ${mappedBones.size} bone(s) to avatar skeleton:`,
|
|
5598
|
+
Array.from(mappedBones.entries()).slice(0, 5).map(([from, to]) => `${from} → ${to}`).join(', '),
|
|
5599
|
+
mappedBones.size > 5 ? '...' : '');
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5386
5602
|
if (missingBones.size > 0) {
|
|
5387
|
-
console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s)
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5603
|
+
console.warn(`FBX animation "${clip.name}" contains tracks for ${missingBones.size} bone(s) that couldn't be mapped:`,
|
|
5604
|
+
Array.from(missingBones).slice(0, 10).join(', '),
|
|
5605
|
+
missingBones.size > 10 ? '...' : '');
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
if (validTracks.length > 0) {
|
|
5609
|
+
console.info(`Filtered ${clip.tracks.length} tracks down to ${validTracks.length} valid tracks (${mappedBones.size} mapped)`);
|
|
5610
|
+
} else {
|
|
5611
|
+
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing or couldn't be mapped.`);
|
|
5391
5612
|
}
|
|
5392
5613
|
|
|
5393
5614
|
// Create a new clip with only valid tracks
|
|
5394
5615
|
if (validTracks.length === 0) {
|
|
5395
|
-
console.error(`No valid tracks found for animation "${clip.name}". All bones are missing from avatar skeleton.`);
|
|
5396
5616
|
return null;
|
|
5397
5617
|
}
|
|
5398
5618
|
|