@threlte/core
<T>
The component <T>
provides the means to use any three.js export as a Svelte component.
It does this by leveraging the rigid three.js naming and object property structure to add and
remove objects to and from the scene graph, attach objects to parent object properties or add
event listeners.
<T>
is the main building block of any Threlte application. Components available in '@threlte/extras'
are built on top of <T>
and may provide a more convenient API for specific three.js classes.
Usage Types
Primer on Terminology
// Class definition:
class Mesh extends THREE.Object3D {
constructor(geometry, material) {
/* … */
}
}
// Creating a class instance:
const mesh = new Mesh()
// Creating a class instance with constructor arguments:
const mesh = new Mesh(geometry, material)
There are two ways to use <T>
:
- Use dot-notation to use any three.js class available at Three.js’ main namespace
'three'
:
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh />
- Pass the property
is
to<T>
:
<script>
import { T } from '@threlte/core'
import { Mesh } from 'three'
</script>
<T is={Mesh} />
Both ways are equivalent and can be used interchangeably. The latter is more explicit and allows
you to use any class definition (even if it’s not exported from Three.js’ main namespace
'three'
), object instance or virtually any other value.
The next section will discuss both ways in more detail.
Dot-Notation
Any three.js class available at Three.js’ main namespace 'three'
can be used as a component with full type-safety.
The name of the import is the same as the name of the component. For example, the class THREE.Mesh
can be used with the component <T.Mesh>
.
Let’s take a look at a simple example:
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh>
<T.BoxGeometry />
<T.MeshBasicMaterial />
</T.Mesh>
Let’s break this down:
- The component
<T.Mesh>
creates an instance ofTHREE.Mesh
which is automatically added to the scene graph. - The component
<T.BoxGeometry>
creates an instance ofTHREE.BoxGeometry
which is automatically “attached” to the propertygeometry
of the parentTHREE.Mesh
. - The component
<T.MeshBasicMaterial>
creates an instance ofTHREE.MeshBasicMaterial
which is automatically “attached” to the propertymaterial
of the parentTHREE.Mesh
.
Extend the default component catalogue
If you want to use a class that is not available at Three.js’ main namespace 'three'
with dot-notation, you can extend the default component catalogue. Be aware that components used this way do not offer type-safety. Once extended, the updated catalogue is available to all components in your application.
<script>
import { T, extend, useThrelte } from '@threlte/core'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
extend({
OrbitControls
})
const { renderer } = useThrelte()
</script>
<T.PerspectiveCamera makeDefault>
{#snippet children({ ref })}
<T.OrbitControls args={[ref, renderer.domElement]} />
{/snippet}
</T.PerspectiveCamera>
is
Property To explicitly pass a class definition to the component <T>
, use the property is
. Let’s take a look at the same example as above but using the property is
:
<script>
import { T } from '@threlte/core'
import { Mesh, BoxGeometry, MeshBasicMaterial } from 'three'
</script>
<T is={Mesh}>
<T is={BoxGeometry} />
<T is={MeshBasicMaterial} />
</T>
The two examples are equivalent and can be used interchangeably.
The “vanilla” Three.js equivalent of both examples would be:
import { Mesh, BoxGeometry, MeshBasicMaterial } from 'three'
const mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial())
scene.add(mesh)
Using the property is
comes in handy when using classes that are not exported from Three.js’ main namespace 'three'
, such as the OrbitControls
class:
<script>
import { T } from '@threlte/core'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
</script>
<T is={OrbitControls} />
What’s happening under the hood?
If a class definition such as THREE.Mesh
is provided to the property is
, it creates an instance of that class which we call ref
– the component’s reference to the three.js object:
<T is={Mesh} />
If a class instance (such as new THREE.Mesh()
) or any other value is provided, the component uses this value as-is:
<script>
const mesh = new Mesh()
</script>
<T is={mesh} />
Depending on the is
property value types, Threlte makes certain assumptions:
- If the value passed to
is
is extendingTHREE.Object3D
it’s added to the scene graph. - If the value passed to
is
is a disposable, it’s disposedonDestroy
or whenever theargs
change and a newref
is created. - If the value passed to
is
is has a propertyaddEventListener
, you can add event callbacks. - If the value passed to
is
is extendingTHREE.Camera
, certain camera-related properties are available.
Props
The <T>
component has a set of fixed props (namely args
, is
, attach
, manual
, makeDefault
and dispose
) that are used to set up the Three.js object. On top of that, you can use arbitrary props to reactively set any property of the underlying Three.js object.
Three.js object props
To understand how it works, let’s have a look at a simple example. Let’s say we want to render a simple cube. We can do this by using the <T>
component:
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh>
<T.BoxGeometry />
<T.MeshBasicMaterial />
</T.Mesh>
Using attachments, the geometry as well as the material are assigned to the mesh. What if we want to change the color of the material to "red"
? We can do this by using the color
prop:
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh>
<T.BoxGeometry />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
Keep in mind that this property is not hard-wired. We can use any property of the underlying Three.js object as a prop on the <T>
component. For example, we can also set the position
property of the THREE.Mesh
:
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh position={[0, 1, 0]}>
<T.BoxGeometry />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
Because the property position
of a THREE.Mesh
is a THREE.Vector3
the value we have to provide is what is
passed to the set
function of the THREE.Vector3
class. In this case, we pass an array of three numbers ([x, y, z]
). Using
an editor like VS Code, you benefit from type hints and auto-completion.
We only changed the y
coordinate of the position, so we can use a pierced prop to only change the y
coordinate:
<script>
import { T } from '@threlte/core'
</script>
<T.Mesh position.y={1}>
<T.BoxGeometry />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
This way, we get less updates of the underlying Three.js object because the value of the prop is a primitive value which can easily be compared to the previous value. Unfortunately with pierced props we miss out on the type hints and auto-completion.
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.
args
Three.js objects are class instances. When Instantiating these objects, they can receive one-time constructor arguments (new THREE.SphereGeometry(1, 32)
). In Threlte, constructor arguments are always passed as an array via the property args
. If args
change later on, the object must naturally get reconstructed from scratch!
- If a class definition such as
THREE.BoxGeometry
is provided to the propertyis
, the propertyargs
is used to instantiate the class:<T is={BoxGeometry} args={[1, 2, 1]}>
equalsnew BoxGeometry(1, 2, 1)
.
attach
Use attach
to bind objects to their parent. If you unmount the attached object it will be taken off its parent automatically.
The following attaches a material to the material property of a mesh and a geometry to the geometry property:
<T.Mesh>
<T.MeshBasicMaterial attach="material" />
<T.BoxGeometry attach="geometry" />
</T.Mesh>
All objects ending with “Material” (such as MeshStandardMaterial
) receive attach="material"
,
and all objects ending with “Geometry” receive attach="geometry"
automatically. You do not
strictly have to type it out!
- The value inferred from the property
is
is “attached” to a parent property.
<script>
import { MeshStandardMaterial } from 'three'
export let texture
</script>
<T is={MeshStandardMaterial}>
<!-- Attaches the texture to the property "map" of the parent material -->
<T
is={texture}
attach="map"
/>
</T>
attach
can be a dot-notated path to a nested parent property:
<T is={DirectionalLight}>
<!--
Attaches an instance of a THREE.OrthographicCamera
to the property camera of the property shadow of the
parent THREE.DirectionalLight
-->
<T
is={OrthographicCamera}
args={[-1, 1, 1, -1, 0.1, 100]}
attach="shadow.camera"
/>
</T>
attach
can also be a function which is called on mounting with the parent and the value inferred from the propertyis
. It can return a cleanup function which is calledonDestroy
:
<T is={DirectionalLight}>
<!--
Attaches an instance of a THREE.OrthographicCamera
to the property camera of the property shadow of the
parent THREE.DirectionalLight
-->
<T
is={OrthographicCamera}
args={[-1, 1, 1, -1, 0.1, 100]}
attach={(parent, self) => {
parent.shadow.camera = self
return () => {
parent.shadow.camera = null
}
}}
/>
</T>
Camera Props
By default Threlte is responsive and will set up cameras properly on resize (aspect ratio etc).
Cameras can be controlled manually by setting manual
to true
. This will opt out of projection matrix recalculation when the drawing area resizes or other camera-related properties change.
<T
is={PerspectiveCamera}
manual
/>
Use the property makeDefault
to set a camera to the default rendering camera.
<T
is={PerspectiveCamera}
makeDefault
/>
A common mistake is to forget setting makeDefault
. If you do not set a camera to be the default
camera, the scene will not be rendered through this camera but through Threltes default camera.
Events
Adding an event listener to a component will also add the corresponding event listener to the three.js class instance. The event will be forwarded and the native payload is available as the first argument to the event listener.
For easier payload access as well as conforming to Three.js as much as possible, Threlte does not use Svelte’s
CustomEvent<Payload>
payload type. Instead, the payload is provided as is.
This will listen to the “change” event on the THREE.OrbitControls
:
<T
is={OrbitControls}
onchange={(e) => console.log('change:', e)}
/>
All <T>
components also emit the create
event when the underlying three.js class instance is created.
This can be used to access the instance from the parent component or do tasks on the objects upon creation.
The event handler is called with an object containing a reference to the object as well as a function cleanup
that you may invoke with a callback to clean up after the object is destroyed when the component unmounts or the args
change.
<T.PerspectiveCamera
oncreate={({ ref, cleanup }) => {
// Look at the center
ref.lookAt(0, 0, 0)
cleanup(() => {
// Do something when the camera is disposed
})
}}
/>
Snippet Props
The value infered from the property is
is available as the snippet prop ref
:
<T.PerspectiveCamera>
{#snippet children({ ref: camera })}
<!--
The prop "ref" is used to reference the
camera and instantiate the OrbitControls
-->
<T
is={OrbitControls}
args={[camera, renderer.domElement]}
/>
{/snippet}
</T.PerspectiveCamera>
Bindings
The value infered from the property is
is available as the binding ref
:
<script>
let camera
$: console.log(camera) // THREE.PerspectiveCamera
</script>
<T
is={PerspectiveCamera}
bind:ref={camera}
/>
<!-- or -->
<T.PerspectiveCamera bind:ref={camera} />
Extending the default component catalogue
By default when using the dot-notation to access Three.js objects (e.g. <T.Mesh>
), Threlte will automatically import
the corresponding class from the namespance 'three'
. If you want to use a custom class or classes from Three.js that
are available elsewhere (like OrbitControls
), you can extend the default catalogue with the extend
function:
<script>
import { extend, T } from '@threlte/core'
import { CustomMesh } from './MyCustomMesh.ts'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
extend({
CustomMesh
})
</script>
<T.CustomMesh />
Be aware that in this case <T>
can’t provide type-safety on properties and events.
If you need type-safety, you can use the is
property instead: <T is={CustomMesh} />
.
This way, Threlte can infer the type of the object and provide type-safety on properties and events.