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/physics.js ADDED
@@ -0,0 +1,313 @@
1
+ import * as THREE from 'three';
2
+
3
+ // Soft-body physics for jiggle/sag on a group of bones (pecs, glutes, …).
4
+ // Each instance drives one configurable bone group with its own parameters;
5
+ // see the constructor opts.
6
+ //
7
+ // Two decoupled channels per pec bone:
8
+ //
9
+ // 1. Jiggle — a "dynamic bone" spring model (cf. Unity DynamicBone, Unreal
10
+ // AnimDynamics): a particle springs toward the rigid attachment point, and
11
+ // its velocity is damped RELATIVE to that (moving) attachment — so a steady
12
+ // glide adds no drag and produces no offset (no walk-forward "compression").
13
+ // The inertia kick is driven by the parent's *acceleration* (the change in
14
+ // its per-frame displacement), so constant-velocity motion injects nothing
15
+ // while footfalls / starts / stops / turns spike it and make the tissue
16
+ // lurch. Damping is anisotropic: vertical bounce stays lively while the
17
+ // fore/aft + lateral sway settles faster (the horizontalDamping control).
18
+ //
19
+ // 2. Gravity sag — applied kinematically, not as a force. The sag offset
20
+ // tracks the *change* in the gravity direction relative to the chest's
21
+ // rest orientation (zero when standing: the rest-pose sag is already
22
+ // modeled into the mesh), smoothed at its own rate. Leaning re-aims the
23
+ // sag in ~0.1-0.2 s no matter how soft the jiggle spring is, the
24
+ // standing shape is never distorted, and the full jiggle clamp budget
25
+ // stays available for motion.
26
+ //
27
+ // We track a "dynamic world position" for each pec bone, apply forces in
28
+ // world space, then convert the resulting offset back to bone-local for
29
+ // application.
30
+
31
+ const PEC_BONES = ['LEFT_PEC', 'RIGHT_PEC'];
32
+ const DEFAULT_TRACK = ['mChest', 'mPelvis']; // first present bone drives sag re-aim
33
+ const DOWN = new THREE.Vector3(0, -1, 0);
34
+ const SAG_RESPONSE = 10; // 1/s — smoothing rate for sag re-aiming on lean
35
+ const MAX_SAG = 0.08; // m at sag slider 100 (scaled by pec size)
36
+ const MAX_JIGGLE = 0.07; // m — jiggle offset clamp (scaled by pec size)
37
+ const MAX_TOTAL = 0.11; // m — hard cap on the combined offset from base (anti-invert)
38
+ const MAX_PARENT_STEP = 0.05; // m — cap inertia source so fast body motion can't explode the spring
39
+ const MAX_VEL = 4.0; // m/s — hard cap on jiggle velocity
40
+
41
+ // Reusable temps (never alias two different meanings in the same scope).
42
+ const _tv0 = new THREE.Vector3();
43
+ const _tv1 = new THREE.Vector3();
44
+ const _tv2 = new THREE.Vector3();
45
+ const _tv3 = new THREE.Vector3();
46
+ const _tv4 = new THREE.Vector3();
47
+ const _tv5 = new THREE.Vector3();
48
+ const _tq0 = new THREE.Quaternion();
49
+ const _tq1 = new THREE.Quaternion();
50
+
51
+ export class SoftBodyPhysics {
52
+ // opts.bones — bone names this instance drives (default: the pecs)
53
+ // opts.trackBones — candidate bones whose orientation aims the gravity sag;
54
+ // the first one present is used (default: chest → pelvis)
55
+ constructor(avatar, opts = {}) {
56
+ this.avatar = avatar;
57
+ this.enabled = true;
58
+
59
+ this.bones = opts.bones ?? PEC_BONES;
60
+ this._trackNames = opts.trackBones ?? DEFAULT_TRACK;
61
+
62
+ this._bounciness = 0.3; // 0–1
63
+ this._damping = 0.65; // 0–1 — VERTICAL settle (the up/down bounce)
64
+ this._horizontalDamping = 0.85; // 0–1 — fore/aft + lateral sway settle (firmer)
65
+ this._sag = 0.4; // 0–1
66
+
67
+ // Per-bone state
68
+ this._dynPos = {}; // dynamic world position
69
+ this._dynVel = {}; // dynamic world velocity
70
+ this._prevRigidPos = {}; // rigid world position from previous frame
71
+ this._prevParentDelta = {}; // last frame's rigid displacement (for acceleration)
72
+ this._basePos = {}; // bone-local rest reference
73
+ this._sagOffset = {}; // bone-local smoothed sag offset
74
+
75
+ // World "down" expressed in the chest's rest-pose local frame; the sag
76
+ // target is the deviation of the current local down from this.
77
+ this._restDownLocal = new THREE.Vector3(0, -1, 0);
78
+
79
+ this._initialized = false;
80
+
81
+ for (const name of this.bones) {
82
+ this._dynPos[name] = new THREE.Vector3();
83
+ this._dynVel[name] = new THREE.Vector3();
84
+ this._prevRigidPos[name] = new THREE.Vector3();
85
+ this._prevParentDelta[name] = new THREE.Vector3();
86
+ this._basePos[name] = new THREE.Vector3();
87
+ this._sagOffset[name] = new THREE.Vector3();
88
+ }
89
+ }
90
+
91
+ // ---- public API ----
92
+
93
+ get bounciness() { return this._bounciness; }
94
+ set bounciness(v) { this._bounciness = THREE.MathUtils.clamp(v, 0, 1); }
95
+
96
+ get damping() { return this._damping; }
97
+ set damping(v) { this._damping = THREE.MathUtils.clamp(v, 0, 1); }
98
+
99
+ // Damping of the horizontal (fore/aft + side-to-side) jiggle, independent of
100
+ // the vertical bounce. Higher = the sway from walking/turning settles faster.
101
+ get horizontalDamping() { return this._horizontalDamping; }
102
+ set horizontalDamping(v) { this._horizontalDamping = THREE.MathUtils.clamp(v, 0, 1); }
103
+
104
+ get sag() { return this._sag; }
105
+ set sag(v) { this._sag = THREE.MathUtils.clamp(v, 0, 1); }
106
+
107
+ captureBasePositions() {
108
+ const body = this.avatar.parts?.body;
109
+ if (!body) return;
110
+ for (const name of this.bones) {
111
+ const bone = body.bones.get(name);
112
+ if (bone) this._basePos[name].copy(bone.position);
113
+ }
114
+ this._captureRestDown(body);
115
+ }
116
+
117
+ // Compose the chest's rest-pose world quaternion from the captured rest
118
+ // rotations (the live quaternions may be mid-animation when this runs)
119
+ // and record where world-down points in that frame.
120
+ _captureRestDown(body) {
121
+ const trackBone = this._resolveTrackBone(body);
122
+ if (!trackBone) return;
123
+ const chain = [];
124
+ for (let obj = trackBone; obj; obj = obj.parent) chain.push(obj);
125
+ const q = _tq0.identity();
126
+ for (let i = chain.length - 1; i >= 0; i--) {
127
+ const obj = chain[i];
128
+ q.multiply(obj.isBone ? (body.rest.get(obj.name)?.q ?? obj.quaternion) : obj.quaternion);
129
+ }
130
+ this._restDownLocal.copy(DOWN).applyQuaternion(q.invert());
131
+ }
132
+
133
+ _resolveTrackBone(body) {
134
+ for (const name of this._trackNames) {
135
+ const bone = body.bones.get(name);
136
+ if (bone) return bone;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ // ---- per-frame update ----
142
+
143
+ update(dt) {
144
+ if (!this.enabled) return;
145
+
146
+ const body = this.avatar.parts?.body;
147
+ if (!body) return;
148
+
149
+ const trackBone = this._resolveTrackBone(body);
150
+ if (!trackBone) return;
151
+
152
+ // No time elapsed → nothing to integrate. Bail before any per-frame math so
153
+ // a dt=0 tick (e.g. avatar.update(0), which locomotion calls to evaluate
154
+ // frame 0 of a new clip) can't divide by zero and inject NaN into the rig.
155
+ dt = Math.min(dt, 0.1);
156
+ if (dt <= 0) return;
157
+
158
+ this.avatar.group.updateMatrixWorld();
159
+
160
+ // Tracking bone world orientation/scale (for converting offsets to local).
161
+ const trackQuat = trackBone.getWorldQuaternion(_tq0);
162
+ const trackQuatInv = _tq1.copy(trackQuat).invert();
163
+ const trackScale = trackBone.getWorldScale(_tv4);
164
+
165
+ // ---- parameters ----
166
+
167
+ // Fixed spring character; bounciness scales the visible jiggle OUTPUT below
168
+ // (so a slider at 0 = no jiggle at all, regardless of the damping settings).
169
+ const inert = 0.5; // inertia kick from parent acceleration
170
+ const stiffness = 12; // spring toward the rigid attachment
171
+ // Anisotropic damping rates (1/s). Vertical is the lively bounce; horizontal
172
+ // (fore/aft + lateral) is firmer so translating the body — e.g. walking
173
+ // forward — doesn't read as the tissue compressing back into the torso.
174
+ // The floor (4) keeps even a slider at 0 from resonating with the gait.
175
+ const dampVf = Math.exp(-(4 + this._damping * 8) * dt); // 4 → 12
176
+ const dampHf = Math.exp(-(4 + this._horizontalDamping * 16) * dt); // 4 → 20
177
+ const sagBlend = 1 - Math.exp(-SAG_RESPONSE * dt);
178
+
179
+ // Current chest-local down; how far it has swung from the rest pose
180
+ // determines the sag direction and magnitude.
181
+ const downLocal = _tv5.copy(DOWN).applyQuaternion(trackQuatInv);
182
+
183
+ for (const name of this.bones) {
184
+ const bone = body.bones.get(name);
185
+ if (!bone) continue;
186
+
187
+ const base = this._basePos[name];
188
+ const rest = body.rest.get(name);
189
+ // The breast-size slider scales the pec bones; bigger pecs swing and
190
+ // sag proportionally further.
191
+ const sizeScale = rest
192
+ ? (bone.scale.x / (rest.s.x || 1) +
193
+ bone.scale.y / (rest.s.y || 1) +
194
+ bone.scale.z / (rest.s.z || 1)) / 3
195
+ : 1;
196
+
197
+ // ---- rigid world position (where the pec WOULD be, no physics) ----
198
+
199
+ bone.position.copy(base);
200
+ const rigidWorld = bone.getWorldPosition(_tv0); // refreshes the matrix chain
201
+
202
+ // ---- initialise on first frame ----
203
+
204
+ if (!this._initialized) {
205
+ this._dynPos[name].copy(rigidWorld);
206
+ this._dynVel[name].set(0, 0, 0);
207
+ this._prevRigidPos[name].copy(rigidWorld);
208
+ this._prevParentDelta[name].set(0, 0, 0);
209
+ this._sagOffset[name].set(0, 0, 0);
210
+ continue; // bone stays at base this frame
211
+ }
212
+
213
+ const dynPos = this._dynPos[name];
214
+ const dynVel = this._dynVel[name];
215
+ const prevRigid = this._prevRigidPos[name];
216
+
217
+ // ---- jiggle: forces on the dynamic particle ----
218
+
219
+ // Rigid attachment velocity this frame, and how much that velocity
220
+ // CHANGED since last frame (parent acceleration).
221
+ const parentDelta = _tv2.subVectors(rigidWorld, prevRigid);
222
+ const pvx = parentDelta.x / dt, pvy = parentDelta.y / dt, pvz = parentDelta.z / dt;
223
+ const accelDelta = _tv3.subVectors(parentDelta, this._prevParentDelta[name]);
224
+ if (accelDelta.length() > MAX_PARENT_STEP) accelDelta.setLength(MAX_PARENT_STEP);
225
+ this._prevParentDelta[name].copy(parentDelta); // (before _tv2 is reused below)
226
+
227
+ // 1. Spring toward current rigid position.
228
+ const springForce = _tv1.subVectors(rigidWorld, dynPos).multiplyScalar(stiffness);
229
+
230
+ // ---- integrate (with a hard velocity cap for stability) ----
231
+
232
+ dynVel.addScaledVector(springForce, dt);
233
+ // 2. Inertia from parent ACCELERATION (the change in motion): a steady
234
+ // glide — walking forward at constant speed — adds nothing, so it can't
235
+ // pump a standing offset (the old "compression"). Footfalls / starts /
236
+ // stops / turns spike it and make the tissue lurch.
237
+ dynVel.addScaledVector(accelDelta, inert);
238
+ // Damp the velocity RELATIVE to the moving attachment (parent velocity),
239
+ // not the absolute world velocity — so constant translation contributes no
240
+ // drag/lag, and only true oscillations settle. Vertical (world Y) and
241
+ // horizontal (world XZ) relax at independent rates (dampVf / dampHf).
242
+ dynVel.x = pvx + (dynVel.x - pvx) * dampHf;
243
+ dynVel.z = pvz + (dynVel.z - pvz) * dampHf;
244
+ dynVel.y = pvy + (dynVel.y - pvy) * dampVf;
245
+ if (dynVel.length() > MAX_VEL) dynVel.setLength(MAX_VEL);
246
+ dynPos.addScaledVector(dynVel, dt);
247
+
248
+ // ---- compute offset & clamp ----
249
+
250
+ const worldOffset = _tv2.subVectors(dynPos, rigidWorld);
251
+ const maxOff = MAX_JIGGLE * sizeScale;
252
+ if (worldOffset.length() > maxOff) {
253
+ worldOffset.normalize().multiplyScalar(maxOff);
254
+ dynPos.copy(rigidWorld).add(worldOffset);
255
+ }
256
+
257
+ // ---- convert to bone-local ----
258
+
259
+ // Undo the world scale (height slider scales the group, shape sliders
260
+ // can scale the chest) so the local offset is metrically correct.
261
+ const localJiggle = worldOffset.applyQuaternion(trackQuatInv);
262
+ localJiggle.x /= Math.max(trackScale.x, 1e-6);
263
+ localJiggle.y /= Math.max(trackScale.y, 1e-6);
264
+ localJiggle.z /= Math.max(trackScale.z, 1e-6);
265
+ // Bounciness is the master "amount" of jiggle: 0 = the tissue rigidly
266
+ // tracks the body (no secondary motion at all), 1 = the full spring swing.
267
+ localJiggle.multiplyScalar(this._bounciness);
268
+
269
+ // ---- sag: re-aim toward current gravity, smoothed ----
270
+
271
+ // (downLocal - restDownLocal) grows with lean angle (up to ~2 when
272
+ // upside down), so cap it or a big recline/flight pose sags the pec
273
+ // far enough to invert the mesh.
274
+ const sagCap = this._sag * MAX_SAG * sizeScale;
275
+ const sagTarget = _tv3.subVectors(downLocal, this._restDownLocal).multiplyScalar(sagCap);
276
+ if (sagTarget.length() > sagCap) sagTarget.setLength(sagCap);
277
+ this._sagOffset[name].lerp(sagTarget, sagBlend);
278
+
279
+ // ---- apply (cap the combined offset as a final anti-invert guard) ----
280
+
281
+ const total = _tv1.copy(localJiggle).add(this._sagOffset[name]);
282
+ const maxTotal = MAX_TOTAL * sizeScale;
283
+ if (total.length() > maxTotal) total.setLength(maxTotal);
284
+ bone.position.copy(base).add(total);
285
+
286
+ // Save rigid position for next frame's parent-delta.
287
+ this._prevRigidPos[name].copy(rigidWorld);
288
+ }
289
+
290
+ if (!this._initialized) this._initialized = true;
291
+ }
292
+
293
+ reset() {
294
+ for (const name of this.bones) {
295
+ this._dynVel[name].set(0, 0, 0);
296
+ this._sagOffset[name].set(0, 0, 0);
297
+ }
298
+ // Restore pecs to the clean base captured by applyShape/load. Do NOT
299
+ // recapture here: bone.position still holds this frame's physics offset,
300
+ // so re-reading it would bake that offset into the base — and since
301
+ // reset() runs on every clip switch (stop → playClip), the base would
302
+ // drift further out of place each time, permanently. The shape sliders
303
+ // own the base; physics only ever borrows and restores it.
304
+ const body = this.avatar.parts?.body;
305
+ if (body) {
306
+ for (const name of this.bones) {
307
+ const bone = body.bones.get(name);
308
+ if (bone) bone.position.copy(this._basePos[name]);
309
+ }
310
+ }
311
+ this._initialized = false;
312
+ }
313
+ }
package/skeleton.js ADDED
@@ -0,0 +1,70 @@
1
+ // Canonical bone ownership across Ruth's split parts (body / hands / feet).
2
+ // These helpers resolve which part owns a bone and read/reset its rest transform,
3
+ // used by the BVH editor and the MCP example's tools (examples/mcp/avatarApi.js).
4
+
5
+ export function partForBone(name) {
6
+ if (
7
+ name.startsWith('mHand') || name === 'mWristLeft' || name === 'mWristRight'
8
+ || name === 'mCollarLeft' || name === 'mCollarRight'
9
+ || name === 'mShoulderLeft' || name === 'mShoulderRight'
10
+ || name === 'mElbowLeft' || name === 'mElbowRight'
11
+ ) return 'hands';
12
+ if (
13
+ name.startsWith('mFoot') || name === 'mAnkleLeft' || name === 'mAnkleRight'
14
+ || name === 'mHipLeft' || name === 'mHipRight'
15
+ || name === 'mKneeLeft' || name === 'mKneeRight'
16
+ ) return 'feet';
17
+ return 'body';
18
+ }
19
+
20
+ export function getCanonicalBone(avatar, boneName) {
21
+ const preferred = partForBone(boneName);
22
+ const fromPreferred = avatar.parts[preferred]?.bones.get(boneName);
23
+ if (fromPreferred) return fromPreferred;
24
+ for (const part of Object.values(avatar.parts)) {
25
+ const bone = part.bones.get(boneName);
26
+ if (bone) return bone;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ export function listBones(avatar) {
32
+ const names = new Set();
33
+ for (const part of Object.values(avatar.parts)) {
34
+ for (const name of part.bones.keys()) names.add(name);
35
+ }
36
+ return [...names].sort();
37
+ }
38
+
39
+ export function getBoneRest(avatar, boneName) {
40
+ return avatar.parts[partForBone(boneName)]?.rest.get(boneName) ?? null;
41
+ }
42
+
43
+ // Reset a bone's POSE only (local rotation). Scale and non-pelvis position are
44
+ // owned by the shape sliders (RuthAvatar.applyShape), so we leave them alone.
45
+ export function resetBone(avatar, boneName) {
46
+ const bone = getCanonicalBone(avatar, boneName);
47
+ const rest = getBoneRest(avatar, boneName);
48
+ if (!bone || !rest) return false;
49
+ bone.quaternion.copy(rest.q);
50
+ if (boneName === 'mPelvis') bone.position.copy(rest.p);
51
+ return syncBoneToAllParts(avatar, boneName);
52
+ }
53
+
54
+ // Fan a bone's rotation out to every (non-grafted) part's copy of it.
55
+ // Rotation only — scale/position stay slider-owned (pelvis position excepted,
56
+ // since it is pose, not shape).
57
+ export function syncBoneToAllParts(avatar, boneName) {
58
+ const src = getCanonicalBone(avatar, boneName);
59
+ if (!src) return false;
60
+ for (const part of Object.values(avatar.parts)) {
61
+ if (part.grafted) continue;
62
+ const dst = part.bones.get(boneName);
63
+ if (dst && dst !== src) {
64
+ dst.quaternion.copy(src.quaternion);
65
+ if (boneName === 'mPelvis') dst.position.copy(src.position);
66
+ }
67
+ }
68
+ avatar.group.updateMatrixWorld(true);
69
+ return true;
70
+ }