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:

  1. Script scans obj/ for .gltf/.glb files (recursively)

  2. Converts each file using @threlte/gltf

  3. Generates Svelte components with TypeScript types

  4. Optimizes and transforms GLB files

  5. Creates lazy-loading registry

  6. 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.glbprops_tree
  • Hyphens and camelCase converted to snake_case: TreeLarge.glbtree_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_player
  • game_characters_enemy
  • game_environment_trees
  • game_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
FlagPurpose
--typesGenerate TypeScript type definitions
--keepnamesPreserve original mesh/material names
--metaOutput metadata JSON file
--rootSet base path for GLB URLs (prevents /models/models/ duplication)
--transformOptimize 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/: Check vite.config.ts publicDir

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 color or map property

“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


Questions? Check the @threlte/gltf documentation for advanced usage.