@threlte/gltf

Getting Started

A small command-line tool that turns GLTF assets into declarative and re-usable Threlte components.

The GLTF workflow on the web is not ideal.

  • GLTF is thrown wholesale into the scene which prevents re-use, in threejs objects can only be mounted once
  • Contents can only be found by traversal which is cumbersome and slow
  • Changes to queried nodes are made by mutation, which alters the source data and prevents re-use
  • Re-structuring content, making nodes conditional or adding/removing is cumbersome
  • Model compression is complex and not easily achieved
  • Models often have unnecessary nodes that cause extra work and matrix updates

@threlte/gltf fixes that.

  • It creates a virtual graph of all objects and materials. Now you can easily alter contents and re-use.
  • The graph gets pruned (empty groups, unnecessary transforms, …) for better performance.
  • It will optionally compress your model with up to 70%-90% size reduction.

Usage

npx @threlte/gltf@latest /path/to/Model.glb [options]
The CLI supports only npx at this moment.

Options

OptionDescription
--output, -oOutput file name/path
--types, -tAdd Typescript definitions
--keepnames, -kKeep original names
--meta, -mInclude metadata (as userData)
--shadows, -sLet meshes cast and receive shadows
--printwidth, -wPrettier printWidth (default: 120)
--precision, -pNumber of fractional digits (default: 2)
--draco, -dDraco binary path
--preload -PAdd preload method to module script
--suspense -uMake the component suspense-ready
--isolated, -iOutput as isolated module (No $$restProps usage)
--root, -rSets directory from which .gltf file is served
--transform, -TTransform the asset for the web (draco, prune, resize)
     --resolution, -RTransform resolution for texture resizing (default: 1024)
     --simplify, -STransform simplification (default: false, experimental)
          --weldWeld tolerance (default: 0.0001)
          --ratioSimplifier ratio (default: 0.75)
          --errorSimplifier error threshold (default: 0.001)
--debug, -DDebug output

Example

This example assumes you have your model set up and exported from an application like Blender as a GLTF file.

First you run your model through @threlte/gltf. npx allows you to use npm packages without installing them.

npx @threlte/gltf@latest model.gltf --transform

This will create a Model.svelte file that plots out all of the assets contents.

<!--
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@0.0.1 ./stacy.glb
-->
<script>
  import { Group } from 'three'
  import { T } from '@threlte/core'
  import { useGltf, useGltfAnimations } from '@threlte/extras'

  let {
    ref = $bindable(),
    actions = $bindable(),
    mixer = $bindable(),
    children,
    ...props
  } = $props()

  const gltf = useGltf('/stacy.glb')
  const animations = useGltfAnimations(gltf, ref)
  actions = animations.actions
  mixer = animations.mixer
</script>

{#if $gltf}
  <T
    is={ref}
    {...props}
  >
    <T.Group name="Scene">
      <T.Group
        name="Stacy"
        rotation={[Math.PI / 2, 0, 0]}
        scale={0.01}
      >
        <T is={$gltf.nodes.mixamorigHips} />
        <T.SkinnedMesh
          name="stacy"
          geometry={$gltf.nodes.stacy.geometry}
          material={$gltf.nodes.stacy.material}
          skeleton={$gltf.nodes.stacy.skeleton}
          rotation={[-Math.PI / 2, 0, 0]}
          scale={100}
        />
      </T.Group>
    </T.Group>

    {@render children?.({ ref })}
  </T>
{/if}

Add your model to your /static folder as you would normally do. With the --transform flag it has created a compressed copy of it (in the above case model-transformed.glb). Without the flag just copy the original model.

static/
  model-transformed.glb

The component can now be dropped into your scene.

<script>
  import { Canvas } from '@threlte/core'
  import Model from './Model.svelte'
</script>

<Canvas>
  <Model />
</Canvas>

You can re-use it, it will re-use geometries and materials out of the box:

<Model position={[0, 0, 0]} />
<Model position={[10, 0, -10]} />

Or make the model dynamic. Change its colors for example:

<T.Mesh
  geometry={$gltf.nodes.robot.geometry}
  material={$gltf.materials.metal}
  material.color="green"
/>

Or exchange materials:

<T.Mesh geometry={$gltf.nodes.robot.geometry}>
  <T.MeshPhysicalMaterial color="hotpink" />
</T.Mesh>

Make contents conditional:

{#if condition}
  <T.Mesh
    geometry={$gltf.nodes.robot.geometry}
    material={$gltf.materials.metal}
  />
{/if}

DRACO Compression

You don’t need to do anything if your models are draco compressed, since useGltf defaults to a draco CDN. By adding the --draco flag you can refer to local binaries which must reside in your /public folder.

Auto-Transform

With the --transform flag it creates a binary-packed, draco-compressed, texture-resized (1024x1024), webp compressed, deduped, instanced and pruned *.glb ready to be consumed on a web site. It uses glTF-Transform. This can reduce the size of an asset by 70%-90%.

It will not alter the original but create a copy and append [modelname]-transformed.glb.

Type-Safety

Add the --types flag and your component will be typesafe.

<!--
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@0.0.1 ./stacy.glb -t
-->
<script lang="ts">
  import type * as THREE from 'three'
  import { Group } from 'three'
  import { T, type Props } from '@threlte/core'
  import { useGltf, useGltfAnimations } from '@threlte/extras'
  import type { Snippet } from 'svelte'

  type Props = {
    ref?: Group
    actions?: ReturnType<typeof useGltfAnimations>['actions']
    mixer?: ReturnType<typeof useGltfAnimations>['mixer']
    children?: Snippet<[{ ref: Group }]>
  }

  let {
    ref = $bindable()
    actions = $bindable(),
    mixer = $bindable(),
    children,
    ...props
  }: Props = $props()

  const group = new Group()

  type ActionName =
    | 'pockets'
    | 'rope'
    | 'swingdance'
    | 'jump'
    | 'react'
    | 'shrug'
    | 'wave'
    | 'golf'
    | 'idle'
  type GLTFResult = {
    nodes: {
      stacy: THREE.SkinnedMesh
      mixamorigHips: THREE.Bone
    }
    materials: {}
  }

  const { actions, mixer } = useGltfAnimations<ActionName>(gltf, ref)
  const gltf = useGltf<GLTFResult>('/stacy.glb')

  actions = animations.actions
  mixer = animations.mixer
</script>

{#if $gltf}
  <T
    bind:ref
    is={group}
    {...props}
  >
    <T.Group name="Scene">
      <T.Group
        name="Stacy"
        rotation={[Math.PI / 2, 0, 0]}
        scale={0.01}
      >
        <T is={$gltf.nodes.mixamorigHips} />
        <T.SkinnedMesh
          name="stacy"
          geometry={$gltf.nodes.stacy.geometry}
          material={$gltf.nodes.stacy.material}
          skeleton={$gltf.nodes.stacy.skeleton}
          rotation={[-Math.PI / 2, 0, 0]}
          scale={100}
        />
      </T.Group>
    </T.Group>

    {@render children?.({ ref })}
  </T>
{/if}

Animations

If your GLTF contains animations it will add @threlte/extras’s useGltfAnimations hook, which extracts all clips and prepares them as actions:

const gltf = useGltf('/stacy.glb') export const {(actions, mixer)} = useGltfAnimations(gltf, ref)

If you want to play an animation you can do so at any time:

const onEvent = () => {
  $actions.jump.play()
}

Suspense

If you want to use the component <Suspense> to suspend the rendering of loading components (and therefore models) and optionally show a fallback in a parent component component, you can do so by passing the flag --suspense to make the generated Threlte component suspense-ready:

<script>
  import Model from './Model.svelte'
  import Fallback from './Fallback.svelte'
  import { Suspense } from '@threlte/extras'
</script>

<Suspense>
  {#snippet fallback()}
    <Fallback />
  {/snippet}
  <Model />
</Suspense>

Asset Pipeline

In larger projects with a lot of models and assets, it’s recommended to set up an asset pipeline with tools like npm-watch and Node.js scripts to automatically transform models to Threlte components and copy them to the right place as this makes iterating over models and assets much faster.