loomlarge 0.2.0 → 0.2.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/README.md +126 -20
- package/dist/index.cjs +357 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -30
- package/dist/index.d.ts +114 -30
- package/dist/index.js +358 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -245,7 +245,7 @@ var VISEME_KEYS = [
|
|
|
245
245
|
];
|
|
246
246
|
var BONE_AU_TO_BINDINGS = {
|
|
247
247
|
// Head turn and tilt (M51-M56) - use HEAD bone only (NECK should not rotate with head)
|
|
248
|
-
//
|
|
248
|
+
// Axis is derived from COMPOSITE_ROTATIONS, not stored here
|
|
249
249
|
51: [
|
|
250
250
|
{ node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30 }
|
|
251
251
|
// Head turn left
|
|
@@ -270,7 +270,7 @@ var BONE_AU_TO_BINDINGS = {
|
|
|
270
270
|
{ node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15 }
|
|
271
271
|
// Head tilt right
|
|
272
272
|
],
|
|
273
|
-
// Eyes
|
|
273
|
+
// Eyes - CC4 rigs use rz for horizontal, rx for vertical
|
|
274
274
|
61: [
|
|
275
275
|
{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32 },
|
|
276
276
|
// Eyes look left
|
|
@@ -283,45 +283,47 @@ var BONE_AU_TO_BINDINGS = {
|
|
|
283
283
|
],
|
|
284
284
|
63: [
|
|
285
285
|
{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32 },
|
|
286
|
+
// Eyes Up
|
|
286
287
|
{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 32 }
|
|
287
288
|
],
|
|
288
289
|
64: [
|
|
289
290
|
{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32 },
|
|
291
|
+
// Eyes Down
|
|
290
292
|
{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 32 }
|
|
291
293
|
],
|
|
292
294
|
// Single-eye (Left) — horizontal (rz for CC4) and vertical (rx)
|
|
293
295
|
65: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 15 }],
|
|
294
296
|
66: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 15 }],
|
|
295
297
|
67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 12 }],
|
|
298
|
+
// Left Eye Up
|
|
296
299
|
68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 12 }],
|
|
300
|
+
// Left Eye Down
|
|
297
301
|
// Single-eye (Right) — horizontal (rz for CC4) and vertical (rx)
|
|
298
302
|
69: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 15 }],
|
|
299
303
|
70: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 15 }],
|
|
300
304
|
71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 12 }],
|
|
305
|
+
// Right Eye Up
|
|
301
306
|
72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 12 }],
|
|
307
|
+
// Right Eye Down
|
|
302
308
|
// Jaw / Mouth
|
|
303
309
|
8: [
|
|
304
310
|
// Lips Toward Each Other - slight jaw open helps sell the lip press
|
|
305
311
|
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 8 }
|
|
306
|
-
// Small downward rotation (jaw opening slightly)
|
|
307
312
|
],
|
|
308
313
|
25: [
|
|
309
314
|
// Lips Part — small jaw open
|
|
310
315
|
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84 }
|
|
311
|
-
// 73% of 8
|
|
312
316
|
],
|
|
313
317
|
26: [
|
|
314
318
|
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 28 }
|
|
315
|
-
// 73% of 20
|
|
316
319
|
],
|
|
317
320
|
27: [
|
|
318
321
|
// Mouth Stretch — larger jaw open
|
|
319
322
|
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 32 }
|
|
320
|
-
// 73% of 25
|
|
321
323
|
],
|
|
322
324
|
29: [
|
|
323
325
|
{ node: "JAW", channel: "tz", scale: -1, maxUnits: 0.02 }
|
|
324
|
-
//
|
|
326
|
+
// Translation
|
|
325
327
|
],
|
|
326
328
|
30: [
|
|
327
329
|
// Jaw Left
|
|
@@ -334,6 +336,7 @@ var BONE_AU_TO_BINDINGS = {
|
|
|
334
336
|
// Tongue
|
|
335
337
|
19: [
|
|
336
338
|
{ node: "TONGUE", channel: "tz", scale: -1, maxUnits: 8e-3 }
|
|
339
|
+
// Translation
|
|
337
340
|
],
|
|
338
341
|
37: [
|
|
339
342
|
// Tongue Up
|
|
@@ -413,7 +416,7 @@ var COMPOSITE_ROTATIONS = [
|
|
|
413
416
|
}
|
|
414
417
|
];
|
|
415
418
|
var CONTINUUM_PAIRS_MAP = {
|
|
416
|
-
// Eyes horizontal
|
|
419
|
+
// Eyes horizontal - both eyes share same AUs (yaw maps to rz via COMPOSITE_ROTATIONS)
|
|
417
420
|
61: { pairId: 62, isNegative: true, axis: "yaw", node: "EYE_L" },
|
|
418
421
|
62: { pairId: 61, isNegative: false, axis: "yaw", node: "EYE_L" },
|
|
419
422
|
// Eyes vertical (pitch)
|
|
@@ -650,6 +653,25 @@ var CC4_PRESET = {
|
|
|
650
653
|
|
|
651
654
|
// src/engines/three/LoomLargeThree.ts
|
|
652
655
|
var deg2rad = (d) => d * Math.PI / 180;
|
|
656
|
+
var X_AXIS = new three.Vector3(1, 0, 0);
|
|
657
|
+
var Y_AXIS = new three.Vector3(0, 1, 0);
|
|
658
|
+
var Z_AXIS = new three.Vector3(0, 0, 1);
|
|
659
|
+
var AU_TO_COMPOSITE_MAP = /* @__PURE__ */ new Map();
|
|
660
|
+
COMPOSITE_ROTATIONS.forEach((comp) => {
|
|
661
|
+
["pitch", "yaw", "roll"].forEach((axisName) => {
|
|
662
|
+
const axisConfig = comp[axisName];
|
|
663
|
+
if (axisConfig) {
|
|
664
|
+
axisConfig.aus.forEach((auId) => {
|
|
665
|
+
const existing = AU_TO_COMPOSITE_MAP.get(auId);
|
|
666
|
+
if (existing) {
|
|
667
|
+
existing.nodes.push(comp.node);
|
|
668
|
+
} else {
|
|
669
|
+
AU_TO_COMPOSITE_MAP.set(auId, { nodes: [comp.node], axis: axisName });
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
});
|
|
653
675
|
function clamp01(x) {
|
|
654
676
|
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
655
677
|
}
|
|
@@ -795,13 +817,31 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
795
817
|
this.setMorph(k, base, meshNames);
|
|
796
818
|
}
|
|
797
819
|
}
|
|
820
|
+
const compositeInfo = AU_TO_COMPOSITE_MAP.get(id);
|
|
821
|
+
if (compositeInfo) {
|
|
822
|
+
for (const nodeKey of compositeInfo.nodes) {
|
|
823
|
+
const config = COMPOSITE_ROTATIONS.find((c) => c.node === nodeKey);
|
|
824
|
+
if (!config) continue;
|
|
825
|
+
const axisConfig = config[compositeInfo.axis];
|
|
826
|
+
if (!axisConfig) continue;
|
|
827
|
+
let axisValue;
|
|
828
|
+
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
829
|
+
const negValue = this.auValues[axisConfig.negative] ?? 0;
|
|
830
|
+
const posValue = this.auValues[axisConfig.positive] ?? 0;
|
|
831
|
+
axisValue = posValue - negValue;
|
|
832
|
+
} else if (axisConfig.aus.length > 1) {
|
|
833
|
+
axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
|
|
834
|
+
} else {
|
|
835
|
+
axisValue = v;
|
|
836
|
+
}
|
|
837
|
+
this.updateBoneRotation(nodeKey, compositeInfo.axis, axisValue);
|
|
838
|
+
this.pendingCompositeNodes.add(nodeKey);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
798
841
|
const bindings = this.config.auToBones[id];
|
|
799
842
|
if (bindings) {
|
|
800
843
|
for (const binding of bindings) {
|
|
801
|
-
if (binding.channel === "
|
|
802
|
-
const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
|
|
803
|
-
this.updateBoneRotation(binding.node, axis, v * binding.scale, binding.maxDegrees ?? 0);
|
|
804
|
-
} else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
|
|
844
|
+
if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
|
|
805
845
|
if (binding.maxUnits !== void 0) {
|
|
806
846
|
this.updateBoneTranslation(binding.node, binding.channel, v * binding.scale, binding.maxUnits);
|
|
807
847
|
}
|
|
@@ -836,11 +876,28 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
836
876
|
for (const k of centerKeys) {
|
|
837
877
|
handles.push(this.transitionMorph(k, base, durationMs, meshNames));
|
|
838
878
|
}
|
|
879
|
+
const compositeInfo = AU_TO_COMPOSITE_MAP.get(numId);
|
|
880
|
+
if (compositeInfo) {
|
|
881
|
+
for (const nodeKey of compositeInfo.nodes) {
|
|
882
|
+
const config = COMPOSITE_ROTATIONS.find((c) => c.node === nodeKey);
|
|
883
|
+
if (!config) continue;
|
|
884
|
+
const axisConfig = config[compositeInfo.axis];
|
|
885
|
+
if (!axisConfig) continue;
|
|
886
|
+
let axisValue;
|
|
887
|
+
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
888
|
+
const negValue = this.auValues[axisConfig.negative] ?? 0;
|
|
889
|
+
const posValue = this.auValues[axisConfig.positive] ?? 0;
|
|
890
|
+
axisValue = posValue - negValue;
|
|
891
|
+
} else if (axisConfig.aus.length > 1) {
|
|
892
|
+
axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
|
|
893
|
+
} else {
|
|
894
|
+
axisValue = target;
|
|
895
|
+
}
|
|
896
|
+
handles.push(this.transitionBoneRotation(nodeKey, compositeInfo.axis, axisValue, durationMs));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
839
899
|
for (const binding of bindings) {
|
|
840
|
-
if (binding.channel === "
|
|
841
|
-
const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
|
|
842
|
-
handles.push(this.transitionBoneRotation(binding.node, axis, target * binding.scale, binding.maxDegrees ?? 0, durationMs));
|
|
843
|
-
} else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
|
|
900
|
+
if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
|
|
844
901
|
if (binding.maxUnits !== void 0) {
|
|
845
902
|
handles.push(this.transitionBoneTranslation(binding.node, binding.channel, target * binding.scale, binding.maxUnits, durationMs));
|
|
846
903
|
}
|
|
@@ -852,10 +909,53 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
852
909
|
return this.auValues[id] ?? 0;
|
|
853
910
|
}
|
|
854
911
|
// ============================================================================
|
|
855
|
-
//
|
|
912
|
+
// CONTINUUM CONTROL (for paired AUs like eyes left/right, head up/down)
|
|
856
913
|
// ============================================================================
|
|
857
|
-
|
|
914
|
+
/**
|
|
915
|
+
* Set a continuum AU pair immediately (no animation).
|
|
916
|
+
*
|
|
917
|
+
* Sign convention:
|
|
918
|
+
* - Negative value (-1 to 0): activates negAU (e.g., head left, eyes left)
|
|
919
|
+
* - Positive value (0 to +1): activates posAU (e.g., head right, eyes right)
|
|
920
|
+
*
|
|
921
|
+
* @param negAU - AU ID for negative direction (e.g., 61 for eyes left)
|
|
922
|
+
* @param posAU - AU ID for positive direction (e.g., 62 for eyes right)
|
|
923
|
+
* @param continuumValue - Value from -1 (full negative) to +1 (full positive)
|
|
924
|
+
*/
|
|
925
|
+
setContinuum(negAU, posAU, continuumValue) {
|
|
926
|
+
const value = Math.max(-1, Math.min(1, continuumValue));
|
|
927
|
+
const negVal = value < 0 ? Math.abs(value) : 0;
|
|
928
|
+
const posVal = value > 0 ? value : 0;
|
|
929
|
+
this.setAU(negAU, negVal);
|
|
930
|
+
this.setAU(posAU, posVal);
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Smoothly transition a continuum AU pair (e.g., eyes left/right, head up/down).
|
|
934
|
+
* Takes a continuum value from -1 to +1 and internally manages both AU values.
|
|
935
|
+
*
|
|
936
|
+
* @param negAU - AU ID for negative direction (e.g., 61 for eyes left)
|
|
937
|
+
* @param posAU - AU ID for positive direction (e.g., 62 for eyes right)
|
|
938
|
+
* @param continuumValue - Target value from -1 (full negative) to +1 (full positive)
|
|
939
|
+
* @param durationMs - Transition duration in milliseconds
|
|
940
|
+
*/
|
|
941
|
+
transitionContinuum(negAU, posAU, continuumValue, durationMs = 200) {
|
|
942
|
+
const target = Math.max(-1, Math.min(1, continuumValue));
|
|
943
|
+
const driverKey = `continuum_${negAU}_${posAU}`;
|
|
944
|
+
const currentNeg = this.auValues[negAU] ?? 0;
|
|
945
|
+
const currentPos = this.auValues[posAU] ?? 0;
|
|
946
|
+
const currentContinuum = currentPos - currentNeg;
|
|
947
|
+
return this.animation.addTransition(driverKey, currentContinuum, target, durationMs, (value) => this.setContinuum(negAU, posAU, value));
|
|
948
|
+
}
|
|
949
|
+
setMorph(key, v, meshNamesOrTargets) {
|
|
858
950
|
const val = clamp01(v);
|
|
951
|
+
if (Array.isArray(meshNamesOrTargets) && meshNamesOrTargets.length > 0 && typeof meshNamesOrTargets[0] === "object" && "infl" in meshNamesOrTargets[0]) {
|
|
952
|
+
const targets2 = meshNamesOrTargets;
|
|
953
|
+
for (const target of targets2) {
|
|
954
|
+
target.infl[target.idx] = val;
|
|
955
|
+
}
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const meshNames = meshNamesOrTargets;
|
|
859
959
|
const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
|
|
860
960
|
const cached = this.morphCache.get(key);
|
|
861
961
|
if (cached) {
|
|
@@ -894,11 +994,54 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
894
994
|
this.morphCache.set(key, targets);
|
|
895
995
|
}
|
|
896
996
|
}
|
|
997
|
+
/**
|
|
998
|
+
* Resolve morph key to direct targets for ultra-fast repeated access.
|
|
999
|
+
* Use this when you need to set the same morph many times (e.g., in animation loops).
|
|
1000
|
+
*/
|
|
1001
|
+
resolveMorphTargets(key, meshNames) {
|
|
1002
|
+
const cached = this.morphCache.get(key);
|
|
1003
|
+
if (cached) return cached;
|
|
1004
|
+
const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
|
|
1005
|
+
const targets = [];
|
|
1006
|
+
if (targetMeshes.length) {
|
|
1007
|
+
for (const name of targetMeshes) {
|
|
1008
|
+
const mesh = this.meshByName.get(name);
|
|
1009
|
+
if (!mesh) continue;
|
|
1010
|
+
const dict = mesh.morphTargetDictionary;
|
|
1011
|
+
const infl = mesh.morphTargetInfluences;
|
|
1012
|
+
if (!dict || !infl) continue;
|
|
1013
|
+
const idx = dict[key];
|
|
1014
|
+
if (idx !== void 0) {
|
|
1015
|
+
targets.push({ infl, idx });
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
} else {
|
|
1019
|
+
for (const mesh of this.meshes) {
|
|
1020
|
+
const dict = mesh.morphTargetDictionary;
|
|
1021
|
+
const infl = mesh.morphTargetInfluences;
|
|
1022
|
+
if (!dict || !infl) continue;
|
|
1023
|
+
const idx = dict[key];
|
|
1024
|
+
if (idx !== void 0) {
|
|
1025
|
+
targets.push({ infl, idx });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (targets.length > 0) {
|
|
1030
|
+
this.morphCache.set(key, targets);
|
|
1031
|
+
}
|
|
1032
|
+
return targets;
|
|
1033
|
+
}
|
|
897
1034
|
transitionMorph(key, to, durationMs = 120, meshNames) {
|
|
898
1035
|
const transitionKey = `morph_${key}`;
|
|
899
1036
|
const from = this.getMorphValue(key);
|
|
900
1037
|
const target = clamp01(to);
|
|
901
|
-
|
|
1038
|
+
const targets = this.resolveMorphTargets(key, meshNames);
|
|
1039
|
+
return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => {
|
|
1040
|
+
const val = clamp01(value);
|
|
1041
|
+
for (const t of targets) {
|
|
1042
|
+
t.infl[t.idx] = val;
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
902
1045
|
}
|
|
903
1046
|
// ============================================================================
|
|
904
1047
|
// VISEME CONTROL
|
|
@@ -911,7 +1054,7 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
911
1054
|
this.setMorph(morphKey, val);
|
|
912
1055
|
const jawAmount = _LoomLargeThree.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
|
|
913
1056
|
if (Math.abs(jawScale) > 1e-6 && Math.abs(jawAmount) > 1e-6) {
|
|
914
|
-
this.updateBoneRotation("JAW", "
|
|
1057
|
+
this.updateBoneRotation("JAW", "pitch", jawAmount);
|
|
915
1058
|
}
|
|
916
1059
|
}
|
|
917
1060
|
transitionViseme(visemeIndex, to, durationMs = 80, jawScale = 1) {
|
|
@@ -929,7 +1072,7 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
929
1072
|
if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
|
|
930
1073
|
return morphHandle;
|
|
931
1074
|
}
|
|
932
|
-
const jawHandle = this.transitionBoneRotation("JAW", "
|
|
1075
|
+
const jawHandle = this.transitionBoneRotation("JAW", "pitch", jawAmount, durationMs);
|
|
933
1076
|
return this.combineHandles([morphHandle, jawHandle]);
|
|
934
1077
|
}
|
|
935
1078
|
// ============================================================================
|
|
@@ -992,15 +1135,44 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
992
1135
|
const result = [];
|
|
993
1136
|
this.model.traverse((obj) => {
|
|
994
1137
|
if (obj.isMesh) {
|
|
1138
|
+
const meshInfo = CC4_MESHES[obj.name];
|
|
995
1139
|
result.push({
|
|
996
1140
|
name: obj.name,
|
|
997
1141
|
visible: obj.visible,
|
|
998
|
-
morphCount: obj.morphTargetInfluences?.length || 0
|
|
1142
|
+
morphCount: obj.morphTargetInfluences?.length || 0,
|
|
1143
|
+
category: meshInfo?.category || "other"
|
|
999
1144
|
});
|
|
1000
1145
|
}
|
|
1001
1146
|
});
|
|
1002
1147
|
return result;
|
|
1003
1148
|
}
|
|
1149
|
+
/** Get all morph targets grouped by mesh name */
|
|
1150
|
+
getMorphTargets() {
|
|
1151
|
+
const result = {};
|
|
1152
|
+
for (const mesh of this.meshes) {
|
|
1153
|
+
const dict = mesh.morphTargetDictionary;
|
|
1154
|
+
if (dict) {
|
|
1155
|
+
result[mesh.name] = Object.keys(dict).sort();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return result;
|
|
1159
|
+
}
|
|
1160
|
+
/** Get all resolved bone names and their current transforms */
|
|
1161
|
+
getBones() {
|
|
1162
|
+
const result = {};
|
|
1163
|
+
for (const name of Object.keys(this.bones)) {
|
|
1164
|
+
const entry = this.bones[name];
|
|
1165
|
+
if (entry) {
|
|
1166
|
+
const pos = entry.obj.position;
|
|
1167
|
+
const rot = entry.obj.rotation;
|
|
1168
|
+
result[name] = {
|
|
1169
|
+
position: [pos.x, pos.y, pos.z],
|
|
1170
|
+
rotation: [rot.x * 180 / Math.PI, rot.y * 180 / Math.PI, rot.z * 180 / Math.PI]
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return result;
|
|
1175
|
+
}
|
|
1004
1176
|
setMeshVisible(meshName, visible) {
|
|
1005
1177
|
if (!this.model) return;
|
|
1006
1178
|
this.model.traverse((obj) => {
|
|
@@ -1009,6 +1181,70 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
1009
1181
|
}
|
|
1010
1182
|
});
|
|
1011
1183
|
}
|
|
1184
|
+
/** Get material config for a mesh */
|
|
1185
|
+
getMeshMaterialConfig(meshName) {
|
|
1186
|
+
if (!this.model) return null;
|
|
1187
|
+
let result = null;
|
|
1188
|
+
this.model.traverse((obj) => {
|
|
1189
|
+
if (obj.isMesh && obj.name === meshName) {
|
|
1190
|
+
const mat = obj.material;
|
|
1191
|
+
if (mat) {
|
|
1192
|
+
let blendingName = "Normal";
|
|
1193
|
+
for (const [name, value] of Object.entries(_LoomLargeThree.BLENDING_MODES)) {
|
|
1194
|
+
if (mat.blending === value) {
|
|
1195
|
+
blendingName = name;
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
result = {
|
|
1200
|
+
renderOrder: obj.renderOrder,
|
|
1201
|
+
transparent: mat.transparent,
|
|
1202
|
+
opacity: mat.opacity,
|
|
1203
|
+
depthWrite: mat.depthWrite,
|
|
1204
|
+
depthTest: mat.depthTest,
|
|
1205
|
+
blending: blendingName
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
return result;
|
|
1211
|
+
}
|
|
1212
|
+
/** Set material config for a mesh */
|
|
1213
|
+
setMeshMaterialConfig(meshName, config) {
|
|
1214
|
+
if (!this.model) return;
|
|
1215
|
+
this.model.traverse((obj) => {
|
|
1216
|
+
if (obj.isMesh && obj.name === meshName) {
|
|
1217
|
+
const mat = obj.material;
|
|
1218
|
+
if (config.renderOrder !== void 0) {
|
|
1219
|
+
obj.renderOrder = config.renderOrder;
|
|
1220
|
+
}
|
|
1221
|
+
if (mat) {
|
|
1222
|
+
if (config.opacity !== void 0) {
|
|
1223
|
+
mat.opacity = config.opacity;
|
|
1224
|
+
if (config.opacity < 1 && config.transparent === void 0) {
|
|
1225
|
+
mat.transparent = true;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (config.transparent !== void 0) {
|
|
1229
|
+
mat.transparent = config.transparent;
|
|
1230
|
+
}
|
|
1231
|
+
if (config.depthWrite !== void 0) {
|
|
1232
|
+
mat.depthWrite = config.depthWrite;
|
|
1233
|
+
}
|
|
1234
|
+
if (config.depthTest !== void 0) {
|
|
1235
|
+
mat.depthTest = config.depthTest;
|
|
1236
|
+
}
|
|
1237
|
+
if (config.blending !== void 0) {
|
|
1238
|
+
const blendValue = _LoomLargeThree.BLENDING_MODES[config.blending];
|
|
1239
|
+
if (blendValue !== void 0) {
|
|
1240
|
+
mat.blending = blendValue;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
mat.needsUpdate = true;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1012
1248
|
// ============================================================================
|
|
1013
1249
|
// CONFIGURATION
|
|
1014
1250
|
// ============================================================================
|
|
@@ -1063,20 +1299,20 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
1063
1299
|
return !!(this.config.auToMorphs[id]?.length && this.config.auToBones[id]?.length);
|
|
1064
1300
|
}
|
|
1065
1301
|
initBoneRotations() {
|
|
1066
|
-
const zeroAxis = { value: 0, maxRadians: 0 };
|
|
1067
1302
|
this.rotations = {};
|
|
1068
1303
|
this.pendingCompositeNodes.clear();
|
|
1069
1304
|
const allBoneKeys = Array.from(
|
|
1070
1305
|
new Set(Object.values(this.config.auToBones).flat().map((binding) => binding.node))
|
|
1071
1306
|
);
|
|
1072
1307
|
for (const node of allBoneKeys) {
|
|
1073
|
-
this.rotations[node] = { pitch:
|
|
1308
|
+
this.rotations[node] = { pitch: 0, yaw: 0, roll: 0 };
|
|
1074
1309
|
this.pendingCompositeNodes.add(node);
|
|
1075
1310
|
}
|
|
1076
1311
|
}
|
|
1077
|
-
|
|
1312
|
+
/** Update rotation state - just stores -1 to 1 value like stable version */
|
|
1313
|
+
updateBoneRotation(nodeKey, axis, value) {
|
|
1078
1314
|
if (!this.rotations[nodeKey]) return;
|
|
1079
|
-
this.rotations[nodeKey][axis] =
|
|
1315
|
+
this.rotations[nodeKey][axis] = Math.max(-1, Math.min(1, value));
|
|
1080
1316
|
this.pendingCompositeNodes.add(nodeKey);
|
|
1081
1317
|
}
|
|
1082
1318
|
updateBoneTranslation(nodeKey, channel, value, maxUnits) {
|
|
@@ -1088,11 +1324,11 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
1088
1324
|
else this.translations[nodeKey].z = offset;
|
|
1089
1325
|
this.pendingCompositeNodes.add(nodeKey);
|
|
1090
1326
|
}
|
|
1091
|
-
transitionBoneRotation(nodeKey, axis, to,
|
|
1327
|
+
transitionBoneRotation(nodeKey, axis, to, durationMs = 200) {
|
|
1092
1328
|
const transitionKey = `bone_${nodeKey}_${axis}`;
|
|
1093
|
-
const from = this.rotations[nodeKey]?.[axis]
|
|
1329
|
+
const from = this.rotations[nodeKey]?.[axis] ?? 0;
|
|
1094
1330
|
const target = Math.max(-1, Math.min(1, to));
|
|
1095
|
-
return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.updateBoneRotation(nodeKey, axis, value
|
|
1331
|
+
return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.updateBoneRotation(nodeKey, axis, value));
|
|
1096
1332
|
}
|
|
1097
1333
|
transitionBoneTranslation(nodeKey, channel, to, maxUnits, durationMs = 200) {
|
|
1098
1334
|
const transitionKey = `boneT_${nodeKey}_${channel}`;
|
|
@@ -1109,6 +1345,10 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
1109
1345
|
}
|
|
1110
1346
|
this.pendingCompositeNodes.clear();
|
|
1111
1347
|
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Apply composite rotation using quaternion composition like stable version.
|
|
1350
|
+
* Looks up maxDegrees and channel from BONE_AU_TO_BINDINGS.
|
|
1351
|
+
*/
|
|
1112
1352
|
applyCompositeRotation(nodeKey) {
|
|
1113
1353
|
const entry = this.bones[nodeKey];
|
|
1114
1354
|
if (!entry || !this.model) {
|
|
@@ -1117,12 +1357,60 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
1117
1357
|
}
|
|
1118
1358
|
return;
|
|
1119
1359
|
}
|
|
1120
|
-
const { obj, basePos,
|
|
1360
|
+
const { obj, basePos, baseQuat } = entry;
|
|
1121
1361
|
const rotState = this.rotations[nodeKey];
|
|
1122
1362
|
if (!rotState) return;
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1363
|
+
const config = COMPOSITE_ROTATIONS.find((c) => c.node === nodeKey);
|
|
1364
|
+
if (!config) return;
|
|
1365
|
+
const getBindingForAxis = (axisConfig, direction) => {
|
|
1366
|
+
if (!axisConfig) return null;
|
|
1367
|
+
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
1368
|
+
const auId = direction < 0 ? axisConfig.negative : axisConfig.positive;
|
|
1369
|
+
return BONE_AU_TO_BINDINGS[auId]?.find((b) => b.node === nodeKey);
|
|
1370
|
+
}
|
|
1371
|
+
if (axisConfig.aus.length > 1) {
|
|
1372
|
+
let maxAU = axisConfig.aus[0];
|
|
1373
|
+
let maxValue = this.auValues[maxAU] ?? 0;
|
|
1374
|
+
for (const auId of axisConfig.aus) {
|
|
1375
|
+
const val = this.auValues[auId] ?? 0;
|
|
1376
|
+
if (val > maxValue) {
|
|
1377
|
+
maxValue = val;
|
|
1378
|
+
maxAU = auId;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return BONE_AU_TO_BINDINGS[maxAU]?.find((b) => b.node === nodeKey);
|
|
1382
|
+
}
|
|
1383
|
+
return BONE_AU_TO_BINDINGS[axisConfig.aus[0]]?.find((b) => b.node === nodeKey);
|
|
1384
|
+
};
|
|
1385
|
+
const getAxis = (channel) => channel === "rx" ? X_AXIS : channel === "ry" ? Y_AXIS : Z_AXIS;
|
|
1386
|
+
const compositeQ = new three.Quaternion().copy(baseQuat);
|
|
1387
|
+
if (config.yaw && rotState.yaw !== 0) {
|
|
1388
|
+
const binding = getBindingForAxis(config.yaw, rotState.yaw);
|
|
1389
|
+
if (binding?.maxDegrees && binding.channel) {
|
|
1390
|
+
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.yaw) * binding.scale;
|
|
1391
|
+
const axis = getAxis(binding.channel);
|
|
1392
|
+
const deltaQ = new three.Quaternion().setFromAxisAngle(axis, radians);
|
|
1393
|
+
compositeQ.multiply(deltaQ);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (config.pitch && rotState.pitch !== 0) {
|
|
1397
|
+
const binding = getBindingForAxis(config.pitch, rotState.pitch);
|
|
1398
|
+
if (binding?.maxDegrees && binding.channel) {
|
|
1399
|
+
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.pitch) * binding.scale;
|
|
1400
|
+
const axis = getAxis(binding.channel);
|
|
1401
|
+
const deltaQ = new three.Quaternion().setFromAxisAngle(axis, radians);
|
|
1402
|
+
compositeQ.multiply(deltaQ);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (config.roll && rotState.roll !== 0) {
|
|
1406
|
+
const binding = getBindingForAxis(config.roll, rotState.roll);
|
|
1407
|
+
if (binding?.maxDegrees && binding.channel) {
|
|
1408
|
+
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.roll) * binding.scale;
|
|
1409
|
+
const axis = getAxis(binding.channel);
|
|
1410
|
+
const deltaQ = new three.Quaternion().setFromAxisAngle(axis, radians);
|
|
1411
|
+
compositeQ.multiply(deltaQ);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1126
1414
|
obj.position.copy(basePos);
|
|
1127
1415
|
const t = this.translations[nodeKey];
|
|
1128
1416
|
if (t) {
|
|
@@ -1130,7 +1418,7 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
1130
1418
|
obj.position.y += t.y;
|
|
1131
1419
|
obj.position.z += t.z;
|
|
1132
1420
|
}
|
|
1133
|
-
obj.
|
|
1421
|
+
obj.quaternion.copy(compositeQ);
|
|
1134
1422
|
obj.updateMatrixWorld(false);
|
|
1135
1423
|
this.model.updateMatrixWorld(true);
|
|
1136
1424
|
}
|
|
@@ -1200,6 +1488,13 @@ var _LoomLargeThree = class _LoomLargeThree {
|
|
|
1200
1488
|
if (typeof settings.depthTest === "boolean") {
|
|
1201
1489
|
obj.material.depthTest = settings.depthTest;
|
|
1202
1490
|
}
|
|
1491
|
+
if (typeof settings.blending === "string") {
|
|
1492
|
+
const blendValue = _LoomLargeThree.BLENDING_MODES[settings.blending];
|
|
1493
|
+
if (blendValue !== void 0) {
|
|
1494
|
+
obj.material.blending = blendValue;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
obj.material.needsUpdate = true;
|
|
1203
1498
|
}
|
|
1204
1499
|
});
|
|
1205
1500
|
}
|
|
@@ -1223,6 +1518,19 @@ __publicField(_LoomLargeThree, "VISEME_JAW_AMOUNTS", [
|
|
|
1223
1518
|
0.4
|
|
1224
1519
|
]);
|
|
1225
1520
|
__publicField(_LoomLargeThree, "JAW_MAX_DEGREES", 28);
|
|
1521
|
+
/** Blending mode options for Three.js materials */
|
|
1522
|
+
__publicField(_LoomLargeThree, "BLENDING_MODES", {
|
|
1523
|
+
"Normal": 1,
|
|
1524
|
+
// THREE.NormalBlending
|
|
1525
|
+
"Additive": 2,
|
|
1526
|
+
// THREE.AdditiveBlending
|
|
1527
|
+
"Subtractive": 3,
|
|
1528
|
+
// THREE.SubtractiveBlending
|
|
1529
|
+
"Multiply": 4,
|
|
1530
|
+
// THREE.MultiplyBlending
|
|
1531
|
+
"None": 0
|
|
1532
|
+
// THREE.NoBlending
|
|
1533
|
+
});
|
|
1226
1534
|
var LoomLargeThree = _LoomLargeThree;
|
|
1227
1535
|
function collectMorphMeshes(root) {
|
|
1228
1536
|
const meshes = [];
|
|
@@ -1236,6 +1544,20 @@ function collectMorphMeshes(root) {
|
|
|
1236
1544
|
return meshes;
|
|
1237
1545
|
}
|
|
1238
1546
|
|
|
1547
|
+
// src/mappings/types.ts
|
|
1548
|
+
var BLENDING_MODES = {
|
|
1549
|
+
"Normal": 1,
|
|
1550
|
+
// THREE.NormalBlending
|
|
1551
|
+
"Additive": 2,
|
|
1552
|
+
// THREE.AdditiveBlending
|
|
1553
|
+
"Subtractive": 3,
|
|
1554
|
+
// THREE.SubtractiveBlending
|
|
1555
|
+
"Multiply": 4,
|
|
1556
|
+
// THREE.MultiplyBlending
|
|
1557
|
+
"None": 0
|
|
1558
|
+
// THREE.NoBlending
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1239
1561
|
// src/physics/HairPhysics.ts
|
|
1240
1562
|
var DEFAULT_HAIR_PHYSICS_CONFIG = {
|
|
1241
1563
|
mass: 1,
|
|
@@ -1364,6 +1686,7 @@ exports.AU_INFO = AU_INFO;
|
|
|
1364
1686
|
exports.AU_MIX_DEFAULTS = AU_MIX_DEFAULTS;
|
|
1365
1687
|
exports.AU_TO_MORPHS = AU_TO_MORPHS;
|
|
1366
1688
|
exports.AnimationThree = AnimationThree;
|
|
1689
|
+
exports.BLENDING_MODES = BLENDING_MODES;
|
|
1367
1690
|
exports.BONE_AU_TO_BINDINGS = BONE_AU_TO_BINDINGS;
|
|
1368
1691
|
exports.CC4_BONE_NODES = CC4_BONE_NODES;
|
|
1369
1692
|
exports.CC4_EYE_MESH_NODES = CC4_EYE_MESH_NODES;
|