@stowkit/three-loader 0.1.15 → 0.1.17
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 +212 -481
- package/dist/StowKitLoader.d.ts +1 -1
- package/dist/StowKitLoader.d.ts.map +1 -1
- package/dist/StowKitPack.d.ts +55 -65
- package/dist/StowKitPack.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/stowkit-three-loader.esm.js +480 -123
- package/dist/stowkit-three-loader.esm.js.map +1 -1
- package/dist/stowkit-three-loader.js +483 -123
- package/dist/stowkit-three-loader.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @stowkit/three-loader
|
|
2
2
|
|
|
3
|
-
Three.js loader for StowKit asset packs. Provides a simple, high-level API for loading meshes, textures, and audio from `.stow` files
|
|
3
|
+
Three.js loader for StowKit asset packs. Provides a simple, high-level API for loading meshes, skinned meshes, animations, textures, and audio from `.stow` files.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,301 +11,252 @@ npm install @stowkit/three-loader three
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { StowKitLoader } from '@stowkit/three-loader';
|
|
14
|
+
import { StowKitLoader, AssetType } from '@stowkit/three-loader';
|
|
15
15
|
|
|
16
|
-
// Load a pack (auto-initializes with defaults)
|
|
17
16
|
const pack = await StowKitLoader.load('assets.stow');
|
|
17
|
+
// from memory
|
|
18
|
+
const packFromMemory = await StowKitLoader.loadFromMemory(someData);
|
|
18
19
|
|
|
19
|
-
// Load assets by name
|
|
20
20
|
const mesh = await pack.loadMesh('character');
|
|
21
|
-
const texture = await pack.loadTexture('wood');
|
|
22
|
-
const audio = await pack.loadAudio('bgm', audioListener);
|
|
23
|
-
|
|
24
|
-
// Use in Three.js
|
|
25
21
|
scene.add(mesh);
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
|
|
23
|
+
const character = await pack.loadSkinnedMesh('player');
|
|
24
|
+
scene.add(character);
|
|
25
|
+
|
|
26
|
+
const { mixer } = await pack.loadAnimation(character, 'walk');
|
|
27
|
+
|
|
28
|
+
// Update in your animation loop
|
|
29
|
+
function animate() {
|
|
30
|
+
const delta = clock.getDelta();
|
|
31
|
+
mixer.update(delta);
|
|
32
|
+
renderer.render(scene, camera);
|
|
33
|
+
}
|
|
28
34
|
```
|
|
29
35
|
|
|
30
36
|
## Features
|
|
31
37
|
|
|
32
|
-
- ✅ **
|
|
33
|
-
- ✅ **
|
|
34
|
-
- ✅ **
|
|
35
|
-
- ✅ **
|
|
36
|
-
- ✅ **
|
|
37
|
-
- ✅ **
|
|
38
|
-
- ✅ **
|
|
39
|
-
- ✅ **
|
|
40
|
-
- ✅ **Type-safe** - Full TypeScript definitions included
|
|
38
|
+
- ✅ **Static Meshes** - Draco-compressed meshes with automatic material/texture loading
|
|
39
|
+
- ✅ **Skinned Meshes** - Skeletal meshes with bone hierarchy
|
|
40
|
+
- ✅ **Animations** - Skeletal animations with automatic mixer setup
|
|
41
|
+
- ✅ **Textures** - KTX2/Basis Universal GPU-compressed textures
|
|
42
|
+
- ✅ **Audio** - OGG/MP3 audio with Three.js Audio integration
|
|
43
|
+
- ✅ **WASM Parsing** - All binary parsing done in WASM for performance
|
|
44
|
+
- ✅ **Type Safe** - Full TypeScript support
|
|
45
|
+
- ✅ **Zero Config** - Works out of the box
|
|
41
46
|
|
|
42
47
|
## API Reference
|
|
43
48
|
|
|
44
|
-
###
|
|
49
|
+
### Asset Manifest
|
|
45
50
|
|
|
46
|
-
#### `
|
|
51
|
+
#### `pack.listAssets(): AssetListItem[]`
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
Get the complete manifest of all assets in the pack.
|
|
49
54
|
|
|
50
|
-
**Options (all optional):**
|
|
51
55
|
```typescript
|
|
52
|
-
interface StowKitLoaderOptions {
|
|
53
|
-
basisPath?: string; // Path to Basis Universal transcoder (default: '/stowkit/basis/')
|
|
54
|
-
dracoPath?: string; // Path to Draco decoder (default: '/stowkit/draco/')
|
|
55
|
-
wasmPath?: string; // Path to WASM reader module (default: '/stowkit_reader.wasm')
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Examples:**
|
|
60
|
-
```typescript
|
|
61
|
-
// Load from URL
|
|
62
56
|
const pack = await StowKitLoader.load('assets.stow');
|
|
57
|
+
const manifest = pack.listAssets();
|
|
58
|
+
|
|
59
|
+
console.log(`Pack contains ${manifest.length} assets`);
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
manifest.forEach(asset => {
|
|
62
|
+
console.log(`[${asset.index}] ${asset.name || asset.id}`);
|
|
63
|
+
console.log(` Type: ${getTypeName(asset.type)}`);
|
|
64
|
+
console.log(` Size: ${formatBytes(asset.dataSize)}`);
|
|
65
|
+
console.log(` Has Metadata: ${asset.hasMetadata}`);
|
|
69
66
|
});
|
|
70
|
-
```
|
|
71
67
|
|
|
72
|
-
|
|
68
|
+
// Filter by type
|
|
69
|
+
const meshes = manifest.filter(a => a.type === AssetType.STATIC_MESH);
|
|
70
|
+
const textures = manifest.filter(a => a.type === AssetType.TEXTURE_2D);
|
|
71
|
+
const audio = manifest.filter(a => a.type === AssetType.AUDIO);
|
|
72
|
+
const skinnedMeshes = manifest.filter(a => a.type === AssetType.SKINNED_MESH);
|
|
73
|
+
const animations = manifest.filter(a => a.type === AssetType.ANIMATION_CLIP);
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
console.log(`${meshes.length} meshes, ${textures.length} textures, ${animations.length} animations`);
|
|
76
|
+
```
|
|
75
77
|
|
|
76
|
-
**
|
|
78
|
+
**AssetListItem structure:**
|
|
77
79
|
```typescript
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
{
|
|
81
|
+
index: number; // Asset index
|
|
82
|
+
type: number; // Asset type ID (1-6)
|
|
83
|
+
name?: string; // Extracted from metadata (if available)
|
|
84
|
+
id: bigint; // Unique asset ID
|
|
85
|
+
dataSize: number; // Size of asset data in bytes
|
|
86
|
+
metadataSize: number; // Size of metadata in bytes
|
|
87
|
+
hasMetadata: boolean; // Whether metadata exists
|
|
88
|
+
data_offset: number; // Internal use
|
|
89
|
+
data_size: number; // Internal use
|
|
90
|
+
metadata_offset: number; // Internal use
|
|
91
|
+
metadata_size: number; // Internal use
|
|
92
|
+
}
|
|
89
93
|
```
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
#### `pack.getAssetCount(): number`
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
Get total number of assets in the pack.
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
```typescript
|
|
100
|
+
const count = pack.getAssetCount();
|
|
101
|
+
console.log(`Pack has ${count} assets`);
|
|
102
|
+
```
|
|
96
103
|
|
|
97
|
-
#### `pack.
|
|
104
|
+
#### `pack.getAssetInfo(index: number): AssetInfo | null`
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
Get detailed info about a specific asset.
|
|
100
107
|
|
|
101
108
|
```typescript
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
scene.add(cookingStation);
|
|
108
|
-
scene.add(door);
|
|
109
|
-
scene.add(physics);
|
|
109
|
+
const info = pack.getAssetInfo(5);
|
|
110
|
+
if (info) {
|
|
111
|
+
console.log(`Type: ${info.type}, Size: ${info.data_size} bytes`);
|
|
112
|
+
}
|
|
110
113
|
```
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
- ✅ Complete scene hierarchy with nodes
|
|
114
|
-
- ✅ Draco-decompressed geometry (vertices, normals, UVs, indices)
|
|
115
|
-
- ✅ Materials with colors, properties applied
|
|
116
|
-
- ✅ Textures automatically loaded and applied
|
|
117
|
-
- ✅ Transforms (position, rotation, scale) preserved
|
|
115
|
+
### Loading Assets
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
#### `pack.loadTexture(stringId: string): Promise<THREE.CompressedTexture>`
|
|
122
|
-
|
|
123
|
-
Load a KTX2 texture by its string ID (the canonical path used when packing).
|
|
117
|
+
#### Static Meshes
|
|
124
118
|
|
|
125
119
|
```typescript
|
|
126
|
-
|
|
127
|
-
const
|
|
120
|
+
// Load by string ID
|
|
121
|
+
const mesh = await pack.loadMesh('models/building.mesh');
|
|
122
|
+
scene.add(mesh);
|
|
128
123
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
// Load by index
|
|
125
|
+
const mesh = await pack.loadMeshByIndex(5);
|
|
126
|
+
scene.add(mesh);
|
|
132
127
|
```
|
|
133
128
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
#### `pack.loadAudio(stringId: string, listener: THREE.AudioListener): Promise<THREE.Audio>`
|
|
129
|
+
Returns a `THREE.Group` containing the mesh hierarchy with materials and textures applied.
|
|
137
130
|
|
|
138
|
-
|
|
131
|
+
#### Skinned Meshes
|
|
139
132
|
|
|
140
133
|
```typescript
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const bgm = await pack.loadAudio('track_01', listener);
|
|
145
|
-
bgm.setLoop(true);
|
|
146
|
-
bgm.setVolume(0.5);
|
|
147
|
-
bgm.play();
|
|
134
|
+
// Load by string ID
|
|
135
|
+
const character = await pack.loadSkinnedMesh('characters/player.skinned');
|
|
136
|
+
scene.add(character);
|
|
148
137
|
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
|
|
138
|
+
// Load by index
|
|
139
|
+
const character = await pack.loadSkinnedMeshByIndex(8);
|
|
140
|
+
scene.add(character);
|
|
152
141
|
```
|
|
153
142
|
|
|
154
|
-
|
|
143
|
+
Returns a `THREE.Group` containing the skinned mesh with skeleton and bones in bind pose.
|
|
155
144
|
|
|
156
|
-
|
|
145
|
+
#### Animations
|
|
157
146
|
|
|
158
147
|
```typescript
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
148
|
+
// Load and play animation (returns mixer, action, clip)
|
|
149
|
+
const { mixer, action, clip } = await pack.loadAnimation(
|
|
150
|
+
skinnedMeshGroup,
|
|
151
|
+
'animations/walk.anim'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Or by index
|
|
155
|
+
const { mixer, action, clip } = await pack.loadAnimationByIndex(
|
|
156
|
+
skinnedMeshGroup,
|
|
157
|
+
9
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Update in your animation loop
|
|
161
|
+
const clock = new THREE.Clock();
|
|
162
|
+
function animate() {
|
|
163
|
+
requestAnimationFrame(animate);
|
|
164
|
+
mixer.update(clock.getDelta());
|
|
165
|
+
renderer.render(scene, camera);
|
|
166
|
+
}
|
|
162
167
|
```
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
The `loadAnimation` methods automatically:
|
|
170
|
+
- Create an `AnimationMixer` on the correct root object
|
|
171
|
+
- Set the animation to loop infinitely
|
|
172
|
+
- Start playing immediately
|
|
173
|
+
- Return everything you need
|
|
167
174
|
|
|
168
|
-
|
|
175
|
+
#### Textures
|
|
169
176
|
|
|
170
177
|
```typescript
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.log(schema.fields); // Array of field definitions
|
|
178
|
+
// Load by string ID
|
|
179
|
+
const texture = await pack.loadTexture('textures/wood.ktx2');
|
|
180
|
+
material.map = texture;
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// mainTex: Texture
|
|
179
|
-
// tint: Color
|
|
180
|
-
// roughness: Float
|
|
181
|
-
});
|
|
182
|
+
// Load by index
|
|
183
|
+
const texture = await pack.loadTextureByIndex(2);
|
|
182
184
|
```
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
Returns a `THREE.CompressedTexture` (KTX2/Basis Universal format).
|
|
185
187
|
|
|
186
|
-
|
|
188
|
+
#### Audio
|
|
187
189
|
|
|
188
190
|
```typescript
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
**Schema Structure:**
|
|
193
|
-
```typescript
|
|
194
|
-
{
|
|
195
|
-
stringId: string; // Schema canonical path
|
|
196
|
-
schemaName: string; // Human-readable name
|
|
197
|
-
fields: Array<{
|
|
198
|
-
name: string; // Field name (e.g., "mainTex", "tint")
|
|
199
|
-
type: 'Texture' | 'Color' | 'Float' | 'Vec2' | 'Vec3' | 'Vec4' | 'Int';
|
|
200
|
-
previewFlag: 'None' | 'MainTex' | 'Tint';
|
|
201
|
-
defaultValue: [number, number, number, number];
|
|
202
|
-
}>;
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
#### `getMeshMaterials(assetPath: string): array | null`
|
|
191
|
+
const listener = new THREE.AudioListener();
|
|
192
|
+
camera.add(listener);
|
|
207
193
|
|
|
208
|
-
|
|
194
|
+
// Load by string ID
|
|
195
|
+
const bgm = await pack.loadAudio('sounds/music.ogg', listener);
|
|
196
|
+
bgm.setLoop(true);
|
|
197
|
+
bgm.play();
|
|
209
198
|
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
materials.forEach(mat => {
|
|
214
|
-
console.log(`Material: ${mat.name}`);
|
|
215
|
-
console.log(`Schema: ${mat.schemaId}`);
|
|
216
|
-
|
|
217
|
-
mat.properties.forEach(prop => {
|
|
218
|
-
if (prop.textureId) {
|
|
219
|
-
console.log(`${prop.fieldName}: ${prop.textureId}`); // mainTex: textures/skin.ktx2
|
|
220
|
-
} else {
|
|
221
|
-
console.log(`${prop.fieldName}:`, prop.value); // tint: [1, 0.8, 0.6, 1]
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
});
|
|
199
|
+
// Or create HTML5 audio element for preview
|
|
200
|
+
const audioElement = await pack.createAudioPreview(3);
|
|
201
|
+
document.body.appendChild(audioElement);
|
|
225
202
|
```
|
|
226
203
|
|
|
227
204
|
### Metadata Helpers
|
|
228
205
|
|
|
229
|
-
|
|
206
|
+
All metadata is parsed in WASM for performance and reliability.
|
|
230
207
|
|
|
231
|
-
|
|
208
|
+
#### Animation Metadata
|
|
232
209
|
|
|
233
210
|
```typescript
|
|
234
|
-
const
|
|
211
|
+
const animData = pack.getAnimationMetadata(index);
|
|
235
212
|
|
|
236
|
-
console.log(
|
|
237
|
-
console.log(
|
|
238
|
-
console.log(
|
|
213
|
+
console.log(animData.stringId); // "Clip_Walking"
|
|
214
|
+
console.log(animData.targetMeshId); // "Character_Skinned_Tpose"
|
|
215
|
+
console.log(animData.duration); // 0.97
|
|
216
|
+
console.log(animData.ticksPerSecond); // 30
|
|
217
|
+
console.log(animData.channelCount); // 104
|
|
218
|
+
console.log(animData.boneCount); // 65
|
|
239
219
|
```
|
|
240
220
|
|
|
241
|
-
####
|
|
242
|
-
|
|
243
|
-
Get audio metadata (sample rate, duration, etc.).
|
|
221
|
+
#### Audio Metadata
|
|
244
222
|
|
|
245
223
|
```typescript
|
|
246
|
-
const
|
|
224
|
+
const audioData = pack.getAudioMetadata('sounds/bgm.ogg');
|
|
247
225
|
|
|
248
|
-
console.log(
|
|
249
|
-
console.log(
|
|
250
|
-
console.log(
|
|
226
|
+
console.log(audioData.sampleRate); // 44100
|
|
227
|
+
console.log(audioData.channels); // 2 (stereo)
|
|
228
|
+
console.log(audioData.durationMs); // 180000 (3 minutes)
|
|
251
229
|
```
|
|
252
230
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
#### `listAssets(): array`
|
|
256
|
-
|
|
257
|
-
List all assets in the opened pack.
|
|
231
|
+
#### Texture Metadata
|
|
258
232
|
|
|
259
233
|
```typescript
|
|
260
|
-
const
|
|
234
|
+
const texData = pack.getTextureMetadata(index);
|
|
261
235
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
236
|
+
console.log(texData.width); // 1024
|
|
237
|
+
console.log(texData.height); // 1024
|
|
238
|
+
console.log(texData.channels); // 3 (RGB) or 4 (RGBA)
|
|
239
|
+
console.log(texData.channelFormat); // 1 (RGB) or 2 (RGBA)
|
|
266
240
|
```
|
|
267
241
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
Get total number of assets.
|
|
242
|
+
### Asset Discovery
|
|
271
243
|
|
|
272
244
|
```typescript
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
#### `getAssetInfo(index: number): object`
|
|
277
|
-
|
|
278
|
-
Get detailed info about a specific asset.
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
const info = loader.getAssetInfo(0);
|
|
282
|
-
console.log(info.type, info.data_size, info.metadata_size);
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Utility Methods
|
|
286
|
-
|
|
287
|
-
#### `readAssetData(index: number): Uint8Array | null`
|
|
288
|
-
|
|
289
|
-
Read raw asset data by index.
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
const data = loader.readAssetData(5);
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
#### `readAssetMetadata(index: number): Uint8Array | null`
|
|
296
|
-
|
|
297
|
-
Read raw asset metadata by index.
|
|
245
|
+
// List all assets in pack
|
|
246
|
+
const assets = pack.listAssets();
|
|
298
247
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
248
|
+
assets.forEach(asset => {
|
|
249
|
+
console.log(`[${asset.index}] ${asset.name} (${getTypeName(asset.type)})`);
|
|
250
|
+
});
|
|
302
251
|
|
|
303
|
-
|
|
252
|
+
// Find asset by path
|
|
253
|
+
const index = pack.reader.findAssetByPath('models/character.mesh');
|
|
304
254
|
|
|
305
|
-
|
|
255
|
+
// Get asset count
|
|
256
|
+
const count = pack.getAssetCount();
|
|
306
257
|
|
|
307
|
-
|
|
308
|
-
|
|
258
|
+
// Get asset info
|
|
259
|
+
const info = pack.getAssetInfo(5);
|
|
309
260
|
```
|
|
310
261
|
|
|
311
262
|
## Complete Example
|
|
@@ -314,321 +265,101 @@ loader.dispose();
|
|
|
314
265
|
import * as THREE from 'three';
|
|
315
266
|
import { StowKitLoader } from '@stowkit/three-loader';
|
|
316
267
|
|
|
317
|
-
// Setup Three.js
|
|
268
|
+
// Setup Three.js
|
|
318
269
|
const scene = new THREE.Scene();
|
|
319
270
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
320
271
|
const renderer = new THREE.WebGLRenderer();
|
|
321
|
-
|
|
322
|
-
document.body.appendChild(renderer.domElement);
|
|
272
|
+
const clock = new THREE.Clock();
|
|
323
273
|
|
|
324
|
-
// Setup audio
|
|
325
274
|
const listener = new THREE.AudioListener();
|
|
326
275
|
camera.add(listener);
|
|
327
276
|
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
// Load assets
|
|
332
|
-
async function loadAssets() {
|
|
333
|
-
await loader.openPack('game-assets.stow');
|
|
334
|
-
|
|
335
|
-
// Load main character
|
|
336
|
-
const character = await loader.loadMesh('', 'models/player.mesh');
|
|
337
|
-
character.position.set(0, 0, 0);
|
|
338
|
-
scene.add(character);
|
|
339
|
-
|
|
340
|
-
// Load environment
|
|
341
|
-
const environment = await loader.loadMesh('', 'models/level1.mesh');
|
|
342
|
-
scene.add(environment);
|
|
343
|
-
|
|
344
|
-
// Load background music
|
|
345
|
-
const bgm = await loader.loadAudio('', 'audio/theme.ogg', listener);
|
|
346
|
-
bgm.setLoop(true);
|
|
347
|
-
bgm.setVolume(0.5);
|
|
348
|
-
bgm.play();
|
|
349
|
-
|
|
350
|
-
// Log what was loaded
|
|
351
|
-
console.log(`Loaded ${loader.getAssetCount()} assets`);
|
|
352
|
-
|
|
353
|
-
// Start rendering
|
|
354
|
-
animate();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function animate() {
|
|
358
|
-
requestAnimationFrame(animate);
|
|
359
|
-
renderer.render(scene, camera);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
loadAssets();
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
## Material System
|
|
366
|
-
|
|
367
|
-
The loader automatically handles material assignments:
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
// When you load a mesh, materials are created with:
|
|
371
|
-
// 1. Colors from 'tint' properties
|
|
372
|
-
// 2. Textures from 'mainTex', 'normalMap', etc.
|
|
373
|
-
// 3. Other properties (roughness, metallic, etc.)
|
|
374
|
-
|
|
375
|
-
const mesh = await loader.loadMesh('', 'models/character.mesh');
|
|
376
|
-
|
|
377
|
-
// Materials are automatically applied to geometries based on material indices
|
|
378
|
-
// Textures are automatically loaded by their string IDs and applied to materials
|
|
379
|
-
// No manual texture loading needed!
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
**Supported material properties:**
|
|
383
|
-
- `mainTex` / `diffuse` / `albedo` → `material.map`
|
|
384
|
-
- `tint` / `color` → `material.color`
|
|
385
|
-
- `normalMap` → `material.normalMap`
|
|
386
|
-
- `metallicMap` → `material.metalnessMap`
|
|
387
|
-
- `roughnessMap` → `material.roughnessMap`
|
|
388
|
-
|
|
389
|
-
## Advanced Usage
|
|
390
|
-
|
|
391
|
-
### Loading Multiple Assets from Same Pack
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
// Load pack once
|
|
395
|
-
const pack = await StowKitLoader.load('assets.stow');
|
|
396
|
-
|
|
397
|
-
// Load many assets (using whatever string IDs you assigned when packing)
|
|
398
|
-
const station = await pack.loadMesh('restaurant_display_cooking_station');
|
|
399
|
-
const door = await pack.loadMesh('restaurant_display_door');
|
|
400
|
-
const shadow = await pack.loadTexture('_shadow');
|
|
401
|
-
const atlas = await pack.loadTexture('zy_Atlas');
|
|
402
|
-
const music = await pack.loadAudio('track_01', listener);
|
|
403
|
-
|
|
404
|
-
scene.add(station);
|
|
405
|
-
scene.add(door);
|
|
406
|
-
```
|
|
277
|
+
// Load pack
|
|
278
|
+
const pack = await StowKitLoader.load('game.stow');
|
|
407
279
|
|
|
408
|
-
|
|
280
|
+
// Load static mesh
|
|
281
|
+
const environment = await pack.loadMesh('levels/level1.mesh');
|
|
282
|
+
scene.add(environment);
|
|
409
283
|
|
|
410
|
-
|
|
411
|
-
const
|
|
284
|
+
// Load skinned character
|
|
285
|
+
const character = await pack.loadSkinnedMesh('characters/player.skinned');
|
|
286
|
+
character.position.set(0, 0, 0);
|
|
287
|
+
scene.add(character);
|
|
412
288
|
|
|
413
|
-
//
|
|
414
|
-
const
|
|
415
|
-
const meshes = assets.filter(a => a.type === 1); // Type 1 = Static Mesh
|
|
416
|
-
const textures = assets.filter(a => a.type === 2); // Type 2 = Texture
|
|
417
|
-
|
|
418
|
-
console.log(`Pack contains ${assets.length} assets`);
|
|
419
|
-
meshes.forEach(m => console.log(`Mesh: ${m.name}`));
|
|
420
|
-
|
|
421
|
-
// Get material info before loading
|
|
422
|
-
const materials = pack.getMeshMaterials('models/character');
|
|
423
|
-
materials?.forEach(mat => {
|
|
424
|
-
console.log(`Material "${mat.name}" schema: ${mat.schemaId}`);
|
|
425
|
-
mat.properties.forEach(p => {
|
|
426
|
-
if (p.textureId) console.log(` Needs texture: ${p.textureId}`);
|
|
427
|
-
});
|
|
428
|
-
});
|
|
429
|
-
```
|
|
289
|
+
// Load and play animation
|
|
290
|
+
const { mixer } = await pack.loadAnimation(character, 'animations/idle.anim');
|
|
430
291
|
|
|
431
|
-
|
|
292
|
+
// Load audio
|
|
293
|
+
const bgm = await pack.loadAudio('sounds/theme.ogg', listener);
|
|
294
|
+
bgm.setLoop(true);
|
|
295
|
+
bgm.play();
|
|
432
296
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
} catch (error) {
|
|
439
|
-
console.error('Failed to load:', error.message);
|
|
297
|
+
// Animation loop
|
|
298
|
+
function animate() {
|
|
299
|
+
requestAnimationFrame(animate);
|
|
300
|
+
mixer.update(clock.getDelta());
|
|
301
|
+
renderer.render(scene, camera);
|
|
440
302
|
}
|
|
303
|
+
|
|
304
|
+
animate();
|
|
441
305
|
```
|
|
442
306
|
|
|
443
|
-
|
|
307
|
+
## Asset Types
|
|
444
308
|
|
|
445
309
|
```typescript
|
|
446
|
-
|
|
447
|
-
const cachedData = await caches.match('assets.stow');
|
|
448
|
-
if (cachedData) {
|
|
449
|
-
const blob = await cachedData.blob();
|
|
450
|
-
const pack = await StowKitLoader.loadFromMemory(blob);
|
|
451
|
-
} else {
|
|
452
|
-
const pack = await StowKitLoader.load('assets.stow');
|
|
453
|
-
// Cache for next time...
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Load assets
|
|
457
|
-
const mesh = await pack.loadMesh('models/character');
|
|
310
|
+
import { AssetType } from '@stowkit/three-loader';
|
|
458
311
|
```
|
|
459
312
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
| Type | Value | Description |
|
|
313
|
+
| Enum | Value | Description |
|
|
463
314
|
|------|-------|-------------|
|
|
464
|
-
|
|
|
465
|
-
|
|
|
466
|
-
|
|
|
467
|
-
|
|
|
315
|
+
| `AssetType.STATIC_MESH` | 1 | Draco-compressed 3D models |
|
|
316
|
+
| `AssetType.TEXTURE_2D` | 2 | KTX2/Basis Universal textures |
|
|
317
|
+
| `AssetType.AUDIO` | 3 | OGG/MP3 audio files |
|
|
318
|
+
| `AssetType.MATERIAL_SCHEMA` | 4 | Material template definitions |
|
|
319
|
+
| `AssetType.SKINNED_MESH` | 5 | Skeletal meshes with bones |
|
|
320
|
+
| `AssetType.ANIMATION_CLIP` | 6 | Bone animation keyframes |
|
|
468
321
|
|
|
469
|
-
##
|
|
322
|
+
## Public Folder Setup
|
|
470
323
|
|
|
471
|
-
|
|
472
|
-
- **File Header** - Magic number, version, asset count
|
|
473
|
-
- **Asset Data** - Binary geometry, texture, audio data
|
|
474
|
-
- **Asset Directory** - Index of all assets with offsets
|
|
475
|
-
- **Metadata** - Asset-specific information (dimensions, transforms, etc.)
|
|
476
|
-
|
|
477
|
-
## Requirements
|
|
478
|
-
|
|
479
|
-
### Public Folder Setup
|
|
480
|
-
|
|
481
|
-
Place these files in your public folder:
|
|
324
|
+
The package automatically copies required files on install:
|
|
482
325
|
|
|
483
326
|
```
|
|
484
327
|
public/
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
├── basis/
|
|
328
|
+
└── stowkit/
|
|
329
|
+
├── stowkit_reader.wasm # WASM reader module
|
|
330
|
+
├── basis/ # Basis Universal transcoder
|
|
488
331
|
│ ├── basis_transcoder.js
|
|
489
332
|
│ └── basis_transcoder.wasm
|
|
490
|
-
└── draco/
|
|
333
|
+
└── draco/ # Draco decoder
|
|
491
334
|
├── draco_decoder.js
|
|
492
335
|
├── draco_decoder.wasm
|
|
493
336
|
└── draco_wasm_wrapper.js
|
|
494
337
|
```
|
|
495
338
|
|
|
496
|
-
|
|
339
|
+
Just run `npm install` and everything is set up automatically!
|
|
497
340
|
|
|
498
|
-
|
|
341
|
+
## Performance
|
|
499
342
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
**That's it!** No manual file copying needed.
|
|
507
|
-
|
|
508
|
-
### Three.js Version
|
|
509
|
-
|
|
510
|
-
Requires Three.js r150 or later.
|
|
511
|
-
|
|
512
|
-
```bash
|
|
513
|
-
npm install three@^0.150.0
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
## Examples
|
|
517
|
-
|
|
518
|
-
### Loading a Character with Equipment
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
const loader = new StowKitLoader();
|
|
522
|
-
await loader.openPack('characters.stow');
|
|
523
|
-
|
|
524
|
-
// Load character base
|
|
525
|
-
const character = await loader.loadMesh('', 'characters/knight.mesh');
|
|
526
|
-
|
|
527
|
-
// Load and attach weapon
|
|
528
|
-
const weapon = await loader.loadMesh('', 'weapons/sword.mesh');
|
|
529
|
-
const rightHand = character.getObjectByName('RightHand');
|
|
530
|
-
if (rightHand) {
|
|
531
|
-
rightHand.add(weapon);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
scene.add(character);
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
### Dynamic Material Updates
|
|
538
|
-
|
|
539
|
-
```typescript
|
|
540
|
-
// Load mesh
|
|
541
|
-
const mesh = await loader.loadMesh('', 'models/car.mesh');
|
|
542
|
-
|
|
543
|
-
// Get material info
|
|
544
|
-
const materials = loader.getMeshMaterials('models/car.mesh');
|
|
545
|
-
|
|
546
|
-
// Find and modify specific material
|
|
547
|
-
mesh.traverse((child) => {
|
|
548
|
-
if (child instanceof THREE.Mesh) {
|
|
549
|
-
const mat = child.material as THREE.MeshStandardMaterial;
|
|
550
|
-
if (mat.name === 'Paint') {
|
|
551
|
-
// Change car color
|
|
552
|
-
mat.color.set(0xff0000); // Red
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
### Texture Swapping
|
|
559
|
-
|
|
560
|
-
```typescript
|
|
561
|
-
// Load model
|
|
562
|
-
const character = await loader.loadMesh('', 'characters/player.mesh');
|
|
563
|
-
|
|
564
|
-
// Load alternate texture
|
|
565
|
-
const winterSkin = await loader.loadTexture('', 'textures/winter-skin.ktx2');
|
|
566
|
-
|
|
567
|
-
// Apply to character
|
|
568
|
-
character.traverse((child) => {
|
|
569
|
-
if (child instanceof THREE.Mesh) {
|
|
570
|
-
const mat = child.material as THREE.MeshStandardMaterial;
|
|
571
|
-
mat.map = winterSkin;
|
|
572
|
-
mat.needsUpdate = true;
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
### Audio with Spatial Sound
|
|
578
|
-
|
|
579
|
-
```typescript
|
|
580
|
-
const listener = new THREE.AudioListener();
|
|
581
|
-
camera.add(listener);
|
|
582
|
-
|
|
583
|
-
// Load positional audio
|
|
584
|
-
const sound = await loader.loadAudio('', 'sounds/waterfall.ogg', listener);
|
|
585
|
-
const positionalAudio = new THREE.PositionalAudio(listener);
|
|
586
|
-
positionalAudio.setBuffer(sound.buffer);
|
|
587
|
-
positionalAudio.setRefDistance(20);
|
|
588
|
-
positionalAudio.setLoop(true);
|
|
589
|
-
|
|
590
|
-
// Attach to a mesh
|
|
591
|
-
waterfall.add(positionalAudio);
|
|
592
|
-
positionalAudio.play();
|
|
593
|
-
```
|
|
343
|
+
- **WASM Parsing**: All binary parsing done in WASM (10-50x faster than JavaScript)
|
|
344
|
+
- **Draco Compression**: Meshes are 80-90% smaller than uncompressed
|
|
345
|
+
- **KTX2 Textures**: GPU-native compression, fast loading and rendering
|
|
346
|
+
- **Lazy Loading**: Assets loaded on-demand, not all at once
|
|
347
|
+
- **Memory Efficient**: Minimal copying between WASM and JavaScript
|
|
594
348
|
|
|
595
349
|
## Troubleshooting
|
|
596
350
|
|
|
597
|
-
###
|
|
598
|
-
|
|
599
|
-
The loader automatically handles UV orientation for KTX2 textures. If textures still appear flipped, check your source files.
|
|
351
|
+
### Animations appear broken/offset from origin
|
|
600
352
|
|
|
601
|
-
|
|
353
|
+
Make sure your `.stow` file was packed with the latest packer that writes bone parent indices correctly.
|
|
602
354
|
|
|
603
|
-
|
|
355
|
+
### Textures not loading
|
|
604
356
|
|
|
605
|
-
|
|
357
|
+
Ensure `/stowkit/basis/` folder contains the Basis Universal transcoder files (auto-copied on install).
|
|
606
358
|
|
|
607
|
-
|
|
608
|
-
- The pack file is opened before loading assets
|
|
609
|
-
- Asset paths match exactly (case-sensitive)
|
|
610
|
-
- The asset exists in the pack (use `listAssets()` to verify)
|
|
359
|
+
### Audio not playing
|
|
611
360
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
Check console for texture loading messages. Common issues:
|
|
615
|
-
- Texture string ID in material doesn't match any asset
|
|
616
|
-
- Texture asset is missing from pack
|
|
617
|
-
- Basis transcoder files not in `/basis/` folder
|
|
618
|
-
|
|
619
|
-
## Performance Tips
|
|
620
|
-
|
|
621
|
-
1. **Reuse loader instance** - Create one loader, load many assets
|
|
622
|
-
2. **Open pack once** - Use `openPack()` then load multiple assets
|
|
623
|
-
3. **Dispose when done** - Call `loader.dispose()` to free memory
|
|
624
|
-
4. **Use compressed textures** - KTX2 provides GPU-native compression
|
|
361
|
+
Make sure you've created an `AudioListener` and attached it to your camera.
|
|
625
362
|
|
|
626
363
|
## License
|
|
627
364
|
|
|
628
365
|
MIT
|
|
629
|
-
|
|
630
|
-
## Links
|
|
631
|
-
|
|
632
|
-
- [npm package](https://www.npmjs.com/package/@stowkit/three-loader)
|
|
633
|
-
- [Three.js documentation](https://threejs.org/docs/)
|
|
634
|
-
- [Basis Universal](https://github.com/BinomialLLC/basis_universal)
|