@stowkit/three-loader 0.1.6 → 0.1.7
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 +639 -639
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,640 +1,640 @@
|
|
|
1
|
-
# @stowkit/three-loader
|
|
2
|
-
|
|
3
|
-
Three.js loader for StowKit asset packs. Provides a simple, high-level API for loading meshes, textures, and audio from `.stow` files into Three.js applications.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @stowkit/three-loader three
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import * as THREE from 'three';
|
|
15
|
-
import { StowKitLoader } from '@stowkit/three-loader';
|
|
16
|
-
|
|
17
|
-
// Create loader (auto-configured with defaults)
|
|
18
|
-
// Basis transcoder: /stowkit/basis/ (auto-included)
|
|
19
|
-
// Draco decoder: /stowkit/draco/ (auto-included)
|
|
20
|
-
const loader = new StowKitLoader();
|
|
21
|
-
|
|
22
|
-
// Optional: Override paths only if you have custom locations
|
|
23
|
-
// loader.setTranscoderPath('/custom-basis-path/');
|
|
24
|
-
// loader.setDracoDecoderPath('/custom-draco-path/');
|
|
25
|
-
|
|
26
|
-
// Open a .stow pack file
|
|
27
|
-
await loader.openPack('assets.stow');
|
|
28
|
-
|
|
29
|
-
// Load a mesh with automatic textures and materials
|
|
30
|
-
const scene = await loader.loadMesh('', 'models/character.mesh');
|
|
31
|
-
threeScene.add(scene);
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Features
|
|
35
|
-
|
|
36
|
-
- ✅ **Zero configuration** - Works out of the box with sensible defaults
|
|
37
|
-
- ✅ **Draco mesh compression** - All meshes use Draco compression for smaller file sizes
|
|
38
|
-
- ✅ **Automatic texture loading** - Materials reference textures by ID, loader handles everything
|
|
39
|
-
- ✅ **Material system** - Full support for material schemas, properties, and assignments
|
|
40
|
-
- ✅ **KTX2/Basis Universal** - GPU-compressed textures with automatic transcoding
|
|
41
|
-
- ✅ **Scene hierarchy** - Preserves transforms, parent-child relationships, and node structure
|
|
42
|
-
- ✅ **Multi-material meshes** - Supports meshes with multiple materials/submeshes
|
|
43
|
-
- ✅ **Draco decoder included** - No need to download separately, auto-copied on install
|
|
44
|
-
- ✅ **Type-safe** - Full TypeScript definitions included
|
|
45
|
-
|
|
46
|
-
## API Reference
|
|
47
|
-
|
|
48
|
-
### Constructor
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
new StowKitLoader(manager?: THREE.LoadingManager, parameters?: StowKitLoaderParameters)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Parameters (all optional):**
|
|
55
|
-
```typescript
|
|
56
|
-
interface StowKitLoaderParameters {
|
|
57
|
-
transcoderPath?: string; // Path to Basis Universal transcoder (default: '/basis/')
|
|
58
|
-
wasmPath?: string; // Path to WASM reader module (default: '/stowkit_reader.wasm')
|
|
59
|
-
reader?: StowKitReader; // Custom reader instance (advanced)
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**Example:**
|
|
64
|
-
```typescript
|
|
65
|
-
// Default configuration
|
|
66
|
-
const loader = new StowKitLoader();
|
|
67
|
-
|
|
68
|
-
// Custom paths
|
|
69
|
-
const loader = new StowKitLoader(undefined, {
|
|
70
|
-
transcoderPath: '/my-basis-path/',
|
|
71
|
-
wasmPath: '/custom/stowkit_reader.wasm'
|
|
72
|
-
});
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Opening Packs
|
|
76
|
-
|
|
77
|
-
#### `openPack(url: string): Promise<void>`
|
|
78
|
-
|
|
79
|
-
Open a .stow pack file from a URL. Call this once, then load multiple assets.
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
await loader.openPack('assets.stow');
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
#### `open(fileOrBuffer: ArrayBuffer | File): Promise<boolean>`
|
|
86
|
-
|
|
87
|
-
Open a pack from a File or ArrayBuffer.
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
// From file input
|
|
91
|
-
const file = input.files[0];
|
|
92
|
-
await loader.open(file);
|
|
93
|
-
|
|
94
|
-
// From fetch
|
|
95
|
-
const response = await fetch('assets.stow');
|
|
96
|
-
const buffer = await response.arrayBuffer();
|
|
97
|
-
await loader.open(buffer);
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Loading Meshes
|
|
101
|
-
|
|
102
|
-
#### `loadMesh(url: string, assetPath: string, onLoad?, onProgress?, onError?): Promise<THREE.Group>`
|
|
103
|
-
|
|
104
|
-
Load a mesh asset with automatic texture and material loading.
|
|
105
|
-
|
|
106
|
-
**Parameters:**
|
|
107
|
-
- `url` - URL to .stow file (empty string if already opened)
|
|
108
|
-
- `assetPath` - Canonical path to mesh within pack
|
|
109
|
-
- `onLoad` - Optional callback when loading completes
|
|
110
|
-
- `onProgress` - Optional progress callback
|
|
111
|
-
- `onError` - Optional error callback
|
|
112
|
-
|
|
113
|
-
**Returns:** `THREE.Group` containing the mesh hierarchy
|
|
114
|
-
|
|
115
|
-
**Example:**
|
|
116
|
-
```typescript
|
|
117
|
-
// Simple async/await
|
|
118
|
-
const mesh = await loader.loadMesh('', 'models/character.mesh');
|
|
119
|
-
scene.add(mesh);
|
|
120
|
-
|
|
121
|
-
// With callbacks (Three.js style)
|
|
122
|
-
loader.loadMesh('assets.stow', 'models/tree.mesh',
|
|
123
|
-
(scene) => {
|
|
124
|
-
threeScene.add(scene);
|
|
125
|
-
},
|
|
126
|
-
(progress) => console.log('Loading...', progress),
|
|
127
|
-
(error) => console.error('Failed:', error)
|
|
128
|
-
);
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**What you get:**
|
|
132
|
-
- Complete scene hierarchy with nodes
|
|
133
|
-
- All geometries (vertices, normals, UVs, indices)
|
|
134
|
-
- Materials with properties applied (colors, etc.)
|
|
135
|
-
- Textures automatically loaded and applied to materials
|
|
136
|
-
- Transforms (position, rotation, scale) preserved
|
|
137
|
-
|
|
138
|
-
### Loading Textures
|
|
139
|
-
|
|
140
|
-
#### `loadTexture(url: string, assetPath: string, onLoad?, onProgress?, onError?): Promise<THREE.CompressedTexture>`
|
|
141
|
-
|
|
142
|
-
Load a texture asset (KTX2 format).
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
const texture = await loader.loadTexture('', 'textures/wood.ktx2');
|
|
146
|
-
material.map = texture;
|
|
147
|
-
material.needsUpdate = true;
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
#### `loadTextureByIndex(index: number, onLoad?, onProgress?, onError?): Promise<THREE.CompressedTexture>`
|
|
151
|
-
|
|
152
|
-
Load a texture by its asset index (when you don't have the path).
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
const texture = await loader.loadTextureByIndex(5);
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Loading Audio
|
|
159
|
-
|
|
160
|
-
#### `loadAudio(url: string, assetPath: string, listener: THREE.AudioListener, onLoad?, onProgress?, onError?): Promise<THREE.Audio>`
|
|
161
|
-
|
|
162
|
-
Load an audio asset as a Three.js Audio object.
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
const listener = new THREE.AudioListener();
|
|
166
|
-
camera.add(listener);
|
|
167
|
-
|
|
168
|
-
const audio = await loader.loadAudio('', 'sounds/bgm.ogg', listener);
|
|
169
|
-
audio.setLoop(true);
|
|
170
|
-
audio.play();
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
#### `createAudioPreview(index: number): Promise<HTMLAudioElement>`
|
|
174
|
-
|
|
175
|
-
Create an HTML5 audio element for previewing audio (simpler than Three.js Audio).
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
const audioElement = await loader.createAudioPreview(index);
|
|
179
|
-
audioElement.autoplay = true;
|
|
180
|
-
container.appendChild(audioElement);
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Material Information
|
|
184
|
-
|
|
185
|
-
#### `loadMaterialSchema(assetPath: string): object | null`
|
|
186
|
-
|
|
187
|
-
Load material schema definition.
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
const schema = loader.loadMaterialSchema('shaders/DefaultShader.stowschema');
|
|
191
|
-
|
|
192
|
-
console.log(schema.schemaName); // "Default Shader"
|
|
193
|
-
console.log(schema.fields); // Array of field definitions
|
|
194
|
-
|
|
195
|
-
schema.fields.forEach(field => {
|
|
196
|
-
console.log(`${field.name}: ${field.type}`);
|
|
197
|
-
// mainTex: Texture
|
|
198
|
-
// tint: Color
|
|
199
|
-
// roughness: Float
|
|
200
|
-
});
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
#### `loadMaterialSchemaByIndex(index: number): object | null`
|
|
204
|
-
|
|
205
|
-
Load material schema by asset index.
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
const schema = loader.loadMaterialSchemaByIndex(6);
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
**Schema Structure:**
|
|
212
|
-
```typescript
|
|
213
|
-
{
|
|
214
|
-
stringId: string; // Schema canonical path
|
|
215
|
-
schemaName: string; // Human-readable name
|
|
216
|
-
fields: Array<{
|
|
217
|
-
name: string; // Field name (e.g., "mainTex", "tint")
|
|
218
|
-
type: 'Texture' | 'Color' | 'Float' | 'Vec2' | 'Vec3' | 'Vec4' | 'Int';
|
|
219
|
-
previewFlag: 'None' | 'MainTex' | 'Tint';
|
|
220
|
-
defaultValue: [number, number, number, number];
|
|
221
|
-
}>;
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
#### `getMeshMaterials(assetPath: string): array | null`
|
|
226
|
-
|
|
227
|
-
Get material information from a mesh (properties and texture references).
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
const materials = loader.getMeshMaterials('models/character.mesh');
|
|
231
|
-
|
|
232
|
-
materials.forEach(mat => {
|
|
233
|
-
console.log(`Material: ${mat.name}`);
|
|
234
|
-
console.log(`Schema: ${mat.schemaId}`);
|
|
235
|
-
|
|
236
|
-
mat.properties.forEach(prop => {
|
|
237
|
-
if (prop.textureId) {
|
|
238
|
-
console.log(`${prop.fieldName}: ${prop.textureId}`); // mainTex: textures/skin.ktx2
|
|
239
|
-
} else {
|
|
240
|
-
console.log(`${prop.fieldName}:`, prop.value); // tint: [1, 0.8, 0.6, 1]
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Metadata Helpers
|
|
247
|
-
|
|
248
|
-
#### `getTextureMetadata(assetPath: string): object | null`
|
|
249
|
-
|
|
250
|
-
Get texture metadata (dimensions, format, etc.).
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
const info = loader.getTextureMetadata('textures/wood.ktx2');
|
|
254
|
-
|
|
255
|
-
console.log(`${info.width}x${info.height}`); // 1024x1024
|
|
256
|
-
console.log(`Channels: ${info.channels}`); // 3 (RGB) or 4 (RGBA)
|
|
257
|
-
console.log(`KTX2: ${info.isKtx2}`); // true
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
#### `getAudioMetadata(assetPath: string): object | null`
|
|
261
|
-
|
|
262
|
-
Get audio metadata (sample rate, duration, etc.).
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
const info = loader.getAudioMetadata('sounds/bgm.ogg');
|
|
266
|
-
|
|
267
|
-
console.log(`Sample Rate: ${info.sampleRate} Hz`);
|
|
268
|
-
console.log(`Channels: ${info.channels}`);
|
|
269
|
-
console.log(`Duration: ${info.durationMs / 1000}s`);
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Asset Listing
|
|
273
|
-
|
|
274
|
-
#### `listAssets(): array`
|
|
275
|
-
|
|
276
|
-
List all assets in the opened pack.
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
const assets = loader.listAssets();
|
|
280
|
-
|
|
281
|
-
assets.forEach(asset => {
|
|
282
|
-
console.log(`[${asset.index}] ${asset.name}`);
|
|
283
|
-
console.log(` Type: ${asset.type}, Size: ${asset.dataSize} bytes`);
|
|
284
|
-
});
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
#### `getAssetCount(): number`
|
|
288
|
-
|
|
289
|
-
Get total number of assets.
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
const count = loader.getAssetCount();
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
#### `getAssetInfo(index: number): object`
|
|
296
|
-
|
|
297
|
-
Get detailed info about a specific asset.
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
const info = loader.getAssetInfo(0);
|
|
301
|
-
console.log(info.type, info.data_size, info.metadata_size);
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Utility Methods
|
|
305
|
-
|
|
306
|
-
#### `readAssetData(index: number): Uint8Array | null`
|
|
307
|
-
|
|
308
|
-
Read raw asset data by index.
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
const data = loader.readAssetData(5);
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
#### `readAssetMetadata(index: number): Uint8Array | null`
|
|
315
|
-
|
|
316
|
-
Read raw asset metadata by index.
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
const metadata = loader.readAssetMetadata(5);
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
#### `dispose(): void`
|
|
323
|
-
|
|
324
|
-
Clean up resources and free memory.
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
loader.dispose();
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
## Complete Example
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
import * as THREE from 'three';
|
|
334
|
-
import { StowKitLoader } from '@stowkit/three-loader';
|
|
335
|
-
|
|
336
|
-
// Setup Three.js scene
|
|
337
|
-
const scene = new THREE.Scene();
|
|
338
|
-
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
339
|
-
const renderer = new THREE.WebGLRenderer();
|
|
340
|
-
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
341
|
-
document.body.appendChild(renderer.domElement);
|
|
342
|
-
|
|
343
|
-
// Setup audio
|
|
344
|
-
const listener = new THREE.AudioListener();
|
|
345
|
-
camera.add(listener);
|
|
346
|
-
|
|
347
|
-
// Initialize loader
|
|
348
|
-
const loader = new StowKitLoader();
|
|
349
|
-
|
|
350
|
-
// Load assets
|
|
351
|
-
async function loadAssets() {
|
|
352
|
-
await loader.openPack('game-assets.stow');
|
|
353
|
-
|
|
354
|
-
// Load main character
|
|
355
|
-
const character = await loader.loadMesh('', 'models/player.mesh');
|
|
356
|
-
character.position.set(0, 0, 0);
|
|
357
|
-
scene.add(character);
|
|
358
|
-
|
|
359
|
-
// Load environment
|
|
360
|
-
const environment = await loader.loadMesh('', 'models/level1.mesh');
|
|
361
|
-
scene.add(environment);
|
|
362
|
-
|
|
363
|
-
// Load background music
|
|
364
|
-
const bgm = await loader.loadAudio('', 'audio/theme.ogg', listener);
|
|
365
|
-
bgm.setLoop(true);
|
|
366
|
-
bgm.setVolume(0.5);
|
|
367
|
-
bgm.play();
|
|
368
|
-
|
|
369
|
-
// Log what was loaded
|
|
370
|
-
console.log(`Loaded ${loader.getAssetCount()} assets`);
|
|
371
|
-
|
|
372
|
-
// Start rendering
|
|
373
|
-
animate();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function animate() {
|
|
377
|
-
requestAnimationFrame(animate);
|
|
378
|
-
renderer.render(scene, camera);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
loadAssets();
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
## Material System
|
|
385
|
-
|
|
386
|
-
The loader automatically handles material assignments:
|
|
387
|
-
|
|
388
|
-
```typescript
|
|
389
|
-
// When you load a mesh, materials are created with:
|
|
390
|
-
// 1. Colors from 'tint' properties
|
|
391
|
-
// 2. Textures from 'mainTex', 'normalMap', etc.
|
|
392
|
-
// 3. Other properties (roughness, metallic, etc.)
|
|
393
|
-
|
|
394
|
-
const mesh = await loader.loadMesh('', 'models/character.mesh');
|
|
395
|
-
|
|
396
|
-
// Materials are automatically applied to geometries based on material indices
|
|
397
|
-
// Textures are automatically loaded by their string IDs and applied to materials
|
|
398
|
-
// No manual texture loading needed!
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
**Supported material properties:**
|
|
402
|
-
- `mainTex` / `diffuse` / `albedo` → `material.map`
|
|
403
|
-
- `tint` / `color` → `material.color`
|
|
404
|
-
- `normalMap` → `material.normalMap`
|
|
405
|
-
- `metallicMap` → `material.metalnessMap`
|
|
406
|
-
- `roughnessMap` → `material.roughnessMap`
|
|
407
|
-
|
|
408
|
-
## Advanced Usage
|
|
409
|
-
|
|
410
|
-
### Multiple Assets from Same Pack
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
// Open once
|
|
414
|
-
await loader.openPack('assets.stow');
|
|
415
|
-
|
|
416
|
-
// Load many (no URL needed)
|
|
417
|
-
const mesh1 = await loader.loadMesh('', 'models/char1.mesh');
|
|
418
|
-
const mesh2 = await loader.loadMesh('', 'models/char2.mesh');
|
|
419
|
-
const tex1 = await loader.loadTexture('', 'textures/t1.ktx2');
|
|
420
|
-
const tex2 = await loader.loadTexture('', 'textures/t2.ktx2');
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### Inspect Before Loading
|
|
424
|
-
|
|
425
|
-
```typescript
|
|
426
|
-
await loader.openPack('assets.stow');
|
|
427
|
-
|
|
428
|
-
// List all assets
|
|
429
|
-
const assets = loader.listAssets();
|
|
430
|
-
const meshes = assets.filter(a => a.type === 1); // Type 1 = Static Mesh
|
|
431
|
-
const textures = assets.filter(a => a.type === 2); // Type 2 = Texture
|
|
432
|
-
|
|
433
|
-
// Get material info before loading
|
|
434
|
-
const materials = loader.getMeshMaterials('models/character.mesh');
|
|
435
|
-
materials.forEach(mat => {
|
|
436
|
-
console.log(`Material "${mat.name}" needs:`, mat.properties.map(p => p.textureId).filter(Boolean));
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// Preload textures
|
|
440
|
-
for (const mat of materials) {
|
|
441
|
-
for (const prop of mat.properties) {
|
|
442
|
-
if (prop.textureId) {
|
|
443
|
-
await loader.loadTexture('', prop.textureId);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Now load the mesh (textures already cached)
|
|
449
|
-
const mesh = await loader.loadMesh('', 'models/character.mesh');
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### Error Handling
|
|
453
|
-
|
|
454
|
-
```typescript
|
|
455
|
-
try {
|
|
456
|
-
const mesh = await loader.loadMesh('assets.stow', 'models/character.mesh');
|
|
457
|
-
scene.add(mesh);
|
|
458
|
-
} catch (error) {
|
|
459
|
-
console.error('Failed to load mesh:', error.message);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Or with callbacks
|
|
463
|
-
loader.loadMesh('assets.stow', 'models/tree.mesh',
|
|
464
|
-
(scene) => threeScene.add(scene),
|
|
465
|
-
undefined,
|
|
466
|
-
(error) => console.error('Error:', error)
|
|
467
|
-
);
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
## Asset Types
|
|
471
|
-
|
|
472
|
-
| Type | Value | Description |
|
|
473
|
-
|------|-------|-------------|
|
|
474
|
-
| Static Mesh | 1 | 3D models with geometry, materials, and hierarchy |
|
|
475
|
-
| Texture 2D | 2 | KTX2 compressed textures |
|
|
476
|
-
| Audio | 3 | Audio files (OGG, MP3, etc.) |
|
|
477
|
-
| Material Schema | 4 | Material template definitions |
|
|
478
|
-
|
|
479
|
-
## File Structure
|
|
480
|
-
|
|
481
|
-
`.stow` files contain:
|
|
482
|
-
- **File Header** - Magic number, version, asset count
|
|
483
|
-
- **Asset Data** - Binary geometry, texture, audio data
|
|
484
|
-
- **Asset Directory** - Index of all assets with offsets
|
|
485
|
-
- **Metadata** - Asset-specific information (dimensions, transforms, etc.)
|
|
486
|
-
|
|
487
|
-
## Requirements
|
|
488
|
-
|
|
489
|
-
### Public Folder Setup
|
|
490
|
-
|
|
491
|
-
Place these files in your public folder:
|
|
492
|
-
|
|
493
|
-
```
|
|
494
|
-
public/
|
|
495
|
-
├── stowkit_reader.wasm # WASM reader module (copy from node_modules/@stowkit/reader/dist/)
|
|
496
|
-
└── stowkit/ # Auto-copied by @stowkit/three-loader on install
|
|
497
|
-
├── basis/ # Basis Universal transcoder (for textures) - included!
|
|
498
|
-
│ ├── basis_transcoder.js
|
|
499
|
-
│ └── basis_transcoder.wasm
|
|
500
|
-
└── draco/ # Draco decoder (for meshes) - included!
|
|
501
|
-
├── draco_decoder.js
|
|
502
|
-
├── draco_decoder.wasm
|
|
503
|
-
└── draco_wasm_wrapper.js
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
**Setup:**
|
|
507
|
-
|
|
508
|
-
The package automatically includes and copies all decoder files on install!
|
|
509
|
-
|
|
510
|
-
You only need to:
|
|
511
|
-
1. Copy `stowkit_reader.wasm` from `node_modules/@stowkit/reader/dist/` to `public/`
|
|
512
|
-
2. That's it! Basis and Draco decoders are included automatically.
|
|
513
|
-
|
|
514
|
-
### Three.js Version
|
|
515
|
-
|
|
516
|
-
Requires Three.js r150 or later.
|
|
517
|
-
|
|
518
|
-
```bash
|
|
519
|
-
npm install three@^0.150.0
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
## Examples
|
|
523
|
-
|
|
524
|
-
### Loading a Character with Equipment
|
|
525
|
-
|
|
526
|
-
```typescript
|
|
527
|
-
const loader = new StowKitLoader();
|
|
528
|
-
await loader.openPack('characters.stow');
|
|
529
|
-
|
|
530
|
-
// Load character base
|
|
531
|
-
const character = await loader.loadMesh('', 'characters/knight.mesh');
|
|
532
|
-
|
|
533
|
-
// Load and attach weapon
|
|
534
|
-
const weapon = await loader.loadMesh('', 'weapons/sword.mesh');
|
|
535
|
-
const rightHand = character.getObjectByName('RightHand');
|
|
536
|
-
if (rightHand) {
|
|
537
|
-
rightHand.add(weapon);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
scene.add(character);
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Dynamic Material Updates
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
// Load mesh
|
|
547
|
-
const mesh = await loader.loadMesh('', 'models/car.mesh');
|
|
548
|
-
|
|
549
|
-
// Get material info
|
|
550
|
-
const materials = loader.getMeshMaterials('models/car.mesh');
|
|
551
|
-
|
|
552
|
-
// Find and modify specific material
|
|
553
|
-
mesh.traverse((child) => {
|
|
554
|
-
if (child instanceof THREE.Mesh) {
|
|
555
|
-
const mat = child.material as THREE.MeshStandardMaterial;
|
|
556
|
-
if (mat.name === 'Paint') {
|
|
557
|
-
// Change car color
|
|
558
|
-
mat.color.set(0xff0000); // Red
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Texture Swapping
|
|
565
|
-
|
|
566
|
-
```typescript
|
|
567
|
-
// Load model
|
|
568
|
-
const character = await loader.loadMesh('', 'characters/player.mesh');
|
|
569
|
-
|
|
570
|
-
// Load alternate texture
|
|
571
|
-
const winterSkin = await loader.loadTexture('', 'textures/winter-skin.ktx2');
|
|
572
|
-
|
|
573
|
-
// Apply to character
|
|
574
|
-
character.traverse((child) => {
|
|
575
|
-
if (child instanceof THREE.Mesh) {
|
|
576
|
-
const mat = child.material as THREE.MeshStandardMaterial;
|
|
577
|
-
mat.map = winterSkin;
|
|
578
|
-
mat.needsUpdate = true;
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
### Audio with Spatial Sound
|
|
584
|
-
|
|
585
|
-
```typescript
|
|
586
|
-
const listener = new THREE.AudioListener();
|
|
587
|
-
camera.add(listener);
|
|
588
|
-
|
|
589
|
-
// Load positional audio
|
|
590
|
-
const sound = await loader.loadAudio('', 'sounds/waterfall.ogg', listener);
|
|
591
|
-
const positionalAudio = new THREE.PositionalAudio(listener);
|
|
592
|
-
positionalAudio.setBuffer(sound.buffer);
|
|
593
|
-
positionalAudio.setRefDistance(20);
|
|
594
|
-
positionalAudio.setLoop(true);
|
|
595
|
-
|
|
596
|
-
// Attach to a mesh
|
|
597
|
-
waterfall.add(positionalAudio);
|
|
598
|
-
positionalAudio.play();
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
## Troubleshooting
|
|
602
|
-
|
|
603
|
-
### Textures appear upside down
|
|
604
|
-
|
|
605
|
-
The loader automatically handles UV orientation for KTX2 textures. If textures still appear flipped, check your source files.
|
|
606
|
-
|
|
607
|
-
### "Missing initialization with .detectSupport()"
|
|
608
|
-
|
|
609
|
-
The loader automatically calls `detectSupport()` on initialization. If you see this error, ensure you're creating the loader before loading textures.
|
|
610
|
-
|
|
611
|
-
### "Asset not found"
|
|
612
|
-
|
|
613
|
-
Make sure:
|
|
614
|
-
- The pack file is opened before loading assets
|
|
615
|
-
- Asset paths match exactly (case-sensitive)
|
|
616
|
-
- The asset exists in the pack (use `listAssets()` to verify)
|
|
617
|
-
|
|
618
|
-
### Textures not loading on meshes
|
|
619
|
-
|
|
620
|
-
Check console for texture loading messages. Common issues:
|
|
621
|
-
- Texture string ID in material doesn't match any asset
|
|
622
|
-
- Texture asset is missing from pack
|
|
623
|
-
- Basis transcoder files not in `/basis/` folder
|
|
624
|
-
|
|
625
|
-
## Performance Tips
|
|
626
|
-
|
|
627
|
-
1. **Reuse loader instance** - Create one loader, load many assets
|
|
628
|
-
2. **Open pack once** - Use `openPack()` then load multiple assets
|
|
629
|
-
3. **Dispose when done** - Call `loader.dispose()` to free memory
|
|
630
|
-
4. **Use compressed textures** - KTX2 provides GPU-native compression
|
|
631
|
-
|
|
632
|
-
## License
|
|
633
|
-
|
|
634
|
-
MIT
|
|
635
|
-
|
|
636
|
-
## Links
|
|
637
|
-
|
|
638
|
-
- [npm package](https://www.npmjs.com/package/@stowkit/three-loader)
|
|
639
|
-
- [Three.js documentation](https://threejs.org/docs/)
|
|
1
|
+
# @stowkit/three-loader
|
|
2
|
+
|
|
3
|
+
Three.js loader for StowKit asset packs. Provides a simple, high-level API for loading meshes, textures, and audio from `.stow` files into Three.js applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @stowkit/three-loader three
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import * as THREE from 'three';
|
|
15
|
+
import { StowKitLoader } from '@stowkit/three-loader';
|
|
16
|
+
|
|
17
|
+
// Create loader (auto-configured with defaults)
|
|
18
|
+
// Basis transcoder: /stowkit/basis/ (auto-included)
|
|
19
|
+
// Draco decoder: /stowkit/draco/ (auto-included)
|
|
20
|
+
const loader = new StowKitLoader();
|
|
21
|
+
|
|
22
|
+
// Optional: Override paths only if you have custom locations
|
|
23
|
+
// loader.setTranscoderPath('/custom-basis-path/');
|
|
24
|
+
// loader.setDracoDecoderPath('/custom-draco-path/');
|
|
25
|
+
|
|
26
|
+
// Open a .stow pack file
|
|
27
|
+
await loader.openPack('assets.stow');
|
|
28
|
+
|
|
29
|
+
// Load a mesh with automatic textures and materials
|
|
30
|
+
const scene = await loader.loadMesh('', 'models/character.mesh');
|
|
31
|
+
threeScene.add(scene);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- ✅ **Zero configuration** - Works out of the box with sensible defaults
|
|
37
|
+
- ✅ **Draco mesh compression** - All meshes use Draco compression for smaller file sizes
|
|
38
|
+
- ✅ **Automatic texture loading** - Materials reference textures by ID, loader handles everything
|
|
39
|
+
- ✅ **Material system** - Full support for material schemas, properties, and assignments
|
|
40
|
+
- ✅ **KTX2/Basis Universal** - GPU-compressed textures with automatic transcoding
|
|
41
|
+
- ✅ **Scene hierarchy** - Preserves transforms, parent-child relationships, and node structure
|
|
42
|
+
- ✅ **Multi-material meshes** - Supports meshes with multiple materials/submeshes
|
|
43
|
+
- ✅ **Draco decoder included** - No need to download separately, auto-copied on install
|
|
44
|
+
- ✅ **Type-safe** - Full TypeScript definitions included
|
|
45
|
+
|
|
46
|
+
## API Reference
|
|
47
|
+
|
|
48
|
+
### Constructor
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
new StowKitLoader(manager?: THREE.LoadingManager, parameters?: StowKitLoaderParameters)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Parameters (all optional):**
|
|
55
|
+
```typescript
|
|
56
|
+
interface StowKitLoaderParameters {
|
|
57
|
+
transcoderPath?: string; // Path to Basis Universal transcoder (default: '/basis/')
|
|
58
|
+
wasmPath?: string; // Path to WASM reader module (default: '/stowkit_reader.wasm')
|
|
59
|
+
reader?: StowKitReader; // Custom reader instance (advanced)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Example:**
|
|
64
|
+
```typescript
|
|
65
|
+
// Default configuration
|
|
66
|
+
const loader = new StowKitLoader();
|
|
67
|
+
|
|
68
|
+
// Custom paths
|
|
69
|
+
const loader = new StowKitLoader(undefined, {
|
|
70
|
+
transcoderPath: '/my-basis-path/',
|
|
71
|
+
wasmPath: '/custom/stowkit_reader.wasm'
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Opening Packs
|
|
76
|
+
|
|
77
|
+
#### `openPack(url: string): Promise<void>`
|
|
78
|
+
|
|
79
|
+
Open a .stow pack file from a URL. Call this once, then load multiple assets.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
await loader.openPack('assets.stow');
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### `open(fileOrBuffer: ArrayBuffer | File): Promise<boolean>`
|
|
86
|
+
|
|
87
|
+
Open a pack from a File or ArrayBuffer.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// From file input
|
|
91
|
+
const file = input.files[0];
|
|
92
|
+
await loader.open(file);
|
|
93
|
+
|
|
94
|
+
// From fetch
|
|
95
|
+
const response = await fetch('assets.stow');
|
|
96
|
+
const buffer = await response.arrayBuffer();
|
|
97
|
+
await loader.open(buffer);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Loading Meshes
|
|
101
|
+
|
|
102
|
+
#### `loadMesh(url: string, assetPath: string, onLoad?, onProgress?, onError?): Promise<THREE.Group>`
|
|
103
|
+
|
|
104
|
+
Load a mesh asset with automatic texture and material loading.
|
|
105
|
+
|
|
106
|
+
**Parameters:**
|
|
107
|
+
- `url` - URL to .stow file (empty string if already opened)
|
|
108
|
+
- `assetPath` - Canonical path to mesh within pack
|
|
109
|
+
- `onLoad` - Optional callback when loading completes
|
|
110
|
+
- `onProgress` - Optional progress callback
|
|
111
|
+
- `onError` - Optional error callback
|
|
112
|
+
|
|
113
|
+
**Returns:** `THREE.Group` containing the mesh hierarchy
|
|
114
|
+
|
|
115
|
+
**Example:**
|
|
116
|
+
```typescript
|
|
117
|
+
// Simple async/await
|
|
118
|
+
const mesh = await loader.loadMesh('', 'models/character.mesh');
|
|
119
|
+
scene.add(mesh);
|
|
120
|
+
|
|
121
|
+
// With callbacks (Three.js style)
|
|
122
|
+
loader.loadMesh('assets.stow', 'models/tree.mesh',
|
|
123
|
+
(scene) => {
|
|
124
|
+
threeScene.add(scene);
|
|
125
|
+
},
|
|
126
|
+
(progress) => console.log('Loading...', progress),
|
|
127
|
+
(error) => console.error('Failed:', error)
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**What you get:**
|
|
132
|
+
- Complete scene hierarchy with nodes
|
|
133
|
+
- All geometries (vertices, normals, UVs, indices)
|
|
134
|
+
- Materials with properties applied (colors, etc.)
|
|
135
|
+
- Textures automatically loaded and applied to materials
|
|
136
|
+
- Transforms (position, rotation, scale) preserved
|
|
137
|
+
|
|
138
|
+
### Loading Textures
|
|
139
|
+
|
|
140
|
+
#### `loadTexture(url: string, assetPath: string, onLoad?, onProgress?, onError?): Promise<THREE.CompressedTexture>`
|
|
141
|
+
|
|
142
|
+
Load a texture asset (KTX2 format).
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const texture = await loader.loadTexture('', 'textures/wood.ktx2');
|
|
146
|
+
material.map = texture;
|
|
147
|
+
material.needsUpdate = true;
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `loadTextureByIndex(index: number, onLoad?, onProgress?, onError?): Promise<THREE.CompressedTexture>`
|
|
151
|
+
|
|
152
|
+
Load a texture by its asset index (when you don't have the path).
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const texture = await loader.loadTextureByIndex(5);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Loading Audio
|
|
159
|
+
|
|
160
|
+
#### `loadAudio(url: string, assetPath: string, listener: THREE.AudioListener, onLoad?, onProgress?, onError?): Promise<THREE.Audio>`
|
|
161
|
+
|
|
162
|
+
Load an audio asset as a Three.js Audio object.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const listener = new THREE.AudioListener();
|
|
166
|
+
camera.add(listener);
|
|
167
|
+
|
|
168
|
+
const audio = await loader.loadAudio('', 'sounds/bgm.ogg', listener);
|
|
169
|
+
audio.setLoop(true);
|
|
170
|
+
audio.play();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### `createAudioPreview(index: number): Promise<HTMLAudioElement>`
|
|
174
|
+
|
|
175
|
+
Create an HTML5 audio element for previewing audio (simpler than Three.js Audio).
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const audioElement = await loader.createAudioPreview(index);
|
|
179
|
+
audioElement.autoplay = true;
|
|
180
|
+
container.appendChild(audioElement);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Material Information
|
|
184
|
+
|
|
185
|
+
#### `loadMaterialSchema(assetPath: string): object | null`
|
|
186
|
+
|
|
187
|
+
Load material schema definition.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const schema = loader.loadMaterialSchema('shaders/DefaultShader.stowschema');
|
|
191
|
+
|
|
192
|
+
console.log(schema.schemaName); // "Default Shader"
|
|
193
|
+
console.log(schema.fields); // Array of field definitions
|
|
194
|
+
|
|
195
|
+
schema.fields.forEach(field => {
|
|
196
|
+
console.log(`${field.name}: ${field.type}`);
|
|
197
|
+
// mainTex: Texture
|
|
198
|
+
// tint: Color
|
|
199
|
+
// roughness: Float
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### `loadMaterialSchemaByIndex(index: number): object | null`
|
|
204
|
+
|
|
205
|
+
Load material schema by asset index.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const schema = loader.loadMaterialSchemaByIndex(6);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Schema Structure:**
|
|
212
|
+
```typescript
|
|
213
|
+
{
|
|
214
|
+
stringId: string; // Schema canonical path
|
|
215
|
+
schemaName: string; // Human-readable name
|
|
216
|
+
fields: Array<{
|
|
217
|
+
name: string; // Field name (e.g., "mainTex", "tint")
|
|
218
|
+
type: 'Texture' | 'Color' | 'Float' | 'Vec2' | 'Vec3' | 'Vec4' | 'Int';
|
|
219
|
+
previewFlag: 'None' | 'MainTex' | 'Tint';
|
|
220
|
+
defaultValue: [number, number, number, number];
|
|
221
|
+
}>;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### `getMeshMaterials(assetPath: string): array | null`
|
|
226
|
+
|
|
227
|
+
Get material information from a mesh (properties and texture references).
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const materials = loader.getMeshMaterials('models/character.mesh');
|
|
231
|
+
|
|
232
|
+
materials.forEach(mat => {
|
|
233
|
+
console.log(`Material: ${mat.name}`);
|
|
234
|
+
console.log(`Schema: ${mat.schemaId}`);
|
|
235
|
+
|
|
236
|
+
mat.properties.forEach(prop => {
|
|
237
|
+
if (prop.textureId) {
|
|
238
|
+
console.log(`${prop.fieldName}: ${prop.textureId}`); // mainTex: textures/skin.ktx2
|
|
239
|
+
} else {
|
|
240
|
+
console.log(`${prop.fieldName}:`, prop.value); // tint: [1, 0.8, 0.6, 1]
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Metadata Helpers
|
|
247
|
+
|
|
248
|
+
#### `getTextureMetadata(assetPath: string): object | null`
|
|
249
|
+
|
|
250
|
+
Get texture metadata (dimensions, format, etc.).
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const info = loader.getTextureMetadata('textures/wood.ktx2');
|
|
254
|
+
|
|
255
|
+
console.log(`${info.width}x${info.height}`); // 1024x1024
|
|
256
|
+
console.log(`Channels: ${info.channels}`); // 3 (RGB) or 4 (RGBA)
|
|
257
|
+
console.log(`KTX2: ${info.isKtx2}`); // true
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### `getAudioMetadata(assetPath: string): object | null`
|
|
261
|
+
|
|
262
|
+
Get audio metadata (sample rate, duration, etc.).
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const info = loader.getAudioMetadata('sounds/bgm.ogg');
|
|
266
|
+
|
|
267
|
+
console.log(`Sample Rate: ${info.sampleRate} Hz`);
|
|
268
|
+
console.log(`Channels: ${info.channels}`);
|
|
269
|
+
console.log(`Duration: ${info.durationMs / 1000}s`);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Asset Listing
|
|
273
|
+
|
|
274
|
+
#### `listAssets(): array`
|
|
275
|
+
|
|
276
|
+
List all assets in the opened pack.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const assets = loader.listAssets();
|
|
280
|
+
|
|
281
|
+
assets.forEach(asset => {
|
|
282
|
+
console.log(`[${asset.index}] ${asset.name}`);
|
|
283
|
+
console.log(` Type: ${asset.type}, Size: ${asset.dataSize} bytes`);
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `getAssetCount(): number`
|
|
288
|
+
|
|
289
|
+
Get total number of assets.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const count = loader.getAssetCount();
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### `getAssetInfo(index: number): object`
|
|
296
|
+
|
|
297
|
+
Get detailed info about a specific asset.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const info = loader.getAssetInfo(0);
|
|
301
|
+
console.log(info.type, info.data_size, info.metadata_size);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Utility Methods
|
|
305
|
+
|
|
306
|
+
#### `readAssetData(index: number): Uint8Array | null`
|
|
307
|
+
|
|
308
|
+
Read raw asset data by index.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const data = loader.readAssetData(5);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### `readAssetMetadata(index: number): Uint8Array | null`
|
|
315
|
+
|
|
316
|
+
Read raw asset metadata by index.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
const metadata = loader.readAssetMetadata(5);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### `dispose(): void`
|
|
323
|
+
|
|
324
|
+
Clean up resources and free memory.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
loader.dispose();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Complete Example
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import * as THREE from 'three';
|
|
334
|
+
import { StowKitLoader } from '@stowkit/three-loader';
|
|
335
|
+
|
|
336
|
+
// Setup Three.js scene
|
|
337
|
+
const scene = new THREE.Scene();
|
|
338
|
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
339
|
+
const renderer = new THREE.WebGLRenderer();
|
|
340
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
341
|
+
document.body.appendChild(renderer.domElement);
|
|
342
|
+
|
|
343
|
+
// Setup audio
|
|
344
|
+
const listener = new THREE.AudioListener();
|
|
345
|
+
camera.add(listener);
|
|
346
|
+
|
|
347
|
+
// Initialize loader
|
|
348
|
+
const loader = new StowKitLoader();
|
|
349
|
+
|
|
350
|
+
// Load assets
|
|
351
|
+
async function loadAssets() {
|
|
352
|
+
await loader.openPack('game-assets.stow');
|
|
353
|
+
|
|
354
|
+
// Load main character
|
|
355
|
+
const character = await loader.loadMesh('', 'models/player.mesh');
|
|
356
|
+
character.position.set(0, 0, 0);
|
|
357
|
+
scene.add(character);
|
|
358
|
+
|
|
359
|
+
// Load environment
|
|
360
|
+
const environment = await loader.loadMesh('', 'models/level1.mesh');
|
|
361
|
+
scene.add(environment);
|
|
362
|
+
|
|
363
|
+
// Load background music
|
|
364
|
+
const bgm = await loader.loadAudio('', 'audio/theme.ogg', listener);
|
|
365
|
+
bgm.setLoop(true);
|
|
366
|
+
bgm.setVolume(0.5);
|
|
367
|
+
bgm.play();
|
|
368
|
+
|
|
369
|
+
// Log what was loaded
|
|
370
|
+
console.log(`Loaded ${loader.getAssetCount()} assets`);
|
|
371
|
+
|
|
372
|
+
// Start rendering
|
|
373
|
+
animate();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function animate() {
|
|
377
|
+
requestAnimationFrame(animate);
|
|
378
|
+
renderer.render(scene, camera);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
loadAssets();
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Material System
|
|
385
|
+
|
|
386
|
+
The loader automatically handles material assignments:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// When you load a mesh, materials are created with:
|
|
390
|
+
// 1. Colors from 'tint' properties
|
|
391
|
+
// 2. Textures from 'mainTex', 'normalMap', etc.
|
|
392
|
+
// 3. Other properties (roughness, metallic, etc.)
|
|
393
|
+
|
|
394
|
+
const mesh = await loader.loadMesh('', 'models/character.mesh');
|
|
395
|
+
|
|
396
|
+
// Materials are automatically applied to geometries based on material indices
|
|
397
|
+
// Textures are automatically loaded by their string IDs and applied to materials
|
|
398
|
+
// No manual texture loading needed!
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Supported material properties:**
|
|
402
|
+
- `mainTex` / `diffuse` / `albedo` → `material.map`
|
|
403
|
+
- `tint` / `color` → `material.color`
|
|
404
|
+
- `normalMap` → `material.normalMap`
|
|
405
|
+
- `metallicMap` → `material.metalnessMap`
|
|
406
|
+
- `roughnessMap` → `material.roughnessMap`
|
|
407
|
+
|
|
408
|
+
## Advanced Usage
|
|
409
|
+
|
|
410
|
+
### Multiple Assets from Same Pack
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// Open once
|
|
414
|
+
await loader.openPack('assets.stow');
|
|
415
|
+
|
|
416
|
+
// Load many (no URL needed)
|
|
417
|
+
const mesh1 = await loader.loadMesh('', 'models/char1.mesh');
|
|
418
|
+
const mesh2 = await loader.loadMesh('', 'models/char2.mesh');
|
|
419
|
+
const tex1 = await loader.loadTexture('', 'textures/t1.ktx2');
|
|
420
|
+
const tex2 = await loader.loadTexture('', 'textures/t2.ktx2');
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Inspect Before Loading
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
await loader.openPack('assets.stow');
|
|
427
|
+
|
|
428
|
+
// List all assets
|
|
429
|
+
const assets = loader.listAssets();
|
|
430
|
+
const meshes = assets.filter(a => a.type === 1); // Type 1 = Static Mesh
|
|
431
|
+
const textures = assets.filter(a => a.type === 2); // Type 2 = Texture
|
|
432
|
+
|
|
433
|
+
// Get material info before loading
|
|
434
|
+
const materials = loader.getMeshMaterials('models/character.mesh');
|
|
435
|
+
materials.forEach(mat => {
|
|
436
|
+
console.log(`Material "${mat.name}" needs:`, mat.properties.map(p => p.textureId).filter(Boolean));
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Preload textures
|
|
440
|
+
for (const mat of materials) {
|
|
441
|
+
for (const prop of mat.properties) {
|
|
442
|
+
if (prop.textureId) {
|
|
443
|
+
await loader.loadTexture('', prop.textureId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Now load the mesh (textures already cached)
|
|
449
|
+
const mesh = await loader.loadMesh('', 'models/character.mesh');
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Error Handling
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
try {
|
|
456
|
+
const mesh = await loader.loadMesh('assets.stow', 'models/character.mesh');
|
|
457
|
+
scene.add(mesh);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error('Failed to load mesh:', error.message);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Or with callbacks
|
|
463
|
+
loader.loadMesh('assets.stow', 'models/tree.mesh',
|
|
464
|
+
(scene) => threeScene.add(scene),
|
|
465
|
+
undefined,
|
|
466
|
+
(error) => console.error('Error:', error)
|
|
467
|
+
);
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Asset Types
|
|
471
|
+
|
|
472
|
+
| Type | Value | Description |
|
|
473
|
+
|------|-------|-------------|
|
|
474
|
+
| Static Mesh | 1 | 3D models with geometry, materials, and hierarchy |
|
|
475
|
+
| Texture 2D | 2 | KTX2 compressed textures |
|
|
476
|
+
| Audio | 3 | Audio files (OGG, MP3, etc.) |
|
|
477
|
+
| Material Schema | 4 | Material template definitions |
|
|
478
|
+
|
|
479
|
+
## File Structure
|
|
480
|
+
|
|
481
|
+
`.stow` files contain:
|
|
482
|
+
- **File Header** - Magic number, version, asset count
|
|
483
|
+
- **Asset Data** - Binary geometry, texture, audio data
|
|
484
|
+
- **Asset Directory** - Index of all assets with offsets
|
|
485
|
+
- **Metadata** - Asset-specific information (dimensions, transforms, etc.)
|
|
486
|
+
|
|
487
|
+
## Requirements
|
|
488
|
+
|
|
489
|
+
### Public Folder Setup
|
|
490
|
+
|
|
491
|
+
Place these files in your public folder:
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
public/
|
|
495
|
+
├── stowkit_reader.wasm # WASM reader module (copy from node_modules/@stowkit/reader/dist/)
|
|
496
|
+
└── stowkit/ # Auto-copied by @stowkit/three-loader on install
|
|
497
|
+
├── basis/ # Basis Universal transcoder (for textures) - included!
|
|
498
|
+
│ ├── basis_transcoder.js
|
|
499
|
+
│ └── basis_transcoder.wasm
|
|
500
|
+
└── draco/ # Draco decoder (for meshes) - included!
|
|
501
|
+
├── draco_decoder.js
|
|
502
|
+
├── draco_decoder.wasm
|
|
503
|
+
└── draco_wasm_wrapper.js
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Setup:**
|
|
507
|
+
|
|
508
|
+
The package automatically includes and copies all decoder files on install!
|
|
509
|
+
|
|
510
|
+
You only need to:
|
|
511
|
+
1. Copy `stowkit_reader.wasm` from `node_modules/@stowkit/reader/dist/` to `public/`
|
|
512
|
+
2. That's it! Basis and Draco decoders are included automatically.
|
|
513
|
+
|
|
514
|
+
### Three.js Version
|
|
515
|
+
|
|
516
|
+
Requires Three.js r150 or later.
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
npm install three@^0.150.0
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## Examples
|
|
523
|
+
|
|
524
|
+
### Loading a Character with Equipment
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
const loader = new StowKitLoader();
|
|
528
|
+
await loader.openPack('characters.stow');
|
|
529
|
+
|
|
530
|
+
// Load character base
|
|
531
|
+
const character = await loader.loadMesh('', 'characters/knight.mesh');
|
|
532
|
+
|
|
533
|
+
// Load and attach weapon
|
|
534
|
+
const weapon = await loader.loadMesh('', 'weapons/sword.mesh');
|
|
535
|
+
const rightHand = character.getObjectByName('RightHand');
|
|
536
|
+
if (rightHand) {
|
|
537
|
+
rightHand.add(weapon);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
scene.add(character);
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Dynamic Material Updates
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// Load mesh
|
|
547
|
+
const mesh = await loader.loadMesh('', 'models/car.mesh');
|
|
548
|
+
|
|
549
|
+
// Get material info
|
|
550
|
+
const materials = loader.getMeshMaterials('models/car.mesh');
|
|
551
|
+
|
|
552
|
+
// Find and modify specific material
|
|
553
|
+
mesh.traverse((child) => {
|
|
554
|
+
if (child instanceof THREE.Mesh) {
|
|
555
|
+
const mat = child.material as THREE.MeshStandardMaterial;
|
|
556
|
+
if (mat.name === 'Paint') {
|
|
557
|
+
// Change car color
|
|
558
|
+
mat.color.set(0xff0000); // Red
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Texture Swapping
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// Load model
|
|
568
|
+
const character = await loader.loadMesh('', 'characters/player.mesh');
|
|
569
|
+
|
|
570
|
+
// Load alternate texture
|
|
571
|
+
const winterSkin = await loader.loadTexture('', 'textures/winter-skin.ktx2');
|
|
572
|
+
|
|
573
|
+
// Apply to character
|
|
574
|
+
character.traverse((child) => {
|
|
575
|
+
if (child instanceof THREE.Mesh) {
|
|
576
|
+
const mat = child.material as THREE.MeshStandardMaterial;
|
|
577
|
+
mat.map = winterSkin;
|
|
578
|
+
mat.needsUpdate = true;
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Audio with Spatial Sound
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
const listener = new THREE.AudioListener();
|
|
587
|
+
camera.add(listener);
|
|
588
|
+
|
|
589
|
+
// Load positional audio
|
|
590
|
+
const sound = await loader.loadAudio('', 'sounds/waterfall.ogg', listener);
|
|
591
|
+
const positionalAudio = new THREE.PositionalAudio(listener);
|
|
592
|
+
positionalAudio.setBuffer(sound.buffer);
|
|
593
|
+
positionalAudio.setRefDistance(20);
|
|
594
|
+
positionalAudio.setLoop(true);
|
|
595
|
+
|
|
596
|
+
// Attach to a mesh
|
|
597
|
+
waterfall.add(positionalAudio);
|
|
598
|
+
positionalAudio.play();
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Troubleshooting
|
|
602
|
+
|
|
603
|
+
### Textures appear upside down
|
|
604
|
+
|
|
605
|
+
The loader automatically handles UV orientation for KTX2 textures. If textures still appear flipped, check your source files.
|
|
606
|
+
|
|
607
|
+
### "Missing initialization with .detectSupport()"
|
|
608
|
+
|
|
609
|
+
The loader automatically calls `detectSupport()` on initialization. If you see this error, ensure you're creating the loader before loading textures.
|
|
610
|
+
|
|
611
|
+
### "Asset not found"
|
|
612
|
+
|
|
613
|
+
Make sure:
|
|
614
|
+
- The pack file is opened before loading assets
|
|
615
|
+
- Asset paths match exactly (case-sensitive)
|
|
616
|
+
- The asset exists in the pack (use `listAssets()` to verify)
|
|
617
|
+
|
|
618
|
+
### Textures not loading on meshes
|
|
619
|
+
|
|
620
|
+
Check console for texture loading messages. Common issues:
|
|
621
|
+
- Texture string ID in material doesn't match any asset
|
|
622
|
+
- Texture asset is missing from pack
|
|
623
|
+
- Basis transcoder files not in `/basis/` folder
|
|
624
|
+
|
|
625
|
+
## Performance Tips
|
|
626
|
+
|
|
627
|
+
1. **Reuse loader instance** - Create one loader, load many assets
|
|
628
|
+
2. **Open pack once** - Use `openPack()` then load multiple assets
|
|
629
|
+
3. **Dispose when done** - Call `loader.dispose()` to free memory
|
|
630
|
+
4. **Use compressed textures** - KTX2 provides GPU-native compression
|
|
631
|
+
|
|
632
|
+
## License
|
|
633
|
+
|
|
634
|
+
MIT
|
|
635
|
+
|
|
636
|
+
## Links
|
|
637
|
+
|
|
638
|
+
- [npm package](https://www.npmjs.com/package/@stowkit/three-loader)
|
|
639
|
+
- [Three.js documentation](https://threejs.org/docs/)
|
|
640
640
|
- [Basis Universal](https://github.com/BinomialLLC/basis_universal)
|