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