loomlarge 0.1.0
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 +714 -0
- package/dist/index.cjs +1176 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +631 -0
- package/dist/index.d.ts +631 -0
- package/dist/index.js +1166 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/engines/three/AnimationThree.ts
|
|
6
|
+
var easeInOutQuad = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
7
|
+
var AnimationThree = class {
|
|
8
|
+
constructor() {
|
|
9
|
+
__publicField(this, "transitions", /* @__PURE__ */ new Map());
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Tick all active transitions by dt seconds.
|
|
13
|
+
* Applies eased interpolation and removes completed transitions.
|
|
14
|
+
* Respects individual transition pause state.
|
|
15
|
+
*/
|
|
16
|
+
tick(dtSeconds) {
|
|
17
|
+
if (dtSeconds <= 0) return;
|
|
18
|
+
const completed = [];
|
|
19
|
+
this.transitions.forEach((t, key) => {
|
|
20
|
+
if (t.paused) return;
|
|
21
|
+
t.elapsed += dtSeconds;
|
|
22
|
+
const progress = Math.min(t.elapsed / t.duration, 1);
|
|
23
|
+
const easedProgress = t.easing(progress);
|
|
24
|
+
const value = t.from + (t.to - t.from) * easedProgress;
|
|
25
|
+
t.apply(value);
|
|
26
|
+
if (progress >= 1) {
|
|
27
|
+
completed.push(key);
|
|
28
|
+
t.resolve?.();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
completed.forEach((key) => this.transitions.delete(key));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Add or replace a transition for the given key.
|
|
35
|
+
* If a transition with the same key exists, it is cancelled and replaced.
|
|
36
|
+
* @returns TransitionHandle with { promise, pause, resume, cancel }
|
|
37
|
+
*/
|
|
38
|
+
addTransition(key, from, to, durationMs, apply, easing = easeInOutQuad) {
|
|
39
|
+
const durationSec = durationMs / 1e3;
|
|
40
|
+
const existing = this.transitions.get(key);
|
|
41
|
+
if (existing?.resolve) {
|
|
42
|
+
existing.resolve();
|
|
43
|
+
}
|
|
44
|
+
if (durationSec <= 0 || Math.abs(to - from) < 1e-6) {
|
|
45
|
+
apply(to);
|
|
46
|
+
return {
|
|
47
|
+
promise: Promise.resolve(),
|
|
48
|
+
pause: () => {
|
|
49
|
+
},
|
|
50
|
+
resume: () => {
|
|
51
|
+
},
|
|
52
|
+
cancel: () => {
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const promise = new Promise((resolve) => {
|
|
57
|
+
const transitionObj = {
|
|
58
|
+
key,
|
|
59
|
+
from,
|
|
60
|
+
to,
|
|
61
|
+
duration: durationSec,
|
|
62
|
+
elapsed: 0,
|
|
63
|
+
apply,
|
|
64
|
+
easing,
|
|
65
|
+
resolve,
|
|
66
|
+
paused: false
|
|
67
|
+
};
|
|
68
|
+
this.transitions.set(key, transitionObj);
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
promise,
|
|
72
|
+
pause: () => {
|
|
73
|
+
const t = this.transitions.get(key);
|
|
74
|
+
if (t) t.paused = true;
|
|
75
|
+
},
|
|
76
|
+
resume: () => {
|
|
77
|
+
const t = this.transitions.get(key);
|
|
78
|
+
if (t) t.paused = false;
|
|
79
|
+
},
|
|
80
|
+
cancel: () => {
|
|
81
|
+
const t = this.transitions.get(key);
|
|
82
|
+
if (t) {
|
|
83
|
+
t.resolve?.();
|
|
84
|
+
this.transitions.delete(key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Clear all running transitions. */
|
|
90
|
+
clearTransitions() {
|
|
91
|
+
this.transitions.forEach((t) => t.resolve?.());
|
|
92
|
+
this.transitions.clear();
|
|
93
|
+
}
|
|
94
|
+
/** Get count of active transitions. */
|
|
95
|
+
getActiveTransitionCount() {
|
|
96
|
+
return this.transitions.size;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/presets/cc4.ts
|
|
101
|
+
var AU_TO_MORPHS = {
|
|
102
|
+
// Brows / Forehead
|
|
103
|
+
1: ["Brow_Raise_Inner_L", "Brow_Raise_Inner_R"],
|
|
104
|
+
2: ["Brow_Raise_Outer_L", "Brow_Raise_Outer_R"],
|
|
105
|
+
4: ["Brow_Drop_L", "Brow_Drop_R"],
|
|
106
|
+
// Eyes / Lids
|
|
107
|
+
5: ["Eye_Wide_L", "Eye_Wide_R"],
|
|
108
|
+
6: ["Cheek_Raise_L", "Cheek_Raise_R"],
|
|
109
|
+
7: ["Eye_Squint_L", "Eye_Squint_R"],
|
|
110
|
+
43: ["Eye_Blink_L", "Eye_Blink_R"],
|
|
111
|
+
45: ["Eye_Blink_L", "Eye_Blink_R"],
|
|
112
|
+
// Blink alias
|
|
113
|
+
// Nose / Midface
|
|
114
|
+
9: ["Nose_Sneer_L", "Nose_Sneer_R"],
|
|
115
|
+
34: ["Cheek_Puff_L", "Cheek_Puff_R"],
|
|
116
|
+
// Mouth / Lips
|
|
117
|
+
8: ["Mouth_Press_L", "Mouth_Press_R", "Mouth_Close"],
|
|
118
|
+
10: ["Nose_Sneer_L", "Nose_Sneer_R"],
|
|
119
|
+
// Upper Lip Raiser (levator labii superioris) - raises upper lip in disgust/sneer
|
|
120
|
+
11: ["Mouth_Up_Upper_L", "Mouth_Up_Upper_R"],
|
|
121
|
+
// Nasolabial Deepener (zygomaticus minor) - no dedicated morph
|
|
122
|
+
12: ["Mouth_Smile_L", "Mouth_Smile_R"],
|
|
123
|
+
13: ["Mouth_Dimple_L", "Mouth_Dimple_R"],
|
|
124
|
+
// Sharp Lip Puller (levator anguli oris) - pulls lip corners up
|
|
125
|
+
14: ["Mouth_Press_L", "Mouth_Press_R"],
|
|
126
|
+
15: ["Mouth_Frown_L", "Mouth_Frown_R"],
|
|
127
|
+
16: ["Mouth_Down_Lower_L", "Mouth_Down_Lower_R"],
|
|
128
|
+
17: ["Mouth_Shrug_Lower"],
|
|
129
|
+
18: ["Mouth_Pucker"],
|
|
130
|
+
20: ["Mouth_Stretch_L", "Mouth_Stretch_R"],
|
|
131
|
+
22: ["Mouth_Funnel"],
|
|
132
|
+
23: ["Mouth_Press_L", "Mouth_Press_R"],
|
|
133
|
+
24: ["Mouth_Press_L", "Mouth_Press_R"],
|
|
134
|
+
25: ["Jaw_Open"],
|
|
135
|
+
// Lips Part - small jaw open with morph
|
|
136
|
+
26: ["Jaw_Open"],
|
|
137
|
+
// Jaw Drop - mixed: bone rotation + Jaw_Open morph
|
|
138
|
+
27: ["Jaw_Open"],
|
|
139
|
+
// Mouth Stretch - larger jaw open with morph
|
|
140
|
+
28: ["Mouth_Roll_In_Upper", "Mouth_Roll_In_Lower"],
|
|
141
|
+
32: ["Mouth_Roll_In_Lower"],
|
|
142
|
+
// Lip Bite - using roll in lower as approximation
|
|
143
|
+
// Tongue
|
|
144
|
+
19: ["Tongue_Out"],
|
|
145
|
+
36: ["Tongue_Bulge_L", "Tongue_Bulge_R"],
|
|
146
|
+
37: ["Tongue_Up"],
|
|
147
|
+
// Tongue Up - morph + bone rotation
|
|
148
|
+
38: ["Tongue_Down"],
|
|
149
|
+
// Tongue Down - morph + bone rotation
|
|
150
|
+
39: ["Tongue_L"],
|
|
151
|
+
// Tongue Left - morph + bone rotation
|
|
152
|
+
40: ["Tongue_R"],
|
|
153
|
+
// Tongue Right - morph + bone rotation
|
|
154
|
+
41: [],
|
|
155
|
+
// Tongue Tilt Left - BONE ONLY
|
|
156
|
+
42: [],
|
|
157
|
+
// Tongue Tilt Right - BONE ONLY
|
|
158
|
+
// Extended tongue morphs (CC4-specific)
|
|
159
|
+
73: ["Tongue_Narrow"],
|
|
160
|
+
74: ["Tongue_Wide"],
|
|
161
|
+
75: ["Tongue_Roll"],
|
|
162
|
+
76: ["Tongue_Tip_Up"],
|
|
163
|
+
77: ["Tongue_Tip_Down"],
|
|
164
|
+
// Jaw
|
|
165
|
+
29: ["Jaw_Forward"],
|
|
166
|
+
30: ["Jaw_L"],
|
|
167
|
+
// Jaw Left - mixed: bone rotation + Jaw_L morph
|
|
168
|
+
31: [],
|
|
169
|
+
// Jaw Clencher (masseter/temporalis) - no dedicated CC4 morph
|
|
170
|
+
35: ["Jaw_R"],
|
|
171
|
+
// Jaw Right - mixed: bone rotation + Jaw_R morph
|
|
172
|
+
// Head position (M51-M56 in FACS notation)
|
|
173
|
+
51: ["Head_Turn_L"],
|
|
174
|
+
// Head turn left
|
|
175
|
+
52: ["Head_Turn_R"],
|
|
176
|
+
// Head turn right
|
|
177
|
+
53: ["Head_Turn_Up"],
|
|
178
|
+
// Head up
|
|
179
|
+
54: ["Head_Turn_Down"],
|
|
180
|
+
55: ["Head_Tilt_L"],
|
|
181
|
+
56: ["Head_Tilt_R"],
|
|
182
|
+
// Eye Direction (convenience)
|
|
183
|
+
61: ["Eye_L_Look_L", "Eye_R_Look_L"],
|
|
184
|
+
62: ["Eye_L_Look_R", "Eye_R_Look_R"],
|
|
185
|
+
63: ["Eye_L_Look_Up", "Eye_R_Look_Up"],
|
|
186
|
+
64: ["Eye_L_Look_Down", "Eye_R_Look_Down"],
|
|
187
|
+
// Single-eye controls (Left eye)
|
|
188
|
+
65: ["Eye_L_Look_L"],
|
|
189
|
+
66: ["Eye_L_Look_R"],
|
|
190
|
+
67: ["Eye_L_Look_Up"],
|
|
191
|
+
68: ["Eye_L_Look_Down"],
|
|
192
|
+
// Single-eye controls (Right eye)
|
|
193
|
+
69: ["Eye_R_Look_L"],
|
|
194
|
+
70: ["Eye_R_Look_R"],
|
|
195
|
+
71: ["Eye_R_Look_Up"],
|
|
196
|
+
72: ["Eye_R_Look_Down"],
|
|
197
|
+
// Eye morphs (CC_Base_Eye meshes)
|
|
198
|
+
// EO morphs control the shadow/depth around the eyes
|
|
199
|
+
80: ["EO Bulge L", "EO Bulge R"],
|
|
200
|
+
81: ["EO Depth L", "EO Depth R"],
|
|
201
|
+
82: ["EO Inner Depth L", "EO Inner Depth R"],
|
|
202
|
+
83: ["EO Inner Height L", "EO Inner Height R"],
|
|
203
|
+
84: ["EO Inner Width L", "EO Inner Width R"],
|
|
204
|
+
85: ["EO Outer Depth L", "EO Outer Depth R"],
|
|
205
|
+
86: ["EO Outer Height L", "EO Outer Height R"],
|
|
206
|
+
87: ["EO Outer Width L", "EO Outer Width R"],
|
|
207
|
+
88: ["EO Upper Depth L", "EO Upper Depth R"],
|
|
208
|
+
89: ["EO Lower Depth L", "EO Lower Depth R"],
|
|
209
|
+
90: ["EO Center Upper Depth L", "EO Center Upper Depth R"],
|
|
210
|
+
91: ["EO Center Upper Height L", "EO Center Upper Height R"],
|
|
211
|
+
92: ["EO Center Lower Depth L", "EO Center Lower Depth R"],
|
|
212
|
+
93: ["EO Center Lower Height L", "EO Center Lower Height R"],
|
|
213
|
+
94: ["EO Inner Upper Depth L", "EO Inner Upper Depth R"],
|
|
214
|
+
95: ["EO Inner Upper Height L", "EO Inner Upper Height R"],
|
|
215
|
+
96: ["EO Inner Lower Depth L", "EO Inner Lower Depth R"],
|
|
216
|
+
97: ["EO Inner Lower Height L", "EO Inner Lower Height R"],
|
|
217
|
+
98: ["EO Outer Upper Depth L", "EO Outer Upper Depth R"],
|
|
218
|
+
99: ["EO Outer Upper Height L", "EO Outer Upper Height R"],
|
|
219
|
+
100: ["EO Outer Lower Depth L", "EO Outer Lower Depth R"],
|
|
220
|
+
101: ["EO Outer Lower Height L", "EO Outer Lower Height R"],
|
|
221
|
+
102: ["EO Duct Depth L", "EO Duct Depth R"]
|
|
222
|
+
};
|
|
223
|
+
var VISEME_KEYS = [
|
|
224
|
+
"EE",
|
|
225
|
+
"Er",
|
|
226
|
+
"IH",
|
|
227
|
+
"Ah",
|
|
228
|
+
"Oh",
|
|
229
|
+
"W_OO",
|
|
230
|
+
"S_Z",
|
|
231
|
+
"Ch_J",
|
|
232
|
+
"F_V",
|
|
233
|
+
"TH",
|
|
234
|
+
"T_L_D_N",
|
|
235
|
+
"B_M_P",
|
|
236
|
+
"K_G_H_NG",
|
|
237
|
+
"AE",
|
|
238
|
+
"R"
|
|
239
|
+
];
|
|
240
|
+
var BONE_AU_TO_BINDINGS = {
|
|
241
|
+
// Head turn and tilt (M51-M56) - use HEAD bone only (NECK should not rotate with head)
|
|
242
|
+
// Three.js Y rotation: positive = counter-clockwise from above = head turns LEFT (character POV)
|
|
243
|
+
51: [
|
|
244
|
+
{ node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30 }
|
|
245
|
+
// Head turn left
|
|
246
|
+
],
|
|
247
|
+
52: [
|
|
248
|
+
{ node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30 }
|
|
249
|
+
// Head turn right
|
|
250
|
+
],
|
|
251
|
+
53: [
|
|
252
|
+
{ node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20 }
|
|
253
|
+
// Head up
|
|
254
|
+
],
|
|
255
|
+
54: [
|
|
256
|
+
{ node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20 }
|
|
257
|
+
// Head down
|
|
258
|
+
],
|
|
259
|
+
55: [
|
|
260
|
+
{ node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15 }
|
|
261
|
+
// Head tilt left
|
|
262
|
+
],
|
|
263
|
+
56: [
|
|
264
|
+
{ node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15 }
|
|
265
|
+
// Head tilt right
|
|
266
|
+
],
|
|
267
|
+
// Eyes horizontal (yaw) - CC4 rigs use rz for horizontal eye rotation
|
|
268
|
+
61: [
|
|
269
|
+
{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32 },
|
|
270
|
+
// Eyes look left
|
|
271
|
+
{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32 }
|
|
272
|
+
],
|
|
273
|
+
62: [
|
|
274
|
+
{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32 },
|
|
275
|
+
// Eyes look right
|
|
276
|
+
{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32 }
|
|
277
|
+
],
|
|
278
|
+
63: [
|
|
279
|
+
{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32 },
|
|
280
|
+
{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 32 }
|
|
281
|
+
],
|
|
282
|
+
64: [
|
|
283
|
+
{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32 },
|
|
284
|
+
{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 32 }
|
|
285
|
+
],
|
|
286
|
+
// Single-eye (Left) — horizontal (rz for CC4) and vertical (rx)
|
|
287
|
+
65: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 15 }],
|
|
288
|
+
66: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 15 }],
|
|
289
|
+
67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 12 }],
|
|
290
|
+
68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 12 }],
|
|
291
|
+
// Single-eye (Right) — horizontal (rz for CC4) and vertical (rx)
|
|
292
|
+
69: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 15 }],
|
|
293
|
+
70: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 15 }],
|
|
294
|
+
71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 12 }],
|
|
295
|
+
72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 12 }],
|
|
296
|
+
// Jaw / Mouth
|
|
297
|
+
8: [
|
|
298
|
+
// Lips Toward Each Other - slight jaw open helps sell the lip press
|
|
299
|
+
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 8 }
|
|
300
|
+
// Small downward rotation (jaw opening slightly)
|
|
301
|
+
],
|
|
302
|
+
25: [
|
|
303
|
+
// Lips Part — small jaw open
|
|
304
|
+
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84 }
|
|
305
|
+
// 73% of 8
|
|
306
|
+
],
|
|
307
|
+
26: [
|
|
308
|
+
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 28 }
|
|
309
|
+
// 73% of 20
|
|
310
|
+
],
|
|
311
|
+
27: [
|
|
312
|
+
// Mouth Stretch — larger jaw open
|
|
313
|
+
{ node: "JAW", channel: "rz", scale: 1, maxDegrees: 32 }
|
|
314
|
+
// 73% of 25
|
|
315
|
+
],
|
|
316
|
+
29: [
|
|
317
|
+
{ node: "JAW", channel: "tz", scale: -1, maxUnits: 0.02 }
|
|
318
|
+
// Negative for forward thrust
|
|
319
|
+
],
|
|
320
|
+
30: [
|
|
321
|
+
// Jaw Left
|
|
322
|
+
{ node: "JAW", channel: "ry", scale: -1, maxDegrees: 5 }
|
|
323
|
+
],
|
|
324
|
+
35: [
|
|
325
|
+
// Jaw Right
|
|
326
|
+
{ node: "JAW", channel: "ry", scale: 1, maxDegrees: 5 }
|
|
327
|
+
],
|
|
328
|
+
// Tongue
|
|
329
|
+
19: [
|
|
330
|
+
{ node: "TONGUE", channel: "tz", scale: -1, maxUnits: 8e-3 }
|
|
331
|
+
],
|
|
332
|
+
37: [
|
|
333
|
+
// Tongue Up
|
|
334
|
+
{ node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45 }
|
|
335
|
+
],
|
|
336
|
+
38: [
|
|
337
|
+
// Tongue Down
|
|
338
|
+
{ node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45 }
|
|
339
|
+
],
|
|
340
|
+
39: [
|
|
341
|
+
// Tongue Left
|
|
342
|
+
{ node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10 }
|
|
343
|
+
],
|
|
344
|
+
40: [
|
|
345
|
+
// Tongue Right
|
|
346
|
+
{ node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10 }
|
|
347
|
+
],
|
|
348
|
+
41: [
|
|
349
|
+
// Tongue Tilt Left
|
|
350
|
+
{ node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20 }
|
|
351
|
+
],
|
|
352
|
+
42: [
|
|
353
|
+
// Tongue Tilt Right
|
|
354
|
+
{ node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20 }
|
|
355
|
+
]
|
|
356
|
+
};
|
|
357
|
+
var CC4_BONE_NODES = {
|
|
358
|
+
EYE_L: "CC_Base_L_Eye",
|
|
359
|
+
EYE_R: "CC_Base_R_Eye",
|
|
360
|
+
HEAD: "CC_Base_Head",
|
|
361
|
+
NECK: "CC_Base_NeckTwist01",
|
|
362
|
+
NECK_TWIST: "CC_Base_NeckTwist02",
|
|
363
|
+
JAW: "CC_Base_JawRoot",
|
|
364
|
+
TONGUE: "CC_Base_Tongue01"
|
|
365
|
+
};
|
|
366
|
+
var CC4_EYE_MESH_NODES = {
|
|
367
|
+
LEFT: "CC_Base_Eye",
|
|
368
|
+
RIGHT: "CC_Base_Eye_1"
|
|
369
|
+
};
|
|
370
|
+
var AU_INFO = {
|
|
371
|
+
// Forehead / Brow (Upper)
|
|
372
|
+
"1": { id: "1", name: "Inner Brow Raiser", muscularBasis: "frontalis (pars medialis)", links: ["https://en.wikipedia.org/wiki/Frontalis_muscle"], faceArea: "Upper", facePart: "Forehead" },
|
|
373
|
+
"2": { id: "2", name: "Outer Brow Raiser", muscularBasis: "frontalis (pars lateralis)", links: ["https://en.wikipedia.org/wiki/Frontalis_muscle"], faceArea: "Upper", facePart: "Forehead" },
|
|
374
|
+
"4": { id: "4", name: "Brow Lowerer", muscularBasis: "corrugator/depressor supercilii", links: ["https://en.wikipedia.org/wiki/Corrugator_supercilii"], faceArea: "Upper", facePart: "Forehead" },
|
|
375
|
+
// Eyelids / Eyes (Upper)
|
|
376
|
+
"5": { id: "5", name: "Upper Lid Raiser", muscularBasis: "levator palpebrae superioris", links: ["https://en.wikipedia.org/wiki/Levator_palpebrae_superioris"], faceArea: "Upper", facePart: "Eyelids" },
|
|
377
|
+
"6": { id: "6", name: "Cheek Raiser", muscularBasis: "orbicularis oculi (pars orbitalis)", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi"], faceArea: "Upper", facePart: "Cheeks" },
|
|
378
|
+
"7": { id: "7", name: "Lid Tightener", muscularBasis: "orbicularis oculi (pars palpebralis)", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi"], faceArea: "Upper", facePart: "Eyelids" },
|
|
379
|
+
"43": { id: "43", name: "Eyes Closed", muscularBasis: "orbicularis oculi", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi_muscle"], faceArea: "Upper", facePart: "Eyelids" },
|
|
380
|
+
"45": { id: "45", name: "Blink", muscularBasis: "orbicularis oculi", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi_muscle"], faceArea: "Upper", facePart: "Eyelids" },
|
|
381
|
+
"61": { id: "61", name: "Eyes Turn Left", faceArea: "Upper", facePart: "Eyes" },
|
|
382
|
+
"62": { id: "62", name: "Eyes Turn Right", faceArea: "Upper", facePart: "Eyes" },
|
|
383
|
+
"63": { id: "63", name: "Eyes Up", faceArea: "Upper", facePart: "Eyes" },
|
|
384
|
+
"64": { id: "64", name: "Eyes Down", faceArea: "Upper", facePart: "Eyes" },
|
|
385
|
+
"65": { id: "65", name: "Left Eye Look Left", faceArea: "Upper", facePart: "Eyes" },
|
|
386
|
+
"66": { id: "66", name: "Left Eye Look Right", faceArea: "Upper", facePart: "Eyes" },
|
|
387
|
+
"67": { id: "67", name: "Left Eye Look Up", faceArea: "Upper", facePart: "Eyes" },
|
|
388
|
+
"68": { id: "68", name: "Left Eye Look Down", faceArea: "Upper", facePart: "Eyes" },
|
|
389
|
+
"69": { id: "69", name: "Right Eye Look Left", faceArea: "Upper", facePart: "Eyes" },
|
|
390
|
+
"70": { id: "70", name: "Right Eye Look Right", faceArea: "Upper", facePart: "Eyes" },
|
|
391
|
+
"71": { id: "71", name: "Right Eye Look Up", faceArea: "Upper", facePart: "Eyes" },
|
|
392
|
+
"72": { id: "72", name: "Right Eye Look Down", faceArea: "Upper", facePart: "Eyes" },
|
|
393
|
+
// Nose / Cheeks
|
|
394
|
+
"9": { id: "9", name: "Nose Wrinkler", muscularBasis: "levator labii superioris alaeque nasi", links: ["https://en.wikipedia.org/wiki/Levator_labii_superioris_alaeque_nasi"], faceArea: "Upper", facePart: "Nose" },
|
|
395
|
+
"34": { id: "34", name: "Cheek Puff", faceArea: "Lower", facePart: "Cheeks" },
|
|
396
|
+
// Mouth (Lower)
|
|
397
|
+
"8": { id: "8", name: "Lips Toward Each Other", muscularBasis: "orbicularis oris", links: ["https://en.wikipedia.org/wiki/Orbicularis_oris"], faceArea: "Lower", facePart: "Mouth" },
|
|
398
|
+
"10": { id: "10", name: "Upper Lip Raiser", muscularBasis: "levator labii superioris", links: ["https://en.wikipedia.org/wiki/Levator_labii_superioris"], faceArea: "Lower", facePart: "Mouth" },
|
|
399
|
+
"11": { id: "11", name: "Nasolabial Deepener", muscularBasis: "zygomaticus minor", links: ["https://en.wikipedia.org/wiki/Zygomaticus_minor"], faceArea: "Lower", facePart: "Cheeks" },
|
|
400
|
+
"12": { id: "12", name: "Lip Corner Puller", muscularBasis: "zygomaticus major", links: ["https://en.wikipedia.org/wiki/Zygomaticus_major"], faceArea: "Lower", facePart: "Mouth" },
|
|
401
|
+
"13": { id: "13", name: "Sharp Lip Puller", muscularBasis: "levator anguli oris", links: ["https://en.wikipedia.org/wiki/Levator_anguli_oris"], faceArea: "Lower", facePart: "Mouth" },
|
|
402
|
+
"14": { id: "14", name: "Dimpler", muscularBasis: "buccinator", links: ["https://en.wikipedia.org/wiki/Buccinator"], faceArea: "Lower", facePart: "Cheeks" },
|
|
403
|
+
"15": { id: "15", name: "Lip Corner Depressor", muscularBasis: "depressor anguli oris", links: ["https://en.wikipedia.org/wiki/Depressor_anguli_oris"], faceArea: "Lower", facePart: "Mouth" },
|
|
404
|
+
"16": { id: "16", name: "Lower Lip Depressor", muscularBasis: "depressor labii inferioris", links: ["https://en.wikipedia.org/wiki/Depressor_labii_inferioris"], faceArea: "Lower", facePart: "Mouth" },
|
|
405
|
+
"17": { id: "17", name: "Chin Raiser", muscularBasis: "mentalis", links: ["https://en.wikipedia.org/wiki/Mentalis"], faceArea: "Lower", facePart: "Chin" },
|
|
406
|
+
"18": { id: "18", name: "Lip Pucker", faceArea: "Lower", facePart: "Mouth" },
|
|
407
|
+
"20": { id: "20", name: "Lip Stretcher", muscularBasis: "risorius + platysma", links: ["https://en.wikipedia.org/wiki/Risorius", "https://en.wikipedia.org/wiki/Platysma"], faceArea: "Lower", facePart: "Mouth" },
|
|
408
|
+
"22": { id: "22", name: "Lip Funneler", muscularBasis: "orbicularis oris", links: ["https://en.wikipedia.org/wiki/Orbicularis_oris"], faceArea: "Lower", facePart: "Mouth" },
|
|
409
|
+
"23": { id: "23", name: "Lip Tightener", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
|
|
410
|
+
"24": { id: "24", name: "Lip Presser", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
|
|
411
|
+
"25": { id: "25", name: "Lips Part", faceArea: "Lower", facePart: "Mouth" },
|
|
412
|
+
"27": { id: "27", name: "Mouth Stretch", muscularBasis: "pterygoids + digastric", links: ["https://en.wikipedia.org/wiki/Pterygoid_bone", "https://en.wikipedia.org/wiki/Digastric_muscle"], faceArea: "Lower", facePart: "Mouth" },
|
|
413
|
+
"28": { id: "28", name: "Lip Suck", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
|
|
414
|
+
// Tongue (Lower)
|
|
415
|
+
"19": { id: "19", name: "Tongue Show", faceArea: "Lower", facePart: "Tongue" },
|
|
416
|
+
"36": { id: "36", name: "Tongue Bulge", faceArea: "Lower", facePart: "Tongue" },
|
|
417
|
+
"37": { id: "37", name: "Tongue Up", faceArea: "Lower", facePart: "Tongue" },
|
|
418
|
+
"38": { id: "38", name: "Tongue Down", faceArea: "Lower", facePart: "Tongue" },
|
|
419
|
+
"39": { id: "39", name: "Tongue Left", faceArea: "Lower", facePart: "Tongue" },
|
|
420
|
+
"40": { id: "40", name: "Tongue Right", faceArea: "Lower", facePart: "Tongue" },
|
|
421
|
+
"41": { id: "41", name: "Tongue Tilt Left", faceArea: "Lower", facePart: "Tongue" },
|
|
422
|
+
"42": { id: "42", name: "Tongue Tilt Right", faceArea: "Lower", facePart: "Tongue" },
|
|
423
|
+
// Extended tongue controls (CC4-specific morphs)
|
|
424
|
+
"73": { id: "73", name: "Tongue Narrow", faceArea: "Lower", facePart: "Tongue" },
|
|
425
|
+
"74": { id: "74", name: "Tongue Wide", faceArea: "Lower", facePart: "Tongue" },
|
|
426
|
+
"75": { id: "75", name: "Tongue Roll", faceArea: "Lower", facePart: "Tongue" },
|
|
427
|
+
"76": { id: "76", name: "Tongue Tip Up", faceArea: "Lower", facePart: "Tongue" },
|
|
428
|
+
"77": { id: "77", name: "Tongue Tip Down", faceArea: "Lower", facePart: "Tongue" },
|
|
429
|
+
// Jaw (Lower)
|
|
430
|
+
"26": { id: "26", name: "Jaw Drop", muscularBasis: "masseter (relax temporalis)", links: ["https://en.wikipedia.org/wiki/Masseter_muscle"], faceArea: "Lower", facePart: "Jaw" },
|
|
431
|
+
"29": { id: "29", name: "Jaw Thrust", faceArea: "Lower", facePart: "Jaw" },
|
|
432
|
+
"30": { id: "30", name: "Jaw Left", faceArea: "Lower", facePart: "Jaw" },
|
|
433
|
+
"31": { id: "31", name: "Jaw Clencher", muscularBasis: "masseter + temporalis", faceArea: "Lower", facePart: "Jaw" },
|
|
434
|
+
"32": { id: "32", name: "Lip Bite", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
|
|
435
|
+
"35": { id: "35", name: "Jaw Right", faceArea: "Lower", facePart: "Jaw" },
|
|
436
|
+
// Head position (M51-M56 in FACS notation)
|
|
437
|
+
"51": { id: "51", name: "Head Turn Left", faceArea: "Upper", facePart: "Head" },
|
|
438
|
+
"52": { id: "52", name: "Head Turn Right", faceArea: "Upper", facePart: "Head" },
|
|
439
|
+
"53": { id: "53", name: "Head Up", faceArea: "Upper", facePart: "Head" },
|
|
440
|
+
"54": { id: "54", name: "Head Down", faceArea: "Upper", facePart: "Head" },
|
|
441
|
+
"55": { id: "55", name: "Head Tilt Left", faceArea: "Upper", facePart: "Head" },
|
|
442
|
+
"56": { id: "56", name: "Head Tilt Right", faceArea: "Upper", facePart: "Head" },
|
|
443
|
+
// Eye (Upper) - EO morphs applied to CC_Base_Eye meshes
|
|
444
|
+
"80": { id: "80", name: "Eye Bulge", faceArea: "Upper", facePart: "Eye" },
|
|
445
|
+
"81": { id: "81", name: "Eye Depth", faceArea: "Upper", facePart: "Eye" },
|
|
446
|
+
"82": { id: "82", name: "Eye Inner Depth", faceArea: "Upper", facePart: "Eye" },
|
|
447
|
+
"83": { id: "83", name: "Eye Inner Height", faceArea: "Upper", facePart: "Eye" },
|
|
448
|
+
"84": { id: "84", name: "Eye Inner Width", faceArea: "Upper", facePart: "Eye" },
|
|
449
|
+
"85": { id: "85", name: "Eye Outer Depth", faceArea: "Upper", facePart: "Eye" },
|
|
450
|
+
"86": { id: "86", name: "Eye Outer Height", faceArea: "Upper", facePart: "Eye" },
|
|
451
|
+
"87": { id: "87", name: "Eye Outer Width", faceArea: "Upper", facePart: "Eye" },
|
|
452
|
+
"88": { id: "88", name: "Eye Upper Depth", faceArea: "Upper", facePart: "Eye" },
|
|
453
|
+
"89": { id: "89", name: "Eye Lower Depth", faceArea: "Upper", facePart: "Eye" },
|
|
454
|
+
"90": { id: "90", name: "Eye Center Upper Depth", faceArea: "Upper", facePart: "Eye" },
|
|
455
|
+
"91": { id: "91", name: "Eye Center Upper Height", faceArea: "Upper", facePart: "Eye" },
|
|
456
|
+
"92": { id: "92", name: "Eye Center Lower Depth", faceArea: "Upper", facePart: "Eye" },
|
|
457
|
+
"93": { id: "93", name: "Eye Center Lower Height", faceArea: "Upper", facePart: "Eye" },
|
|
458
|
+
"94": { id: "94", name: "Eye Inner Upper Depth", faceArea: "Upper", facePart: "Eye" },
|
|
459
|
+
"95": { id: "95", name: "Eye Inner Upper Height", faceArea: "Upper", facePart: "Eye" },
|
|
460
|
+
"96": { id: "96", name: "Eye Inner Lower Depth", faceArea: "Upper", facePart: "Eye" },
|
|
461
|
+
"97": { id: "97", name: "Eye Inner Lower Height", faceArea: "Upper", facePart: "Eye" },
|
|
462
|
+
"98": { id: "98", name: "Eye Outer Upper Depth", faceArea: "Upper", facePart: "Eye" },
|
|
463
|
+
"99": { id: "99", name: "Eye Outer Upper Height", faceArea: "Upper", facePart: "Eye" },
|
|
464
|
+
"100": { id: "100", name: "Eye Outer Lower Depth", faceArea: "Upper", facePart: "Eye" },
|
|
465
|
+
"101": { id: "101", name: "Eye Outer Lower Height", faceArea: "Upper", facePart: "Eye" },
|
|
466
|
+
"102": { id: "102", name: "Eye Duct Depth", faceArea: "Upper", facePart: "Eye" }
|
|
467
|
+
};
|
|
468
|
+
var AU_MIX_DEFAULTS = {
|
|
469
|
+
31: 0.7,
|
|
470
|
+
32: 0.7,
|
|
471
|
+
33: 0.7,
|
|
472
|
+
54: 0.7,
|
|
473
|
+
55: 0.7,
|
|
474
|
+
56: 0.7,
|
|
475
|
+
// head
|
|
476
|
+
61: 0.5,
|
|
477
|
+
62: 0.5,
|
|
478
|
+
63: 0.5,
|
|
479
|
+
64: 0.5,
|
|
480
|
+
// eyes
|
|
481
|
+
25: 0.5,
|
|
482
|
+
26: 0.5,
|
|
483
|
+
27: 0.5,
|
|
484
|
+
// jaw open (lips part, jaw drop, mouth stretch)
|
|
485
|
+
30: 0.5,
|
|
486
|
+
35: 0.5
|
|
487
|
+
// jaw left/right
|
|
488
|
+
};
|
|
489
|
+
var MORPH_TO_MESH = {
|
|
490
|
+
// Face/AU morphs affect the main face mesh and both eyebrow meshes.
|
|
491
|
+
face: ["CC_Base_Body_1", "Male_Bushy_1", "Male_Bushy_2"],
|
|
492
|
+
viseme: ["CC_Base_Body_1"],
|
|
493
|
+
eye: ["CC_Base_EyeOcclusion_1", "CC_Base_EyeOcclusion_2"],
|
|
494
|
+
tearLine: ["CC_Base_TearLine_1", "CC_Base_TearLine_2"],
|
|
495
|
+
tongue: ["CC_Base_Tongue"],
|
|
496
|
+
hair: ["Side_part_wavy_1", "Side_part_wavy_2"]
|
|
497
|
+
};
|
|
498
|
+
var CC4_PRESET = {
|
|
499
|
+
auToMorphs: AU_TO_MORPHS,
|
|
500
|
+
auToBones: BONE_AU_TO_BINDINGS,
|
|
501
|
+
boneNodes: CC4_BONE_NODES,
|
|
502
|
+
morphToMesh: MORPH_TO_MESH,
|
|
503
|
+
visemeKeys: VISEME_KEYS,
|
|
504
|
+
auMixDefaults: AU_MIX_DEFAULTS,
|
|
505
|
+
auInfo: AU_INFO,
|
|
506
|
+
eyeMeshNodes: CC4_EYE_MESH_NODES
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// src/engines/three/LoomLargeThree.ts
|
|
510
|
+
var deg2rad = (d) => d * Math.PI / 180;
|
|
511
|
+
function clamp01(x) {
|
|
512
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
513
|
+
}
|
|
514
|
+
var _LoomLargeThree = class _LoomLargeThree {
|
|
515
|
+
constructor(config = {}, animation) {
|
|
516
|
+
// Configuration
|
|
517
|
+
__publicField(this, "config");
|
|
518
|
+
// Animation system (injectable)
|
|
519
|
+
__publicField(this, "animation");
|
|
520
|
+
// State
|
|
521
|
+
__publicField(this, "auValues", {});
|
|
522
|
+
__publicField(this, "rigReady", false);
|
|
523
|
+
__publicField(this, "missingBoneWarnings", /* @__PURE__ */ new Set());
|
|
524
|
+
// Rotation state
|
|
525
|
+
__publicField(this, "rotations", {});
|
|
526
|
+
__publicField(this, "pendingCompositeNodes", /* @__PURE__ */ new Set());
|
|
527
|
+
__publicField(this, "isPaused", false);
|
|
528
|
+
__publicField(this, "translations", {});
|
|
529
|
+
// Mesh references
|
|
530
|
+
__publicField(this, "faceMesh", null);
|
|
531
|
+
__publicField(this, "meshes", []);
|
|
532
|
+
__publicField(this, "model", null);
|
|
533
|
+
__publicField(this, "meshByName", /* @__PURE__ */ new Map());
|
|
534
|
+
__publicField(this, "morphCache", /* @__PURE__ */ new Map());
|
|
535
|
+
// Bones
|
|
536
|
+
__publicField(this, "bones", {});
|
|
537
|
+
__publicField(this, "mixWeights", {});
|
|
538
|
+
// Viseme state
|
|
539
|
+
__publicField(this, "visemeValues", new Array(15).fill(0));
|
|
540
|
+
this.config = config.auMappings || CC4_PRESET;
|
|
541
|
+
this.mixWeights = { ...this.config.auMixDefaults };
|
|
542
|
+
this.animation = animation || new AnimationThree();
|
|
543
|
+
}
|
|
544
|
+
// ============================================================================
|
|
545
|
+
// PUBLIC API
|
|
546
|
+
// ============================================================================
|
|
547
|
+
onReady(payload) {
|
|
548
|
+
const { meshes, model } = payload;
|
|
549
|
+
this.meshes = meshes;
|
|
550
|
+
this.model = model;
|
|
551
|
+
this.meshByName.clear();
|
|
552
|
+
this.morphCache.clear();
|
|
553
|
+
model.traverse((obj) => {
|
|
554
|
+
if (obj.isMesh && obj.name) {
|
|
555
|
+
const infl = obj.morphTargetInfluences;
|
|
556
|
+
if (Array.isArray(infl) && infl.length > 0) {
|
|
557
|
+
this.meshByName.set(obj.name, obj);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
const faceMeshNames = this.config.morphToMesh?.face || [];
|
|
562
|
+
const defaultFace = meshes.find((m) => faceMeshNames.includes(m.name));
|
|
563
|
+
if (defaultFace) {
|
|
564
|
+
this.faceMesh = defaultFace;
|
|
565
|
+
} else {
|
|
566
|
+
const candidate = meshes.find((m) => {
|
|
567
|
+
const dict = m.morphTargetDictionary;
|
|
568
|
+
return dict && typeof dict === "object" && "Brow_Drop_L" in dict;
|
|
569
|
+
});
|
|
570
|
+
this.faceMesh = candidate || null;
|
|
571
|
+
}
|
|
572
|
+
this.bones = this.resolveBones(model);
|
|
573
|
+
this.rigReady = true;
|
|
574
|
+
this.missingBoneWarnings.clear();
|
|
575
|
+
this.initBoneRotations();
|
|
576
|
+
}
|
|
577
|
+
update(deltaSeconds) {
|
|
578
|
+
const dtSeconds = Math.max(0, deltaSeconds || 0);
|
|
579
|
+
if (dtSeconds <= 0 || this.isPaused) return;
|
|
580
|
+
this.animation.tick(dtSeconds);
|
|
581
|
+
this.flushPendingComposites();
|
|
582
|
+
}
|
|
583
|
+
dispose() {
|
|
584
|
+
this.clearTransitions();
|
|
585
|
+
this.meshes = [];
|
|
586
|
+
this.model = null;
|
|
587
|
+
this.bones = {};
|
|
588
|
+
}
|
|
589
|
+
// ============================================================================
|
|
590
|
+
// AU CONTROL
|
|
591
|
+
// ============================================================================
|
|
592
|
+
setAU(id, v, balance) {
|
|
593
|
+
if (typeof id === "string") {
|
|
594
|
+
const match = id.match(/^(\d+)([LR])$/i);
|
|
595
|
+
if (match) {
|
|
596
|
+
const au = Number(match[1]);
|
|
597
|
+
const side = match[2].toUpperCase();
|
|
598
|
+
const sideBalance = side === "L" ? -1 : 1;
|
|
599
|
+
this.setAU(au, v, sideBalance);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const n = Number(id);
|
|
603
|
+
if (!Number.isNaN(n)) {
|
|
604
|
+
this.setAU(n, v, balance);
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
this.auValues[id] = v;
|
|
609
|
+
const keys = this.config.auToMorphs[id] || [];
|
|
610
|
+
if (keys.length) {
|
|
611
|
+
const mixWeight = this.isMixedAU(id) ? this.getAUMixWeight(id) : 1;
|
|
612
|
+
const base = clamp01(v) * mixWeight;
|
|
613
|
+
const meshNames = this.getMeshNamesForAU(id);
|
|
614
|
+
const leftKeys = keys.filter((k) => /(_L$| L$|Left$)/i.test(k));
|
|
615
|
+
const rightKeys = keys.filter((k) => /(_R$| R$|Right$)/i.test(k));
|
|
616
|
+
const centerKeys = keys.filter((k) => !/(_L$| L$|Left$|_R$| R$|Right$)/i.test(k));
|
|
617
|
+
const { left: leftVal, right: rightVal } = this.computeSideValues(base, balance);
|
|
618
|
+
if (leftKeys.length || rightKeys.length) {
|
|
619
|
+
for (const k of leftKeys) this.setMorph(k, leftVal, meshNames);
|
|
620
|
+
for (const k of rightKeys) this.setMorph(k, rightVal, meshNames);
|
|
621
|
+
} else {
|
|
622
|
+
centerKeys.push(...keys);
|
|
623
|
+
}
|
|
624
|
+
for (const k of centerKeys) {
|
|
625
|
+
this.setMorph(k, base, meshNames);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const bindings = this.config.auToBones[id];
|
|
629
|
+
if (bindings) {
|
|
630
|
+
for (const binding of bindings) {
|
|
631
|
+
if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
|
|
632
|
+
const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
|
|
633
|
+
this.updateBoneRotation(binding.node, axis, v * binding.scale, binding.maxDegrees ?? 0);
|
|
634
|
+
} else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
|
|
635
|
+
if (binding.maxUnits !== void 0) {
|
|
636
|
+
this.updateBoneTranslation(binding.node, binding.channel, v * binding.scale, binding.maxUnits);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
transitionAU(id, to, durationMs = 200, balance) {
|
|
643
|
+
const numId = typeof id === "string" ? Number(id.replace(/[^\d]/g, "")) : id;
|
|
644
|
+
const target = clamp01(to);
|
|
645
|
+
const morphKeys = this.config.auToMorphs[numId] || [];
|
|
646
|
+
const bindings = this.config.auToBones[numId] || [];
|
|
647
|
+
const mixWeight = this.isMixedAU(numId) ? this.getAUMixWeight(numId) : 1;
|
|
648
|
+
const base = target * mixWeight;
|
|
649
|
+
const { left: leftVal, right: rightVal } = this.computeSideValues(base, balance);
|
|
650
|
+
this.auValues[numId] = target;
|
|
651
|
+
const handles = [];
|
|
652
|
+
const meshNames = this.getMeshNamesForAU(numId);
|
|
653
|
+
const leftKeys = morphKeys.filter((k) => /(_L$|Left$)/.test(k));
|
|
654
|
+
const rightKeys = morphKeys.filter((k) => /(_R$|Right$)/.test(k));
|
|
655
|
+
const centerKeys = morphKeys.filter((k) => !/(_L$|Left$|_R$|Right$)/.test(k));
|
|
656
|
+
if (leftKeys.length || rightKeys.length) {
|
|
657
|
+
for (const k of leftKeys) {
|
|
658
|
+
handles.push(this.transitionMorph(k, leftVal, durationMs, meshNames));
|
|
659
|
+
}
|
|
660
|
+
for (const k of rightKeys) {
|
|
661
|
+
handles.push(this.transitionMorph(k, rightVal, durationMs, meshNames));
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
centerKeys.push(...morphKeys);
|
|
665
|
+
}
|
|
666
|
+
for (const k of centerKeys) {
|
|
667
|
+
handles.push(this.transitionMorph(k, base, durationMs, meshNames));
|
|
668
|
+
}
|
|
669
|
+
for (const binding of bindings) {
|
|
670
|
+
if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
|
|
671
|
+
const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
|
|
672
|
+
handles.push(this.transitionBoneRotation(binding.node, axis, target * binding.scale, binding.maxDegrees ?? 0, durationMs));
|
|
673
|
+
} else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
|
|
674
|
+
if (binding.maxUnits !== void 0) {
|
|
675
|
+
handles.push(this.transitionBoneTranslation(binding.node, binding.channel, target * binding.scale, binding.maxUnits, durationMs));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return this.combineHandles(handles);
|
|
680
|
+
}
|
|
681
|
+
getAU(id) {
|
|
682
|
+
return this.auValues[id] ?? 0;
|
|
683
|
+
}
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// MORPH CONTROL
|
|
686
|
+
// ============================================================================
|
|
687
|
+
setMorph(key, v, meshNames) {
|
|
688
|
+
const val = clamp01(v);
|
|
689
|
+
const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
|
|
690
|
+
const cached = this.morphCache.get(key);
|
|
691
|
+
if (cached) {
|
|
692
|
+
for (const target of cached) {
|
|
693
|
+
target.infl[target.idx] = val;
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const targets = [];
|
|
698
|
+
if (targetMeshes.length) {
|
|
699
|
+
for (const name of targetMeshes) {
|
|
700
|
+
const mesh = this.meshByName.get(name);
|
|
701
|
+
if (!mesh) continue;
|
|
702
|
+
const dict = mesh.morphTargetDictionary;
|
|
703
|
+
const infl = mesh.morphTargetInfluences;
|
|
704
|
+
if (!dict || !infl) continue;
|
|
705
|
+
const idx = dict[key];
|
|
706
|
+
if (idx !== void 0) {
|
|
707
|
+
targets.push({ infl, idx });
|
|
708
|
+
infl[idx] = val;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
for (const mesh of this.meshes) {
|
|
713
|
+
const dict = mesh.morphTargetDictionary;
|
|
714
|
+
const infl = mesh.morphTargetInfluences;
|
|
715
|
+
if (!dict || !infl) continue;
|
|
716
|
+
const idx = dict[key];
|
|
717
|
+
if (idx !== void 0) {
|
|
718
|
+
targets.push({ infl, idx });
|
|
719
|
+
infl[idx] = val;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (targets.length > 0) {
|
|
724
|
+
this.morphCache.set(key, targets);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
transitionMorph(key, to, durationMs = 120, meshNames) {
|
|
728
|
+
const transitionKey = `morph_${key}`;
|
|
729
|
+
const from = this.getMorphValue(key);
|
|
730
|
+
const target = clamp01(to);
|
|
731
|
+
return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.setMorph(key, value, meshNames));
|
|
732
|
+
}
|
|
733
|
+
// ============================================================================
|
|
734
|
+
// VISEME CONTROL
|
|
735
|
+
// ============================================================================
|
|
736
|
+
setViseme(visemeIndex, value, jawScale = 1) {
|
|
737
|
+
if (visemeIndex < 0 || visemeIndex >= this.config.visemeKeys.length) return;
|
|
738
|
+
const val = clamp01(value);
|
|
739
|
+
this.visemeValues[visemeIndex] = val;
|
|
740
|
+
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
741
|
+
this.setMorph(morphKey, val);
|
|
742
|
+
const jawAmount = _LoomLargeThree.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
|
|
743
|
+
if (Math.abs(jawScale) > 1e-6 && Math.abs(jawAmount) > 1e-6) {
|
|
744
|
+
this.updateBoneRotation("JAW", "roll", jawAmount, _LoomLargeThree.JAW_MAX_DEGREES);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
transitionViseme(visemeIndex, to, durationMs = 80, jawScale = 1) {
|
|
748
|
+
if (visemeIndex < 0 || visemeIndex >= this.config.visemeKeys.length) {
|
|
749
|
+
return { promise: Promise.resolve(), pause: () => {
|
|
750
|
+
}, resume: () => {
|
|
751
|
+
}, cancel: () => {
|
|
752
|
+
} };
|
|
753
|
+
}
|
|
754
|
+
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
755
|
+
const target = clamp01(to);
|
|
756
|
+
this.visemeValues[visemeIndex] = target;
|
|
757
|
+
const morphHandle = this.transitionMorph(morphKey, target, durationMs);
|
|
758
|
+
const jawAmount = _LoomLargeThree.VISEME_JAW_AMOUNTS[visemeIndex] * target * jawScale;
|
|
759
|
+
if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
|
|
760
|
+
return morphHandle;
|
|
761
|
+
}
|
|
762
|
+
const jawHandle = this.transitionBoneRotation("JAW", "roll", jawAmount, _LoomLargeThree.JAW_MAX_DEGREES, durationMs);
|
|
763
|
+
return this.combineHandles([morphHandle, jawHandle]);
|
|
764
|
+
}
|
|
765
|
+
// ============================================================================
|
|
766
|
+
// MIX WEIGHT CONTROL
|
|
767
|
+
// ============================================================================
|
|
768
|
+
setAUMixWeight(id, weight) {
|
|
769
|
+
this.mixWeights[id] = clamp01(weight);
|
|
770
|
+
const v = this.auValues[id] ?? 0;
|
|
771
|
+
if (v > 0) this.setAU(id, v);
|
|
772
|
+
const boneBindings = this.config.auToBones[id];
|
|
773
|
+
if (boneBindings) {
|
|
774
|
+
for (const binding of boneBindings) {
|
|
775
|
+
this.pendingCompositeNodes.add(binding.node);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
getAUMixWeight(id) {
|
|
780
|
+
return this.mixWeights[id] ?? this.config.auMixDefaults?.[id] ?? 1;
|
|
781
|
+
}
|
|
782
|
+
// ============================================================================
|
|
783
|
+
// PLAYBACK CONTROL
|
|
784
|
+
// ============================================================================
|
|
785
|
+
pause() {
|
|
786
|
+
this.isPaused = true;
|
|
787
|
+
}
|
|
788
|
+
resume() {
|
|
789
|
+
this.isPaused = false;
|
|
790
|
+
}
|
|
791
|
+
getPaused() {
|
|
792
|
+
return this.isPaused;
|
|
793
|
+
}
|
|
794
|
+
clearTransitions() {
|
|
795
|
+
this.animation.clearTransitions();
|
|
796
|
+
}
|
|
797
|
+
getActiveTransitionCount() {
|
|
798
|
+
return this.animation.getActiveTransitionCount();
|
|
799
|
+
}
|
|
800
|
+
resetToNeutral() {
|
|
801
|
+
this.auValues = {};
|
|
802
|
+
this.initBoneRotations();
|
|
803
|
+
this.clearTransitions();
|
|
804
|
+
for (const m of this.meshes) {
|
|
805
|
+
const infl = m.morphTargetInfluences;
|
|
806
|
+
if (!infl) continue;
|
|
807
|
+
for (let i = 0; i < infl.length; i++) {
|
|
808
|
+
infl[i] = 0;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
Object.values(this.bones).forEach((entry) => {
|
|
812
|
+
if (!entry) return;
|
|
813
|
+
entry.obj.position.copy(entry.basePos);
|
|
814
|
+
entry.obj.quaternion.copy(entry.baseQuat);
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
// ============================================================================
|
|
818
|
+
// MESH CONTROL
|
|
819
|
+
// ============================================================================
|
|
820
|
+
getMeshList() {
|
|
821
|
+
if (!this.model) return [];
|
|
822
|
+
const result = [];
|
|
823
|
+
this.model.traverse((obj) => {
|
|
824
|
+
if (obj.isMesh) {
|
|
825
|
+
result.push({
|
|
826
|
+
name: obj.name,
|
|
827
|
+
visible: obj.visible,
|
|
828
|
+
morphCount: obj.morphTargetInfluences?.length || 0
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
return result;
|
|
833
|
+
}
|
|
834
|
+
setMeshVisible(meshName, visible) {
|
|
835
|
+
if (!this.model) return;
|
|
836
|
+
this.model.traverse((obj) => {
|
|
837
|
+
if (obj.isMesh && obj.name === meshName) {
|
|
838
|
+
obj.visible = visible;
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
// ============================================================================
|
|
843
|
+
// CONFIGURATION
|
|
844
|
+
// ============================================================================
|
|
845
|
+
setAUMappings(mappings) {
|
|
846
|
+
this.config = mappings;
|
|
847
|
+
this.mixWeights = { ...mappings.auMixDefaults };
|
|
848
|
+
}
|
|
849
|
+
getAUMappings() {
|
|
850
|
+
return this.config;
|
|
851
|
+
}
|
|
852
|
+
// ============================================================================
|
|
853
|
+
// PRIVATE METHODS
|
|
854
|
+
// ============================================================================
|
|
855
|
+
computeSideValues(base, balance) {
|
|
856
|
+
const b = Math.max(-1, Math.min(1, balance ?? 0));
|
|
857
|
+
if (b === 0) return { left: base, right: base };
|
|
858
|
+
if (b < 0) return { left: base, right: base * (1 + b) };
|
|
859
|
+
return { left: base * (1 - b), right: base };
|
|
860
|
+
}
|
|
861
|
+
getMeshNamesForAU(auId) {
|
|
862
|
+
const info = this.config.auInfo?.[String(auId)];
|
|
863
|
+
if (!info?.facePart) return this.config.morphToMesh?.face || [];
|
|
864
|
+
switch (info.facePart) {
|
|
865
|
+
case "Tongue":
|
|
866
|
+
return this.config.morphToMesh?.tongue || [];
|
|
867
|
+
case "Eye":
|
|
868
|
+
return this.config.morphToMesh?.eye || [];
|
|
869
|
+
default:
|
|
870
|
+
return this.config.morphToMesh?.face || [];
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
getMorphValue(key) {
|
|
874
|
+
if (this.faceMesh) {
|
|
875
|
+
const dict = this.faceMesh.morphTargetDictionary;
|
|
876
|
+
const infl = this.faceMesh.morphTargetInfluences;
|
|
877
|
+
if (dict && infl) {
|
|
878
|
+
const idx = dict[key];
|
|
879
|
+
if (idx !== void 0) return infl[idx] ?? 0;
|
|
880
|
+
}
|
|
881
|
+
return 0;
|
|
882
|
+
}
|
|
883
|
+
for (const mesh of this.meshes) {
|
|
884
|
+
const dict = mesh.morphTargetDictionary;
|
|
885
|
+
const infl = mesh.morphTargetInfluences;
|
|
886
|
+
if (!dict || !infl) continue;
|
|
887
|
+
const idx = dict[key];
|
|
888
|
+
if (idx !== void 0) return infl[idx] ?? 0;
|
|
889
|
+
}
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
isMixedAU(id) {
|
|
893
|
+
return !!(this.config.auToMorphs[id]?.length && this.config.auToBones[id]?.length);
|
|
894
|
+
}
|
|
895
|
+
initBoneRotations() {
|
|
896
|
+
const zeroAxis = { value: 0, maxRadians: 0 };
|
|
897
|
+
this.rotations = {};
|
|
898
|
+
this.pendingCompositeNodes.clear();
|
|
899
|
+
const allBoneKeys = Array.from(
|
|
900
|
+
new Set(Object.values(this.config.auToBones).flat().map((binding) => binding.node))
|
|
901
|
+
);
|
|
902
|
+
for (const node of allBoneKeys) {
|
|
903
|
+
this.rotations[node] = { pitch: { ...zeroAxis }, yaw: { ...zeroAxis }, roll: { ...zeroAxis } };
|
|
904
|
+
this.pendingCompositeNodes.add(node);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
updateBoneRotation(nodeKey, axis, value, maxDegrees) {
|
|
908
|
+
if (!this.rotations[nodeKey]) return;
|
|
909
|
+
this.rotations[nodeKey][axis] = { value: Math.max(-1, Math.min(1, value)), maxRadians: deg2rad(maxDegrees) };
|
|
910
|
+
this.pendingCompositeNodes.add(nodeKey);
|
|
911
|
+
}
|
|
912
|
+
updateBoneTranslation(nodeKey, channel, value, maxUnits) {
|
|
913
|
+
if (!this.translations[nodeKey]) this.translations[nodeKey] = { x: 0, y: 0, z: 0 };
|
|
914
|
+
const clamped = Math.max(-1, Math.min(1, value));
|
|
915
|
+
const offset = clamped * maxUnits;
|
|
916
|
+
if (channel === "tx") this.translations[nodeKey].x = offset;
|
|
917
|
+
else if (channel === "ty") this.translations[nodeKey].y = offset;
|
|
918
|
+
else this.translations[nodeKey].z = offset;
|
|
919
|
+
this.pendingCompositeNodes.add(nodeKey);
|
|
920
|
+
}
|
|
921
|
+
transitionBoneRotation(nodeKey, axis, to, maxDegrees, durationMs = 200) {
|
|
922
|
+
const transitionKey = `bone_${nodeKey}_${axis}`;
|
|
923
|
+
const from = this.rotations[nodeKey]?.[axis]?.value ?? 0;
|
|
924
|
+
const target = Math.max(-1, Math.min(1, to));
|
|
925
|
+
return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.updateBoneRotation(nodeKey, axis, value, maxDegrees));
|
|
926
|
+
}
|
|
927
|
+
transitionBoneTranslation(nodeKey, channel, to, maxUnits, durationMs = 200) {
|
|
928
|
+
const transitionKey = `boneT_${nodeKey}_${channel}`;
|
|
929
|
+
const current = this.translations[nodeKey] || { x: 0, y: 0, z: 0 };
|
|
930
|
+
const currentOffset = channel === "tx" ? current.x : channel === "ty" ? current.y : current.z;
|
|
931
|
+
const from = maxUnits !== 0 ? Math.max(-1, Math.min(1, currentOffset / maxUnits)) : 0;
|
|
932
|
+
const target = Math.max(-1, Math.min(1, to));
|
|
933
|
+
return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.updateBoneTranslation(nodeKey, channel, value, maxUnits));
|
|
934
|
+
}
|
|
935
|
+
flushPendingComposites() {
|
|
936
|
+
if (this.pendingCompositeNodes.size === 0) return;
|
|
937
|
+
for (const nodeKey of this.pendingCompositeNodes) {
|
|
938
|
+
this.applyCompositeRotation(nodeKey);
|
|
939
|
+
}
|
|
940
|
+
this.pendingCompositeNodes.clear();
|
|
941
|
+
}
|
|
942
|
+
applyCompositeRotation(nodeKey) {
|
|
943
|
+
const entry = this.bones[nodeKey];
|
|
944
|
+
if (!entry || !this.model) {
|
|
945
|
+
if (!entry && this.rigReady && !this.missingBoneWarnings.has(nodeKey)) {
|
|
946
|
+
this.missingBoneWarnings.add(nodeKey);
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const { obj, basePos, baseEuler } = entry;
|
|
951
|
+
const rotState = this.rotations[nodeKey];
|
|
952
|
+
if (!rotState) return;
|
|
953
|
+
const yawRad = rotState.yaw.maxRadians * rotState.yaw.value;
|
|
954
|
+
const pitchRad = rotState.pitch.maxRadians * rotState.pitch.value;
|
|
955
|
+
const rollRad = rotState.roll.maxRadians * rotState.roll.value;
|
|
956
|
+
obj.position.copy(basePos);
|
|
957
|
+
const t = this.translations[nodeKey];
|
|
958
|
+
if (t) {
|
|
959
|
+
obj.position.x += t.x;
|
|
960
|
+
obj.position.y += t.y;
|
|
961
|
+
obj.position.z += t.z;
|
|
962
|
+
}
|
|
963
|
+
obj.rotation.set(baseEuler.x + pitchRad, baseEuler.y + yawRad, baseEuler.z + rollRad, baseEuler.order);
|
|
964
|
+
obj.updateMatrixWorld(false);
|
|
965
|
+
this.model.updateMatrixWorld(true);
|
|
966
|
+
}
|
|
967
|
+
resolveBones(root) {
|
|
968
|
+
const resolved = {};
|
|
969
|
+
const snapshot = (obj) => ({
|
|
970
|
+
obj,
|
|
971
|
+
basePos: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
|
|
972
|
+
baseQuat: obj.quaternion.clone(),
|
|
973
|
+
baseEuler: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, order: obj.rotation.order }
|
|
974
|
+
});
|
|
975
|
+
const findNode = (name) => {
|
|
976
|
+
if (!name) return void 0;
|
|
977
|
+
return root.getObjectByName(name);
|
|
978
|
+
};
|
|
979
|
+
for (const [key, nodeName] of Object.entries(this.config.boneNodes)) {
|
|
980
|
+
const node = findNode(nodeName);
|
|
981
|
+
if (node) resolved[key] = snapshot(node);
|
|
982
|
+
}
|
|
983
|
+
if (!resolved.EYE_L && this.config.eyeMeshNodes) {
|
|
984
|
+
const node = findNode(this.config.eyeMeshNodes.LEFT);
|
|
985
|
+
if (node) resolved.EYE_L = snapshot(node);
|
|
986
|
+
}
|
|
987
|
+
if (!resolved.EYE_R && this.config.eyeMeshNodes) {
|
|
988
|
+
const node = findNode(this.config.eyeMeshNodes.RIGHT);
|
|
989
|
+
if (node) resolved.EYE_R = snapshot(node);
|
|
990
|
+
}
|
|
991
|
+
return resolved;
|
|
992
|
+
}
|
|
993
|
+
combineHandles(handles) {
|
|
994
|
+
if (handles.length === 0) return { promise: Promise.resolve(), pause: () => {
|
|
995
|
+
}, resume: () => {
|
|
996
|
+
}, cancel: () => {
|
|
997
|
+
} };
|
|
998
|
+
if (handles.length === 1) return handles[0];
|
|
999
|
+
return {
|
|
1000
|
+
promise: Promise.all(handles.map((h) => h.promise)).then(() => {
|
|
1001
|
+
}),
|
|
1002
|
+
pause: () => handles.forEach((h) => h.pause()),
|
|
1003
|
+
resume: () => handles.forEach((h) => h.resume()),
|
|
1004
|
+
cancel: () => handles.forEach((h) => h.cancel())
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
// Viseme jaw amounts
|
|
1009
|
+
__publicField(_LoomLargeThree, "VISEME_JAW_AMOUNTS", [
|
|
1010
|
+
0.15,
|
|
1011
|
+
0.35,
|
|
1012
|
+
0.25,
|
|
1013
|
+
0.7,
|
|
1014
|
+
0.55,
|
|
1015
|
+
0.3,
|
|
1016
|
+
0.1,
|
|
1017
|
+
0.2,
|
|
1018
|
+
0.08,
|
|
1019
|
+
0.12,
|
|
1020
|
+
0.18,
|
|
1021
|
+
0.02,
|
|
1022
|
+
0.25,
|
|
1023
|
+
0.6,
|
|
1024
|
+
0.4
|
|
1025
|
+
]);
|
|
1026
|
+
__publicField(_LoomLargeThree, "JAW_MAX_DEGREES", 28);
|
|
1027
|
+
var LoomLargeThree = _LoomLargeThree;
|
|
1028
|
+
function collectMorphMeshes(root) {
|
|
1029
|
+
const meshes = [];
|
|
1030
|
+
root.traverse((obj) => {
|
|
1031
|
+
if (obj.isMesh) {
|
|
1032
|
+
if (Array.isArray(obj.morphTargetInfluences) && obj.morphTargetInfluences.length > 0) {
|
|
1033
|
+
meshes.push(obj);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
return meshes;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/physics/HairPhysics.ts
|
|
1041
|
+
var DEFAULT_HAIR_PHYSICS_CONFIG = {
|
|
1042
|
+
mass: 1,
|
|
1043
|
+
stiffness: 15,
|
|
1044
|
+
damping: 0.8,
|
|
1045
|
+
gravity: 9.8,
|
|
1046
|
+
headInfluence: 0.5,
|
|
1047
|
+
windEnabled: false,
|
|
1048
|
+
windStrength: 0,
|
|
1049
|
+
windDirectionX: 1,
|
|
1050
|
+
windDirectionZ: 0,
|
|
1051
|
+
windTurbulence: 0.2,
|
|
1052
|
+
windFrequency: 0.5
|
|
1053
|
+
};
|
|
1054
|
+
var HairPhysics = class {
|
|
1055
|
+
constructor(config = {}) {
|
|
1056
|
+
__publicField(this, "config");
|
|
1057
|
+
__publicField(this, "state");
|
|
1058
|
+
__publicField(this, "time", 0);
|
|
1059
|
+
// Previous head state for velocity calculation
|
|
1060
|
+
__publicField(this, "prevHeadYaw", 0);
|
|
1061
|
+
__publicField(this, "prevHeadPitch", 0);
|
|
1062
|
+
this.config = { ...DEFAULT_HAIR_PHYSICS_CONFIG, ...config };
|
|
1063
|
+
this.state = { x: 0, z: 0, vx: 0, vz: 0 };
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Update physics simulation
|
|
1067
|
+
* @param dt Delta time in seconds
|
|
1068
|
+
* @param headState Current head orientation and velocity
|
|
1069
|
+
* @returns Morph values to apply to hair meshes
|
|
1070
|
+
*/
|
|
1071
|
+
update(dt, headState) {
|
|
1072
|
+
if (dt <= 0 || dt > 0.1) {
|
|
1073
|
+
return this.computeMorphOutput();
|
|
1074
|
+
}
|
|
1075
|
+
this.time += dt;
|
|
1076
|
+
const { mass, stiffness, damping, gravity, headInfluence, windEnabled } = this.config;
|
|
1077
|
+
const headYawVel = headState.yawVelocity !== 0 ? headState.yawVelocity : (headState.yaw - this.prevHeadYaw) / dt;
|
|
1078
|
+
const headPitchVel = headState.pitchVelocity !== 0 ? headState.pitchVelocity : (headState.pitch - this.prevHeadPitch) / dt;
|
|
1079
|
+
this.prevHeadYaw = headState.yaw;
|
|
1080
|
+
this.prevHeadPitch = headState.pitch;
|
|
1081
|
+
const springFx = -stiffness * this.state.x;
|
|
1082
|
+
const springFz = -stiffness * this.state.z;
|
|
1083
|
+
const dampFx = -damping * this.state.vx;
|
|
1084
|
+
const dampFz = -damping * this.state.vz;
|
|
1085
|
+
const gravityFz = gravity * Math.sin(headState.pitch) * 0.1;
|
|
1086
|
+
const inertiaFx = -headYawVel * headInfluence * mass * 2;
|
|
1087
|
+
const inertiaFz = -headPitchVel * headInfluence * mass * 2;
|
|
1088
|
+
let windFx = 0;
|
|
1089
|
+
let windFz = 0;
|
|
1090
|
+
if (windEnabled && this.config.windStrength > 0) {
|
|
1091
|
+
const { windStrength, windDirectionX, windDirectionZ, windTurbulence, windFrequency } = this.config;
|
|
1092
|
+
const windPhase = this.time * windFrequency * Math.PI * 2;
|
|
1093
|
+
const windOscillation = Math.sin(windPhase);
|
|
1094
|
+
const turbulencePhase = this.time * windFrequency * 3.7;
|
|
1095
|
+
const turbulence = Math.sin(turbulencePhase) * windTurbulence;
|
|
1096
|
+
windFx = windStrength * windDirectionX * (0.5 + 0.5 * windOscillation) + turbulence * -windDirectionZ;
|
|
1097
|
+
windFz = windStrength * windDirectionZ * (0.5 + 0.5 * windOscillation) + turbulence * windDirectionX;
|
|
1098
|
+
}
|
|
1099
|
+
const totalFx = springFx + dampFx + inertiaFx + windFx;
|
|
1100
|
+
const totalFz = springFz + dampFz + gravityFz + inertiaFz + windFz;
|
|
1101
|
+
const ax = totalFx / mass;
|
|
1102
|
+
const az = totalFz / mass;
|
|
1103
|
+
this.state.vx += ax * dt;
|
|
1104
|
+
this.state.vz += az * dt;
|
|
1105
|
+
const maxVel = 10;
|
|
1106
|
+
this.state.vx = Math.max(-maxVel, Math.min(maxVel, this.state.vx));
|
|
1107
|
+
this.state.vz = Math.max(-maxVel, Math.min(maxVel, this.state.vz));
|
|
1108
|
+
this.state.x += this.state.vx * dt;
|
|
1109
|
+
this.state.z += this.state.vz * dt;
|
|
1110
|
+
this.state.x = Math.max(-1, Math.min(1, this.state.x));
|
|
1111
|
+
this.state.z = Math.max(-1, Math.min(1, this.state.z));
|
|
1112
|
+
return this.computeMorphOutput();
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Convert physics state to morph target values
|
|
1116
|
+
* Maps pendulum position to left/right/front morphs for both sides
|
|
1117
|
+
*/
|
|
1118
|
+
computeMorphOutput() {
|
|
1119
|
+
const { x, z } = this.state;
|
|
1120
|
+
const L_Hair_Left = Math.max(0, -x);
|
|
1121
|
+
const L_Hair_Right = Math.max(0, x);
|
|
1122
|
+
const L_Hair_Front = Math.max(0, z);
|
|
1123
|
+
const R_Hair_Left = Math.max(0, -x);
|
|
1124
|
+
const R_Hair_Right = Math.max(0, x);
|
|
1125
|
+
const R_Hair_Front = Math.max(0, z);
|
|
1126
|
+
return {
|
|
1127
|
+
L_Hair_Left,
|
|
1128
|
+
L_Hair_Right,
|
|
1129
|
+
L_Hair_Front,
|
|
1130
|
+
R_Hair_Left,
|
|
1131
|
+
R_Hair_Right,
|
|
1132
|
+
R_Hair_Front
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Get current physics state (for debugging/UI)
|
|
1137
|
+
*/
|
|
1138
|
+
getState() {
|
|
1139
|
+
return { ...this.state };
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Update configuration
|
|
1143
|
+
*/
|
|
1144
|
+
setConfig(config) {
|
|
1145
|
+
this.config = { ...this.config, ...config };
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Get current configuration
|
|
1149
|
+
*/
|
|
1150
|
+
getConfig() {
|
|
1151
|
+
return { ...this.config };
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Reset physics state to rest position
|
|
1155
|
+
*/
|
|
1156
|
+
reset() {
|
|
1157
|
+
this.state = { x: 0, z: 0, vx: 0, vz: 0 };
|
|
1158
|
+
this.time = 0;
|
|
1159
|
+
this.prevHeadYaw = 0;
|
|
1160
|
+
this.prevHeadPitch = 0;
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
export { AnimationThree, CC4_PRESET, DEFAULT_HAIR_PHYSICS_CONFIG, HairPhysics, LoomLargeThree, collectMorphMeshes, LoomLargeThree as default };
|
|
1165
|
+
//# sourceMappingURL=index.js.map
|
|
1166
|
+
//# sourceMappingURL=index.js.map
|