From model to component
tres gltf exists because a loaded model is a black box, and a Vue component is not. This page
follows one small team through the difference. If you want the reference instead, go to
tres gltf.
The team
Two people ship a product configurator. An artist owns robot.glb in Blender and re-exports it
several times a week. A Vue developer owns the app and has never opened Blender. Both want the
same thing: change the model without breaking the app, and change the app without touching the
model.
Day one: the black box
The developer loads the model the usual way and drops the scene into the canvas:
<script setup lang="ts">
import { useGLTF } from '@tresjs/cientos'
const { scene, nodes } = await useGLTF('/models/robot.glb')
</script>
<template>
<primitive :object="scene" />
</template>
It renders. Then the first ticket arrives: the head should glow when hovered. The developer now needs one mesh out of the scene graph, and the only handle is a string:
const head = nodes.Head // Object3D, maybe. Is it a Mesh? Does it exist?
;(head as Mesh).material = hologram
Three things are true about this line, and none of them are visible in the editor:
nodes.Headis typed as a genericObject3D. The cast toMeshis a guess.- The name
Headcame from a Blender outliner the developer cannot see. - Nothing checks the string. If it is wrong,
headisundefinedat runtime and the page throws.
The developer gets it working by logging nodes to the console and copying names out of it. The
app ships.
Day two: the re-export
The artist splits the head into Helmet and Face and re-exports. No code changed, so no test
fails and no type error appears. The build is green. In the browser, the head no longer glows, and
in production it throws on undefined.material.
This is the failure tres gltf is built around. The contract between the model and the app lived
in a string, and a string cannot tell you when it goes stale.
Running the command
The developer generates a component from the model instead:
tres gltf public/models/robot.glb
# ▲ ■ ● Tres gltf robot.glb
#
# ✔ Parse 12 named nodes · 3 meshes · 2 materials 34ms
# ✔ Emit 3 slots 1ms
#
# ✔ src/models/Robot.gen.vue
# slots Helmet, Face, Body, Base
#
# Done in 41ms
The output is a .vue file. That single fact changes what the team can do with the model:
- Read it. Every node is a
<TresMesh>or<TresGroup>with its transform written out. The developer sees the structure of the model without opening Blender. - Diff it. The next re-export produces a new file, and the pull request shows exactly which nodes moved, appeared or disappeared. The artist's change is reviewable like any other change.
- Type-check it. The component declares the nodes, materials and clips the model actually has. The rest of the app compiles against that declaration.
Overriding without owning
The generated file is disposable. The head glow does not go in it. It goes in the parent, as a slot override, and the generated markup stays as the fallback:
<script setup lang="ts">
import Robot from '@/models/Robot.gen.vue'
import { hologram } from './materials'
</script>
<template>
<Robot>
<template #Helmet="{ node }">
<TresMesh :geometry="node.geometry" :material="hologram" @pointerenter="glow" />
</template>
</Robot>
</template>
node here is a Mesh, not an Object3D, because the generator saw a mesh at that node and said
so. The cast is gone. The console logging is gone. And the next re-export overwrites
Robot.gen.vue without touching App.vue, so the override survives every export the artist makes.
The rename becomes a type error
On day two the artist renamed a node and the app broke in production. With the component, the same rename fails in the editor:
App.vue:9:15 - error TS2339: Property 'Head' does not exist on type
'{ Helmet: (props: { node: Mesh }) => any; Face: …; Body: …; Base: … }'.
The developer reads the new slot names off the error, updates the override, and the failure never reaches a browser. The contract now lives in a type, and types go stale loudly.
Animations with names
The artist adds an idle loop. The generated component picks up the clip and hands the bound
actions to @ready, keyed by a union of the clip names:
<Robot @ready="({ actions }) => actions.Idle?.play()" />
actions.Idle autocompletes. actions.Idel is a compile error. When the artist ships the clips in
separate files, as Mixamo and most asset packs do, --animations merges them and the CLI checks
each clip's tracks against the model's node names, which is the one animation failure that is
otherwise silent at runtime.
Physics without a spreadsheet
The configurator grows a showroom floor the robot can fall onto. Instead of the developer measuring
boxes and typing collider sizes, the artist names the meshes in Blender: Floor-col,
Crate-rigid, Trigger-sensor. The command reads the names:
tres gltf public/models/showroom.glb --physics rapier
Each suffixed mesh becomes a RigidBody sized from its own geometry when the model loads. The
artist reshapes the floor, the collider follows, and no number in the codebase has to be updated.
Shipping smaller, shipping many
Two flags close the loop for production:
--transformruns the model through glTF-Transform and points the component at arobot-transformed.glbthat is typically 70 to 90 percent smaller. The original stays untouched.--instancebatches repeated meshes into anInstancedMesh, so the showroom can render twenty robots for the draw calls of one.
Both are decisions the developer makes once, in the command, and the artist never sees them.
What changed for the team
| Before | After | |
|---|---|---|
| Where a node's name lives | In a string the developer typed | In a slot the compiler checks |
| What a renamed node does | Throws in production | Fails to compile |
| Where an override lives | Inside a traverse() over the scene | In the parent, as a slot |
| What a re-export costs | Re-test every override by hand | Re-run one command, read the diff |
| Who authors physics | The developer, with numbers | The artist, with names |
| What a reviewer sees | A new binary in the PR | A readable .vue diff |
The artist still exports from Blender. The developer still writes Vue. The command sits between them and turns the handoff into a type.