Three.js loader for StowKit asset packs
npm install @stowkit/three-loader.stow files.
bash
npm install @stowkit/three-loader three
`
Quick Start
`typescript
import { StowKitLoader, AssetType } from '@stowkit/three-loader';
const pack = await StowKitLoader.load('assets.stow');
// from memory
const packFromMemory = await StowKitLoader.loadFromMemory(someData);
const mesh = await pack.loadMesh('character');
scene.add(mesh);
const character = await pack.loadSkinnedMesh('player');
scene.add(character);
const { mixer } = await pack.loadAnimation(character, 'walk');
// Update in your animation loop
function animate() {
const delta = clock.getDelta();
mixer.update(delta);
renderer.render(scene, camera);
}
`
Features
- ✅ Static Meshes - Draco-compressed meshes with automatic material/texture loading
- ✅ Skinned Meshes - Skeletal meshes with bone hierarchy
- ✅ Animations - Skeletal animations with automatic mixer setup
- ✅ Textures - KTX2/Basis Universal GPU-compressed textures
- ✅ Audio - OGG/MP3 audio with Three.js Audio integration
- ✅ Multiple Packs - Load multiple .stow files simultaneously with isolated state
- ✅ WASM Parsing - All binary parsing done in WASM for performance
- ✅ Type Safe - Full TypeScript support
- ✅ Zero Config - Works out of the box
API Reference
$3
#### pack.listAssets(): AssetListItem[]
Get the complete manifest of all assets in the pack.
`typescript
const pack = await StowKitLoader.load('assets.stow');
const manifest = pack.listAssets();
console.log(Pack contains ${manifest.length} assets);
manifest.forEach(asset => {
console.log([${asset.index}] ${asset.name || asset.id});
console.log( Type: ${getTypeName(asset.type)});
console.log( Size: ${formatBytes(asset.dataSize)});
console.log( Has Metadata: ${asset.hasMetadata});
});
// Filter by type
const meshes = manifest.filter(a => a.type === AssetType.STATIC_MESH);
const textures = manifest.filter(a => a.type === AssetType.TEXTURE_2D);
const audio = manifest.filter(a => a.type === AssetType.AUDIO);
const skinnedMeshes = manifest.filter(a => a.type === AssetType.SKINNED_MESH);
const animations = manifest.filter(a => a.type === AssetType.ANIMATION_CLIP);
console.log(${meshes.length} meshes, ${textures.length} textures, ${animations.length} animations);
`
AssetListItem structure:
`typescript
{
index: number; // Asset index
type: number; // Asset type ID (1-6)
name?: string; // Extracted from metadata (if available)
id: bigint; // Unique asset ID
dataSize: number; // Size of asset data in bytes
metadataSize: number; // Size of metadata in bytes
hasMetadata: boolean; // Whether metadata exists
data_offset: number; // Internal use
data_size: number; // Internal use
metadata_offset: number; // Internal use
metadata_size: number; // Internal use
}
`
#### pack.getAssetCount(): number
Get total number of assets in the pack.
`typescript
const count = pack.getAssetCount();
console.log(Pack has ${count} assets);
`
#### pack.getAssetInfo(index: number): AssetInfo | null
Get detailed info about a specific asset.
`typescript
const info = pack.getAssetInfo(5);
if (info) {
console.log(Type: ${info.type}, Size: ${info.data_size} bytes);
}
`
$3
#### Static Meshes
`typescript
// Load by string ID
const mesh = await pack.loadMesh('models/building.mesh');
scene.add(mesh);
// Load by index
const mesh = await pack.loadMeshByIndex(5);
scene.add(mesh);
`
Returns a THREE.Group containing the mesh hierarchy with materials and textures applied.
#### Skinned Meshes
`typescript
// Load by string ID
const character = await pack.loadSkinnedMesh('characters/player.skinned');
scene.add(character);
// Load by index
const character = await pack.loadSkinnedMeshByIndex(8);
scene.add(character);
`
Returns a THREE.Group containing the skinned mesh with skeleton and bones in bind pose.
#### Animations
`typescript
// Load and play animation (returns mixer, action, clip)
const { mixer, action, clip } = await pack.loadAnimation(
skinnedMeshGroup,
'animations/walk.anim'
);
// Or by index
const { mixer, action, clip } = await pack.loadAnimationByIndex(
skinnedMeshGroup,
9
);
// Update in your animation loop
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
`
The loadAnimation methods automatically:
- Create an AnimationMixer on the correct root object
- Set the animation to loop infinitely
- Start playing immediately
- Return everything you need
#### Textures
`typescript
// Load by string ID
const texture = await pack.loadTexture('textures/wood.ktx2');
material.map = texture;
// Load by index
const texture = await pack.loadTextureByIndex(2);
`
Returns a THREE.CompressedTexture (KTX2/Basis Universal format).
#### Audio
`typescript
const listener = new THREE.AudioListener();
camera.add(listener);
// Load by string ID
const bgm = await pack.loadAudio('sounds/music.ogg', listener);
bgm.setLoop(true);
bgm.play();
// Or create HTML5 audio element for preview
const audioElement = await pack.createAudioPreview(3);
document.body.appendChild(audioElement);
`
$3
All metadata is parsed in WASM for performance and reliability.
#### Animation Metadata
`typescript
const animData = pack.getAnimationMetadata(index);
console.log(animData.stringId); // "Clip_Walking"
console.log(animData.targetMeshId); // "Character_Skinned_Tpose"
console.log(animData.duration); // 0.97
console.log(animData.ticksPerSecond); // 30
console.log(animData.channelCount); // 104
console.log(animData.boneCount); // 65
`
#### Audio Metadata
`typescript
const audioData = pack.getAudioMetadata('sounds/bgm.ogg');
console.log(audioData.sampleRate); // 44100
console.log(audioData.channels); // 2 (stereo)
console.log(audioData.durationMs); // 180000 (3 minutes)
`
#### Texture Metadata
`typescript
const texData = pack.getTextureMetadata(index);
console.log(texData.width); // 1024
console.log(texData.height); // 1024
console.log(texData.channels); // 3 (RGB) or 4 (RGBA)
console.log(texData.channelFormat); // 1 (RGB) or 2 (RGBA)
`
$3
`typescript
// List all assets in pack
const assets = pack.listAssets();
assets.forEach(asset => {
console.log([${asset.index}] ${asset.name} (${getTypeName(asset.type)}));
});
// Find asset by path
const index = pack.reader.findAssetByPath('models/character.mesh');
// Get asset count
const count = pack.getAssetCount();
// Get asset info
const info = pack.getAssetInfo(5);
`
Complete Example
`typescript
import * as THREE from 'three';
import { StowKitLoader } from '@stowkit/three-loader';
// Setup Three.js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
const clock = new THREE.Clock();
const listener = new THREE.AudioListener();
camera.add(listener);
// Load pack
const pack = await StowKitLoader.load('game.stow');
// Load static mesh
const environment = await pack.loadMesh('levels/level1.mesh');
scene.add(environment);
// Load skinned character
const character = await pack.loadSkinnedMesh('characters/player.skinned');
character.position.set(0, 0, 0);
scene.add(character);
// Load and play animation
const { mixer } = await pack.loadAnimation(character, 'animations/idle.anim');
// Load audio
const bgm = await pack.loadAudio('sounds/theme.ogg', listener);
bgm.setLoop(true);
bgm.play();
// Animation loop
function animate() {
requestAnimationFrame(animate);
mixer.update(clock.getDelta());
renderer.render(scene, camera);
}
animate();
`
Loading Multiple Packs
Each pack is fully isolated with its own state. You can load multiple packs simultaneously without any interference:
`typescript
// Load multiple packs at once
const [environmentPack, characterPack, audioPack] = await Promise.all([
StowKitLoader.load('environment.stow'),
StowKitLoader.load('characters.stow'),
StowKitLoader.load('audio.stow')
]);
// Load assets from different packs
const level = await environmentPack.loadMesh('level1');
const player = await characterPack.loadSkinnedMesh('player');
const bgm = await audioPack.loadAudio('theme', listener);
scene.add(level);
scene.add(player);
bgm.play();
// Each pack maintains its own asset catalog
console.log(Environment: ${environmentPack.getAssetCount()} assets);
console.log(Characters: ${characterPack.getAssetCount()} assets);
console.log(Audio: ${audioPack.getAssetCount()} assets);
`
Note: Each pack creates its own WASM instance for isolated state. Dispose packs when no longer needed:
`typescript
environmentPack.dispose();
`
Asset Types
`typescript
import { AssetType } from '@stowkit/three-loader';
`
| Enum | Value | Description |
|------|-------|-------------|
| AssetType.STATIC_MESH | 1 | Draco-compressed 3D models |
| AssetType.TEXTURE_2D | 2 | KTX2/Basis Universal textures |
| AssetType.AUDIO | 3 | OGG/MP3 audio files |
| AssetType.MATERIAL_SCHEMA | 4 | Material template definitions |
| AssetType.SKINNED_MESH | 5 | Skeletal meshes with bones |
| AssetType.ANIMATION_CLIP | 6 | Bone animation keyframes |
Public Folder Setup
The package automatically copies required files on install:
`
public/
└── stowkit/
├── stowkit_reader.wasm # WASM reader module
├── basis/ # Basis Universal transcoder
│ ├── basis_transcoder.js
│ └── basis_transcoder.wasm
└── draco/ # Draco decoder
├── draco_decoder.js
├── draco_decoder.wasm
└── draco_wasm_wrapper.js
`
Just run npm install and everything is set up automatically!
Performance
- WASM Parsing: All binary parsing done in WASM (10-50x faster than JavaScript)
- Draco Compression: Meshes are 80-90% smaller than uncompressed
- KTX2 Textures: GPU-native compression, fast loading and rendering
- Lazy Loading: Assets loaded on-demand, not all at once
- Memory Efficient: Minimal copying between WASM and JavaScript
Troubleshooting
$3
Make sure your .stow file was packed with the latest packer that writes bone parent indices correctly.
$3
Ensure /stowkit/basis/ folder contains the Basis Universal transcoder files (auto-copied on install).
$3
Make sure you've created an AudioListener` and attached it to your camera.