Core Concepts
TroisCanvas
A Trois app needs to be wrapped in a TroisCanvas
component:
<TroisCanvas>
<!-- Your code -->
</TroisCanvas>
In a .vue single file component, this will be inside the <template>
tag.
Three.js and Trois
Most Trois concepts are direct translations of Three.js concepts - let's look at meshes as an example.
A standard Three.js mesh requires a geometry and a material. Three.js code to create and add a yellow cube to the scene might look like this:
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
In Trois, this produces the same result:
<TroisCanvas>
<mesh>
<boxGeometry :args="[1, 1 ,1]"/>
<meshBasicMaterial :color="0xffff00"/>
</mesh>
</TroisCanvas>
A few points to note:
- Trois component names match class names in Three.js.
// three.js
new THREE.Mesh()
<!-- trois -->
<mesh/>
// three.js
new THREE.BoxGeometry()
<!-- trois -->
<boxGeometry/>
- Every component can accept an
args
prop that take an array of arguments to pass to class constructors. This in Three.js:
new THREE.BoxGeometry(1, 2, 3)
would map to this in Trois:
<boxGeometry :args="[1, 2, 3]"/>
- Any property you can add to a Three.js object (for example, the
color
property of aMeshBasicMaterial
) can be added as a reactive prop on a Trois component:
const material = new THREE.MeshBasicMaterial()
material.color = 0xffff00
would map to this in Trois:
<meshBasicMaterial :color="0xffff00"/>
- You can set a property that would normally use dot notation with a dash in the prop name. For example, this in Three.js:
const mesh = new THREE.Mesh()
mesh.position.z = 5
would map to this in Trois:
<mesh :position-z="5"/>
Components
You can use most Three.js classes in Trois, such as:
Group
Mesh
- Geometries (box, icosahedron, plane, etc)
- Materials (standard, basic, points, shader, etc)
- Lights (spotlights, point lights, directional, etc)
For more information, see Components.
To add any features not in that list, see the instructions under Extend.
Hierarchy
Items that inherit from THREE.Object3D will follow the hierarchy of your Trois template. For example, this Trois code:
<!-- 1 -->
<group>
<!-- 2 -->
<mesh/>
<!-- 3 -->
<mesh>
<!-- 4 -->
<mesh/>
</mesh>
</group>
would create:
- A
Group
(1) with two direct children (2 and 3) - A
Mesh
(2) with no children - Another
Mesh
(3) with one child (4) - Another
Mesh
(4) with no children
Just like with Three.js, any transform changes you apply to a parent element will be applied to the children as well. Changing the group
's position-x
value would move all four objects as a single unit.
attach
There are several classes in Three.js that do not inherit from Object3D
. You can access instances of those classes with the attach
property.
Summary
Any component with an attach
prop will save itself as $attached.<propValue>
in its parent.
Its parent can access these values in any prop by using the string '$attached.<propValue>'
.
<!-- $attached.bar will be replaced with whatever the <child> component instantiates -->
<parent :foo="'$attached.bar'">
<child attach="bar"/>
</parent>
This is most commonly used in args
:
<parent :args="[1, 2, 3, '$attached.foo']">
<child attach="foo"/>
</parent>
<!-- equivalent to:
const foo = new THREE.Child()
const parent = new THREE.Parent(1, 2, 3, foo)
-->
Auto-registration
Geometries and materials are so frequently used that Trois handles attaching them automatically.
<mesh>
<boxGeometry />
<meshBasicMaterial />
</mesh>
is syntactic sugar for
<mesh :args="['$attached.geometry', '$attached.material']">
<boxGeometry attach="geometry"/>
<meshBasicMaterial attach="material" />
</mesh>
In-Depth
In a standard Three.js setup, a Mesh requires both a Geometry and a Material at instantiation:
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshBasicMaterial()
// geometry and material are passed to the mesh
const mesh = new THREE.Mesh(geometry, material)
In Trois, that same setup looks like this:
<mesh>
<boxGeometry />
<meshBasicMaterial />
</mesh>
How does the Trois <mesh>
know to use the <boxGeometry>
as its geometry and the <meshBasicMaterial>
as the material? Behind the scenes, Trois treats the above code like this:
<mesh :args="['$attached.geometry', '$attached.material']">
<boxGeometry attach="geometry"/>
<meshBasicMaterial attach="material" />
</mesh>
and takes the following steps:
- A
new THREE.BoxGeometry()
is created. (Trois instantiates items from the most nested child upwards.) - Trois registers the
attach
prop on the<boxGeometry>
. This prop tells the new instance to save itself as a property calledgeometry
on its parent's$attached
property. At this point, this is the structure in JS:
const unknownParent = { $attached: {} }
// geometry
const newGeometry = new THREE.BoxGeometry()
// the `attach="geometry"` prop does the following:
unknownParent.$attached.geometry = newGeometry
- A
new THREE.MeshBasicMaterial()
is created. - Trois attaches the new material to its parent's
$attached.material
property, so the structure is:
// unknownParent = { $attached: { geometry: newGeometry } }
// material
const newMaterial = new THREE.MeshBasicMaterial()
// the `attach="material"` prop does the following:
unknownParent.$attached.material = newMaterial
- The array in
args
is registered:
// from the mesh's `args` property
let args = ['$attached.geometry', '$attached.material']
- Trois will automatically take strings starting with
$attached
and attempt to replace them with matching values:
/*
unknownParent = {
$attached: {
geometry: newGeometry,
material: newMaterial
}
}
*/
let args = ['$attached.geometry', '$attached.material']
args = args.map(arg => {
if (arg.startsWith('$attached')){
// `$attached.geometry` becomes `geometry`
// `$attached.material` becomes `material`
const property = arg.replace('$attached.', '')
// fallback to the arg if nothing found
return unknownParent.$attached[property] ?? arg
} else {
// non `$attached` properties stay as-is
return arg
}
})
// args === [unknownParent.$attached.newGeometry, unknownParent.$attached.newMaterial]
// args === [newGeometry, newMaterial]
- The transformed
args
array is spread into the instantiation call:
// args === [newGeometry, newMaterial]
const finalParent = new THREE.Mesh(...args)
instance
Sometimes you'll need to access a Three.js object in your JS. You can do this by accessing an element's instance
property. For example:
<template>
<TroisCanvas>
<mesh ref="mesh"/>
</TroisCanvas>
</template>
<script>
export default {
mounted(){
console.log(this.$refs.mesh.instance)
}
}
</script>
The Vue reactivity system is great for occasional changes, but for smooth motion, changing properties on instance
will be give you the best result:
<template>
<TroisCanvas>
<mesh ref="mesh">
<meshBasicMaterial :color="color"/>
</mesh>
</TroisCanvas>
</template>
<script>
export default {
data(){
return {
color: 'blue'
}
},
mounted(){
// `color` only changes a couple times a second, so that works well with reactive values
setInterval(() => {
this.color = this.color === 'blue' ? 'green' : 'blue'
}, 500)
// start the update loop
this.update()
},
methods: {
update(){
requestAnimationFrame(this.update)
const mesh = this.$refs.mesh.instance
// cancel if no mesh yet
if (!mesh) return
// this bobs the mesh up and down smoothly -
// since it happens so frequently (ideally ~60 times per second),
// it makes sense to change the property directly instead of relying on reactivity
mesh.position.y = Math.sin(Date.now() * 0.001)
}
}
}
</script>