threlte logo
Advanced

Plugins

Plugins are one of Threlte’s superpowers. Plugins allow you to globally extend Threlte’s <T> component functionality. This means you can not only add arbitrary props and event handlers to your <T> components but also override the default behavior of the <T> component entirely.

Plugins add code to every <T> component instance. That code acts as if it were part of the <T> component itself. You have full access to props, event listeners and the component instance itself.

Injecting a Plugin

Plugins are injected to a plugin context and are accessible to all child <T> components.

Scene.svelte
<script>
  import { injectPlugin } from '@threlte/core'
  import myPlugin from './myPlugin'
  import OtherComponent from './OtherComponent.svelte'

  injectPlugin('my-plugin', myPlugin)
</script>

<!--
This component is affected by the plugin 'my-plugin'
-->
<T.Mesh />

<!--
<T> components in this component are
also affected by the plugin 'my-plugin'
-->
<OtherComponent />

What it looks like

Plugins open up the component <T> to external code that will be injected via context into every child instance of a <T> component. The callback function receives a reactive args object that contains the ref of the respective <T> component, all base props (makeDefault, args, attach, manual, makeDefault and dispose) and all props (anything else) passed to it.

import { injectPlugin } from '@threlte/core'

injectPlugin('plugin-name', (args) => {
  console.log(args.ref) // e.g. a Mesh
  console.log(args.props) // e.g. { position: [0, 10, 0] }
})

If a plugin decides via args.ref or args.props analysis that it doesn’t need to act in the context of a certain <T> component, it can return early.

import { injectPlugin, isInstanceOf } from '@threlte/core'

injectPlugin('raycast-plugin', (args) => {
  if (!isInstanceOf(args.ref, 'Object3D') || !('raycast' in args.props)) return
})

The code of a plugin acts as if it would be part of the <T> component itself and has access to all properties. A plugin can run arbitrary code in lifecycle functions such as onMount, onDestroy and effects.

import { injectPlugin } from '@threlte/core'
import { onMount } from 'svelte'

injectPlugin('plugin-name', (args) => {
  // Use lifecycle hooks as if it would run inside a <T> component.
  // This code runs when the `<T>` component this plugin is injected
  // into is mounted.
  onMount(() => {
    console.log('onMount')
  })

  // Use any prop that is defined on the <T> component, in this
  // example `count`: <T.Mesh count={10} />
  const count = $derived(args.props.count ?? 0)

  $effect(() => {
    // This code runs whenever count changes.
    console.log(count)
  })

  return {
    // Claiming the property "count" so that the <T> component
    // does not act on it.
    pluginProps: ['count']
  }
})

A Plugin can also claim properties so that the component <T> does not act on it.

import { injectPlugin } from '@threlte/core'

injectPlugin('ecs', () => {
  return {
    // Without claiming the properties, <T> would apply the
    // property to the object.
    pluginProps: ['entity', 'health', 'velocity', 'position']
  }
})

Plugins are passed down by context and can be overridden to prevent the effects of a plugin for a certain tree.

import { injectPlugin } from '@threlte/core'

// this overrides the plugin with the name "plugin-name" for all child components.
injectPlugin('plugin-name', () => {})

Creating a Plugin

Plugins can also be created for external consumption. This creates a named plugin. The name is used to identify the plugin and to override it.

import { createPlugin } from '@threlte/core'

export const layersPlugin = createPlugin('layers', () => {
  // ... Plugin Code
})
// somewhere else, e.g. in a component

import { injectPlugin } from '@threlte/core'
import { layersPlugin } from '$plugins'

injectPlugin(layersPlugin)

Examples

lookAt

This is en example implementation that adds the property lookAt to all <T> components, so that <T.Mesh lookAt={[0, 10, 0]} /> is possible:

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
</script>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { DEG2RAD } from 'three/src/math/MathUtils.js'
  import { injectLookAtPlugin } from './lookAtPlugin.svelte'

  const cubePos = $state([0, 0.8, 0]) as [number, number, number]

  useTask(() => {
    cubePos[0] = Math.sin(Date.now() / 1000) * 2
    cubePos[2] = Math.cos(Date.now() / 1000) * 2
  })

  injectLookAtPlugin()
</script>

<T.OrthographicCamera
  zoom={80}
  position={[0, 5, 10]}
  makeDefault
  lookAt={[0, 2, 0]}
/>

<T.Mesh
  receiveShadow
  rotation.x={DEG2RAD * -90}
>
  <T.CircleGeometry args={[4, 60]} />
  <T.MeshStandardMaterial />
</T.Mesh>

<T.Mesh
  position={cubePos}
  receiveShadow
  castShadow
  rotation.x={DEG2RAD * -90}
>
  <T.BoxGeometry />
  <T.MeshStandardMaterial color="#FE3D00" />
</T.Mesh>

<T.Group
  lookAt={cubePos}
  position={[0, 4, 0]}
>
  <T.Mesh
    receiveShadow
    castShadow
    rotation.x={DEG2RAD * 90}
  >
    <T.ConeGeometry args={[1, 2]} />
    <T.MeshStandardMaterial
      color="#FE3D00"
      flatShading
    />
  </T.Mesh>
</T.Group>

<T.DirectionalLight
  position={[-3, 20, -10]}
  intensity={1}
  castShadow
/>
<T.AmbientLight intensity={0.2} />
import { injectPlugin, isInstanceOf, useThrelte } from '@threlte/core'

export const injectLookAtPlugin = () => {
  injectPlugin<{
    lookAt: [number, number, number]
  }>('lookAt', (args) => {
    // skip injection if ref is not an Object3D
    if (!isInstanceOf(args.ref, 'Object3D')) return

    // get the invalidate function from the useThrelte hook
    const { invalidate } = useThrelte()

    $effect(() => {
      if (!args.props.lookAt) return
      args.ref.lookAt(args.props.lookAt[0], args.props.lookAt[1], args.props.lookAt[2])
      invalidate()
    })

    return {
      pluginProps: ['lookAt']
    }
  })
}

BVH Raycast Plugin

A Plugin that implements BVH raycasting on all child meshes and geometries.

bvhRaycasting.svelte.ts
import { injectPlugin, isInstanceOf } from '@threlte/core'
import type { BufferGeometry, Mesh } from 'three'
import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh'

const bvhRaycasting = () => {
  injectPlugin('bvh-raycast', (args) => {
    $effect(() => {
      if (isInstanceOf(args.ref, 'BufferGeometry')) {
        args.ref.computeBoundsTree = computeBoundsTree
        args.ref.disposeBoundsTree = disposeBoundsTree
        args.ref.computeBoundsTree()
      }
      if (isInstanceOf(args.ref, 'Mesh')) {
        args.ref.raycast = acceleratedRaycast
      }
      return () => {
        if (isInstanceOf(args.ref, 'BufferGeometry')) {
          args.ref.disposeBoundsTree()
        }
      }
    })
  })
}

Implementing this plugin in your Scene:

Scene.svelte
<script lang="ts">
  import { T } from '@threlte/core'
  import bvhRaycasting from './plugins/bvhRaycasting.svelte'

  bvhRaycasting()
</script>

<T.Mesh>
  <T.MeshBasicMaterial />
  <T.BoxGeometry />
</T.Mesh>

TypeScript

Using TypeScript, we can achieve end-to-end type safety for plugins, from the plugin implementation to the props of the <T> component. The example below shows how to type the props of the lookAt plugin so that the prop lookAt is strictly typed on the <T> component as well as in the plugin implementation.

Typing a Plugin

The function injectPlugin accepts a type argument that you may use to type the props passed to a plugin.

injectPlugin<{ lookAt?: [number, number, number] }>('lookAt', (args) => {
  // args.props.lookAt is now typed as [number, number, number] | undefined
})

Typing the <T> Component Props

By default, the custom props of plugins are not present on the types of the <T> component. You can however extend the types of the <T> component by defining the Threlte.UserProps type in your ambient type definitions. In a typical SvelteKit application, you can find these type definitions in src/app.d.ts.

src/app.d.ts
declare global {
  namespace App {
    // interface Error {}
    // interface Locals {}
    // interface PageData {}
    // interface PageState {}
    // interface Platform {}
  }

  namespace Threlte {
    interface UserProps {
      lookAt?: [number, number, number]
    }
  }
}

export {}

The prop lookAt is now available on the <T> component and is typed as [number, number, number] | undefined.

Svelte.svelte
<script lang="ts">
  import { T } from '@threlte/core'
</script>

<!-- This is now type safe -->
<T.Mesh lookAt={[0, 10, 0]} />

<!-- This will throw an error -->
<T.Mesh lookAt="this object please" />

As soon as your app grows in size, you should consider moving these type these type definitions to a separate file and merge all available props to a single type definition. This type may then be used by injectPlugin as well as your ambient type defintions.