5.8.3

tres gltf

Generate a typed Vue component from a .glb/.gltf model, with a slot for every node.

tres gltf turns a model into a Vue component you can read, diff and type-check, instead of a <primitive :object="scene" /> black box.

tres gltf public/models/robot.glb
# ✔ src/models/Robot.gen.vue
#   3 slots: Head, Body, Base

Then use it like any other component:

App.vue
<script setup lang="ts">
import { OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import Robot from '@/models/Robot.gen.vue'
</script>

<template>
  <TresCanvas>
    <TresPerspectiveCamera :position="[3, 2, 5]" />
    <OrbitControls />
    <Robot />
    <TresAmbientLight :intensity="1" />
  </TresCanvas>
</template>

What gets generated

Every node in the model becomes a real element, and every node the artist named becomes a <slot> whose fallback is the generated markup:

src/models/Mug.gen.vue
<script setup lang="ts">
/*
Auto-generated by @tresjs/cli. Do not edit.
Command: tres gltf public/models/Mug.glb --slots all
Override the named slots from the parent instead; regenerating keeps your overrides.
*/
import type { Group, Mesh, MeshStandardMaterial } from 'three'
import { useGLTF } from '@tresjs/cientos'

interface ModelNodes {
  Scene: Group
  Mug: Group
  Mesh: Mesh
  Mesh_1: Mesh
}

interface ModelMaterials {
  '4k-Metal-Worn': MeshStandardMaterial
  'coat': MeshStandardMaterial
}

defineSlots<{
  Mesh?: (props: { node: Mesh, material: MeshStandardMaterial }) => any
  Mesh_1?: (props: { node: Mesh, material: MeshStandardMaterial }) => any
  Mug?: (props: { node: Group }) => any
}>()

const { nodes, materials, isLoading } = useGLTF<ModelNodes, ModelMaterials>('/models/Mug.glb', { draco: true })

defineExpose({ nodes, materials })
</script>

<template>
  <TresGroup :dispose="null">
    <template v-if="!isLoading">
      <slot name="Mug" :node="nodes.Mug">
        <TresGroup :position="[0, 1, 0]">
          <slot name="Mesh" :node="nodes.Mesh" :material="materials['4k-Metal-Worn']">
            <TresMesh :geometry="nodes.Mesh.geometry" :material="materials['4k-Metal-Worn']" />
          </slot>
          <slot name="Mesh_1" :node="nodes.Mesh_1" :material="materials.coat">
            <TresMesh :geometry="nodes.Mesh_1.geometry" :material="materials.coat" />
          </slot>
        </TresGroup>
      </slot>
    </template>
  </TresGroup>
</template>

A few things are worth pointing out:

  • ModelNodes and ModelMaterials describe this export, so nodes.Mug is a Group and not an any.
  • Draco is detected, and { draco: true } is passed automatically. A Draco model renders nothing without it.
  • Lights, cameras and bones are emitted as <primitive :object="..." />: their props live on the parsed object, and a bone must be the parsed object for skinning to work.
  • The root TresGroup stays mounted and gates its children with v-if="!isLoading", so refs on it are stable.

Overriding a node

The generated file is disposable. Anything you want to change lives in the parent, as a slot override:

App.vue
<script setup lang="ts">
import Robot from '@/models/Robot.gen.vue'
import { hologram } from './materials'
</script>

<template>
  <Robot>
    <template #Head="{ node }">
      <TresMesh :geometry="node.geometry" :material="hologram" @click="explode" />
    </template>
  </Robot>
</template>

Re-run tres gltf after the artist re-exports and the override survives, because it never lived in the generated file. If the artist renames Head, the override becomes a type error instead of silently disappearing at runtime.

--slots named (the default) skips exporter noise like Object_12, Sketchfab_model or Armature, since overriding those means nothing. On marketplace assets where every mesh is exporter-named, the CLI says so and points you at --slots all.

Nodes and materials from the parent

The component exposes nodes and materials, so a parent can reach into the model without re-loading it:

<script setup lang="ts">
import { ref } from 'vue'
import Robot from '@/models/Robot.gen.vue'

const robot = ref<InstanceType<typeof Robot>>()

function paintItRed() {
  robot.value?.materials.Body.color.set('#ff0055')
}
</script>

<template>
  <Robot ref="robot" />
</template>

Animated models

When the model ships animation clips, the generated component also wires up useAnimations and exposes actions, keyed by a union of the clip names:

type ActionName
  = | 'Idle'
    | 'Walk'
    | 'Death_A'
App.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Knight from '@/models/Knight.gen.vue'

const knight = ref<InstanceType<typeof Knight>>()

// actions.Idle, not actions['Idle'] — and a typo is a compile error
onMounted(() => knight.value?.actions.Idle?.play())
</script>

<template>
  <Knight ref="knight" />
</template>

Where the file is written

By default the component is written next to the model as <Model>.gen.vue, with one exception: if the model lives under public/, the component goes to src/models/ (or app/models/ on Nuxt) instead. A bundler copies public/ verbatim and never compiles it, so a component written there could never be imported.

tres gltf public/models/robot.glb        # → src/models/Robot.gen.vue
tres gltf src/assets/robot.glb           # → src/assets/Robot.gen.vue
tres gltf public/models/robot.glb -o src/components/Robot.vue

models/ rather than components/ is deliberate: component directories are auto-scanned by unplugin-vue-components and Nuxt, and the dot in .gen.vue produces an invalid identifier in the generated components.d.ts.

The CLI refuses to overwrite a file it did not generate. Pass --force to replace a hand-written file anyway.

The model url

The url baked into useGLTF is inferred from the nearest public/ directory above the model: public/models/robot.glb becomes /models/robot.glb. With no public/ in sight the CLI warns and guesses, so set it yourself:

tres gltf assets/robot.glb --url https://cdn.example.com/robot.glb

Inspecting a model

--dry-run reports what the parser sees and writes nothing:

tres gltf public/models/artificer.glb --dry-run
# public/models/artificer.glb
#   51 named nodes (8 meshes)
#   1 material
#   76 animation clips
#   run without --dry-run to generate a component

--json dumps the full parse, and --console prints the component to stdout instead of writing it, which is handy for piping or for a quick look before committing.

Options

FlagDefaultDescription
-o, --output <path><Model>.gen.vueWhere to write the component.
-u, --url <url>inferred from public/The url the model is served from at runtime.
-s, --slots <mode>namednamed, all or none.
--shadowsfalseAdd cast-shadow and receive-shadow to every mesh.
-K, --keepgroupsfalseKeep pass-through groups that carry nothing but nesting.
-k, --keepnamesfalseKeep name attributes on the emitted elements.
-r, --root <name>scene rootGenerate from this subtree only.
-p, --precision <digits>2Fractional digits kept on transforms.
-m, --metafalseEmit glTF extras as :user-data.
-c, --consolefalsePrint the component instead of writing it.
-f, --forcefalseOverwrite a file this tool did not generate.
--dry-runfalseReport what the parser sees, generate nothing.
--jsonfalsePrint the full parse as JSON.

Supported models

Both .glb and unpacked .gltf + .bin + textures work, as do Draco-compressed models. Pass the path to the file, not the url it is served from:

tres gltf /models/robot.glb        # ✗ that is the url
tres gltf public/models/robot.glb  # ✓ that is the file
Compare this workflow with useGLTF and GLTFModel in the cientos docs.