@lovelace_lol/loom3 1.0.38 → 1.0.40
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 +46 -26
- package/dist/index.cjs +196 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +197 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,14 +33,14 @@ These demo site links open the LoomLarge drawer on the matching tab. The demo si
|
|
|
33
33
|
|
|
34
34
|
| Goal | Open in LoomLarge |
|
|
35
35
|
|------|-------------------|
|
|
36
|
-
| Start with the main runtime surface | [Animation tab](https://
|
|
37
|
-
| Inspect preset and profile settings | [Properties tab](https://
|
|
38
|
-
| Inspect AU, morph, and bone routing | [Mappings tab](https://
|
|
39
|
-
| Inspect meshes and material state | [Meshes tab](https://
|
|
40
|
-
| Inspect resolved bones | [Bones tab](https://
|
|
41
|
-
| Tune expressions and continuum pairs | [Action Units tab](https://
|
|
42
|
-
| Inspect lip-sync views | [Visemes tab](https://
|
|
43
|
-
| Tune hair behavior | [Hair tab](https://
|
|
36
|
+
| Start with the main runtime surface | [Animation tab](https://www.characterloom.com/?drawer=open&tab=animation) |
|
|
37
|
+
| Inspect preset and profile settings | [Properties tab](https://www.characterloom.com/?drawer=open&tab=properties) |
|
|
38
|
+
| Inspect AU, morph, and bone routing | [Mappings tab](https://www.characterloom.com/?drawer=open&tab=mappings) |
|
|
39
|
+
| Inspect meshes and material state | [Meshes tab](https://www.characterloom.com/?drawer=open&tab=meshes) |
|
|
40
|
+
| Inspect resolved bones | [Bones tab](https://www.characterloom.com/?drawer=open&tab=bones) |
|
|
41
|
+
| Tune expressions and continuum pairs | [Action Units tab](https://www.characterloom.com/?drawer=open&tab=action-units) |
|
|
42
|
+
| Inspect lip-sync views | [Visemes tab](https://www.characterloom.com/?drawer=open&tab=visemes) and [Speech tab](https://www.characterloom.com/?drawer=open&tab=speech) |
|
|
43
|
+
| Tune hair behavior | [Hair tab](https://www.characterloom.com/?drawer=open&tab=hair) |
|
|
44
44
|
|
|
45
45
|
Most screenshots below were captured from LoomLarge with the matching tab open so the docs and the live product are easy to compare. The viseme grid image is the main exception: it still shows older labels from the captured UI, so the viseme table later in the README should be treated as the source of truth.
|
|
46
46
|
|
|
@@ -84,7 +84,7 @@ Additional:
|
|
|
84
84
|
|
|
85
85
|
## 1. Installation & Setup
|
|
86
86
|
|
|
87
|
-
Open in LoomLarge: [Animation tab](https://
|
|
87
|
+
Open in LoomLarge: [Animation tab](https://www.characterloom.com/?drawer=open&tab=animation)
|
|
88
88
|
|
|
89
89
|

|
|
90
90
|
|
|
@@ -201,7 +201,7 @@ const meshes = collectMorphMeshes(gltf.scene);
|
|
|
201
201
|
|
|
202
202
|
## 2. Using Presets
|
|
203
203
|
|
|
204
|
-
Open in LoomLarge: [Properties tab](https://
|
|
204
|
+
Open in LoomLarge: [Properties tab](https://www.characterloom.com/?drawer=open&tab=properties) | [Mappings tab](https://www.characterloom.com/?drawer=open&tab=mappings)
|
|
205
205
|
|
|
206
206
|

|
|
207
207
|
|
|
@@ -376,7 +376,7 @@ see [ANNOTATION_CONFIGURATION.md](./ANNOTATION_CONFIGURATION.md).
|
|
|
376
376
|
|
|
377
377
|
## 3. Preset Selection & Validation
|
|
378
378
|
|
|
379
|
-
Open in LoomLarge: [Properties tab](https://
|
|
379
|
+
Open in LoomLarge: [Properties tab](https://www.characterloom.com/?drawer=open&tab=properties) | [Mappings tab](https://www.characterloom.com/?drawer=open&tab=mappings) | [Bones tab](https://www.characterloom.com/?drawer=open&tab=bones)
|
|
380
380
|
|
|
381
381
|
Before you tune AUs or hand-edit a profile, confirm that you picked the right preset and that the model actually matches it. Loom3 exposes a full preset-selection and validation workflow, not just low-level control APIs.
|
|
382
382
|
|
|
@@ -477,7 +477,7 @@ Use this section when you need to:
|
|
|
477
477
|
|
|
478
478
|
## 4. Getting to Know Your Character
|
|
479
479
|
|
|
480
|
-
Open in LoomLarge: [Meshes tab](https://
|
|
480
|
+
Open in LoomLarge: [Meshes tab](https://www.characterloom.com/?drawer=open&tab=meshes) | [Bones tab](https://www.characterloom.com/?drawer=open&tab=bones) | [Mappings tab](https://www.characterloom.com/?drawer=open&tab=mappings)
|
|
481
481
|
|
|
482
482
|

|
|
483
483
|
|
|
@@ -667,7 +667,7 @@ This is especially useful for:
|
|
|
667
667
|
|
|
668
668
|
## 5. Extending & Custom Presets
|
|
669
669
|
|
|
670
|
-
Open in LoomLarge: [Properties tab](https://
|
|
670
|
+
Open in LoomLarge: [Properties tab](https://www.characterloom.com/?drawer=open&tab=properties) | [Mappings tab](https://www.characterloom.com/?drawer=open&tab=mappings)
|
|
671
671
|
|
|
672
672
|

|
|
673
673
|
|
|
@@ -746,7 +746,7 @@ const current = loom.getProfile();
|
|
|
746
746
|
|
|
747
747
|
## 6. Creating Skeletal Animation Presets
|
|
748
748
|
|
|
749
|
-
Open in LoomLarge: [Bones tab](https://
|
|
749
|
+
Open in LoomLarge: [Bones tab](https://www.characterloom.com/?drawer=open&tab=bones) | [Action Units tab](https://www.characterloom.com/?drawer=open&tab=action-units) | [Animation tab](https://www.characterloom.com/?drawer=open&tab=animation)
|
|
750
750
|
|
|
751
751
|

|
|
752
752
|
|
|
@@ -1029,7 +1029,7 @@ async function swimCycle() {
|
|
|
1029
1029
|
|
|
1030
1030
|
## 7. Action Unit Control
|
|
1031
1031
|
|
|
1032
|
-
Open in LoomLarge: [Action Units tab](https://
|
|
1032
|
+
Open in LoomLarge: [Action Units tab](https://www.characterloom.com/?drawer=open&tab=action-units)
|
|
1033
1033
|
|
|
1034
1034
|

|
|
1035
1035
|
|
|
@@ -1107,7 +1107,7 @@ loom.setAU(12, 0.8, 1); // Right side only
|
|
|
1107
1107
|
|
|
1108
1108
|
## 8. Mix Weight System
|
|
1109
1109
|
|
|
1110
|
-
Open in LoomLarge: [Action Units tab](https://
|
|
1110
|
+
Open in LoomLarge: [Action Units tab](https://www.characterloom.com/?drawer=open&tab=action-units)
|
|
1111
1111
|
|
|
1112
1112
|

|
|
1113
1113
|
|
|
@@ -1156,7 +1156,7 @@ if (isMixedAU(26)) {
|
|
|
1156
1156
|
|
|
1157
1157
|
## 9. Composite Rotation System
|
|
1158
1158
|
|
|
1159
|
-
Open in LoomLarge: [Action Units tab](https://
|
|
1159
|
+
Open in LoomLarge: [Action Units tab](https://www.characterloom.com/?drawer=open&tab=action-units) | [Bones tab](https://www.characterloom.com/?drawer=open&tab=bones)
|
|
1160
1160
|
|
|
1161
1161
|

|
|
1162
1162
|
|
|
@@ -1218,7 +1218,7 @@ loom.setAU(64, 0.4);
|
|
|
1218
1218
|
|
|
1219
1219
|
## 10. Continuum Pairs
|
|
1220
1220
|
|
|
1221
|
-
Open in LoomLarge: [Action Units tab](https://
|
|
1221
|
+
Open in LoomLarge: [Action Units tab](https://www.characterloom.com/?drawer=open&tab=action-units)
|
|
1222
1222
|
|
|
1223
1223
|

|
|
1224
1224
|
|
|
@@ -1320,7 +1320,7 @@ const pair = CONTINUUM_PAIRS_MAP[51];
|
|
|
1320
1320
|
|
|
1321
1321
|
## 11. Direct Morph Control
|
|
1322
1322
|
|
|
1323
|
-
Open in LoomLarge: [Meshes tab](https://
|
|
1323
|
+
Open in LoomLarge: [Meshes tab](https://www.characterloom.com/?drawer=open&tab=meshes) | [Mappings tab](https://www.characterloom.com/?drawer=open&tab=mappings)
|
|
1324
1324
|
|
|
1325
1325
|

|
|
1326
1326
|
|
|
@@ -1356,6 +1356,25 @@ const targets = loom.resolveMorphTargets('Mouth_Smile_L', ['CC_Base_Body']);
|
|
|
1356
1356
|
const value = targets.length > 0 ? (targets[0].infl[targets[0].idx] ?? 0) : 0;
|
|
1357
1357
|
```
|
|
1358
1358
|
|
|
1359
|
+
### Adding runtime morph targets
|
|
1360
|
+
|
|
1361
|
+
Generated or sidecar morph targets can be registered after a model loads. Deltas use the same relative `POSITION` format as glTF morph targets: one XYZ delta per base mesh vertex. Optional `normal` and `tangent` deltas can be supplied when available.
|
|
1362
|
+
|
|
1363
|
+
```typescript
|
|
1364
|
+
const index = loom.addMorphTarget({
|
|
1365
|
+
meshName: 'CC_Base_Body',
|
|
1366
|
+
name: 'BodyType_Muscular',
|
|
1367
|
+
position: bodyTypeMuscularDeltas,
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
loom.setMorphInfluence(index, 0.6, ['CC_Base_Body']);
|
|
1371
|
+
loom.setMorph('BodyType_Muscular', 0.6, ['CC_Base_Body']);
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
By default, Loom3 replaces and disposes the mesh `BufferGeometry` before appending morph attributes. This is intentional: Three.js does not support mutating `geometry.morphAttributes` in place after a geometry has rendered. For pre-render authoring paths, pass `{ forceGeometryReplacement: false }`.
|
|
1375
|
+
|
|
1376
|
+
If you need a named slot before real deltas are available, use `ensureMorphInfluence(meshName, morphName)`. It creates a zero-delta target and returns the assigned `morphTargetInfluences` index. After external code changes morph dictionaries or geometry, call `refreshMorphTargets()` so AU, viseme, hair, and clip-building caches see the updated targets.
|
|
1377
|
+
|
|
1359
1378
|
### Morph caching
|
|
1360
1379
|
|
|
1361
1380
|
Loom3 caches morph target lookups for performance. The first time you access a morph, it searches all meshes and caches the index. Subsequent accesses are O(1).
|
|
@@ -1364,7 +1383,7 @@ Loom3 caches morph target lookups for performance. The first time you access a m
|
|
|
1364
1383
|
|
|
1365
1384
|
## 12. Viseme System
|
|
1366
1385
|
|
|
1367
|
-
Open in LoomLarge: [Visemes tab](https://
|
|
1386
|
+
Open in LoomLarge: [Visemes tab](https://www.characterloom.com/?drawer=open&tab=visemes) | [Speech tab](https://www.characterloom.com/?drawer=open&tab=speech)
|
|
1368
1387
|
|
|
1369
1388
|

|
|
1370
1389
|
|
|
@@ -1450,7 +1469,7 @@ speak([5, 0, 10, 4]);
|
|
|
1450
1469
|
|
|
1451
1470
|
## 13. Transition System
|
|
1452
1471
|
|
|
1453
|
-
Open in LoomLarge: [Animation tab](https://
|
|
1472
|
+
Open in LoomLarge: [Animation tab](https://www.characterloom.com/?drawer=open&tab=animation)
|
|
1454
1473
|
|
|
1455
1474
|

|
|
1456
1475
|
|
|
@@ -1534,7 +1553,7 @@ loom.clearTransitions();
|
|
|
1534
1553
|
|
|
1535
1554
|
## 14. Playback & State Control
|
|
1536
1555
|
|
|
1537
|
-
Open in LoomLarge: [Animation tab](https://
|
|
1556
|
+
Open in LoomLarge: [Animation tab](https://www.characterloom.com/?drawer=open&tab=animation)
|
|
1538
1557
|
|
|
1539
1558
|

|
|
1540
1559
|
|
|
@@ -1593,7 +1612,7 @@ loom.dispose();
|
|
|
1593
1612
|
|
|
1594
1613
|
## 15. Hair Physics
|
|
1595
1614
|
|
|
1596
|
-
Open in LoomLarge: [Hair tab](https://
|
|
1615
|
+
Open in LoomLarge: [Hair tab](https://www.characterloom.com/?drawer=open&tab=hair)
|
|
1597
1616
|
|
|
1598
1617
|

|
|
1599
1618
|
|
|
@@ -1783,7 +1802,7 @@ loom.setMorphOnMeshes(
|
|
|
1783
1802
|
|
|
1784
1803
|
## 16. Baked Animations
|
|
1785
1804
|
|
|
1786
|
-
Open in LoomLarge: [Animation tab](https://
|
|
1805
|
+
Open in LoomLarge: [Animation tab](https://www.characterloom.com/?drawer=open&tab=animation)
|
|
1787
1806
|
|
|
1788
1807
|
Loom3 can play baked skeletal animations from your GLB/GLTF files using Three.js AnimationMixer. This allows you to combine pre-made animations (idle, walk, gestures) with real-time facial control.
|
|
1789
1808
|
|
|
@@ -2052,7 +2071,7 @@ loom.transitionAU(45, 1.0, 100); // Blink
|
|
|
2052
2071
|
|
|
2053
2072
|
## 17. Regions & Geometry Helpers
|
|
2054
2073
|
|
|
2055
|
-
Open in LoomLarge: [Bones tab](https://
|
|
2074
|
+
Open in LoomLarge: [Bones tab](https://www.characterloom.com/?drawer=open&tab=bones) | [Mappings tab](https://www.characterloom.com/?drawer=open&tab=mappings)
|
|
2056
2075
|
|
|
2057
2076
|
These helpers are for applications that need semantic face regions, marker anchors, or camera targets in addition to direct animation control.
|
|
2058
2077
|
|
|
@@ -2117,7 +2136,7 @@ Use these helpers when you need to:
|
|
|
2117
2136
|
|
|
2118
2137
|
## 18. API Reference
|
|
2119
2138
|
|
|
2120
|
-
Open in LoomLarge: [Animation tab](https://
|
|
2139
|
+
Open in LoomLarge: [Animation tab](https://www.characterloom.com/?drawer=open&tab=animation)
|
|
2121
2140
|
|
|
2122
2141
|
This is a compact reference for the public surface exported by `@lovelace_lol/loom3`.
|
|
2123
2142
|
|
|
@@ -2128,6 +2147,7 @@ This is a compact reference for the public surface exported by `@lovelace_lol/lo
|
|
|
2128
2147
|
- Lifecycle: `onReady()`, `update()`, `start()`, `stop()`, `dispose()`.
|
|
2129
2148
|
- Preset state: `setProfile()`, `getProfile()`.
|
|
2130
2149
|
- Control APIs: `setAU()`, `transitionAU()`, `setContinuum()`, `transitionContinuum()`, `setMorph()`, `transitionMorph()`, `setViseme()`, `transitionViseme()`.
|
|
2150
|
+
- Runtime morph authoring: `addMorphTarget()`, `addMorphTargets()`, `ensureMorphInfluence()`, `refreshMorphTargets()`.
|
|
2131
2151
|
- Transition state: `pause()`, `resume()`, `getPaused()`, `clearTransitions()`, `getActiveTransitionCount()`, `resetToNeutral()`.
|
|
2132
2152
|
|
|
2133
2153
|
### Presets and profiles
|
package/dist/index.cjs
CHANGED
|
@@ -4638,11 +4638,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4638
4638
|
this.morphIndexCache.clear();
|
|
4639
4639
|
model.traverse((obj) => {
|
|
4640
4640
|
if (obj.isMesh && obj.name) {
|
|
4641
|
-
|
|
4642
|
-
const dict = obj.morphTargetDictionary;
|
|
4643
|
-
if (Array.isArray(infl) && infl.length > 0 || dict && Object.keys(dict).length > 0) {
|
|
4644
|
-
this.meshByName.set(obj.name, obj);
|
|
4645
|
-
}
|
|
4641
|
+
this.meshByName.set(obj.name, obj);
|
|
4646
4642
|
}
|
|
4647
4643
|
});
|
|
4648
4644
|
this.bones = this.resolveBones(model);
|
|
@@ -5010,6 +5006,58 @@ var _Loom3 = class _Loom3 {
|
|
|
5010
5006
|
const currentContinuum = currentPos - currentNeg;
|
|
5011
5007
|
return this.animation.addTransition(driverKey, currentContinuum, target, durationMs, (value) => this.setContinuum(negAU, posAU, value, balance));
|
|
5012
5008
|
}
|
|
5009
|
+
// ============================================================================
|
|
5010
|
+
// MORPH CONTROL
|
|
5011
|
+
// ============================================================================
|
|
5012
|
+
addMorphTarget(target, options = {}) {
|
|
5013
|
+
const staleMorphTargets = this.collectResolvedExpressionMorphTargets();
|
|
5014
|
+
const index = this.applyMorphTargetDelta(target, options);
|
|
5015
|
+
this.refreshMorphTargets([target.meshName]);
|
|
5016
|
+
this.reinitializeRuntimeStateFromCurrentControls(staleMorphTargets);
|
|
5017
|
+
return index;
|
|
5018
|
+
}
|
|
5019
|
+
addMorphTargets(targets, options = {}) {
|
|
5020
|
+
const staleMorphTargets = this.collectResolvedExpressionMorphTargets();
|
|
5021
|
+
const result = {};
|
|
5022
|
+
for (const target of targets) {
|
|
5023
|
+
const index = this.applyMorphTargetDelta(target, options);
|
|
5024
|
+
result[`${target.meshName}:${target.name}`] = index;
|
|
5025
|
+
}
|
|
5026
|
+
this.refreshMorphTargets(Array.from(new Set(targets.map((target) => target.meshName))));
|
|
5027
|
+
this.reinitializeRuntimeStateFromCurrentControls(staleMorphTargets);
|
|
5028
|
+
return result;
|
|
5029
|
+
}
|
|
5030
|
+
ensureMorphInfluence(meshName, morphName) {
|
|
5031
|
+
const mesh = this.requireNamedMesh(meshName);
|
|
5032
|
+
const dict = this.getMeshMorphDictionary(mesh);
|
|
5033
|
+
const existing = dict[morphName];
|
|
5034
|
+
if (existing !== void 0) return existing;
|
|
5035
|
+
const position = mesh.geometry.getAttribute("position");
|
|
5036
|
+
if (!position) {
|
|
5037
|
+
throw new Error(`Cannot create morph target "${morphName}" on mesh "${meshName}": geometry has no position attribute.`);
|
|
5038
|
+
}
|
|
5039
|
+
return this.addMorphTarget({
|
|
5040
|
+
meshName,
|
|
5041
|
+
name: morphName,
|
|
5042
|
+
position: new Float32Array(position.count * position.itemSize),
|
|
5043
|
+
relative: true
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
refreshMorphTargets(_meshNames) {
|
|
5047
|
+
this.morphKeyCache.clear();
|
|
5048
|
+
this.morphIndexCache.clear();
|
|
5049
|
+
if (this.model) {
|
|
5050
|
+
this.meshByName.clear();
|
|
5051
|
+
this.model.traverse((obj) => {
|
|
5052
|
+
if (obj.isMesh && obj.name) {
|
|
5053
|
+
this.meshByName.set(obj.name, obj);
|
|
5054
|
+
}
|
|
5055
|
+
});
|
|
5056
|
+
this.meshes = collectMorphMeshes(this.model);
|
|
5057
|
+
}
|
|
5058
|
+
this.rebuildMorphTargetsCache();
|
|
5059
|
+
this.hairPhysics.refreshMeshSelection();
|
|
5060
|
+
}
|
|
5013
5061
|
setMorph(key, v, meshNamesOrTargets) {
|
|
5014
5062
|
const val = clamp012(v);
|
|
5015
5063
|
if (Array.isArray(meshNamesOrTargets) && meshNamesOrTargets.length > 0 && typeof meshNamesOrTargets[0] === "object" && "infl" in meshNamesOrTargets[0]) {
|
|
@@ -5628,6 +5676,148 @@ var _Loom3 = class _Loom3 {
|
|
|
5628
5676
|
}
|
|
5629
5677
|
return 0;
|
|
5630
5678
|
}
|
|
5679
|
+
applyMorphTargetDelta(target, options) {
|
|
5680
|
+
const mesh = this.requireNamedMesh(target.meshName);
|
|
5681
|
+
const sourceGeometry = mesh.geometry;
|
|
5682
|
+
const position = sourceGeometry.getAttribute("position");
|
|
5683
|
+
if (!position) {
|
|
5684
|
+
throw new Error(`Cannot add morph target "${target.name}" to mesh "${target.meshName}": geometry has no position attribute.`);
|
|
5685
|
+
}
|
|
5686
|
+
if (!target.name || !target.name.trim()) {
|
|
5687
|
+
throw new Error(`Cannot add morph target to mesh "${target.meshName}": target name is required.`);
|
|
5688
|
+
}
|
|
5689
|
+
const replace = options.replace === true;
|
|
5690
|
+
const resetInfluence = options.resetInfluence !== false;
|
|
5691
|
+
const forceGeometryReplacement = options.forceGeometryReplacement !== false;
|
|
5692
|
+
const previousInfluences = mesh.morphTargetInfluences ? [...mesh.morphTargetInfluences] : [];
|
|
5693
|
+
const previousDictionary = this.getMeshMorphDictionary(mesh);
|
|
5694
|
+
const existingIndex = previousDictionary[target.name];
|
|
5695
|
+
if (existingIndex !== void 0 && !replace) {
|
|
5696
|
+
throw new Error(`Morph target "${target.name}" already exists on mesh "${target.meshName}". Pass replace: true to overwrite it.`);
|
|
5697
|
+
}
|
|
5698
|
+
const geometry = forceGeometryReplacement ? sourceGeometry.clone() : sourceGeometry;
|
|
5699
|
+
const dictionary = { ...previousDictionary };
|
|
5700
|
+
const usedIndices = Object.values(dictionary).filter(Number.isInteger);
|
|
5701
|
+
const existingAttributeTargetCount = Math.max(
|
|
5702
|
+
0,
|
|
5703
|
+
...Object.values(geometry.morphAttributes).map((attributes) => attributes?.length ?? 0)
|
|
5704
|
+
);
|
|
5705
|
+
const nextIndex = Math.max(existingAttributeTargetCount, usedIndices.length ? Math.max(...usedIndices) + 1 : 0);
|
|
5706
|
+
const index = existingIndex ?? nextIndex;
|
|
5707
|
+
dictionary[target.name] = index;
|
|
5708
|
+
this.setMorphAttributeAtIndex(geometry, "position", target.position, position.itemSize, position.count, index, target.name);
|
|
5709
|
+
const normal = geometry.getAttribute("normal");
|
|
5710
|
+
if (target.normal) {
|
|
5711
|
+
this.setMorphAttributeAtIndex(geometry, "normal", target.normal, normal?.itemSize ?? 3, position.count, index, target.name);
|
|
5712
|
+
} else {
|
|
5713
|
+
this.setZeroMorphAttributeAtIndex(geometry, "normal", normal?.itemSize ?? 3, position.count, index, target.name);
|
|
5714
|
+
}
|
|
5715
|
+
const tangent = geometry.getAttribute("tangent");
|
|
5716
|
+
if (target.tangent) {
|
|
5717
|
+
this.setMorphAttributeAtIndex(geometry, "tangent", target.tangent, tangent?.itemSize ?? 4, position.count, index, target.name);
|
|
5718
|
+
} else {
|
|
5719
|
+
this.setZeroMorphAttributeAtIndex(geometry, "tangent", tangent?.itemSize ?? 4, position.count, index, target.name);
|
|
5720
|
+
}
|
|
5721
|
+
const color = geometry.getAttribute("color");
|
|
5722
|
+
const existingColorMorph = geometry.morphAttributes.color?.find(Boolean);
|
|
5723
|
+
this.setZeroMorphAttributeAtIndex(
|
|
5724
|
+
geometry,
|
|
5725
|
+
"color",
|
|
5726
|
+
color?.itemSize ?? existingColorMorph?.itemSize ?? 3,
|
|
5727
|
+
position.count,
|
|
5728
|
+
index,
|
|
5729
|
+
target.name
|
|
5730
|
+
);
|
|
5731
|
+
geometry.morphTargetsRelative = target.relative !== false;
|
|
5732
|
+
geometry.morphTargetDictionary = dictionary;
|
|
5733
|
+
if (forceGeometryReplacement) {
|
|
5734
|
+
mesh.geometry = geometry;
|
|
5735
|
+
sourceGeometry.dispose();
|
|
5736
|
+
}
|
|
5737
|
+
const influenceLength = Math.max(previousInfluences.length, index + 1);
|
|
5738
|
+
const influences = previousInfluences.slice(0, influenceLength);
|
|
5739
|
+
while (influences.length < influenceLength) {
|
|
5740
|
+
influences.push(0);
|
|
5741
|
+
}
|
|
5742
|
+
if (resetInfluence) {
|
|
5743
|
+
influences[index] = 0;
|
|
5744
|
+
}
|
|
5745
|
+
mesh.morphTargetDictionary = dictionary;
|
|
5746
|
+
mesh.morphTargetInfluences = influences;
|
|
5747
|
+
this.addRuntimeMorphMesh(mesh);
|
|
5748
|
+
if (!this.config.morphToMesh?.face?.length) {
|
|
5749
|
+
this.config.morphToMesh = {
|
|
5750
|
+
...this.config.morphToMesh,
|
|
5751
|
+
face: [mesh.name]
|
|
5752
|
+
};
|
|
5753
|
+
}
|
|
5754
|
+
return index;
|
|
5755
|
+
}
|
|
5756
|
+
requireNamedMesh(meshName) {
|
|
5757
|
+
const mesh = this.meshByName.get(meshName);
|
|
5758
|
+
if (mesh) return mesh;
|
|
5759
|
+
if (this.model) {
|
|
5760
|
+
let found = null;
|
|
5761
|
+
this.model.traverse((obj) => {
|
|
5762
|
+
if (!found && obj.isMesh && obj.name === meshName) {
|
|
5763
|
+
found = obj;
|
|
5764
|
+
}
|
|
5765
|
+
});
|
|
5766
|
+
if (found) {
|
|
5767
|
+
this.meshByName.set(meshName, found);
|
|
5768
|
+
return found;
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
throw new Error(`Mesh "${meshName}" was not found in the current model.`);
|
|
5772
|
+
}
|
|
5773
|
+
getMeshMorphDictionary(mesh) {
|
|
5774
|
+
const meshDictionary = mesh.morphTargetDictionary;
|
|
5775
|
+
const geometryDictionary = mesh.geometry.morphTargetDictionary;
|
|
5776
|
+
const dictionary = meshDictionary || geometryDictionary || {};
|
|
5777
|
+
mesh.morphTargetDictionary = dictionary;
|
|
5778
|
+
mesh.geometry.morphTargetDictionary = dictionary;
|
|
5779
|
+
return dictionary;
|
|
5780
|
+
}
|
|
5781
|
+
setMorphAttributeAtIndex(geometry, semantic, data, itemSize, vertexCount, index, name) {
|
|
5782
|
+
const expectedLength = vertexCount * itemSize;
|
|
5783
|
+
if (data.length !== expectedLength) {
|
|
5784
|
+
throw new Error(
|
|
5785
|
+
`Morph target "${name}" ${semantic} data has ${data.length} values; expected ${expectedLength} (${vertexCount} vertices * itemSize ${itemSize}).`
|
|
5786
|
+
);
|
|
5787
|
+
}
|
|
5788
|
+
const attributes = geometry.morphAttributes[semantic] ? [...geometry.morphAttributes[semantic]] : [];
|
|
5789
|
+
while (attributes.length < index) {
|
|
5790
|
+
const empty = new THREE2.BufferAttribute(new Float32Array(expectedLength), itemSize);
|
|
5791
|
+
empty.name = `morph_${attributes.length}`;
|
|
5792
|
+
attributes.push(empty);
|
|
5793
|
+
}
|
|
5794
|
+
const values = data instanceof Float32Array ? new Float32Array(data) : Float32Array.from(data);
|
|
5795
|
+
const attribute = new THREE2.BufferAttribute(values, itemSize);
|
|
5796
|
+
attribute.name = name;
|
|
5797
|
+
attributes[index] = attribute;
|
|
5798
|
+
geometry.morphAttributes[semantic] = attributes;
|
|
5799
|
+
}
|
|
5800
|
+
setZeroMorphAttributeAtIndex(geometry, semantic, itemSize, vertexCount, index, name) {
|
|
5801
|
+
if (!geometry.morphAttributes[semantic]?.length) return;
|
|
5802
|
+
const expectedLength = vertexCount * itemSize;
|
|
5803
|
+
const attributes = [...geometry.morphAttributes[semantic]];
|
|
5804
|
+
while (attributes.length < index) {
|
|
5805
|
+
const empty2 = new THREE2.BufferAttribute(new Float32Array(expectedLength), itemSize);
|
|
5806
|
+
empty2.name = `morph_${attributes.length}`;
|
|
5807
|
+
attributes.push(empty2);
|
|
5808
|
+
}
|
|
5809
|
+
const empty = new THREE2.BufferAttribute(new Float32Array(expectedLength), itemSize);
|
|
5810
|
+
empty.name = name;
|
|
5811
|
+
attributes[index] = empty;
|
|
5812
|
+
geometry.morphAttributes[semantic] = attributes;
|
|
5813
|
+
}
|
|
5814
|
+
addRuntimeMorphMesh(mesh) {
|
|
5815
|
+
const key = mesh.name || mesh.uuid;
|
|
5816
|
+
const exists = this.meshes.some((candidate) => (candidate.name || candidate.uuid) === key);
|
|
5817
|
+
if (!exists) {
|
|
5818
|
+
this.meshes.push(mesh);
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5631
5821
|
getMorphKeyCacheKey(key, meshNames) {
|
|
5632
5822
|
return meshNames?.length ? `key:${key}@${meshNames.join(",")}` : `key:${key}`;
|
|
5633
5823
|
}
|
|
@@ -7465,7 +7655,7 @@ function extractMorphs(meshes) {
|
|
|
7465
7655
|
for (const mesh of meshes) {
|
|
7466
7656
|
const geo = mesh.geometry;
|
|
7467
7657
|
if (!geo.morphAttributes) continue;
|
|
7468
|
-
const dict = geo.morphTargetDictionary;
|
|
7658
|
+
const dict = mesh.morphTargetDictionary || geo.morphTargetDictionary;
|
|
7469
7659
|
if (dict) {
|
|
7470
7660
|
for (const [name, index] of Object.entries(dict)) {
|
|
7471
7661
|
morphs.push({
|