metaverse-avatar 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.
Files changed (46) hide show
  1. package/ASSET_LICENSES.md +122 -0
  2. package/Avatar.js +860 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +407 -0
  5. package/anims/UAL1_Standard.glb +0 -0
  6. package/anims/UAL2_Standard.glb +0 -0
  7. package/anims/pirouette.bvh +867 -0
  8. package/attachments.js +388 -0
  9. package/avatarManager.js +110 -0
  10. package/blink.js +58 -0
  11. package/bvh.js +110 -0
  12. package/gltfAnim.js +271 -0
  13. package/index.js +61 -0
  14. package/licenses/AGPL-3.0.txt +661 -0
  15. package/models/body.glb +0 -0
  16. package/models/eyes.glb +0 -0
  17. package/models/feet.glb +0 -0
  18. package/models/hands.glb +0 -0
  19. package/models/head.glb +0 -0
  20. package/models/textures/android_face.png +0 -0
  21. package/models/textures/android_face_ao.jpg +0 -0
  22. package/models/textures/android_face_metallic.jpg +0 -0
  23. package/models/textures/android_face_normal.jpg +0 -0
  24. package/models/textures/android_face_roughness.jpg +0 -0
  25. package/models/textures/android_lower.png +0 -0
  26. package/models/textures/android_lower_ao.jpg +0 -0
  27. package/models/textures/android_lower_metallic.jpg +0 -0
  28. package/models/textures/android_lower_normal.jpg +0 -0
  29. package/models/textures/android_lower_roughness.jpg +0 -0
  30. package/models/textures/android_upper.png +0 -0
  31. package/models/textures/android_upper_ao.jpg +0 -0
  32. package/models/textures/android_upper_metallic.jpg +0 -0
  33. package/models/textures/android_upper_normal.jpg +0 -0
  34. package/models/textures/android_upper_roughness.jpg +0 -0
  35. package/models/textures/blue_eyes.png +0 -0
  36. package/models/textures/layers/cute_pants.png +0 -0
  37. package/models/textures/layers/cute_shirt.png +0 -0
  38. package/nipple.js +218 -0
  39. package/package.json +48 -0
  40. package/pbr.js +225 -0
  41. package/physics.js +313 -0
  42. package/skeleton.js +70 -0
  43. package/sliders.js +590 -0
  44. package/speech.js +130 -0
  45. package/visemes.js +66 -0
  46. package/voice.js +75 -0
package/sliders.js ADDED
@@ -0,0 +1,590 @@
1
+ // Shape sliders for the Ruth2 fitted-mesh body.
2
+ //
3
+ // How fitted-mesh shape sliders work: collision-volume bones are scaled and
4
+ // offset to deform the skin. In upstream viewers that mechanism reads
5
+ // avatar_lad.xml, where
6
+ // each visual parameter drives some combination of (a) morph targets baked
7
+ // into the system avatar mesh and (b) scale/offset transforms on skeleton
8
+ // bones — in particular the invisible "collision volume" bones (BELLY, BUTT,
9
+ // CHEST, LEFT_PEC, ...). Mesh bodies like Ruth2 can't use the system morphs,
10
+ // so fitted-mesh bodies are weighted to the collision volumes and respond to
11
+ // the bone-driven subset of sliders. We reproduce that idea here: every
12
+ // slider multiplies the rest scale (and optionally offsets the rest
13
+ // position) of a set of bones.
14
+ //
15
+ // Each effect: { bones: [...], scale: [x,y,z amounts], offset: [x,y,z meters],
16
+ // rot: [x,y,z radians] }. Applied as: boneScale = rest * (1 + amount * t),
17
+ // bonePos = rest + offset * t, boneRot = rest * euler(rot * t), with t in
18
+ // [-1, 1]. NOTE: the rig is Z-up inside the armature (Blender convention)
19
+ // and the avatar faces +X, so Z is "up", Y is left/right, X is front/back.
20
+
21
+ // Bento finger bone name helpers: mHand<Finger><Joint><Side>
22
+ const FINGERS = ['Index', 'Middle', 'Ring', 'Pinky'];
23
+ const fingerBones = (side, fingers = FINGERS, joints = [1, 2, 3]) =>
24
+ fingers.flatMap((f) => joints.map((j) => `mHand${f}${j}${side}`));
25
+
26
+ // Lip bone trios (left / centre / right), upper and lower — independently
27
+ // addressable, so the top and bottom lips can be shaped separately. Plus the
28
+ // two mouth corners. In the armature frame: X = front/back (protrusion),
29
+ // Y = left/right, Z = up/down.
30
+ const UPPER_LIP = ['mFaceLipUpperLeft', 'mFaceLipUpperCenter', 'mFaceLipUpperRight'];
31
+ const LOWER_LIP = ['mFaceLipLowerLeft', 'mFaceLipLowerCenter', 'mFaceLipLowerRight'];
32
+ const LIP_CORNERS = ['mFaceLipCornerLeft', 'mFaceLipCornerRight'];
33
+ const ALL_LIP = [...UPPER_LIP, ...LOWER_LIP];
34
+ const MOUTH_ALL = [...ALL_LIP, ...LIP_CORNERS]; // every lip bone, for whole-mouth moves
35
+
36
+ export const SLIDERS = [
37
+ // ---- Body ----
38
+ { id: 'height', group: 'Body', label: 'Height', special: 'height' },
39
+ {
40
+ id: 'thickness', group: 'Body', label: 'Body Thickness',
41
+ effects: [{ bones: ['PELVIS', 'BELLY', 'CHEST', 'UPPER_BACK', 'LOWER_BACK', 'NECK', 'BUTT'], scale: [0.22, 0.22, 0] },
42
+ { bones: ['L_CLAVICLE', 'R_CLAVICLE'], scale: [0, 0.22, 0] }],
43
+ },
44
+
45
+ // ---- Head ----
46
+ // (the grafted Ruth2 v4 head: face bones under the synthesized mHead)
47
+ {
48
+ id: 'head_size', group: 'Head', label: 'Head Size',
49
+ effects: [{ bones: ['mHead'], scale: [0.25, 0.25, 0.25] }],
50
+ },
51
+ {
52
+ // Overall face elongation (mHead Z = vertical). Long faces (oval/diamond)
53
+ // vs short faces (round/square) — combine with the width controls below.
54
+ id: 'face_length', group: 'Head', label: 'Face Length',
55
+ effects: [{ bones: ['mHead'], scale: [0, 0, 0.2] }],
56
+ },
57
+ {
58
+ // Overall face width (mHead Y = lateral). Broad vs narrow whole head.
59
+ id: 'face_width', group: 'Head', label: 'Face Width',
60
+ effects: [{ bones: ['mHead'], scale: [0, 0.18, 0] }],
61
+ },
62
+ {
63
+ // Width at the forehead/temple level — the top of the face-shape triangle.
64
+ // Wide forehead + narrow jaw = heart shape.
65
+ id: 'forehead_width', group: 'Head', label: 'Forehead Width',
66
+ effects: [
67
+ { bones: ['mFaceForeheadLeft'], offset: [0, 0.007, 0] },
68
+ { bones: ['mFaceForeheadRight'], offset: [0, -0.007, 0] },
69
+ ],
70
+ },
71
+ {
72
+ // Width at the cheekbone level — the middle of the face-shape triangle.
73
+ // Wide here with narrow forehead + jaw = diamond shape.
74
+ id: 'cheek_width', group: 'Head', label: 'Cheekbone Width',
75
+ effects: [
76
+ { bones: ['mFaceCheekUpperLeft'], offset: [0, 0.007, 0] },
77
+ { bones: ['mFaceCheekUpperRight'], offset: [0, -0.007, 0] },
78
+ ],
79
+ },
80
+ {
81
+ id: 'ear_size', group: 'Head', label: 'Ear Size',
82
+ effects: [{ bones: ['mFaceEar1Left', 'mFaceEar1Right'], scale: [0.6, 0.6, 0.6] }],
83
+ },
84
+ {
85
+ // Ear protrusion: push the whole ear laterally outward (Y) so it stands off
86
+ // the head; negative tucks it flat against the skull.
87
+ id: 'ear_angle', group: 'Head', label: 'Ear Protrusion',
88
+ effects: [
89
+ { bones: ['mFaceEar1Left', 'mFaceEar2Left'], offset: [0, 0.006, 0] },
90
+ { bones: ['mFaceEar1Right', 'mFaceEar2Right'], offset: [0, -0.006, 0] },
91
+ ],
92
+ },
93
+ {
94
+ // Ear length/height (Z). Scales the base bone; the tip bone inherits it, so
95
+ // the whole ear stretches vertically.
96
+ id: 'ear_length', group: 'Head', label: 'Ear Length',
97
+ effects: [{ bones: ['mFaceEar1Left', 'mFaceEar1Right'], scale: [0, 0, 0.5] }],
98
+ },
99
+ {
100
+ // Pointed / elf ears: extend the tip bone upward (Z) while narrowing its
101
+ // cross-section (X depth + Y width) so it tapers to a point, plus raise and
102
+ // flare it outward. Negative gives a rounder, blunter tip.
103
+ id: 'ear_point', group: 'Head', label: 'Ear Point',
104
+ effects: [
105
+ { bones: ['mFaceEar2Left', 'mFaceEar2Right'], scale: [-0.35, -0.35, 0.7] },
106
+ { bones: ['mFaceEar2Left'], offset: [0, 0.004, 0.009] },
107
+ { bones: ['mFaceEar2Right'], offset: [0, -0.004, 0.009] },
108
+ ],
109
+ },
110
+ {
111
+ id: 'nose_size', group: 'Head', label: 'Nose Size',
112
+ effects: [{ bones: ['mFaceNoseLeft', 'mFaceNoseRight', 'mFaceNoseCenter', 'mFaceNoseBase', 'mFaceNoseBridge'], scale: [0.5, 0.5, 0.5] }],
113
+ },
114
+ {
115
+ id: 'nose_width', group: 'Head', label: 'Nose Width',
116
+ effects: [
117
+ { bones: ['mFaceNoseLeft'], offset: [0, 0.006, 0] },
118
+ { bones: ['mFaceNoseRight'], offset: [0, -0.006, 0] },
119
+ ],
120
+ },
121
+ {
122
+ // Bridge prominence: push the bridge forward (+X) for a higher/Roman nose,
123
+ // pull it back (-X) for a flatter profile.
124
+ id: 'nose_bridge', group: 'Head', label: 'Nose Bridge',
125
+ effects: [{ bones: ['mFaceNoseBridge'], offset: [0.006, 0, 0] }],
126
+ },
127
+ {
128
+ // Tip projection: how far the nose tip sticks out from the face (+X).
129
+ id: 'nose_tip', group: 'Head', label: 'Nose Tip',
130
+ effects: [{ bones: ['mFaceNoseCenter'], offset: [0.008, 0, 0] }],
131
+ },
132
+ {
133
+ // Tip tilt: raise (+Z, upturned) or lower (-Z, drooping) the tip.
134
+ id: 'nose_tilt', group: 'Head', label: 'Nose Tilt',
135
+ effects: [{ bones: ['mFaceNoseCenter'], offset: [0, 0, 0.006] }],
136
+ },
137
+ {
138
+ id: 'jaw_width', group: 'Head', label: 'Jaw Width',
139
+ effects: [{ bones: ['mFaceJaw'], scale: [0, 0.35, 0] }],
140
+ },
141
+ {
142
+ id: 'chin_depth', group: 'Head', label: 'Chin Depth',
143
+ effects: [{ bones: ['mFaceChin'], offset: [0.012, 0, 0] }],
144
+ },
145
+ {
146
+ id: 'cheek_fullness', group: 'Head', label: 'Cheek Fullness',
147
+ effects: [{ bones: ['mFaceCheekLowerLeft', 'mFaceCheekLowerRight', 'mFaceCheekUpperLeft', 'mFaceCheekUpperRight'], scale: [0.5, 0.5, 0.5] }],
148
+ },
149
+ {
150
+ // High cheekbones: push the upper cheeks forward (+X) and up (+Z) for a more
151
+ // defined, sculpted look.
152
+ id: 'cheekbones', group: 'Head', label: 'Cheekbones',
153
+ effects: [{ bones: ['mFaceCheekUpperLeft', 'mFaceCheekUpperRight'], offset: [0.006, 0, 0.004] }],
154
+ },
155
+ {
156
+ // Gaunt/hollow lower cheeks: positive shrinks them in for a sunken look,
157
+ // negative fills them out (chubby).
158
+ id: 'cheek_hollow', group: 'Head', label: 'Cheek Hollows',
159
+ effects: [{ bones: ['mFaceCheekLowerLeft', 'mFaceCheekLowerRight'], scale: [-0.4, -0.4, -0.4] }],
160
+ },
161
+ {
162
+ id: 'eye_size', group: 'Head', label: 'Eye Size',
163
+ effects: [{ bones: ['mEyeLeft', 'mEyeRight'], scale: [0.3, 0.3, 0.3] }],
164
+ },
165
+ {
166
+ id: 'eye_spacing', group: 'Head', label: 'Eye Spacing',
167
+ effects: [
168
+ { bones: ['mEyeLeft'], offset: [0, 0.005, 0] },
169
+ { bones: ['mEyeRight'], offset: [0, -0.005, 0] },
170
+ ],
171
+ },
172
+ {
173
+ // Brow ridge projection: push the brows forward (+X) and down (-Z) for a
174
+ // heavy, low "hunter" brow; negative recedes them for a smoother forehead.
175
+ id: 'brow_ridge', group: 'Head', label: 'Brow Ridge',
176
+ effects: [{
177
+ bones: ['mFaceEyebrowInnerLeft', 'mFaceEyebrowCenterLeft', 'mFaceEyebrowOuterLeft',
178
+ 'mFaceEyebrowInnerRight', 'mFaceEyebrowCenterRight', 'mFaceEyebrowOuterRight'],
179
+ offset: [0.007, 0, -0.003],
180
+ }],
181
+ },
182
+ {
183
+ // Raise/lower the whole brow line (Z). Positive = higher brows.
184
+ id: 'brow_height', group: 'Head', label: 'Brow Height',
185
+ effects: [{
186
+ bones: ['mFaceEyebrowInnerLeft', 'mFaceEyebrowCenterLeft', 'mFaceEyebrowOuterLeft',
187
+ 'mFaceEyebrowInnerRight', 'mFaceEyebrowCenterRight', 'mFaceEyebrowOuterRight'],
188
+ offset: [0, 0, 0.006],
189
+ }],
190
+ },
191
+ {
192
+ // Brow angle: positive lifts the outer brow and drops the inner for an
193
+ // arched/upswept look; negative gives a flat or sloping brow.
194
+ id: 'brow_tilt', group: 'Head', label: 'Brow Angle',
195
+ effects: [
196
+ { bones: ['mFaceEyebrowInnerLeft', 'mFaceEyebrowInnerRight'], offset: [0, 0, -0.004] },
197
+ { bones: ['mFaceEyebrowOuterLeft', 'mFaceEyebrowOuterRight'], offset: [0, 0, 0.004] },
198
+ ],
199
+ },
200
+ {
201
+ // Eye slant / canthal tilt: drop the inner eye corners (-Z) for an upward
202
+ // almond slant; positive = upslant, negative = downturned.
203
+ id: 'eye_slant', group: 'Head', label: 'Eye Slant',
204
+ effects: [{ bones: ['mFaceEyecornerInnerLeft', 'mFaceEyecornerInnerRight'], offset: [0, 0, -0.004] }],
205
+ },
206
+ {
207
+ // Palpebral aperture — resting eye openness, both eyes. eye_closed shows
208
+ // +Y rotation shuts the upper lid, so negative here narrows to a slit/squint
209
+ // and positive widens the eyes (upper lid up, lower lid down).
210
+ id: 'eye_opening', group: 'Head', label: 'Eye Opening',
211
+ effects: [
212
+ { bones: ['mFaceEyeLidUpperLeft', 'mFaceEyeLidUpperRight'], rot: [0, -0.4, 0] },
213
+ { bones: ['mFaceEyeLidLowerLeft', 'mFaceEyeLidLowerRight'], rot: [0, 0.15, 0] },
214
+ ],
215
+ },
216
+ {
217
+ // Upper-lid fullness — approximates monolid vs double-lid. A true
218
+ // supratarsal crease needs a morph/normal map (bones can't carve a fold);
219
+ // this puffs the upper lid forward (+X) and down (-Z) for a full, smooth
220
+ // monolid look, or recedes it (negative) for a deeper-set, creased eye.
221
+ id: 'lid_fullness', group: 'Head', label: 'Upper Lid Fullness',
222
+ effects: [{ bones: ['mFaceEyeLidUpperLeft', 'mFaceEyeLidUpperRight'], offset: [0.003, 0, -0.002] }],
223
+ },
224
+ {
225
+ id: 'eye_closed_l', group: 'Head', label: 'Left Eye Closed',
226
+ effects: [
227
+ { bones: ['mFaceEyeLidUpperLeft'], rot: [0, 0.55, 0] },
228
+ { bones: ['mFaceEyeLidLowerLeft'], rot: [0, -0.15, 0] },
229
+ ],
230
+ },
231
+ {
232
+ id: 'eye_closed_r', group: 'Head', label: 'Right Eye Closed',
233
+ effects: [
234
+ { bones: ['mFaceEyeLidUpperRight'], rot: [0, 0.55, 0] },
235
+ { bones: ['mFaceEyeLidLowerRight'], rot: [0, -0.15, 0] },
236
+ ],
237
+ },
238
+
239
+ // ---- Mouth (jaw + tongue) ----
240
+ {
241
+ id: 'mouth_open', group: 'Mouth', label: 'Mouth Open',
242
+ effects: [{ bones: ['mFaceJaw'], rot: [0, 0.45, 0] }],
243
+ },
244
+ {
245
+ id: 'tongue_out', group: 'Mouth', label: 'Tongue Out',
246
+ effects: [
247
+ { bones: ['mFaceTongueBase'], offset: [0.02, 0, 0] },
248
+ { bones: ['mFaceTongueTip'], offset: [0.015, 0, 0] },
249
+ ],
250
+ },
251
+
252
+ // ---- Lips ----
253
+ // A character-creator-style lip rig: overall size + position, independent
254
+ // upper/lower fullness and protrusion, pucker, cupid's bow, smile, and corner
255
+ // shaping. Each maps to a distinct DOF on the lip / corner bones.
256
+ {
257
+ id: 'lip_size', group: 'Lips', label: 'Lip Size (overall)',
258
+ effects: [{ bones: MOUTH_ALL, scale: [0.45, 0.45, 0.45] }],
259
+ },
260
+ {
261
+ id: 'mouth_width', group: 'Lips', label: 'Mouth Width',
262
+ effects: [
263
+ { bones: ['mFaceLipCornerLeft'], offset: [0, 0.008, 0] },
264
+ { bones: ['mFaceLipCornerRight'], offset: [0, -0.008, 0] },
265
+ ],
266
+ },
267
+ {
268
+ // Slide the whole mouth up or down the face.
269
+ id: 'mouth_raise', group: 'Lips', label: 'Mouth Up / Down',
270
+ effects: [{ bones: MOUTH_ALL, offset: [0, 0, 0.006] }],
271
+ },
272
+ {
273
+ // Slide the whole mouth forward or back (set into / off the face).
274
+ id: 'mouth_depth', group: 'Lips', label: 'Mouth Forward / Back',
275
+ effects: [{ bones: MOUTH_ALL, offset: [0.006, 0, 0] }],
276
+ },
277
+ {
278
+ // Vertical fullness (height) of the TOP lip only.
279
+ id: 'upper_lip_fullness', group: 'Lips', label: 'Upper Lip Fullness',
280
+ effects: [{ bones: UPPER_LIP, scale: [0, 0, 0.6] }],
281
+ },
282
+ {
283
+ // Vertical fullness (height) of the BOTTOM lip only — thin-top/full-bottom etc.
284
+ id: 'lower_lip_fullness', group: 'Lips', label: 'Lower Lip Fullness',
285
+ effects: [{ bones: LOWER_LIP, scale: [0, 0, 0.6] }],
286
+ },
287
+ {
288
+ // Roll the top lip out (forward) or in.
289
+ id: 'upper_lip_protrude', group: 'Lips', label: 'Upper Lip Protrusion',
290
+ effects: [{ bones: UPPER_LIP, offset: [0.005, 0, 0] }],
291
+ },
292
+ {
293
+ // Roll the bottom lip out (forward) or in.
294
+ id: 'lower_lip_protrude', group: 'Lips', label: 'Lower Lip Protrusion',
295
+ effects: [{ bones: LOWER_LIP, offset: [0.005, 0, 0] }],
296
+ },
297
+ {
298
+ // Kiss/pucker: push both lips forward and draw the corners inward.
299
+ id: 'lip_pucker', group: 'Lips', label: 'Lip Pucker',
300
+ effects: [
301
+ { bones: ALL_LIP, offset: [0.006, 0, 0] },
302
+ { bones: ['mFaceLipCornerLeft'], offset: [0.004, -0.006, 0] },
303
+ { bones: ['mFaceLipCornerRight'], offset: [0.004, 0.006, 0] },
304
+ ],
305
+ },
306
+ {
307
+ // Shape the upper-lip centre peak (height + protrusion) on its own.
308
+ id: 'cupids_bow', group: 'Lips', label: "Cupid's Bow",
309
+ effects: [{ bones: ['mFaceLipUpperCenter'], offset: [0.004, 0, 0.004] }],
310
+ },
311
+ {
312
+ // Smile / frown: lift the corners UP and pull them OUT (not forward).
313
+ id: 'smile', group: 'Lips', label: 'Smile / Frown',
314
+ effects: [
315
+ { bones: ['mFaceLipCornerLeft'], offset: [0, 0.004, 0.006] },
316
+ { bones: ['mFaceLipCornerRight'], offset: [0, -0.004, 0.006] },
317
+ ],
318
+ },
319
+ {
320
+ // Push the corners forward/out or tuck them back into the cheeks (dimple).
321
+ id: 'lip_corner_depth', group: 'Lips', label: 'Lip Corner Depth',
322
+ effects: [{ bones: LIP_CORNERS, offset: [0.005, 0, 0] }],
323
+ },
324
+
325
+ // ---- Torso ----
326
+ {
327
+ id: 'torso_length', group: 'Torso', label: 'Torso Length',
328
+ effects: [{ bones: ['mTorso'], scale: [0, 0, 0.25] }],
329
+ },
330
+ {
331
+ id: 'shoulders', group: 'Torso', label: 'Shoulders',
332
+ effects: [
333
+ { bones: ['mChest'], scale: [0.1, 0, 0] },
334
+ { bones: ['L_CLAVICLE', 'R_CLAVICLE'], scale: [0.35, 0.15, 0.15] },
335
+ ],
336
+ },
337
+ {
338
+ id: 'breast_size', group: 'Torso', label: 'Breast Size',
339
+ effects: [{ bones: ['LEFT_PEC', 'RIGHT_PEC'], scale: [0.55, 0.55, 0.55] }],
340
+ },
341
+ {
342
+ // Spacing along the lateral axis (Y). Pec locals share orientation, so the
343
+ // two sides need opposite signs to move apart/together. Positive = apart.
344
+ id: 'breast_spacing', group: 'Torso', label: 'Breast Spacing',
345
+ effects: [
346
+ { bones: ['LEFT_PEC'], offset: [0, 0.025, 0] },
347
+ { bones: ['RIGHT_PEC'], offset: [0, -0.025, 0] },
348
+ ],
349
+ },
350
+ {
351
+ // Lift: raise the pec volume (+Z is up for these bones) without resizing it.
352
+ id: 'breast_lift', group: 'Torso', label: 'Breast Lift',
353
+ effects: [{ bones: ['LEFT_PEC', 'RIGHT_PEC'], offset: [0, 0, 0.02] }],
354
+ },
355
+ {
356
+ // Nipple protrusion — a vertex morph, not a bone (there is no nipple bone;
357
+ // the breast is skinned to the PEC volumes). + extends the tip forward,
358
+ // - pulls it in / flattens. See nipple.js.
359
+ id: 'nipple', group: 'Torso', label: 'Nipple', special: 'nipple',
360
+ },
361
+ {
362
+ // Mirrors SL's "Chest Male No Pecs" (param 685): translates pecs inward
363
+ // toward the spine to flatten the chest. Positive = flatter (masculine).
364
+ id: 'pec_flatten', group: 'Torso', label: 'Chest Flatten',
365
+ effects: [{ bones: ['LEFT_PEC', 'RIGHT_PEC'], scale: [-0.85, -0.85, -0.85], offset: [-0.05, 0, 0] }],
366
+ },
367
+ {
368
+ id: 'belly_size', group: 'Torso', label: 'Belly Size',
369
+ effects: [{ bones: ['BELLY'], scale: [0.55, 0.8, 0.45] }],
370
+ },
371
+ {
372
+ // Forward-biased belly bulge: depth-dominant scale plus a +X (anterior)
373
+ // offset so the volume grows forward instead of symmetrically into the spine.
374
+ id: 'belly_distend', group: 'Torso', label: 'Belly Distension',
375
+ effects: [{ bones: ['BELLY'], scale: [0.9, 0.25, 0.2], offset: [0.04, 0, 0] }],
376
+ },
377
+ {
378
+ // Raise/lower the belly volume vertically (-Z is up, matching the pec bones)
379
+ // without resizing it. Positive = higher.
380
+ id: 'belly_lift', group: 'Torso', label: 'Belly Lift',
381
+ effects: [{ bones: ['BELLY'], offset: [0, 0, -0.03] }],
382
+ },
383
+ {
384
+ // Cinch/widen the midsection on both cross-section axes (X depth + Y width),
385
+ // leaving bone length (Z) alone. Negative = cinched waist (hourglass).
386
+ id: 'waist', group: 'Torso', label: 'Waist',
387
+ effects: [{ bones: ['LOWER_BACK', 'BELLY'], scale: [0.3, 0.3, 0] }],
388
+ },
389
+ {
390
+ id: 'torso_muscles', group: 'Torso', label: 'Torso Muscles',
391
+ effects: [{ bones: ['UPPER_BACK', 'LOWER_BACK'], scale: [0.35, 0.35, 0.35] }],
392
+ },
393
+ {
394
+ id: 'neck_length', group: 'Torso', label: 'Neck Length',
395
+ effects: [{ bones: ['mNeck'], scale: [0, 0, 0.45] }],
396
+ },
397
+ {
398
+ id: 'neck_thickness', group: 'Torso', label: 'Neck Thickness',
399
+ effects: [{ bones: ['NECK'], scale: [0.35, 0.35, 0] }],
400
+ },
401
+
402
+ // ---- Arms ----
403
+ {
404
+ id: 'arm_length', group: 'Arms', label: 'Arm Length',
405
+ effects: [{ bones: ['mShoulderLeft', 'mShoulderRight', 'mElbowLeft', 'mElbowRight'], scale: [0.16, 0, 0] }],
406
+ },
407
+ {
408
+ id: 'bicep_size', group: 'Arms', label: 'Bicep Size',
409
+ effects: [{ bones: ['L_UPPER_ARM', 'R_UPPER_ARM'], scale: [0, 0.45, 0.45] }],
410
+ },
411
+ {
412
+ id: 'forearm_size', group: 'Arms', label: 'Forearm Size',
413
+ effects: [{ bones: ['L_LOWER_ARM', 'R_LOWER_ARM'], scale: [0, 0.45, 0.45] }],
414
+ },
415
+ {
416
+ id: 'hand_size', group: 'Arms', label: 'Hand Size',
417
+ effects: [{ bones: ['mWristLeft', 'mWristRight'], scale: [0.25, 0.25, 0.25] }],
418
+ },
419
+ {
420
+ // Broaden + thicken the palm on its cross-section axes (Y/Z) without
421
+ // lengthening (X = wrist→fingers). Square, chunky palm = masculine hands.
422
+ id: 'palm_width', group: 'Arms', label: 'Palm Width',
423
+ effects: [{ bones: ['mWristLeft', 'mWristRight'], scale: [0, 0.4, 0.4] }],
424
+ },
425
+
426
+ // ---- Fingers (Bento — upstream rigs only expose Hand Size; finger posing is
427
+ // normally done with Bento animations, but the bones are all here) ----
428
+ {
429
+ id: 'finger_length', group: 'Fingers', label: 'Finger Length',
430
+ effects: [{ bones: [...fingerBones('Left', [...FINGERS, 'Thumb'], [1]), ...fingerBones('Right', [...FINGERS, 'Thumb'], [1])], scale: [0, 0.3, 0] }],
431
+ },
432
+ {
433
+ id: 'finger_thickness', group: 'Fingers', label: 'Finger Thickness',
434
+ effects: [{ bones: [...fingerBones('Left', [...FINGERS, 'Thumb'], [1]), ...fingerBones('Right', [...FINGERS, 'Thumb'], [1])], scale: [0.4, 0, 0.4] }],
435
+ },
436
+ {
437
+ id: 'fist_l', group: 'Left Hand', label: 'Fist Curl',
438
+ effects: [{ bones: fingerBones('Left'), rot: [-0.5, 0, 0] }],
439
+ },
440
+ {
441
+ id: 'spread_l', group: 'Left Hand', label: 'Finger Spread',
442
+ effects: [
443
+ { bones: fingerBones('Left', ['Index'], [1]), rot: [0, 0, -0.18] },
444
+ { bones: fingerBones('Left', ['Ring'], [1]), rot: [0, 0, 0.12] },
445
+ { bones: fingerBones('Left', ['Pinky'], [1]), rot: [0, 0, 0.25] },
446
+ ],
447
+ },
448
+ {
449
+ id: 'thumb_l', group: 'Left Hand', label: 'Thumb Curl',
450
+ effects: [{ bones: fingerBones('Left', ['Thumb']), rot: [0, 0, -0.35] }],
451
+ },
452
+ {
453
+ id: 'fist_r', group: 'Right Hand', label: 'Fist Curl',
454
+ effects: [{ bones: fingerBones('Right'), rot: [0.5, 0, 0] }],
455
+ },
456
+ {
457
+ id: 'spread_r', group: 'Right Hand', label: 'Finger Spread',
458
+ effects: [
459
+ { bones: fingerBones('Right', ['Index'], [1]), rot: [0, 0, 0.18] },
460
+ { bones: fingerBones('Right', ['Ring'], [1]), rot: [0, 0, -0.12] },
461
+ { bones: fingerBones('Right', ['Pinky'], [1]), rot: [0, 0, -0.25] },
462
+ ],
463
+ },
464
+ {
465
+ id: 'thumb_r', group: 'Right Hand', label: 'Thumb Curl',
466
+ effects: [{ bones: fingerBones('Right', ['Thumb']), rot: [0, 0, 0.35] }],
467
+ },
468
+
469
+ // ---- Legs ----
470
+ {
471
+ id: 'leg_length', group: 'Legs', label: 'Leg Length',
472
+ effects: [
473
+ { bones: ['mHipLeft', 'mHipRight'], scale: [0, 0, 0.16] },
474
+ // Lift the pelvis so feet stay near the ground (upper+lower leg ~0.85 m).
475
+ { bones: ['mPelvis'], offset: [0, 0, 0.85 * 0.16] },
476
+ ],
477
+ },
478
+ {
479
+ id: 'thigh_muscles', group: 'Legs', label: 'Thigh Muscles',
480
+ effects: [{ bones: ['L_UPPER_LEG', 'R_UPPER_LEG'], scale: [0.45, 0.45, 0] }],
481
+ },
482
+ {
483
+ id: 'calf_muscles', group: 'Legs', label: 'Calf Muscles',
484
+ effects: [{ bones: ['L_LOWER_LEG', 'R_LOWER_LEG'], scale: [0.45, 0.45, 0] }],
485
+ },
486
+ {
487
+ // Posterior-biased: depth-dominant scale plus a -X (back) offset so the
488
+ // volume projects rearward instead of growing symmetrically into the pelvis.
489
+ id: 'butt_size', group: 'Legs', label: 'Butt Size',
490
+ effects: [{ bones: ['BUTT'], scale: [0.8, 0.45, 0.5], offset: [-0.03, 0, 0] }],
491
+ },
492
+ {
493
+ // Lift/perk rather than enlarge: raise the volume (+Z) without scaling it.
494
+ id: 'butt_lift', group: 'Legs', label: 'Butt Lift',
495
+ effects: [{ bones: ['BUTT'], offset: [0, 0, 0.025] }],
496
+ },
497
+ {
498
+ id: 'hip_width', group: 'Legs', label: 'Hip Width',
499
+ effects: [
500
+ { bones: ['PELVIS'], scale: [0.3, 0, 0] },
501
+ { bones: ['mHipLeft'], offset: [0.018, 0, 0] },
502
+ { bones: ['mHipRight'], offset: [-0.018, 0, 0] },
503
+ ],
504
+ },
505
+ {
506
+ id: 'foot_size', group: 'Legs', label: 'Foot Size',
507
+ effects: [{ bones: ['mAnkleLeft', 'mAnkleRight'], scale: [0.3, 0.3, 0.3] }],
508
+ },
509
+ ];
510
+
511
+ // Turn a { sliderId: t } state object into per-bone adjustments:
512
+ // Map<boneName, { scale: [sx,sy,sz] multipliers, offset: [x,y,z] }>
513
+ export function computeBoneAdjustments(state) {
514
+ const adj = new Map();
515
+ const get = (bone) => {
516
+ if (!adj.has(bone)) adj.set(bone, { scale: [1, 1, 1], offset: [0, 0, 0], rot: [0, 0, 0] });
517
+ return adj.get(bone);
518
+ };
519
+ for (const slider of SLIDERS) {
520
+ if (!slider.effects) continue;
521
+ const t = state[slider.id] ?? 0;
522
+ // No t === 0 skip: every slider-referenced bone always gets an entry so
523
+ // returning a slider to zero restores the bone's rest transform.
524
+ for (const fx of slider.effects) {
525
+ for (const bone of fx.bones) {
526
+ const a = get(bone);
527
+ if (fx.scale) for (let i = 0; i < 3; i++) a.scale[i] *= 1 + fx.scale[i] * t;
528
+ if (fx.offset) for (let i = 0; i < 3; i++) a.offset[i] += fx.offset[i] * t;
529
+ if (fx.rot) for (let i = 0; i < 3; i++) a.rot[i] += fx.rot[i] * t;
530
+ }
531
+ }
532
+ }
533
+ return adj;
534
+ }
535
+
536
+ // ---- sex-based body presets -------------------------------------------------
537
+ // t values (slider range [-1, 1]) for body-group sliders. Head and mouth
538
+ // sliders are left at zero (sex-neutral); the male preset broadens the hands
539
+ // (palm width + finger thickness) for a more masculine silhouette.
540
+ // Derived from the SL Ruth2 binding shape and canonical SL male param defaults.
541
+ export const SEX_PRESETS = {
542
+ female: {
543
+ height: 0,
544
+ thickness: 0,
545
+ shoulders: 0,
546
+ breast_size: 0,
547
+ pec_flatten: 0,
548
+ nipple: -0.5, // default woman: nipple pulled in (slider -50)
549
+ belly_size: 0,
550
+ torso_muscles: 0,
551
+ neck_length: 0,
552
+ neck_thickness: 0,
553
+ arm_length: 0,
554
+ bicep_size: 0,
555
+ forearm_size: 0,
556
+ hand_size: 0,
557
+ palm_width: 0,
558
+ finger_thickness: 0,
559
+ leg_length: 0,
560
+ thigh_muscles: 0,
561
+ calf_muscles: 0,
562
+ butt_size: 0,
563
+ hip_width: 0,
564
+ foot_size: 0,
565
+ },
566
+ male: {
567
+ height: 0,
568
+ thickness: 0.3, // broader torso
569
+ shoulders: 0.5, // wider shoulders
570
+ breast_size: -1, // shrink pecs fully (→ 45%)
571
+ pec_flatten: 1, // push pecs inward + shrink further (→ ~7% total)
572
+ nipple: 0, // neutral nipple protrusion
573
+ belly_size: 0.1, // slightly more belly
574
+ torso_muscles: 0.8, // broader back (V-taper)
575
+ neck_length: 0,
576
+ neck_thickness: 0.5, // thicker neck
577
+ arm_length: 0,
578
+ bicep_size: 0.5, // thicker upper arms
579
+ forearm_size: 0.5, // thicker forearms
580
+ hand_size: 0.3, // slightly larger hands
581
+ palm_width: 0.5, // broad, square palms
582
+ finger_thickness: 0.4, // thicker fingers
583
+ leg_length: 0,
584
+ thigh_muscles: 0.4, // thicker thighs
585
+ calf_muscles: 0.4, // thicker calves
586
+ butt_size: -0.3, // less pronounced butt
587
+ hip_width: -0.5, // narrower hips
588
+ foot_size: 0.1, // slightly larger feet
589
+ },
590
+ };