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/gltfAnim.js ADDED
@@ -0,0 +1,271 @@
1
+ import * as THREE from 'three';
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3
+
4
+ // Bone name mapping: UAL1 glTF node name → Ruth avatar bone name.
5
+ // Both skeletons use hierarchy-compatible names but the UAL1 rig uses
6
+ // glTF convention (lowercase, _l/_r suffix) while Ruth uses Second Life
7
+ // bone names (m-prefix, CamelCase). Leaf bones without a Ruth
8
+ // counterpart (finger tips, ball joint, root) are left unmapped and
9
+ // their tracks are silently dropped.
10
+ const UAL1_TO_RUTH = {
11
+ root: null,
12
+ pelvis: 'mPelvis',
13
+ spine_01: 'mSpine1',
14
+ spine_02: 'mSpine2',
15
+ spine_03: 'mSpine3',
16
+ neck_01: 'mNeck',
17
+ Head: 'mHead',
18
+ clavicle_l: 'mCollarLeft',
19
+ upperarm_l: 'mShoulderLeft',
20
+ lowerarm_l: 'mElbowLeft',
21
+ hand_l: 'mWristLeft',
22
+ thumb_01_l: 'mHandThumb1Left',
23
+ thumb_02_l: 'mHandThumb2Left',
24
+ thumb_03_l: 'mHandThumb3Left',
25
+ thumb_04_leaf_l: null, // fingertip — no Ruth bone
26
+ index_01_l: 'mHandIndex1Left',
27
+ index_02_l: 'mHandIndex2Left',
28
+ index_03_l: 'mHandIndex3Left',
29
+ index_04_leaf_l: null,
30
+ middle_01_l: 'mHandMiddle1Left',
31
+ middle_02_l: 'mHandMiddle2Left',
32
+ middle_03_l: 'mHandMiddle3Left',
33
+ middle_04_leaf_l: null,
34
+ ring_01_l: 'mHandRing1Left',
35
+ ring_02_l: 'mHandRing2Left',
36
+ ring_03_l: 'mHandRing3Left',
37
+ ring_04_leaf_l: null,
38
+ pinky_01_l: 'mHandPinky1Left',
39
+ pinky_02_l: 'mHandPinky2Left',
40
+ pinky_03_l: 'mHandPinky3Left',
41
+ pinky_04_leaf_l: null,
42
+ clavicle_r: 'mCollarRight',
43
+ upperarm_r: 'mShoulderRight',
44
+ lowerarm_r: 'mElbowRight',
45
+ hand_r: 'mWristRight',
46
+ thumb_01_r: 'mHandThumb1Right',
47
+ thumb_02_r: 'mHandThumb2Right',
48
+ thumb_03_r: 'mHandThumb3Right',
49
+ thumb_04_leaf_r: null,
50
+ index_01_r: 'mHandIndex1Right',
51
+ index_02_r: 'mHandIndex2Right',
52
+ index_03_r: 'mHandIndex3Right',
53
+ index_04_leaf_r: null,
54
+ middle_01_r: 'mHandMiddle1Right',
55
+ middle_02_r: 'mHandMiddle2Right',
56
+ middle_03_r: 'mHandMiddle3Right',
57
+ middle_04_leaf_r: null,
58
+ ring_01_r: 'mHandRing1Right',
59
+ ring_02_r: 'mHandRing2Right',
60
+ ring_03_r: 'mHandRing3Right',
61
+ ring_04_leaf_r: null,
62
+ pinky_01_r: 'mHandPinky1Right',
63
+ pinky_02_r: 'mHandPinky2Right',
64
+ pinky_03_r: 'mHandPinky3Right',
65
+ pinky_04_leaf_r: null,
66
+ thigh_l: 'mHipLeft',
67
+ calf_l: 'mKneeLeft',
68
+ foot_l: 'mAnkleLeft',
69
+ ball_l: 'mFootLeft',
70
+ ball_leaf_l: null, // toe tip
71
+ thigh_r: 'mHipRight',
72
+ calf_r: 'mKneeRight',
73
+ foot_r: 'mAnkleRight',
74
+ ball_r: 'mFootRight',
75
+ ball_leaf_r: null,
76
+ };
77
+
78
+ // ---- module state (lazy-initialised, per GLB file) ------------------
79
+
80
+ // Each UAL GLB (UAL1, UAL2, …) shares the identical 67-bone skeleton but
81
+ // carries its own animation set, so clips are cached per file path.
82
+ const _cache = new Map(); // glbFile -> Map<glbAnimName, AnimationClip>
83
+ const _loading = new Map(); // glbFile -> Promise (dedupes concurrent loads)
84
+
85
+ const _qTmp = new THREE.Quaternion();
86
+
87
+ // ---- public API ------------------------------------------------------
88
+
89
+ // Load a UAL GLB, retarget every animation in it, and cache the clips.
90
+ // `avatar` supplies the pelvis rest pose/height needed to retarget root
91
+ // motion. Safe to call repeatedly / concurrently for the same file — it
92
+ // loads once.
93
+ export async function initGltfAnim(glbFile, avatar, signal) {
94
+ if (_cache.has(glbFile)) return;
95
+ if (_loading.has(glbFile)) return _loading.get(glbFile);
96
+ const p = _loadAndRetarget(glbFile, avatar, signal).then((clips) => {
97
+ _cache.set(glbFile, clips);
98
+ _loading.delete(glbFile);
99
+ }, (err) => {
100
+ _loading.delete(glbFile);
101
+ throw err;
102
+ });
103
+ _loading.set(glbFile, p);
104
+ return p;
105
+ }
106
+
107
+ // Return a retargeted AnimationClip for the named animation in a loaded GLB,
108
+ // or null if not found. Throws if the file hasn't been initialised yet.
109
+ export function getGltfClip(glbFile, glbAnimName) {
110
+ const clips = _cache.get(glbFile);
111
+ if (!clips) throw new Error(`glTF animations for ${glbFile} not initialised — call initGltfAnim(glbFile) first`);
112
+ return clips.get(glbAnimName) ?? null;
113
+ }
114
+
115
+ async function _loadAndRetarget(glbFile, avatar, signal) {
116
+ const loader = new GLTFLoader();
117
+ const gltf = await loader.loadAsync(glbFile, undefined, signal ? { signal } : undefined);
118
+ const ctx = _buildXform(gltf.parser.json.nodes, avatar);
119
+
120
+ const clips = new Map();
121
+ for (const glbAnim of gltf.animations) {
122
+ const clip = _retargetGltfClip(glbAnim, ctx);
123
+ if (clip) clips.set(glbAnim.name, clip);
124
+ }
125
+ return clips;
126
+ }
127
+
128
+ // Build the retarget context from a GLB's node list + the target avatar.
129
+ // Returns { xform: Map<glbNodeName, { P, Pinv, restInv }>, pelvisPos } where
130
+ // pelvisPos carries everything needed to retarget root (pelvis) translation.
131
+ function _buildXform(nodes, avatar) {
132
+ // Per-node LOCAL rest rotation (node[].rotation), and child→parent index map.
133
+ const localRest = nodes.map((node) =>
134
+ (node.rotation && node.rotation.length === 4)
135
+ ? new THREE.Quaternion(node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3])
136
+ : new THREE.Quaternion());
137
+ const parentOf = new Array(nodes.length).fill(-1);
138
+ nodes.forEach((node, i) => {
139
+ for (const c of node.children ?? []) parentOf[c] = i;
140
+ });
141
+
142
+ // WORLD rest orientation per node = product of LOCAL rests from the root
143
+ // down (parent-first). This is the bone's bind orientation in the glTF
144
+ // scene frame — exactly what the BVH rig lacks (it is world-aligned).
145
+ // Resolved per-node up the full chain, so node ordering doesn't matter.
146
+ const worldRest = new Array(nodes.length);
147
+ const resolveWorld = (i) => {
148
+ if (worldRest[i]) return worldRest[i];
149
+ const p = parentOf[i];
150
+ const q = p === -1
151
+ ? localRest[i].clone()
152
+ : resolveWorld(p).clone().multiply(localRest[i]);
153
+ worldRest[i] = q;
154
+ return q;
155
+ };
156
+ nodes.forEach((_, i) => resolveWorld(i));
157
+
158
+ // Precompute the per-bone conjugation operator P = C · Wrest[parent] and the
159
+ // source bind inverse, so the per-frame loop is just two quaternion products.
160
+ const xform = new Map();
161
+ nodes.forEach((node, i) => {
162
+ if (!node.name || !UAL1_TO_RUTH[node.name]) return;
163
+ const p = parentOf[i];
164
+ const wrestParent = p === -1 ? _IDENT : worldRest[p];
165
+ const P = _C.clone().multiply(wrestParent);
166
+ xform.set(node.name, {
167
+ P,
168
+ Pinv: P.clone().invert(),
169
+ restInv: localRest[i].clone().invert(),
170
+ });
171
+ });
172
+
173
+ // Root-motion descriptor for the pelvis translation track. The pelvis node's
174
+ // translation lives in its parent (root) authored Z-up frame, so the same P
175
+ // operator that re-frames its rotation also re-frames the translation delta.
176
+ // We scale source units → Ruth units by the pelvis-height ratio and add the
177
+ // result to Ruth's own rest pelvis position (delta-from-bind, so the figure
178
+ // stays planted at rest and only the motion carries over).
179
+ let pelvisPos = null;
180
+ const pelvisX = xform.get('pelvis');
181
+ const pelvisNode = nodes.find((n) => n.name === 'pelvis');
182
+ const ruthRest = avatar?.parts?.body?.rest?.get('mPelvis');
183
+ const t0 = pelvisNode?.translation;
184
+ if (pelvisX && ruthRest && t0 && t0[2] > 1e-4) {
185
+ pelvisPos = {
186
+ P: pelvisX.P, // root-local → Ruth frame
187
+ t0: new THREE.Vector3(t0[0], t0[1], t0[2]), // source bind translation
188
+ scale: avatar.pelvisRestZ / t0[2], // source → Ruth body scale
189
+ restP: ruthRest.p.clone(), // Ruth rest pelvis position
190
+ };
191
+ }
192
+ return { xform, pelvisPos };
193
+ }
194
+
195
+ // ---- retargeting internals -------------------------------------------
196
+
197
+ // glTF → Ruth coordinate-frame change, same as BVH retargeting.
198
+ // glTF uses Y-up; Ruth uses Z-up +X forward (Second Life). The source rig's
199
+ // own Z-up→Y-up tilt lives in its `root` node and is already folded into each
200
+ // bone's Wrest, so C only does the final scene-frame swap.
201
+ const _C = new THREE.Quaternion()
202
+ .setFromEuler(new THREE.Euler(0, 0, Math.PI / 2))
203
+ .multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0)));
204
+ const _IDENT = new THREE.Quaternion();
205
+
206
+ function _retargetGltfClip(glbAnim, { xform, pelvisPos }) {
207
+ // glTF clip tracks: name = "nodeName.quaternion" (property)
208
+ // We need to re-key them as "ruthBoneName.quaternion" and remap quaternions.
209
+ const tracks = [];
210
+ const v = new THREE.Vector3();
211
+
212
+ for (const track of glbAnim.tracks) {
213
+ // Track name format: "boneName.quaternion" (glTF convention)
214
+ const dotIdx = track.name.lastIndexOf('.');
215
+ const glbNodeName = dotIdx > 0 ? track.name.slice(0, dotIdx) : track.name;
216
+ const prop = dotIdx > 0 ? track.name.slice(dotIdx + 1) : '';
217
+ const ruthName = UAL1_TO_RUTH[glbNodeName];
218
+ if (!ruthName) continue; // unmapped (leaf node, root, etc.)
219
+
220
+ // Pelvis root motion: re-frame the source translation delta and add it to
221
+ // Ruth's rest pelvis position. Only the pelvis carries real translation;
222
+ // every other bone's translation track is just its constant bone offset.
223
+ // (GLTFLoader maps the glTF "translation" path to a ".position" track.)
224
+ if (prop === 'position' && glbNodeName === 'pelvis') {
225
+ if (!pelvisPos) continue; // no avatar pelvis data → keep pelvis at rest
226
+ const values = new Float32Array(track.values.length);
227
+ for (let i = 0; i < track.values.length; i += 3) {
228
+ v.fromArray(track.values, i) // t (root-local)
229
+ .sub(pelvisPos.t0) // Δ = t − bind
230
+ .multiplyScalar(pelvisPos.scale) // source → Ruth units
231
+ .applyQuaternion(pelvisPos.P) // root-local → Ruth frame
232
+ .add(pelvisPos.restP); // + Ruth rest position
233
+ v.toArray(values, i);
234
+ }
235
+ tracks.push(new THREE.VectorKeyframeTrack(
236
+ 'mPelvis.position',
237
+ Array.isArray(track.times) ? new Float32Array(track.times) : track.times,
238
+ values,
239
+ ));
240
+ continue;
241
+ }
242
+
243
+ const x = xform.get(glbNodeName);
244
+ if (!x) continue; // unmapped or missing rest data
245
+
246
+ if (prop === 'quaternion') {
247
+ // Delta-from-bind retarget. The source track holds the FULL local
248
+ // rotation f (which equals the bind rotation at rest), so:
249
+ // Δ = f · restSrc⁻¹ — motion relative to bind, in parent frame
250
+ // local_ruth = P · Δ · P⁻¹ — re-express in Ruth's (world-aligned) frame
251
+ // where P = C · Wrest[parent]. At the source bind pose Δ = identity, so
252
+ // Ruth falls back to its own natural rest. For a world-aligned source
253
+ // (Wrest = restSrc = identity) this collapses to the BVH formula C·f·C⁻¹.
254
+ const values = new Float32Array(track.values.length);
255
+ for (let i = 0; i < track.values.length; i += 4) {
256
+ _qTmp.fromArray(track.values, i); // f
257
+ _qTmp.multiply(x.restInv); // Δ = f · restSrc⁻¹
258
+ _qTmp.premultiply(x.P).multiply(x.Pinv); // P · Δ · P⁻¹
259
+ _qTmp.toArray(values, i);
260
+ }
261
+ tracks.push(new THREE.QuaternionKeyframeTrack(
262
+ `${ruthName}.quaternion`,
263
+ Array.isArray(track.times) ? new Float32Array(track.times) : track.times,
264
+ values,
265
+ ));
266
+ }
267
+ }
268
+
269
+ if (tracks.length === 0) return null;
270
+ return new THREE.AnimationClip(glbAnim.name, glbAnim.duration, tracks);
271
+ }
package/index.js ADDED
@@ -0,0 +1,61 @@
1
+ // metaverse-avatar — public library entry point.
2
+ //
3
+ // The whole avatar is a single class: `new Avatar()` builds a self-contained,
4
+ // independently-controllable figure (skeleton, materials, animation, physics,
5
+ // blinking, lip-sync, attachments, eye look-at). Construct as many as you like
6
+ // and drive them separately. `three` is a peer dependency — the host app
7
+ // provides it (via an import map in the browser, or node_modules under a
8
+ // bundler), so multiple avatars share one THREE instance.
9
+ //
10
+ // import { Avatar } from 'metaverse-avatar';
11
+ // const avatar = await new Avatar().load('models/'); // basePath is required
12
+ // scene.add(avatar.group);
13
+ // // each frame: avatar.update(dt)
14
+
15
+ export { Avatar, RuthAvatar } from './Avatar.js';
16
+
17
+ // Composed capability classes — exported for advanced use / custom wiring.
18
+ // Each `Avatar` already owns one of each (avatar.blinker, .voice, .speech,
19
+ // .attachments); these exports are for building your own.
20
+ export { Blinker } from './blink.js';
21
+ export { VoiceMouth } from './voice.js';
22
+ export { SpeechMouth } from './speech.js';
23
+ // NOTE: locomotion is intentionally NOT a library export — moving a figure
24
+ // through the world is the host app's concern. A ready-made controller lives in
25
+ // examples/common/locomotion.js (used by the studio + simple demos); copy it
26
+ // into your app, or drive avatar.group + the animation methods yourself.
27
+ export {
28
+ Attachments,
29
+ ATTACHMENT_POINTS,
30
+ BUILTIN_PRESETS,
31
+ createSword,
32
+ } from './attachments.js';
33
+
34
+ // Clip loading / retargeting (BVH + glTF → the Ruth rig).
35
+ export { parseBVH, loadBVH, retargetToRuth } from './bvh.js';
36
+ export { initGltfAnim, getGltfClip } from './gltfAnim.js';
37
+
38
+ // Shape sliders + sex presets, and the bone-adjustment solver they feed.
39
+ export { SLIDERS, SEX_PRESETS, computeBoneAdjustments } from './sliders.js';
40
+
41
+ // Nipple protrusion vertex morph (the 'nipple' special slider; Avatar owns one).
42
+ export { buildNippleMorph, applyNippleMorph } from './nipple.js';
43
+
44
+ // PBR material plumbing (per-region map channels, clothing-layer stacks).
45
+ export { PBR_CHANNELS, emptyMapSet, PBRMaterialStack } from './pbr.js';
46
+
47
+ // Lip-sync viseme model (shared by SpeechMouth).
48
+ export { VISEMES, charToViseme, buildVisemeTimeline, sampleViseme } from './visemes.js';
49
+
50
+ // Skeleton helpers — bone lookup / reset across the split avatar parts.
51
+ export {
52
+ partForBone,
53
+ getCanonicalBone,
54
+ listBones,
55
+ getBoneRest,
56
+ resetBone,
57
+ syncBoneToAllParts,
58
+ } from './skeleton.js';
59
+
60
+ // Physics (jiggle/sag soft-body driver; avatars own two instances each).
61
+ export { SoftBodyPhysics } from './physics.js';