@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 of THREE.Mesh which is automatically added to the scene graph.
  • The component <T.BoxGeometry> creates an instance of THREE.BoxGeometry which is automatically “attached” to the property geometry of the parent THREE.Mesh.
  • The component <T.MeshBasicMaterial> creates an instance of THREE.MeshBasicMaterial which is automatically “attached” to the property material of the parent THREE.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.

Camera.svelte
<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>

Property is

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 extending THREE.Object3D it’s added to the scene graph.
  • If the value passed to is is a disposable, it’s disposed onDestroy or whenever the args change and a new ref is created.
  • If the value passed to is is has a property addEventListener, you can add event callbacks.
  • If the value passed to is is extending THREE.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 property is, the property args is used to instantiate the class: <T is={BoxGeometry} args={[1, 2, 1]}> equals new 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 property is. It can return a cleanup function which is called onDestroy:
<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.