Model Pipeline Reference
Complete guide to tostada’s 3D model build system: converting GLTF/GLB files into optimized Svelte components.
The model pipeline automates the conversion of 3D assets into type-safe, tree-shakeable Svelte components with lazy loading support.
Overview
The model pipeline transforms raw 3D assets into production-ready Svelte components:
GLTF/GLB files → @threlte/gltf → Svelte components + optimized GLB + Registry Input: .gltf or .glb files in obj/ Output:
- Svelte components in
client/src/lib/models/generated/ - Optimized GLB files in
client/static/models/ - Registry file for lazy loading (
generated-registry.ts) - JSON manifest for server (
server/priv/model_keys.json)
Quick Start
1. Add Your Models
Place .gltf or .glb files in obj/ at the project root:
mkdir -p obj
cp ~/Downloads/robot.glb obj/ Subdirectories supported:
obj/
├── characters/
│ ├── player.glb
│ └── enemy.glb
├── props/
│ ├── tree.gltf
│ ├── rock.gltf
│ └── Textures/ # Automatically copied
│ └── bark.png
└── vehicles/
└── spaceship.glb 2. Run the Build
make models.build What happens:
Script scans
obj/for.gltf/.glbfiles (recursively)Converts each file using
@threlte/gltfGenerates Svelte components with TypeScript types
Optimizes and transforms GLB files
Creates lazy-loading registry
Outputs summary:
Converting 3 models in characters... [1/3] player [2/3] enemy [3/3] boss Done! Generated 3 Svelte components in ./client/src/lib/models/generated/ Generated 3 GLB files in ./client/static/models/ Registry: ./client/src/lib/models/generated-registry.ts
3. Use in Your Scene
<script lang="ts">
import { PlayerModel } from '$lib/models/generated/characters';
</script>
<PlayerModel position={[0, 0, 0]} scale={1.5} /> Directory Structure
Before Build
project/
├── obj/ # Source models (gitignored)
│ ├── robot.glb
│ └── props/
│ ├── tree.gltf
│ └── Textures/
│ └── bark.png
└── scripts/
└── build-models.sh After Build
project/
├── obj/ # (unchanged)
├── client/
│ ├── src/lib/models/
│ │ ├── generated/ # Auto-generated Svelte components
│ │ │ ├── robot.svelte
│ │ │ └── props/
│ │ │ └── tree.svelte
│ │ └── generated-registry.ts # Lazy-load registry
│ └── static/models/ # Optimized GLBs (served at /models/)
│ ├── robot-transformed.glb
│ └── props/
│ ├── tree-transformed.glb
│ └── Textures/
│ └── bark.png
└── server/priv/
└── model_keys.json # Server-side manifest Generated Files
Svelte Components
Each model becomes a fully-typed Svelte component:
Example: client/src/lib/models/generated/robot.svelte
<script lang="ts">
import { useGltf } from '@threlte/extras';
import type { Vector3, Euler } from 'three';
interface Props {
position?: Vector3 | [number, number, number];
rotation?: Euler | [number, number, number];
scale?: number | [number, number, number];
}
let { position = [0, 0, 0], rotation = [0, 0, 0], scale = 1 }: Props = $props();
const gltf = useGltf('/models/robot-transformed.glb');
</script>
{#await gltf then { scene }}
<T.Group {position} {rotation} {scale}>
<T.Primitive object={scene} />
</T.Group>
{/await} Features:
- TypeScript type definitions included
- Props for
position,rotation,scale - Lazy-loads GLB only when component mounts
- Preserves original model structure (meshes, materials, animations)
Registry File
Location: client/src/lib/models/generated-registry.ts
Contains a lookup table for all models with lazy imports:
/**
* Auto-generated by scripts/build-models.sh - do not edit.
*/
export const models = {
robot: () => import('./generated/robot.svelte'),
props_tree: () => import('./generated/props/tree.svelte'),
characters_player: () => import('./generated/characters/player.svelte'),
}; Naming convention:
- Subdirectories become prefixes:
props/tree.glb→props_tree - Hyphens and camelCase converted to snake_case:
TreeLarge.glb→tree_large - Ensures unique keys even when different folders have same filename
Usage:
import { models } from '$lib/models/generated-registry';
// Load model dynamically by key
const modelKey = 'props_tree';
const TreeComponent = await models[modelKey](); JSON Manifest
Location: server/priv/model_keys.json
Server-side list of all available models (for validation, admin tools, etc.):
{
"generated_at": "2026-02-08T10:30:00Z",
"keys": [
"robot",
"props_tree",
"characters_player"
]
} Use case: Validate that a model exists before sending it to the client.
defmodule Tostada.ModelRegistry do
def valid_model?(key) do
keys = File.read!("priv/model_keys.json") |> Jason.decode!() |> Map.get("keys")
key in keys
end
end Usage Patterns
Basic Usage
Import and use like any Svelte component:
<script lang="ts">
import { RobotModel } from '$lib/models/generated';
import { PropsTreeModel } from '$lib/models/generated/props';
</script>
<RobotModel position={[0, 0, 0]} scale={1.5} />
<PropsTreeModel position={[2, 0, 0]} rotation={[0, Math.PI / 4, 0]} /> Dynamic Loading
Load models by key from the registry:
<script lang="ts">
import { models } from '$lib/models/generated-registry';
let currentModel = $state<any>(null);
async function loadModel(key: string) {
const module = await models[key]();
currentModel = module.default;
}
$effect(() => {
loadModel('robot');
});
</script>
{#if currentModel}
<svelte:component this={currentModel} position={[0, 0, 0]} />
{/if} Preloading
Preload models before they’re needed to avoid stuttering:
import { preloadGLTF } from '@threlte/extras';
// Preload during loading screen
await Promise.all([
preloadGLTF('/models/robot-transformed.glb'),
preloadGLTF('/models/props/tree-transformed.glb')
]); Accessing Animations
If your GLB contains animations:
<script lang="ts">
import { useGltf } from '@threlte/extras';
const gltf = useGltf('/models/robot-transformed.glb');
</script>
{#await gltf then { scene, animations }}
<T.Group>
<T.Primitive object={scene} />
</T.Group>
{#if animations.length > 0}
<!-- Use AnimationMixer from @threlte/extras -->
<AnimationMixer {animations} />
{/if}
{/await} Advanced Features
Texture Handling
The pipeline automatically copies Textures/ folders:
obj/props/
├── castle.gltf
└── Textures/
├── colormap.png
└── normalmap.png Result:
client/static/models/props/
├── castle-transformed.glb
└── Textures/
├── colormap.png
└── normalmap.png The GLB references remain valid: /models/props/Textures/colormap.png
Nested Subdirectories
Organize models hierarchically:
obj/
└── game/
├── characters/
│ ├── player.glb
│ └── enemy.glb
└── environment/
├── trees.glb
└── rocks.glb Generated keys:
game_characters_playergame_characters_enemygame_environment_treesgame_environment_rocks
Custom Model Metadata
The @threlte/gltf tool generates metadata files:
Example: client/src/lib/models/generated/robot.meta.json
{
"materials": ["RobotMaterial"],
"meshes": ["RobotMesh"],
"nodes": ["Armature", "Body", "Head"],
"textures": []
} Use case: Introspect model structure without loading it.
Build Process Details
@threlte/gltf CLI
The script calls @threlte/gltf with these flags:
npx @threlte/gltf input.glb
-o ./output-dir
--types
--keepnames
--meta
--root="/models/subdir/"
--transform | Flag | Purpose |
|---|---|
--types | Generate TypeScript type definitions |
--keepnames | Preserve original mesh/material names |
--meta | Output metadata JSON file |
--root | Set base path for GLB URLs (prevents /models/models/ duplication) |
--transform | Optimize GLB (compress, deduplicate, merge geometries) |
Optimization
--transform applies these optimizations:
- Compression: Draco mesh compression (reduces file size by 70-90%)
- Deduplication: Remove duplicate geometries and materials
- Texture optimization: Resize oversized textures, convert to WebP
- Node merging: Flatten unnecessary transform hierarchies
Result: Smaller files, faster loading, better runtime performance.
Common Workflows
Adding a New Model
# 1. Download or export your model
cp ~/Downloads/spaceship.glb obj/
# 2. Build
make models.build
# 3. Use in scene <script>
import { SpaceshipModel } from '$lib/models/generated';
</script>
<SpaceshipModel /> Updating an Existing Model
# 1. Replace the source file
cp ~/Downloads/spaceship-v2.glb obj/spaceship.glb
# 2. Rebuild
make models.build
# 3. Refresh browser (Vite HMR will pick up changes) Removing Models
# 1. Delete source file
rm obj/robot.glb
# 2. Clean generated files
make models.clean
# 3. Rebuild
make models.build Troubleshooting
“No obj/ directory” Error
Cause: obj/ folder doesn’t exist.
Solution:
mkdir -p obj
cp your-model.glb obj/
make models.build GLB Not Loading in Browser
Cause: GLB path is incorrect or file not served.
Solutions:
- Check browser DevTools Network tab for 404 errors
- Verify GLB exists:
ls client/static/models/ - Ensure Vite is serving
/models/: Checkvite.config.tspublicDir
Model Appears Black/Unlit
Cause: Missing textures or lighting in scene.
Solutions:
- Add lights to scene:
<T.DirectionalLight intensity={1} /> - Check if textures copied:
ls client/static/models/Textures/ - Verify material has
colorormapproperty
“Failed to convert” Warning
Cause: Invalid GLTF/GLB file or @threlte/gltf error.
Solutions:
- Validate model in Blender or gltf.report
- Re-export from your 3D software with correct settings
- Check terminal output for specific error details
Registry Not Updating
Cause: Cached old registry file.
Solution:
make models.clean
make models.build Performance Tips
Model Size
- Target: < 5 MB per GLB (after compression)
- Large models: Split into multiple GLBs or use LOD (Level of Detail)
Texture Resolution
- 2K (2048×2048): Good for hero assets
- 1K (1024×1024): Standard for props
- 512×512: Small objects, background elements
Polygon Count
- < 10K triangles: Real-time rendering friendly
- 10K-50K: Fine for hero characters with LOD
- > 50K: Consider decimation or LOD streaming
Lazy Loading
Load models only when needed:
// Don't preload everything
const models = {
'level1': () => import('./generated/level1.svelte'),
'level2': () => import('./generated/level2.svelte'), // Not loaded until needed
};
// Load on demand
const currentLevel = await models['level1'](); Integration with Addons
The model pipeline is part of the model_pipeline addon (enabled by default).
Disabling the Addon
If you don’t need 3D models:
npx tostada create my-app -threlte,-model_pipeline Re-enabling Later
Manually add to package.json:
{
"devDependencies": {
"@threlte/gltf": "^2.0.0"
}
} Then run npm install and use make models.build.
Next Steps
- See the addon docs: Model Pipeline Addon
- Use in a game: Scene Management Guide
- Learn Threlte: Threlte Documentation
Questions? Check the @threlte/gltf documentation for advanced usage.