5.8.1

Loading Models

The three ways to bring a glTF model into a TresJS scene, and when to use each one.

Cientos gives you three ways to get a .glb/.gltf model on screen. They are not alternatives to each other so much as three points on the same line: the further down this page you go, the more control you get over the model's tree.

ApproachControlTypedBest for
<GLTFModel />Whole scene, as-isNoDropping a model in as-is
useGLTFPick nodes and materials by handOptionalReusing parts of a model
tres gltf codegenEvery node is an elementYesProduction scenes, interaction, per-node overrides

Drop the model in

The fastest path. GLTFModel loads the file and renders its scene graph untouched:

TheModel.vue
<script setup lang="ts">
import { GLTFModel } from '@tresjs/cientos'
</script>

<template>
  <GLTFModel path="/models/mug.glb" />
</template>

You get the whole model or nothing. There is no way to swap one mesh's material, attach a click handler to a door, or hide a node, because none of them exist as elements in your template.

Pick the parts you need

useGLTF hands you the parsed nodes and materials, so you compose the scene yourself:

TheModel.vue
<script setup lang="ts">
import { useGLTF } from '@tresjs/cientos'

const { nodes, materials, isLoading } = useGLTF('/models/mug.glb')
</script>

<template>
  <TresMesh
    v-if="!isLoading"
    :geometry="nodes.Mesh.geometry"
    :material="materials.coat"
    @click="onClick"
  />
</template>

This is the right tool when you only want a couple of nodes out of a bigger file. For a whole model it stops scaling: you are hand-writing an element per node, re-reading the tree in Blender to find the names, and re-writing all of it when the artist re-exports.

nodes and materials are keyed by the names in the file, so their shape is only known at runtime. Pass the shape as generics to get them typed, see Typed nodes and materials below.

Generate a component with the CLI

@tresjs/cli writes that element-per-node component for you, typed, from the model itself:

npx @tresjs/cli gltf public/models/mug.glb
# ✔ src/models/Mug.gen.vue
#   1 slot: Mug
src/models/Mug.gen.vue
<script setup lang="ts">
/*
Auto-generated by @tresjs/cli. Do not edit.
Command: tres gltf public/models/mug.glb
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<{
  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]">
          <TresMesh :geometry="nodes.Mesh.geometry" :material="materials['4k-Metal-Worn']" />
          <TresMesh :geometry="nodes.Mesh_1.geometry" :material="materials.coat" />
        </TresGroup>
      </slot>
    </template>
  </TresGroup>
</template>

Import it like any other component:

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

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

Overriding a node

Every named node is a <slot> whose fallback is the generated markup, so you change one mesh from the parent without touching the generated file:

App.vue
<template>
  <Mug>
    <template #Mug="{ node }">
      <TresMesh :geometry="node.geometry" :material="hologram" @click="onClick" />
    </template>
  </Mug>
</template>

Regenerate after the artist re-exports and the override survives, because it never lived in the generated file. If the node is renamed, the override becomes a type error instead of quietly doing nothing at runtime.

Treat *.gen.vue files as build output: edit the parent, not the generated file. The CLI refuses to overwrite a file it did not generate, so a hand-edited one needs --force to regenerate.
Full tres gltf reference: slots, animations, shadows, Draco and every flag.

Typed nodes and materials

useGLTF takes two generics that type nodes and materials. The CLI writes them for you, but you can also declare them by hand:

import type { Mesh, MeshStandardMaterial } from 'three'
import { useGLTF } from '@tresjs/cientos'

interface ModelNodes { Body: Mesh }
interface ModelMaterials { Skin: MeshStandardMaterial }

const { nodes, materials } = useGLTF<ModelNodes, ModelMaterials>('/models/robot.glb')

nodes.value.Body.geometry // Mesh, not any
The names come from the file at runtime, so these interfaces are a claim about the model, not a proof. That is exactly why generating them from the model is worth it.

Animated models

Clips live on the loaded state, and useAnimations turns them into actions:

Knight.vue
<script setup lang="ts">
import { useAnimations, useGLTF } from '@tresjs/cientos'
import { computed, ref, watch } from 'vue'

const { state, nodes, isLoading } = useGLTF('/models/knight.glb')

const modelRef = ref()
const animations = computed(() => state.value?.animations ?? [])
const { actions } = useAnimations(animations, modelRef)

watch(actions, current => current.Idle?.play())
</script>

<template>
  <TresGroup ref="modelRef">
    <primitive v-if="!isLoading" :object="nodes.Rig" />
  </TresGroup>
</template>

A generated component wires all of that up and exposes actions keyed by a union of the model's clip names, so actions.Idle type-checks and actions.Idl does not:

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

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

onMounted(() => knight.value?.actions.Idle?.play())
</script>

<template>
  <Knight ref="knight" />
</template>
Keep the element carrying the animation ref mounted and gate its children on loading. A ref on a v-ifed element is still undefined one flush after the clips land, and the mixer would then be built with no root to bind against.

Draco-compressed models

Pass draco: true and the loader pulls the decoder from Google's CDN, or point decoderPath at a local copy:

const { state } = useGLTF('/models/mug.glb', { draco: true })

A Draco model renders nothing without it. The CLI detects compression while parsing and writes { draco: true } into the generated component for you.

Where to find models