loomlarge 0.1.0 → 0.1.6
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 +672 -539
- package/dist/index.cjs +179 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -2
- package/dist/index.d.ts +84 -2
- package/dist/index.js +166 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,714 +1,847 @@
|
|
|
1
|
-
|
|
1
|
+
# LoomLarge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A FACS-based morph and bone mapping library for controlling high-definition 3D characters in Three.js.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
LoomLarge provides pre-built mappings that connect [Facial Action Coding System (FACS)](https://en.wikipedia.org/wiki/Facial_Action_Coding_System) Action Units to the morph targets and bone transforms found in Character Creator 4 (CC4) characters. Instead of manually figuring out which blend shapes correspond to which facial movements, you can simply say `setAU(12, 0.8)` and the library handles the rest.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
7. [Development](#development)
|
|
24
|
-
8. [Deployment](#deployment)
|
|
25
|
-
9. [License & Acknowledgments](#license--acknowledgments)
|
|
10
|
+
|
|
11
|
+
1. [Installation & Setup](#1-installation--setup)
|
|
12
|
+
2. [Using Presets](#2-using-presets)
|
|
13
|
+
3. [Extending & Custom Presets](#3-extending--custom-presets)
|
|
14
|
+
4. [Action Unit Control](#4-action-unit-control)
|
|
15
|
+
5. [Mix Weight System](#5-mix-weight-system)
|
|
16
|
+
6. [Composite Rotation System](#6-composite-rotation-system)
|
|
17
|
+
7. [Continuum Pairs](#7-continuum-pairs)
|
|
18
|
+
8. [Direct Morph Control](#8-direct-morph-control)
|
|
19
|
+
9. [Viseme System](#9-viseme-system)
|
|
20
|
+
10. [Transition System](#10-transition-system)
|
|
21
|
+
11. [Playback & State Control](#11-playback--state-control)
|
|
22
|
+
12. [Hair Physics](#12-hair-physics)
|
|
26
23
|
|
|
27
24
|
---
|
|
28
25
|
|
|
29
|
-
##
|
|
26
|
+
## 1. Installation & Setup
|
|
27
|
+
|
|
28
|
+
### Install the package
|
|
30
29
|
|
|
31
30
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
cd LoomLarge
|
|
31
|
+
npm install loomlarge
|
|
32
|
+
```
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
yarn install
|
|
34
|
+
### Peer dependency
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
LoomLarge requires Three.js as a peer dependency:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install three
|
|
40
|
+
```
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
yarn build
|
|
42
|
+
### Basic setup
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
```typescript
|
|
45
|
+
import * as THREE from 'three';
|
|
46
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
47
|
+
import { LoomLargeThree, collectMorphMeshes, CC4_PRESET } from 'loomlarge';
|
|
48
|
+
|
|
49
|
+
// 1. Create the LoomLarge controller with a preset
|
|
50
|
+
const loom = new LoomLargeThree({ auMappings: CC4_PRESET });
|
|
51
|
+
|
|
52
|
+
// 2. Set up your Three.js scene
|
|
53
|
+
const scene = new THREE.Scene();
|
|
54
|
+
const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100);
|
|
55
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
56
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
57
|
+
document.body.appendChild(renderer.domElement);
|
|
58
|
+
|
|
59
|
+
// 3. Load your character model
|
|
60
|
+
const loader = new GLTFLoader();
|
|
61
|
+
loader.load('/character.glb', (gltf) => {
|
|
62
|
+
scene.add(gltf.scene);
|
|
63
|
+
|
|
64
|
+
// 4. Collect all meshes that have morph targets
|
|
65
|
+
const meshes = collectMorphMeshes(gltf.scene);
|
|
66
|
+
|
|
67
|
+
// 5. Initialize LoomLarge with the meshes and model
|
|
68
|
+
loom.onReady({ meshes, model: gltf.scene });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 6. In your animation loop, call loom.update(deltaSeconds)
|
|
72
|
+
// This drives all transitions and animations
|
|
47
73
|
```
|
|
48
74
|
|
|
49
|
-
|
|
75
|
+
### Quick start examples
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
Once your character is loaded, you can control facial expressions immediately:
|
|
52
78
|
|
|
53
|
-
|
|
79
|
+
```typescript
|
|
80
|
+
// Make the character smile
|
|
81
|
+
loom.setAU(12, 0.8);
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
// Raise eyebrows
|
|
84
|
+
loom.setAU(1, 0.6);
|
|
85
|
+
loom.setAU(2, 0.6);
|
|
56
86
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- **Lip-Sync Agency**: Phoneme prediction and viseme animation
|
|
60
|
-
- **Prosodic Expression Agency**: Emotional head gestures and speech timing
|
|
61
|
-
- **Eye/Head Tracking Agency**: Gaze control with mouse, webcam, or manual modes
|
|
62
|
-
- **Conversation Agency**: Multi-modal conversational AI orchestration
|
|
87
|
+
// Blink
|
|
88
|
+
loom.setAU(45, 1.0);
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
// Open jaw
|
|
91
|
+
loom.setAU(26, 0.5);
|
|
66
92
|
|
|
67
|
-
|
|
93
|
+
// Turn head left
|
|
94
|
+
loom.setAU(51, 0.4);
|
|
68
95
|
|
|
69
|
-
|
|
96
|
+
// Look up
|
|
97
|
+
loom.setAU(63, 0.6);
|
|
98
|
+
```
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
- **Mix Weights** (0 to 1): Blend between 100% morph (0) and 100% bone (1)
|
|
73
|
-
- **Unified Rotation State**: Prevents axis conflicts when multiple systems control the same bones
|
|
100
|
+
Animate smoothly with transitions:
|
|
74
101
|
|
|
75
|
-
Example:
|
|
76
102
|
```typescript
|
|
77
|
-
//
|
|
78
|
-
|
|
103
|
+
// Smile over 200ms
|
|
104
|
+
await loom.transitionAU(12, 0.8, 200).promise;
|
|
79
105
|
|
|
80
|
-
//
|
|
81
|
-
|
|
106
|
+
// Then fade back to neutral
|
|
107
|
+
await loom.transitionAU(12, 0, 300).promise;
|
|
82
108
|
```
|
|
83
109
|
|
|
84
|
-
###
|
|
110
|
+
### The `collectMorphMeshes` helper
|
|
111
|
+
|
|
112
|
+
This utility function traverses a Three.js scene and returns all meshes that have `morphTargetInfluences` (i.e., blend shapes). It's the recommended way to gather meshes for LoomLarge:
|
|
85
113
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
114
|
+
```typescript
|
|
115
|
+
import { collectMorphMeshes } from 'loomlarge';
|
|
116
|
+
|
|
117
|
+
const meshes = collectMorphMeshes(gltf.scene);
|
|
118
|
+
// Returns: Array of THREE.Mesh objects with morph targets
|
|
119
|
+
```
|
|
89
120
|
|
|
90
121
|
---
|
|
91
122
|
|
|
92
|
-
##
|
|
123
|
+
## 2. Using Presets
|
|
93
124
|
|
|
94
|
-
|
|
125
|
+
Presets define how FACS Action Units map to your character's morph targets and bones. LoomLarge ships with `CC4_PRESET` for Character Creator 4 characters.
|
|
95
126
|
|
|
96
|
-
|
|
97
|
-
- **Yarn** 1.22+ or npm 8+
|
|
98
|
-
- Modern browser with WebGL 2.0 support
|
|
127
|
+
### What's in a preset?
|
|
99
128
|
|
|
100
|
-
|
|
129
|
+
```typescript
|
|
130
|
+
import { CC4_PRESET } from 'loomlarge';
|
|
131
|
+
|
|
132
|
+
// CC4_PRESET contains:
|
|
133
|
+
{
|
|
134
|
+
auToMorphs: {
|
|
135
|
+
// AU number → array of morph target names
|
|
136
|
+
1: ['Brow_Raise_Inner_L', 'Brow_Raise_Inner_R'],
|
|
137
|
+
12: ['Mouth_Smile_L', 'Mouth_Smile_R'],
|
|
138
|
+
45: ['Eye_Blink_L', 'Eye_Blink_R'],
|
|
139
|
+
// ... 87 AUs total
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
auToBones: {
|
|
143
|
+
// AU number → array of bone bindings
|
|
144
|
+
51: [{ node: 'HEAD', channel: 'ry', scale: -1, maxDegrees: 30 }],
|
|
145
|
+
61: [{ node: 'EYE_L', channel: 'rz', scale: 1, maxDegrees: 25 }],
|
|
146
|
+
// ... 32 bone bindings
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
boneNodes: {
|
|
150
|
+
// Logical bone name → actual node name in skeleton
|
|
151
|
+
'HEAD': 'CC_Base_Head',
|
|
152
|
+
'JAW': 'CC_Base_JawRoot',
|
|
153
|
+
'EYE_L': 'CC_Base_L_Eye',
|
|
154
|
+
'EYE_R': 'CC_Base_R_Eye',
|
|
155
|
+
'TONGUE': 'CC_Base_Tongue01',
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
visemeKeys: [
|
|
159
|
+
// 15 viseme morph names for lip-sync
|
|
160
|
+
'V_EE', 'V_Er', 'V_IH', 'V_Ah', 'V_Oh',
|
|
161
|
+
'V_W_OO', 'V_S_Z', 'V_Ch_J', 'V_F_V', 'V_TH',
|
|
162
|
+
'V_T_L_D_N', 'V_B_M_P', 'V_K_G_H_NG', 'V_AE', 'V_R'
|
|
163
|
+
],
|
|
101
164
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
morphToMesh: {
|
|
166
|
+
// Routes morph categories to specific meshes
|
|
167
|
+
'face': ['CC_Base_Body'],
|
|
168
|
+
'tongue': ['CC_Base_Tongue'],
|
|
169
|
+
'eye': ['CC_Base_EyeOcclusion_L', 'CC_Base_EyeOcclusion_R'],
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
auMixDefaults: {
|
|
173
|
+
// Default morph/bone blend weights (0 = morph, 1 = bone)
|
|
174
|
+
26: 0.5, // Jaw drop: 50% morph, 50% bone
|
|
175
|
+
51: 0.7, // Head turn: 70% bone
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
auInfo: {
|
|
179
|
+
// Metadata about each AU
|
|
180
|
+
'12': {
|
|
181
|
+
name: 'Lip Corner Puller',
|
|
182
|
+
muscularBasis: 'zygomaticus major',
|
|
183
|
+
faceArea: 'Lower',
|
|
184
|
+
facePart: 'Mouth',
|
|
185
|
+
},
|
|
186
|
+
// ...
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
108
190
|
|
|
109
|
-
|
|
110
|
-
- Place GLB file in `public/characters/`
|
|
111
|
-
- Update model path in `src/App.tsx`:
|
|
112
|
-
```typescript
|
|
113
|
-
const glbSrc = import.meta.env.BASE_URL + "characters/your-model.glb";
|
|
114
|
-
```
|
|
191
|
+
### Passing a preset to LoomLarge
|
|
115
192
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
VITE_ANTHROPIC_API_KEY=sk-ant-...
|
|
120
|
-
```
|
|
193
|
+
```typescript
|
|
194
|
+
import { LoomLargeThree, CC4_PRESET } from 'loomlarge';
|
|
121
195
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
yarn dev
|
|
125
|
-
```
|
|
196
|
+
const loom = new LoomLargeThree({ auMappings: CC4_PRESET });
|
|
197
|
+
```
|
|
126
198
|
|
|
127
199
|
---
|
|
128
200
|
|
|
129
|
-
##
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
## 3. Extending & Custom Presets
|
|
202
|
+
|
|
203
|
+
### Extending an existing preset
|
|
204
|
+
|
|
205
|
+
Use spread syntax to override specific mappings while keeping the rest:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { CC4_PRESET } from 'loomlarge';
|
|
209
|
+
|
|
210
|
+
const MY_PRESET = {
|
|
211
|
+
...CC4_PRESET,
|
|
212
|
+
|
|
213
|
+
// Override AU12 (smile) with custom morph names
|
|
214
|
+
auToMorphs: {
|
|
215
|
+
...CC4_PRESET.auToMorphs,
|
|
216
|
+
12: ['MySmile_Left', 'MySmile_Right'],
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// Add a new bone binding
|
|
220
|
+
auToBones: {
|
|
221
|
+
...CC4_PRESET.auToBones,
|
|
222
|
+
99: [{ node: 'CUSTOM_BONE', channel: 'ry', scale: 1, maxDegrees: 45 }],
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
// Update bone node paths
|
|
226
|
+
boneNodes: {
|
|
227
|
+
...CC4_PRESET.boneNodes,
|
|
228
|
+
'CUSTOM_BONE': 'MyRig_CustomBone',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const loom = new LoomLargeThree({ auMappings: MY_PRESET });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Creating a preset from scratch
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { AUMappingConfig } from 'loomlarge';
|
|
239
|
+
|
|
240
|
+
const CUSTOM_PRESET: AUMappingConfig = {
|
|
241
|
+
auToMorphs: {
|
|
242
|
+
1: ['brow_inner_up_L', 'brow_inner_up_R'],
|
|
243
|
+
2: ['brow_outer_up_L', 'brow_outer_up_R'],
|
|
244
|
+
12: ['mouth_smile_L', 'mouth_smile_R'],
|
|
245
|
+
45: ['eye_blink_L', 'eye_blink_R'],
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
auToBones: {
|
|
249
|
+
51: [{ node: 'HEAD', channel: 'ry', scale: -1, maxDegrees: 30 }],
|
|
250
|
+
52: [{ node: 'HEAD', channel: 'ry', scale: 1, maxDegrees: 30 }],
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
boneNodes: {
|
|
254
|
+
'HEAD': 'head_bone',
|
|
255
|
+
'JAW': 'jaw_bone',
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
visemeKeys: ['aa', 'ee', 'ih', 'oh', 'oo'],
|
|
259
|
+
|
|
260
|
+
morphToMesh: {
|
|
261
|
+
'face': ['body_mesh'],
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Changing presets at runtime
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// Switch to a different preset
|
|
270
|
+
loom.setAUMappings(ANOTHER_PRESET);
|
|
271
|
+
|
|
272
|
+
// Get current mappings
|
|
273
|
+
const current = loom.getAUMappings();
|
|
184
274
|
```
|
|
185
275
|
|
|
186
276
|
---
|
|
187
277
|
|
|
188
|
-
##
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
setMorph: (key, value) => engine.setMorph(key, value),
|
|
218
|
-
transitionAU: (id, value, duration) => engine.transitionAU(id, value, duration)
|
|
219
|
-
};
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Eye & Head Tracking
|
|
223
|
-
|
|
224
|
-
The **Eye/Head Tracking Service** (`latticework/eyeHeadTracking/eyeHeadTrackingService.ts`) provides three tracking modes:
|
|
225
|
-
|
|
226
|
-
1. **Manual Mode**: Direct slider control of gaze direction
|
|
227
|
-
2. **Mouse Mode**: Character follows cursor with mirror behavior
|
|
228
|
-
- Mouse left → Character looks right (at user)
|
|
229
|
-
- Negative x coordinate for natural gaze
|
|
230
|
-
3. **Webcam Mode**: Face tracking using BlazeFace model
|
|
231
|
-
- Real-time eye position detection
|
|
232
|
-
- Normalized coordinates (-1 to 1)
|
|
233
|
-
|
|
234
|
-
**Key Features**:
|
|
235
|
-
- **Composite Methods**: Uses `applyEyeComposite(yaw, pitch)` and `applyHeadComposite(yaw, pitch, roll)`
|
|
236
|
-
- **Intensity Control**: Separate sliders for eye and head movement intensity
|
|
237
|
-
- **Head Follow Eyes**: Optional delayed head movement matching eye gaze
|
|
238
|
-
- **Global Service**: Created in App.tsx and shared via ModulesContext
|
|
239
|
-
|
|
240
|
-
**Usage**:
|
|
241
|
-
```typescript
|
|
242
|
-
// Initialize service with engine reference
|
|
243
|
-
const service = createEyeHeadTrackingService({
|
|
244
|
-
eyeTrackingEnabled: true,
|
|
245
|
-
headTrackingEnabled: true,
|
|
246
|
-
headFollowEyes: true,
|
|
247
|
-
eyeIntensity: 1.0,
|
|
248
|
-
headIntensity: 0.5,
|
|
249
|
-
engine: engine
|
|
278
|
+
## 4. Action Unit Control
|
|
279
|
+
|
|
280
|
+
Action Units are the core of FACS. Each AU represents a specific muscular movement of the face.
|
|
281
|
+
|
|
282
|
+
### Setting an AU immediately
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Set AU12 (smile) to 80% intensity
|
|
286
|
+
loom.setAU(12, 0.8);
|
|
287
|
+
|
|
288
|
+
// Set AU45 (blink) to full intensity
|
|
289
|
+
loom.setAU(45, 1.0);
|
|
290
|
+
|
|
291
|
+
// Set to 0 to deactivate
|
|
292
|
+
loom.setAU(12, 0);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Transitioning an AU over time
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// Animate AU12 to 0.8 over 200ms
|
|
299
|
+
const handle = loom.transitionAU(12, 0.8, 200);
|
|
300
|
+
|
|
301
|
+
// Wait for completion
|
|
302
|
+
await handle.promise;
|
|
303
|
+
|
|
304
|
+
// Or chain transitions
|
|
305
|
+
loom.transitionAU(12, 1.0, 200).promise.then(() => {
|
|
306
|
+
loom.transitionAU(12, 0, 300); // Fade out
|
|
250
307
|
});
|
|
308
|
+
```
|
|
251
309
|
|
|
252
|
-
|
|
253
|
-
service.setMode('mouse'); // or 'webcam' or 'manual'
|
|
310
|
+
### Getting the current AU value
|
|
254
311
|
|
|
255
|
-
|
|
256
|
-
|
|
312
|
+
```typescript
|
|
313
|
+
const smileAmount = loom.getAU(12);
|
|
314
|
+
console.log(`Current smile: ${smileAmount}`);
|
|
257
315
|
```
|
|
258
316
|
|
|
259
|
-
###
|
|
317
|
+
### Asymmetric control with balance
|
|
260
318
|
|
|
261
|
-
|
|
319
|
+
Many AUs have left and right variants (e.g., `Mouth_Smile_L` and `Mouth_Smile_R`). The `balance` parameter lets you control them independently:
|
|
262
320
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const predictor = new EnhancedPhonemePredictor();
|
|
266
|
-
const phonemes = predictor.predict('Hello world');
|
|
267
|
-
```
|
|
321
|
+
```typescript
|
|
322
|
+
// Balance range: -1 (left only) to +1 (right only), 0 = both equal
|
|
268
323
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
- Coarticulation smoothing between adjacent phonemes
|
|
324
|
+
// Smile on both sides equally
|
|
325
|
+
loom.setAU(12, 0.8, 0);
|
|
272
326
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const snippet = generateLipsyncSnippet(text, {
|
|
276
|
-
speechRate: 1.0,
|
|
277
|
-
intensity: 0.8,
|
|
278
|
-
style: 'relaxed' // or 'precise', 'theatrical', etc.
|
|
279
|
-
});
|
|
280
|
-
```
|
|
327
|
+
// Smile only on left side
|
|
328
|
+
loom.setAU(12, 0.8, -1);
|
|
281
329
|
|
|
282
|
-
|
|
330
|
+
// Smile only on right side
|
|
331
|
+
loom.setAU(12, 0.8, 1);
|
|
283
332
|
|
|
284
|
-
|
|
333
|
+
// 70% left, 30% right
|
|
334
|
+
loom.setAU(12, 0.8, -0.4);
|
|
335
|
+
```
|
|
285
336
|
|
|
286
|
-
|
|
337
|
+
### String-based side selection
|
|
287
338
|
|
|
288
|
-
|
|
289
|
-
2. **Gesture Library**: Pre-defined head nods, tilts, and shakes
|
|
290
|
-
- Nod: Positive affirmation (head pitch down)
|
|
291
|
-
- Shake: Negation (head yaw side-to-side)
|
|
292
|
-
- Tilt: Curiosity/emphasis (head roll)
|
|
339
|
+
You can also specify the side directly in the AU ID:
|
|
293
340
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
//
|
|
298
|
-
// Negation words → head shake
|
|
299
|
-
```
|
|
341
|
+
```typescript
|
|
342
|
+
// These are equivalent:
|
|
343
|
+
loom.setAU('12L', 0.8); // Left side only
|
|
344
|
+
loom.setAU(12, 0.8, -1); // Left side only
|
|
300
345
|
|
|
301
|
-
|
|
346
|
+
loom.setAU('12R', 0.8); // Right side only
|
|
347
|
+
loom.setAU(12, 0.8, 1); // Right side only
|
|
348
|
+
```
|
|
302
349
|
|
|
303
350
|
---
|
|
304
351
|
|
|
305
|
-
##
|
|
306
|
-
|
|
307
|
-
LoomLarge supports pluggable modules for extended functionality:
|
|
308
|
-
|
|
309
|
-
### AI Chat Module
|
|
310
|
-
- **Description**: Real-time conversational AI using Anthropic Claude
|
|
311
|
-
- **Location**: `src/modules/aiChat/`
|
|
312
|
-
- **Features**:
|
|
313
|
-
- Streaming text-to-speech synthesis
|
|
314
|
-
- Lip-sync integration with prosodic expression
|
|
315
|
-
- Eye/head tracking during conversation
|
|
316
|
-
- WebSocket or LiveKit audio streaming
|
|
317
|
-
|
|
318
|
-
**Activation**:
|
|
319
|
-
```typescript
|
|
320
|
-
// Via ModulesMenu UI or programmatically:
|
|
321
|
-
import { AIChatApp } from './modules/aiChat';
|
|
322
|
-
<AIChatApp animationManager={anim} />
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### French Quiz Module
|
|
326
|
-
- **Description**: Interactive language learning demo
|
|
327
|
-
- **Location**: `src/modules/frenchQuiz/`
|
|
328
|
-
- **Features**:
|
|
329
|
-
- Survey-style question flow
|
|
330
|
-
- Facial expressions tied to correct/incorrect answers
|
|
331
|
-
- Modal-based UI with progress tracking
|
|
332
|
-
|
|
333
|
-
### Custom Modules
|
|
334
|
-
|
|
335
|
-
Create your own modules by following this pattern:
|
|
336
|
-
|
|
337
|
-
1. **Define module config** (`src/modules/config.ts`):
|
|
338
|
-
```typescript
|
|
339
|
-
export default {
|
|
340
|
-
modules: [
|
|
341
|
-
{
|
|
342
|
-
name: 'My Module',
|
|
343
|
-
description: 'Custom module description',
|
|
344
|
-
component: './modules/myModule/index.tsx'
|
|
345
|
-
}
|
|
346
|
-
]
|
|
347
|
-
};
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
2. **Create module component**:
|
|
351
|
-
```typescript
|
|
352
|
-
// src/modules/myModule/index.tsx
|
|
353
|
-
import React from 'react';
|
|
354
|
-
import { useModulesContext } from '../../context/ModulesContext';
|
|
355
|
-
|
|
356
|
-
export default function MyModule({ animationManager }: any) {
|
|
357
|
-
const { eyeHeadTrackingService } = useModulesContext();
|
|
358
|
-
|
|
359
|
-
// Your module logic here
|
|
360
|
-
return <div>My Module UI</div>;
|
|
361
|
-
}
|
|
362
|
-
```
|
|
352
|
+
## 5. Mix Weight System
|
|
363
353
|
|
|
364
|
-
|
|
354
|
+
Some AUs can be driven by both morph targets (blend shapes) AND bone rotations. The mix weight controls the blend between them.
|
|
365
355
|
|
|
366
|
-
|
|
356
|
+
### Why mix weights?
|
|
367
357
|
|
|
368
|
-
|
|
358
|
+
Take jaw opening (AU26) as an example:
|
|
359
|
+
- **Morph-only (weight 0)**: Vertices deform to show open mouth, but jaw bone doesn't move
|
|
360
|
+
- **Bone-only (weight 1)**: Jaw bone rotates down, but no soft tissue deformation
|
|
361
|
+
- **Mixed (weight 0.5)**: Both contribute equally for realistic results
|
|
369
362
|
|
|
370
|
-
|
|
371
|
-
|
|
363
|
+
### Setting mix weights
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// Get the default mix weight for AU26
|
|
367
|
+
const weight = loom.getAUMixWeight(26); // e.g., 0.5
|
|
368
|
+
|
|
369
|
+
// Set to pure morph
|
|
370
|
+
loom.setAUMixWeight(26, 0);
|
|
371
|
+
|
|
372
|
+
// Set to pure bone
|
|
373
|
+
loom.setAUMixWeight(26, 1);
|
|
374
|
+
|
|
375
|
+
// Set to 70% bone, 30% morph
|
|
376
|
+
loom.setAUMixWeight(26, 0.7);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Which AUs support mixing?
|
|
380
|
+
|
|
381
|
+
Only AUs that have both `auToMorphs` AND `auToBones` entries support mixing. Common examples:
|
|
382
|
+
- AU26 (Jaw Drop)
|
|
383
|
+
- AU27 (Mouth Stretch)
|
|
384
|
+
- AU51-56 (Head movements)
|
|
385
|
+
- AU61-64 (Eye movements)
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { isMixedAU } from 'loomlarge';
|
|
389
|
+
|
|
390
|
+
if (isMixedAU(26)) {
|
|
391
|
+
console.log('AU26 supports morph/bone mixing');
|
|
392
|
+
}
|
|
372
393
|
```
|
|
373
394
|
|
|
374
|
-
|
|
375
|
-
- Hot module replacement (HMR)
|
|
376
|
-
- Source maps for debugging
|
|
377
|
-
- Console logging for all services
|
|
395
|
+
---
|
|
378
396
|
|
|
379
|
-
|
|
397
|
+
## 6. Composite Rotation System
|
|
380
398
|
|
|
381
|
-
|
|
399
|
+
Bones like the head and eyes need multi-axis rotation (pitch, yaw, roll). The composite rotation system handles this automatically.
|
|
382
400
|
|
|
383
|
-
|
|
384
|
-
// Global handles (auto-exposed in dev mode)
|
|
385
|
-
window.engine // EngineThree instance
|
|
386
|
-
window.anim // Animation service
|
|
401
|
+
### How it works
|
|
387
402
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
403
|
+
When you set an AU that affects a bone rotation, LoomLarge:
|
|
404
|
+
1. Queues the rotation update in `pendingCompositeNodes`
|
|
405
|
+
2. At the end of `update()`, calls `flushPendingComposites()`
|
|
406
|
+
3. Applies all three axes (pitch, yaw, roll) together to prevent gimbal issues
|
|
407
|
+
|
|
408
|
+
### Supported bones and their axes
|
|
409
|
+
|
|
410
|
+
| Bone | Pitch (X) | Yaw (Y) | Roll (Z) |
|
|
411
|
+
|------|-----------|---------|----------|
|
|
412
|
+
| HEAD | AU53 (up) / AU54 (down) | AU51 (left) / AU52 (right) | AU55 (tilt left) / AU56 (tilt right) |
|
|
413
|
+
| EYE_L | AU63 (up) / AU64 (down) | AU61 (left) / AU62 (right) | - |
|
|
414
|
+
| EYE_R | AU63 (up) / AU64 (down) | AU61 (left) / AU62 (right) | - |
|
|
415
|
+
| JAW | AU25-27 (open) | AU30 (left) / AU35 (right) | - |
|
|
416
|
+
| TONGUE | AU37 (up) / AU38 (down) | AU39 (left) / AU40 (right) | AU41 / AU42 (tilt) |
|
|
417
|
+
|
|
418
|
+
### Example: Moving the head
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// Turn head left 50%
|
|
422
|
+
loom.setAU(51, 0.5);
|
|
423
|
+
|
|
424
|
+
// Turn head right 50%
|
|
425
|
+
loom.setAU(52, 0.5);
|
|
426
|
+
|
|
427
|
+
// Tilt head up 30%
|
|
428
|
+
loom.setAU(53, 0.3);
|
|
429
|
+
|
|
430
|
+
// Combine: turn left AND tilt up
|
|
431
|
+
loom.setAU(51, 0.5);
|
|
432
|
+
loom.setAU(53, 0.3);
|
|
433
|
+
// Both are applied together in a single composite rotation
|
|
398
434
|
```
|
|
399
435
|
|
|
400
|
-
###
|
|
436
|
+
### Example: Eye gaze
|
|
401
437
|
|
|
402
|
-
|
|
438
|
+
```typescript
|
|
439
|
+
// Look left
|
|
440
|
+
loom.setAU(61, 0.7);
|
|
403
441
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
window.eyeHeadTrackingService?.getMode(); // 'manual' | 'mouse' | 'webcam'
|
|
442
|
+
// Look right
|
|
443
|
+
loom.setAU(62, 0.7);
|
|
407
444
|
|
|
408
|
-
//
|
|
409
|
-
|
|
445
|
+
// Look up
|
|
446
|
+
loom.setAU(63, 0.5);
|
|
410
447
|
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
headIntensity: 0.7,
|
|
415
|
-
headFollowEyes: true
|
|
416
|
-
});
|
|
448
|
+
// Look down-right (combined)
|
|
449
|
+
loom.setAU(62, 0.6);
|
|
450
|
+
loom.setAU(64, 0.4);
|
|
417
451
|
```
|
|
418
452
|
|
|
419
|
-
|
|
453
|
+
---
|
|
420
454
|
|
|
421
|
-
|
|
422
|
-
|
|
455
|
+
## 7. Continuum Pairs
|
|
456
|
+
|
|
457
|
+
Continuum pairs are bidirectional AU pairs that represent opposite directions on the same axis. They're linked so that activating one should deactivate the other.
|
|
458
|
+
|
|
459
|
+
### Pair mappings
|
|
460
|
+
|
|
461
|
+
| Pair | Description |
|
|
462
|
+
|------|-------------|
|
|
463
|
+
| AU51 ↔ AU52 | Head turn left / right |
|
|
464
|
+
| AU53 ↔ AU54 | Head up / down |
|
|
465
|
+
| AU55 ↔ AU56 | Head tilt left / right |
|
|
466
|
+
| AU61 ↔ AU62 | Eyes look left / right |
|
|
467
|
+
| AU63 ↔ AU64 | Eyes look up / down |
|
|
468
|
+
| AU30 ↔ AU35 | Jaw shift left / right |
|
|
469
|
+
| AU37 ↔ AU38 | Tongue up / down |
|
|
470
|
+
| AU39 ↔ AU40 | Tongue left / right |
|
|
471
|
+
| AU73 ↔ AU74 | Tongue narrow / wide |
|
|
472
|
+
| AU76 ↔ AU77 | Tongue tip up / down |
|
|
473
|
+
|
|
474
|
+
### Working with pairs
|
|
475
|
+
|
|
476
|
+
When using continuum pairs, set one AU from the pair and leave the other at 0:
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
// Head looking left at 50%
|
|
480
|
+
loom.setAU(51, 0.5);
|
|
481
|
+
loom.setAU(52, 0); // Right should be 0
|
|
482
|
+
|
|
483
|
+
// Head looking right at 70%
|
|
484
|
+
loom.setAU(51, 0); // Left should be 0
|
|
485
|
+
loom.setAU(52, 0.7);
|
|
423
486
|
```
|
|
424
487
|
|
|
425
|
-
|
|
488
|
+
### The CONTINUUM_PAIRS_MAP
|
|
489
|
+
|
|
490
|
+
You can access pair information programmatically:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import { CONTINUUM_PAIRS_MAP } from 'loomlarge';
|
|
494
|
+
|
|
495
|
+
const pair = CONTINUUM_PAIRS_MAP[51];
|
|
496
|
+
// { pairId: 52, isNegative: true, axis: 'yaw', node: 'HEAD' }
|
|
497
|
+
```
|
|
426
498
|
|
|
427
499
|
---
|
|
428
500
|
|
|
429
|
-
##
|
|
501
|
+
## 8. Direct Morph Control
|
|
430
502
|
|
|
431
|
-
|
|
503
|
+
Sometimes you need to control morph targets directly by name, bypassing the AU system.
|
|
432
504
|
|
|
433
|
-
|
|
505
|
+
### Setting a morph immediately
|
|
434
506
|
|
|
435
|
-
```
|
|
436
|
-
|
|
507
|
+
```typescript
|
|
508
|
+
// Set a specific morph to 50%
|
|
509
|
+
loom.setMorph('Mouth_Smile_L', 0.5);
|
|
510
|
+
|
|
511
|
+
// Set on specific meshes only
|
|
512
|
+
loom.setMorph('Mouth_Smile_L', 0.5, ['CC_Base_Body']);
|
|
437
513
|
```
|
|
438
514
|
|
|
439
|
-
|
|
440
|
-
1. Builds production bundle (`yarn build`)
|
|
441
|
-
2. Deploys to `gh-pages` branch
|
|
442
|
-
3. Publishes to `https://meekmachine.github.io/LoomLarge`
|
|
515
|
+
### Transitioning a morph
|
|
443
516
|
|
|
444
|
-
**Configuration** (`vite.config.ts`):
|
|
445
517
|
```typescript
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
build: {
|
|
449
|
-
outDir: 'dist',
|
|
450
|
-
assetsDir: 'assets'
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
```
|
|
518
|
+
// Animate morph over 200ms
|
|
519
|
+
const handle = loom.transitionMorph('Mouth_Smile_L', 0.8, 200);
|
|
454
520
|
|
|
455
|
-
|
|
521
|
+
// With mesh targeting
|
|
522
|
+
loom.transitionMorph('Eye_Blink_L', 1.0, 100, ['CC_Base_Body']);
|
|
456
523
|
|
|
457
|
-
|
|
524
|
+
// Wait for completion
|
|
525
|
+
await handle.promise;
|
|
526
|
+
```
|
|
458
527
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
528
|
+
### Reading current morph value
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
const value = loom.getMorphValue('Mouth_Smile_L');
|
|
532
|
+
```
|
|
463
533
|
|
|
464
|
-
|
|
465
|
-
```
|
|
466
|
-
A 185.199.108.153
|
|
467
|
-
A 185.199.109.153
|
|
468
|
-
A 185.199.110.153
|
|
469
|
-
A 185.199.111.153
|
|
470
|
-
CNAME www your-username.github.io
|
|
471
|
-
```
|
|
534
|
+
### Morph caching
|
|
472
535
|
|
|
473
|
-
|
|
474
|
-
```bash
|
|
475
|
-
yarn deploy
|
|
476
|
-
```
|
|
536
|
+
LoomLarge caches morph target lookups for performance. The first time you access a morph, it searches all meshes and caches the index. Subsequent accesses are O(1).
|
|
477
537
|
|
|
478
538
|
---
|
|
479
539
|
|
|
480
|
-
##
|
|
540
|
+
## 9. Viseme System
|
|
541
|
+
|
|
542
|
+
Visemes are mouth shapes used for lip-sync. LoomLarge includes 15 visemes with automatic jaw coupling.
|
|
543
|
+
|
|
544
|
+
### The 15 visemes
|
|
481
545
|
|
|
482
|
-
|
|
546
|
+
| Index | Key | Phoneme Example |
|
|
547
|
+
|-------|-----|-----------------|
|
|
548
|
+
| 0 | EE | "b**ee**" |
|
|
549
|
+
| 1 | Er | "h**er**" |
|
|
550
|
+
| 2 | IH | "s**i**t" |
|
|
551
|
+
| 3 | Ah | "f**a**ther" |
|
|
552
|
+
| 4 | Oh | "g**o**" |
|
|
553
|
+
| 5 | W_OO | "t**oo**" |
|
|
554
|
+
| 6 | S_Z | "**s**un, **z**oo" |
|
|
555
|
+
| 7 | Ch_J | "**ch**ip, **j**ump" |
|
|
556
|
+
| 8 | F_V | "**f**un, **v**an" |
|
|
557
|
+
| 9 | TH | "**th**ink" |
|
|
558
|
+
| 10 | T_L_D_N | "**t**op, **l**ip, **d**og, **n**o" |
|
|
559
|
+
| 11 | B_M_P | "**b**at, **m**an, **p**op" |
|
|
560
|
+
| 12 | K_G_H_NG | "**k**ite, **g**o, **h**at, si**ng**" |
|
|
561
|
+
| 13 | AE | "c**a**t" |
|
|
562
|
+
| 14 | R | "**r**ed" |
|
|
563
|
+
|
|
564
|
+
### Setting a viseme
|
|
483
565
|
|
|
484
566
|
```typescript
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
stop(): void;
|
|
491
|
-
setLoop(loop: boolean): void;
|
|
492
|
-
scrub(time: number): void;
|
|
493
|
-
step(deltaSeconds: number): void;
|
|
494
|
-
dispose(): void;
|
|
495
|
-
}
|
|
567
|
+
// Set viseme 3 (Ah) to full intensity
|
|
568
|
+
loom.setViseme(3, 1.0);
|
|
569
|
+
|
|
570
|
+
// With jaw scale (0-1, default 1)
|
|
571
|
+
loom.setViseme(3, 1.0, 0.5); // Half jaw opening
|
|
496
572
|
```
|
|
497
573
|
|
|
498
|
-
###
|
|
574
|
+
### Transitioning visemes
|
|
499
575
|
|
|
500
576
|
```typescript
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
getMode(): 'manual' | 'mouse' | 'webcam';
|
|
507
|
-
updateConfig(config: Partial<EyeHeadTrackingConfig>): void;
|
|
508
|
-
setSpeaking(isSpeaking: boolean): void;
|
|
509
|
-
setListening(isListening: boolean): void;
|
|
510
|
-
blink(): void;
|
|
511
|
-
dispose(): void;
|
|
512
|
-
}
|
|
577
|
+
// Animate to viseme over 80ms (typical for speech)
|
|
578
|
+
const handle = loom.transitionViseme(3, 1.0, 80);
|
|
579
|
+
|
|
580
|
+
// Disable jaw coupling
|
|
581
|
+
loom.transitionViseme(3, 1.0, 80, 0);
|
|
513
582
|
```
|
|
514
583
|
|
|
515
|
-
###
|
|
584
|
+
### Automatic jaw coupling
|
|
516
585
|
|
|
517
|
-
|
|
518
|
-
class EngineThree {
|
|
519
|
-
// Eye composite rotation (yaw/pitch)
|
|
520
|
-
applyEyeComposite(yaw: number, pitch: number): void;
|
|
586
|
+
Each viseme has a predefined jaw opening amount. When you set a viseme, the jaw automatically opens proportionally:
|
|
521
587
|
|
|
522
|
-
|
|
523
|
-
|
|
588
|
+
| Viseme | Jaw Amount |
|
|
589
|
+
|--------|------------|
|
|
590
|
+
| EE | 0.15 |
|
|
591
|
+
| Ah | 0.70 |
|
|
592
|
+
| Oh | 0.50 |
|
|
593
|
+
| B_M_P | 0.20 |
|
|
524
594
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
595
|
+
The `jawScale` parameter multiplies this amount:
|
|
596
|
+
- `jawScale = 1.0`: Normal jaw opening
|
|
597
|
+
- `jawScale = 0.5`: Half jaw opening
|
|
598
|
+
- `jawScale = 0`: No jaw movement (viseme only)
|
|
528
599
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
600
|
+
### Lip-sync example
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
async function speak(phonemes: number[]) {
|
|
604
|
+
for (const viseme of phonemes) {
|
|
605
|
+
// Clear previous viseme
|
|
606
|
+
for (let i = 0; i < 15; i++) loom.setViseme(i, 0);
|
|
532
607
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
608
|
+
// Transition to new viseme
|
|
609
|
+
await loom.transitionViseme(viseme, 1.0, 80).promise;
|
|
610
|
+
|
|
611
|
+
// Hold briefly
|
|
612
|
+
await new Promise(r => setTimeout(r, 100));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Return to neutral
|
|
616
|
+
for (let i = 0; i < 15; i++) loom.setViseme(i, 0);
|
|
536
617
|
}
|
|
618
|
+
|
|
619
|
+
// "Hello" approximation
|
|
620
|
+
speak([5, 0, 10, 4]);
|
|
537
621
|
```
|
|
538
622
|
|
|
539
623
|
---
|
|
540
624
|
|
|
541
|
-
##
|
|
625
|
+
## 10. Transition System
|
|
542
626
|
|
|
543
|
-
|
|
627
|
+
All animated changes in LoomLarge go through the transition system, which provides smooth interpolation with easing.
|
|
544
628
|
|
|
545
|
-
|
|
546
|
-
```javascript
|
|
547
|
-
console.log(window.engine); // Should show EngineThree instance
|
|
548
|
-
```
|
|
629
|
+
### TransitionHandle
|
|
549
630
|
|
|
550
|
-
|
|
551
|
-
```javascript
|
|
552
|
-
console.log(window.anim?.getSnapshot?.().value); // Should show 'playing' or 'idle'
|
|
553
|
-
```
|
|
631
|
+
Every transition method returns a `TransitionHandle`:
|
|
554
632
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
633
|
+
```typescript
|
|
634
|
+
interface TransitionHandle {
|
|
635
|
+
promise: Promise<void>; // Resolves when transition completes
|
|
636
|
+
pause(): void; // Pause this transition
|
|
637
|
+
resume(): void; // Resume this transition
|
|
638
|
+
cancel(): void; // Cancel immediately
|
|
639
|
+
}
|
|
640
|
+
```
|
|
559
641
|
|
|
560
|
-
###
|
|
642
|
+
### Using handles
|
|
561
643
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
644
|
+
```typescript
|
|
645
|
+
// Start a transition
|
|
646
|
+
const handle = loom.transitionAU(12, 1.0, 500);
|
|
565
647
|
|
|
566
|
-
|
|
648
|
+
// Pause it
|
|
649
|
+
handle.pause();
|
|
567
650
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
- AU keys must match ARKit spec (e.g., 'AU_12', not '12')
|
|
651
|
+
// Resume later
|
|
652
|
+
handle.resume();
|
|
571
653
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
anim.listSnippets(); // Show all loaded snippets
|
|
575
|
-
```
|
|
654
|
+
// Or cancel entirely
|
|
655
|
+
handle.cancel();
|
|
576
656
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
657
|
+
// Wait for completion
|
|
658
|
+
await handle.promise;
|
|
659
|
+
```
|
|
580
660
|
|
|
581
|
-
###
|
|
661
|
+
### Combining multiple transitions
|
|
582
662
|
|
|
583
|
-
|
|
663
|
+
When you call `transitionAU`, it may create multiple internal transitions (one per morph target). The returned handle controls all of them:
|
|
584
664
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
- Results in 100+ bundling errors about missing exports and modules
|
|
665
|
+
```typescript
|
|
666
|
+
// AU12 might affect Mouth_Smile_L and Mouth_Smile_R
|
|
667
|
+
const handle = loom.transitionAU(12, 1.0, 200);
|
|
589
668
|
|
|
590
|
-
|
|
591
|
-
|
|
669
|
+
// Pausing the handle pauses both morph transitions
|
|
670
|
+
handle.pause();
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Easing
|
|
674
|
+
|
|
675
|
+
The default easing is `easeInOutQuad`. Custom easing can be provided when using the Animation system directly:
|
|
592
676
|
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
677
|
+
```typescript
|
|
678
|
+
// The AnimationThree class supports custom easing
|
|
679
|
+
animation.addTransition(
|
|
680
|
+
'custom',
|
|
681
|
+
0,
|
|
682
|
+
1,
|
|
683
|
+
200,
|
|
684
|
+
(v) => console.log(v),
|
|
685
|
+
(t) => t * t // Custom ease-in quadratic
|
|
686
|
+
);
|
|
597
687
|
```
|
|
598
688
|
|
|
599
|
-
|
|
689
|
+
### Active transition count
|
|
690
|
+
|
|
600
691
|
```typescript
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const model = await blazeface.load();
|
|
692
|
+
const count = loom.getActiveTransitionCount();
|
|
693
|
+
console.log(`${count} transitions in progress`);
|
|
604
694
|
```
|
|
605
695
|
|
|
606
|
-
|
|
696
|
+
### Clearing all transitions
|
|
697
|
+
|
|
607
698
|
```typescript
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
exclude: [
|
|
611
|
-
'@tensorflow/tfjs',
|
|
612
|
-
'@tensorflow/tfjs-core',
|
|
613
|
-
'@tensorflow/tfjs-converter',
|
|
614
|
-
'@tensorflow/tfjs-backend-cpu',
|
|
615
|
-
'@tensorflow/tfjs-backend-webgl',
|
|
616
|
-
'@tensorflow-models/blazeface',
|
|
617
|
-
],
|
|
618
|
-
}
|
|
699
|
+
// Cancel everything immediately
|
|
700
|
+
loom.clearTransitions();
|
|
619
701
|
```
|
|
620
702
|
|
|
621
|
-
|
|
622
|
-
1. Ensure TensorFlow packages are NOT in `package.json` dependencies
|
|
623
|
-
2. Clear Vite cache: `rm -rf node_modules/.vite`
|
|
624
|
-
3. Restart dev server: `yarn dev`
|
|
703
|
+
---
|
|
625
704
|
|
|
626
|
-
|
|
705
|
+
## 11. Playback & State Control
|
|
627
706
|
|
|
628
|
-
###
|
|
707
|
+
### Pausing and resuming
|
|
629
708
|
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
709
|
+
```typescript
|
|
710
|
+
// Pause all animation updates
|
|
711
|
+
loom.pause();
|
|
712
|
+
|
|
713
|
+
// Check pause state
|
|
714
|
+
if (loom.getPaused()) {
|
|
715
|
+
console.log('Animation is paused');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Resume
|
|
719
|
+
loom.resume();
|
|
635
720
|
```
|
|
636
721
|
|
|
637
|
-
|
|
722
|
+
When paused, `loom.update()` stops processing transitions, but you can still call `setAU()` for immediate changes.
|
|
638
723
|
|
|
639
|
-
|
|
724
|
+
### Resetting to neutral
|
|
640
725
|
|
|
641
|
-
|
|
726
|
+
```typescript
|
|
727
|
+
// Reset everything to rest state
|
|
728
|
+
loom.resetToNeutral();
|
|
729
|
+
```
|
|
642
730
|
|
|
643
|
-
|
|
644
|
-
-
|
|
645
|
-
-
|
|
731
|
+
This:
|
|
732
|
+
- Clears all AU values to 0
|
|
733
|
+
- Cancels all active transitions
|
|
734
|
+
- Resets all morph targets to 0
|
|
735
|
+
- Returns all bones to their original position/rotation
|
|
646
736
|
|
|
647
|
-
###
|
|
737
|
+
### Mesh visibility
|
|
648
738
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
739
|
+
```typescript
|
|
740
|
+
// Get list of all meshes
|
|
741
|
+
const meshes = loom.getMeshList();
|
|
742
|
+
// Returns: [{ name: 'CC_Base_Body', visible: true, morphCount: 80 }, ...]
|
|
652
743
|
|
|
653
|
-
|
|
744
|
+
// Hide a mesh
|
|
745
|
+
loom.setMeshVisible('CC_Base_Hair', false);
|
|
654
746
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
747
|
+
// Show it again
|
|
748
|
+
loom.setMeshVisible('CC_Base_Hair', true);
|
|
749
|
+
```
|
|
658
750
|
|
|
659
|
-
|
|
751
|
+
### Cleanup
|
|
660
752
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
1. **Fork and clone** the repository
|
|
666
|
-
2. **Create a feature branch**: `git checkout -b feature/my-feature`
|
|
667
|
-
3. **Follow code style**:
|
|
668
|
-
- TypeScript strict mode
|
|
669
|
-
- ESLint/Prettier for formatting
|
|
670
|
-
- Descriptive variable names
|
|
671
|
-
4. **Test thoroughly**:
|
|
672
|
-
- Manual testing in dev mode
|
|
673
|
-
- TypeScript type checking (`yarn typecheck`)
|
|
674
|
-
5. **Commit with descriptive messages**:
|
|
675
|
-
```
|
|
676
|
-
feat: Add webcam eye tracking support
|
|
677
|
-
fix: Correct head yaw direction in mouse mode
|
|
678
|
-
docs: Update README with API reference
|
|
679
|
-
```
|
|
680
|
-
6. **Push and create PR** to `main` branch
|
|
753
|
+
```typescript
|
|
754
|
+
// When done, dispose of resources
|
|
755
|
+
loom.dispose();
|
|
756
|
+
```
|
|
681
757
|
|
|
682
758
|
---
|
|
683
759
|
|
|
684
|
-
##
|
|
760
|
+
## 12. Hair Physics
|
|
685
761
|
|
|
686
|
-
|
|
687
|
-
Licensed under the **Loom Large, Latticework copyleft license**
|
|
762
|
+
LoomLarge includes an experimental hair physics system that simulates hair movement based on head motion.
|
|
688
763
|
|
|
689
|
-
###
|
|
764
|
+
### Basic setup
|
|
690
765
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
- **React** – UI framework
|
|
694
|
-
- **Vite** – Lightning-fast bundler
|
|
695
|
-
- **ARKit** – Facial Action Coding System specification
|
|
696
|
-
- **BlazeFace** – Webcam face detection model
|
|
766
|
+
```typescript
|
|
767
|
+
import { HairPhysics } from 'loomlarge';
|
|
697
768
|
|
|
698
|
-
|
|
769
|
+
const hair = new HairPhysics();
|
|
770
|
+
```
|
|
699
771
|
|
|
700
|
-
|
|
701
|
-
- **eEVA Workbench** – Original survey/conversation platform
|
|
702
|
-
- **Latticework** – Core agency framework
|
|
772
|
+
### Updating in animation loop
|
|
703
773
|
|
|
704
|
-
|
|
774
|
+
```typescript
|
|
775
|
+
function animate() {
|
|
776
|
+
// Get current head state (from your tracking system or AU values)
|
|
777
|
+
const headState = {
|
|
778
|
+
yaw: 0, // Head rotation in radians
|
|
779
|
+
pitch: 0,
|
|
780
|
+
roll: 0,
|
|
781
|
+
yawVelocity: 0.5, // Angular velocity
|
|
782
|
+
pitchVelocity: 0,
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// Update hair physics
|
|
786
|
+
const hairMorphs = hair.update(deltaTime, headState);
|
|
787
|
+
|
|
788
|
+
// Apply hair morphs
|
|
789
|
+
for (const [morphName, value] of Object.entries(hairMorphs)) {
|
|
790
|
+
loom.setMorph(morphName, value);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### Output morphs
|
|
796
|
+
|
|
797
|
+
The physics system outputs 6 morph values:
|
|
705
798
|
|
|
706
|
-
|
|
799
|
+
| Morph | Description |
|
|
800
|
+
|-------|-------------|
|
|
801
|
+
| L_Hair_Left | Left side, swing left |
|
|
802
|
+
| L_Hair_Right | Left side, swing right |
|
|
803
|
+
| L_Hair_Front | Left side, swing forward |
|
|
804
|
+
| R_Hair_Left | Right side, swing left |
|
|
805
|
+
| R_Hair_Right | Right side, swing right |
|
|
806
|
+
| R_Hair_Front | Right side, swing forward |
|
|
707
807
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
808
|
+
### Physics forces
|
|
809
|
+
|
|
810
|
+
The simulation models 5 forces:
|
|
811
|
+
|
|
812
|
+
1. **Spring restoration** - Pulls hair back to rest position
|
|
813
|
+
2. **Damping** - Air resistance prevents infinite oscillation
|
|
814
|
+
3. **Gravity** - Hair swings based on head tilt
|
|
815
|
+
4. **Inertia** - Hair lags behind head movement
|
|
816
|
+
5. **Wind** - Optional oscillating wind force
|
|
817
|
+
|
|
818
|
+
### Configuration
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
const hair = new HairPhysics({
|
|
822
|
+
mass: 1.0,
|
|
823
|
+
stiffness: 50,
|
|
824
|
+
damping: 5,
|
|
825
|
+
gravity: 9.8,
|
|
826
|
+
headInfluence: 0.8, // How much head movement affects hair
|
|
827
|
+
wind: {
|
|
828
|
+
strength: 0,
|
|
829
|
+
direction: { x: 1, y: 0, z: 0 },
|
|
830
|
+
turbulence: 0.2,
|
|
831
|
+
frequency: 1.0,
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
```
|
|
711
835
|
|
|
712
836
|
---
|
|
713
837
|
|
|
714
|
-
|
|
838
|
+
## Resources
|
|
839
|
+
|
|
840
|
+
- [FACS on Wikipedia](https://en.wikipedia.org/wiki/Facial_Action_Coding_System)
|
|
841
|
+
- [Paul Ekman Group - FACS](https://www.paulekman.com/facial-action-coding-system/)
|
|
842
|
+
- [Character Creator 4](https://www.reallusion.com/character-creator/)
|
|
843
|
+
- [Three.js Documentation](https://threejs.org/docs/)
|
|
844
|
+
|
|
845
|
+
## License
|
|
846
|
+
|
|
847
|
+
MIT License - see LICENSE file for details.
|