tres gltf
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:
<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:
<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:
ModelNodesandModelMaterialsdescribe this export, sonodes.Mugis aGroupand not anany.- 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
TresGroupstays mounted and gates its children withv-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:
<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'
<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.
--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
| Flag | Default | Description |
|---|---|---|
-o, --output <path> | <Model>.gen.vue | Where to write the component. |
-u, --url <url> | inferred from public/ | The url the model is served from at runtime. |
-s, --slots <mode> | named | named, all or none. |
--shadows | false | Add cast-shadow and receive-shadow to every mesh. |
-K, --keepgroups | false | Keep pass-through groups that carry nothing but nesting. |
-k, --keepnames | false | Keep name attributes on the emitted elements. |
-r, --root <name> | scene root | Generate from this subtree only. |
-p, --precision <digits> | 2 | Fractional digits kept on transforms. |
-m, --meta | false | Emit glTF extras as :user-data. |
-c, --console | false | Print the component instead of writing it. |
-f, --force | false | Overwrite a file this tool did not generate. |
--dry-run | false | Report what the parser sees, generate nothing. |
--json | false | Print 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