@stowkit/three-loader
v0.1.35
Published
Three.js loader for StowKit asset packs
Readme
@stowkit/three-loader
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.
Installation
npm install @stowkit/three-loader threeQuick Start
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
Asset Manifest
pack.listAssets(): AssetListItem[]
Get the complete manifest of all assets in the pack.
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:
{
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.
const count = pack.getAssetCount();
console.log(`Pack has ${count} assets`);pack.getAssetInfo(index: number): AssetInfo | null
Get detailed info about a specific asset.
const info = pack.getAssetInfo(5);
if (info) {
console.log(`Type: ${info.type}, Size: ${info.data_size} bytes`);
}Loading Assets
Static Meshes
// 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
// 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
// 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
AnimationMixeron the correct root object - Set the animation to loop infinitely
- Start playing immediately
- Return everything you need
Textures
// 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
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);Metadata Helpers
All metadata is parsed in WASM for performance and reliability.
Animation Metadata
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); // 65Audio Metadata
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
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)Asset Discovery
// 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
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:
// 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:
environmentPack.dispose();Asset Types
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.jsJust 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
Animations appear broken/offset from origin
Make sure your .stow file was packed with the latest packer that writes bone parent indices correctly.
Textures not loading
Ensure /stowkit/basis/ folder contains the Basis Universal transcoder files (auto-copied on install).
Audio not playing
Make sure you've created an AudioListener and attached it to your camera.
