npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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 three

Quick 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 AnimationMixer on 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);       // 65

Audio 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.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

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.