Getting Started
Your First Scene
You should be versed in both Svelte and Three.js before rushing into Threlte. If you are unsure about Svelte, consult its Tutorial for a quick introduction. As for Threejs, make sure you at least glance over its official documentation.
Structuring Your App
As a first step we’re creating a new Svelte file called App.svelte
where we are importing the <Canvas>
component.
<script>
import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte'
</script>
<Canvas>
<Scene />
</Canvas>
The <Canvas>
component is the root component of your Threlte application. It creates a
renderer and sets up some sensible defaults for you like antialiasing and color management.
It also creates a default camera and provides the context in which your Threlte application
will run. For improving access to this runtime context, it’s best practice
to create a seperate component called Scene.svelte
and including it in our App.svelte
file.
Creating Objects
At this point we’re looking at a blank screen. Let’s add a simple cube to it.
In Scene.svelte
, we’re importing the <T>
component which is
the main building block of your Threlte application. It’s a generic
component that we use to render any Three.js object. In this case we’re creating a
THREE.Mesh
which is made up from
a THREE.BoxGeometry
and
a THREE.MeshBasicMaterial
.
We should now be looking at a white cube on a transparent background.
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh>
<T.BoxGeometry />
<T.MeshBasicMaterial />
</T.Mesh>
attach
Behind the scenes we’re using the property attach
available on <T>
to attach an object to a property of
its parent. Binding geometries to the property geometry
and materials to the property material
is a common
pattern so Threlte takes care of it for you.
Learn more
We’re using the property attach
available on <T>
to
attach an object to a property of its parent. In our case we’re attaching the underlying Three.js
object of <T.BoxGeometry>
to the property geometry
of the <T.Mesh>
component. We’re also attaching
the underlying Three.js object of <T.MeshBasicMaterial>
to the property material
of the <T.Mesh>
component.
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh>
<T.BoxGeometry attach="geometry" />
<T.MeshBasicMaterial attach="material" />
</T.Mesh>
Binding geometries to the property geometry
and materials to the property material
is a common
pattern so Threlte will take care of it. It checks for the properties isMaterial
and isGeometry
on
the underlying Three.js object and attaches it to the correct property.
Three.js equivalent
// creating the objects
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshBasicMaterial()
const mesh = new THREE.Mesh()
// "attaching" the objects
mesh.geometry = geometry
mesh.material = material
Modifying Objects
That cube is still a bit boring. Let’s add some color to it, and make it a bit bigger! We also want to move it up a little to highlight it. We can do this by passing props to the
<T>
component.
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh position.y={1}>
<T.BoxGeometry args={[1, 2, 1]} />
<T.MeshBasicMaterial color="hotpink" />
</T.Mesh>
Threlte automatically generates props for <T>
based on the underlying Three.js object. This means you can easily guess most <T>
props
based on the Three.js docs for the class you are using.
Three.js equivalent
const mesh = new THREE.Mesh()
const geometry = new THREE.BoxGeometry(1, 2, 1)
const material = new THREE.MeshBasicMaterial()
mesh.position.y = 1
material.color.set('hotpink')
The special args prop we use in <T.BoxGeometry>
corresponds to the
object’s constructor arguments. Props interpreted from the underlying Three.js
object are called auto props, like color
in our <T.MeshBasicMaterial>
.
Leveraging Threlte’s Pierced Props you can directly assigned to attributes
of props like position.y
in our <T.Mesh>
.
Learn more
args
In Three.js objects are classes that are instantiated. These classes can
receive one-time constructor arguments (new THREE.SphereGeometry(1, 32)
). In
Threlte, constructor arguments are always passed as an array via the prop
args
. If args
change later on, the object must naturally get reconstructed
from scratch!
Auto Props
For all other props, Threlte tries to automatically interpret props passed to <T>
component.
Step 1. Find Properties - First, Threlte will try to find the property
on the underlying Three.js object based on the name of the prop. In our
example, color
is a property of
THREE.MeshBasicMaterial
.
Step 2. Try set
Methods - Next, Threlte will look for a set
method
on that property and use it to set the new value. In our example it will call
material.color.set('hotpink')
to set the color of our material.
Step 3. Try setting the property directly - If there’s no set
method,
it will try to set the property directly. In our example, this equated to
mesh.position.y = 1
.
Step 4. Check for array values - When setting a property that accepts
more than one value (such as a THREE.Vector3
: vec3.set(1, 2, 3)
), we can
pass an array as a prop.
Step 5. Keep the prop type constant for the lifetime of the component - If the prop value changes, Threlte will try to set the property again. If the type of the prop value changes, Threlte won’t be able to reliably do that. For instance the type of the value of a variable that is used as a prop should not change from a single number to an array of numbers.
Pierced Props
Because the property position
of our THREE.Mesh
is a THREE.Vector3
, it
also has x
, y
and z
properties which we can set directly via
dot-notation, we call this Pierced Props.
Primitive Values
From a performance perspective, it’s often better to use pierced props because primitive prop values can safely be compared for equality. This means that if the value of a prop doesn’t change, Threlte will skip any updates to the underlying Three.js object.
Constant prop types
The type of an inferred prop (or “auto prop”) must be constant. This means that the type of a prop must not change for the lifetime of the component. For instance you can’t use a variable as a prop that is an array of numbers and then later on change the value of that variable to a single number. This is considered a type change and therefore not allowed.
Pointing the Camera
We’re still staring at the side of a cube, let’s add a camera and offset it from the center:
<script>
import { T } from '@threlte/core'
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
oncreate={({ ref }) => {
ref.lookAt(0, 1, 0)
}}
/>
<T.Mesh position.y={1}>
<T.BoxGeometry args={[1, 2, 1]} />
<T.MeshBasicMaterial color="hotpink" />
</T.Mesh>
We’re again using the <T>
component to create a THREE.PerspectiveCamera
.
We’re also passing a makeDefault
prop which will make this camera the default camera of our application.
The renderer now uses this camera to render our scene.
Events
Threlte supports listening to certain events on <T/>
components. Here, we use the create
event to get a reference to the underlying Three.js object as soon as it’s created and use the method lookAt
to look at the cube.
Enabling Interactivity
Let’s say we want to scale our cube as soon as we hover over it. We first have to import the
plugin interactivity
from
@threlte/extras
and invoke it in our Scene.svelte file;
We can now add interaction event listeners to our <T>
components. We will add pointerenter
and
pointerleave
event listeners to our cube. In the event handlers we’ll update the value of a Svelte spring store
and apply the stores value to the property scale
of the component <T.Mesh>
.
<script>
import { T } from '@threlte/core'
import { interactivity } from '@threlte/extras'
import { spring } from 'svelte/motion'
interactivity()
const scale = spring(1)
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
oncreate={({ ref }) => {
ref.lookAt(0, 1, 0)
}}
/>
<T.Mesh
position.y={1}
scale={$scale}
onpointerenter={() => scale.set(1.5)}
onpointerleave={() => scale.set(1)}
>
<T.BoxGeometry args={[1, 2, 1]} />
<T.MeshBasicMaterial color="hotpink" />
</T.Mesh>
Automatic Vector & Scalar Detection
You might have noticed that we’re only passing a single number to the prop scale
on <T.Mesh>
. Threlte automatically
figures out whether you are passing an array or a number and uses the appropriate underlying Three.js method.
Learn more
The component <T>
will first look for a property setScalar
on the underlying Three.js object and use that method if
only a single number is passed. This is equivalent to calling scale.setScalar($scale)
.
Realtime Variables
When working with realtime apps where variables e.g. position and rotation change constantly, an easy way observe the values is with live expressions.
Adding Animation
Let’s add some motion to our cube. We will use Threlte’s useTask
hook to tap
into Threlte’s unified frame loop and run a function on every frame. We again use a Pierced Prop to let the
cube rotate around its y-axis.
<script>
import { T, useTask } from '@threlte/core'
import { interactivity } from '@threlte/extras'
import { spring } from 'svelte/motion'
interactivity()
const scale = spring(1)
let rotation = 0
useTask((delta) => {
rotation += delta
})
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
oncreate={({ ref }) => {
ref.lookAt(0, 1, 0)
}}
/>
<T.Mesh
rotation.y={rotation}
position.y={1}
scale={$scale}
onpointerenter={() => scale.set(1.5)}
onpointerleave={() => scale.set(1)}
>
<T.BoxGeometry args={[1, 2, 1]} />
<T.MeshBasicMaterial color="hotpink" />
</T.Mesh>
useTask
registers a callback that will be invoked on every frame. The callback receives the time delta since the last frame as an argument. We use
the delta to update the rotation
independent of the frame rate
– the cube will rotate at the same speed regardless of the frame rate.
Adjusting the Lighting
We’re almost done. Let’s add some shading to our cube and a light source. We’ll use a
THREE.MeshStandardMaterial
on our cube and a THREE.DirectionalLight
to illuminate our scene.
<script>
import { T, useTask } from '@threlte/core'
import { interactivity } from '@threlte/extras'
import { spring } from 'svelte/motion'
interactivity()
const scale = spring(1)
let rotation = 0
useTask((delta) => {
rotation += delta
})
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
oncreate={({ ref }) => {
ref.lookAt(0, 1, 0)
}}
/>
<T.DirectionalLight position={[0, 10, 10]} />
<T.Mesh
rotation.y={rotation}
position.y={1}
scale={$scale}
onpointerenter={() => scale.set(1.5)}
onpointerleave={() => scale.set(1)}
>
<T.BoxGeometry args={[1, 2, 1]} />
<T.MeshStandardMaterial color="hotpink" />
</T.Mesh>
Casting Shadows
We would like our cube to cast a shadow. To do so, we need a floor for it to cast a shadow on,
so we add a new <T.Mesh>
but this time with <T.CircleGeometry>
. To enable shadows, we need to
set castShadow
on both the light and our cube, and set receiveShadow
on our new floor:
<script>
import { T, useTask } from '@threlte/core'
import { interactivity } from '@threlte/extras'
import { spring } from 'svelte/motion'
interactivity()
const scale = spring(1)
let rotation = 0
useTask((delta) => {
rotation += delta
})
</script>
<T.PerspectiveCamera
makeDefault
position={[10, 10, 10]}
oncreate={({ ref }) => {
ref.lookAt(0, 1, 0)
}}
/>
<T.DirectionalLight
position={[0, 10, 10]}
castShadow
/>
<T.Mesh
rotation.y={rotation}
position.y={1}
scale={$scale}
onpointerenter={() => scale.set(1.5)}
onpointerleave={() => scale.set(1)}
castShadow
>
<T.BoxGeometry args={[1, 2, 1]} />
<T.MeshStandardMaterial color="hotpink" />
</T.Mesh>
<T.Mesh
rotation.x={-Math.PI / 2}
receiveShadow
>
<T.CircleGeometry args={[4, 40]} />
<T.MeshStandardMaterial color="white" />
</T.Mesh>
Conclusion
Congratulations, you’ve just created your first Three.js scene with Threlte! It includes important Three.js and Threlte concepts and should give you a good starting point for your first Threlte project.