threlte logo
Advanced

Plugins

Injecting a Plugin

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.

import { injectPlugin } from '@threlte/core'

injectPlugin('plugin-name', ({ ref, props }) => {
  console.log(ref, props)
})

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

import { injectPlugin } from '@threlte/core'
import type { Object3D } from 'three'

const refIsObject3D = (ref: any): ref is Object3D => ref.isObject3D

injectPlugin('raycast-plugin', ({ ref, props }) => {
  if (!refIsObject3D(ref) || !props.raycast) 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 is notified about property or ref changes and can run code in lifecycle functions such as onMount or onDestroy.

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

injectPlugin('plugin-name', () => {
  // Use lifecycle hooks as if it would run inside a <T> component.
  onMount(() => {
    console.log('onMount')
  })

  return {
    // This is called when the ref changes and on initialization.
    onRefChange(ref) {
      console.log(ref)

      // You can return a cleanup function that will be called when the ref
      // changes again or when the component is destroyed.
      return () => {
        console.log('cleanup')
      }
    },

    // This is called when the props change and on initialization. This includes
    // props like "args", "manual" and other base props of <T> but also
    // props that are not part of the base props.
    onPropsChange(props) {
      console.log(props)
    },

    // This is called when the props change that are not part of the <T>
    // components base props and on initialization.
    onRestPropsChange(restProps) {
      console.log(restProps)
    }
  }
})

It 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 property "position", <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>

<Canvas>
  <Scene />
</Canvas>
<script lang="ts">
  import { T, useFrame } from '@threlte/core'
  import { DEG2RAD } from 'three/src/math/MathUtils'
  import { injectLookAtPlugin } from './lookAtPlugin'

  const cubePos = [0, 0.8, 0] as [number, number, number]

  useFrame(() => {
    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, useThrelte } from '@threlte/core'
import { Object3D, Vector3 } from 'three'

export const injectLookAtPlugin = () => {
  injectPlugin('lookAt', ({ ref, props }) => {
    // skip injection if ref is not an Object3D
    if (!ref.isObject3D || !('lookAt' in props)) return

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

    // create some variables to store the current ref and props
    let currentRef = ref
    let currentProps = props

    // create a temp vector to avoid creating new vectors on every iteration
    const tempV3 = new Vector3()

    const applyProps = (p: typeof props, r: typeof ref) => {
      if (!('lookAt' in p)) return
      const prop = p.lookAt
      if (prop.isVector3) tempV3.copy(prop)
      if (Array.isArray(prop) && prop.length === 3) {
        tempV3.set(prop[0], prop[1], prop[2])
      } else if (typeof prop === 'object') {
        tempV3.set(prop.x ?? 0, prop.y ?? 0, prop.z ?? 0)
      }

      r.lookAt(tempV3)
      invalidate()
    }

    applyProps(currentProps, currentRef)

    return {
      onRefChange(ref) {
        currentRef = ref
        applyProps(currentProps, currentRef)
      },
      onPropsChange(props) {
        currentProps = props
        applyProps(currentProps, currentRef)
      },
      pluginProps: ['lookAt']
    }
  })
}

BVH Raycast Plugin

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

<script lang="ts">
  import { injectPlugin } from '@threlte/core'
  import type { BufferGeometry, Mesh } from 'three'
  import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh'

  const isBufferGeometry = (ref: any): ref is BufferGeometry => {
    return ref.isBufferGeometry
  }

  const isMesh = (ref: any): ref is Mesh => {
    return ref.isMesh
  }

  injectPlugin('bvh-raycast', () => {
    return {
      onRefChange(ref) {
        if (isBufferGeometry(ref)) {
          ;(ref as any).computeBoundsTree = computeBoundsTree
          ;(ref as any).disposeBoundsTree = disposeBoundsTree
          ;(ref as any).computeBoundsTree()
        }
        if (isMesh(ref)) {
          ;(ref as any).raycast = acceleratedRaycast
        }
        return () => {
          if (isBufferGeometry(ref)) {
            ;(ref as any).disposeBoundsTree()
          }
        }
      }
    }
  })
</script>

<slot />

Implementing this plugin in your Scene:

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

<Canvas>
  <BvhRaycast>
    <Scene />
  </BvhRaycast>
</Canvas>

Threlte-managed Matrix Updates

By default, Three.js is automatically updating the matrix and matrixWorld properties of all objects every frame. This can be a performance problem in large apps, because it is not necessary in certain situations. This plugin listens for changes to certain transform-related properties and updates the matrix and matrixWorld properties only when necessary.

<script lang="ts">
  import { Object3D } from 'three'
  import { injectPlugin, useFrame } from '@threlte/core'

  const isObject3D = (obj: any): obj is Object3D => {
    return obj.isObject3D
  }

  const propKeysRequiringMatrixUpdate = [
    'position',
    'position.x',
    'position.y',
    'position.z',
    'rotation',
    'rotation.x',
    'rotation.y',
    'rotation.z',
    'rotation.order',
    'quaternion',
    'quaternion.x',
    'quaternion.y',
    'quaternion.z',
    'quaternion.w',
    'scale',
    'scale.x',
    'scale.y',
    'scale.z'
  ]

  const objectsToUpdate: Set<Object3D> = new Set()

  type MatrixPluginProps = {
    matrixAutoUpdate?: boolean
  }

  injectPlugin<MatrixPluginProps>('matrix-update', ({ ref, props }) => {
    if (!isObject3D(ref)) return
    if (props.matrixAutoUpdate) return
    ref.matrixAutoUpdate = false

    const checkForMatrixUpdate = (props: Record<string, any>) => {
      if (Object.keys(props).some((key) => propKeysRequiringMatrixUpdate.includes(key))) {
        objectsToUpdate.add(ref)
      }
    }
    checkForMatrixUpdate(props)

    return {
      pluginProps: ['matrixAutoUpdate'],
      onRestPropsChange(restProps) {
        checkForMatrixUpdate(restProps)
      }
    }
  })

  useFrame(
    ({ invalidate }) => {
      if (!objectsToUpdate.size) return
      objectsToUpdate.forEach((obj) => obj.updateMatrix())
      objectsToUpdate.clear()
      invalidate()
    },
    {
      order: -Infinity,
      invalidate: false
    }
  )
</script>

<slot />

Now when applying props like position.x or scale to any <T> component, the matrix of the object will update but doesn’t just update every frame as Three.js does by default. If an object is transformed without props (like a camera being transformed by THREE.OrbitControls) you can apply the flag matrixAutoUpdate:

<T.PerspectiveCamera
  matrixAutoUpdate
  makeDefault
>
  <OrbitControls />
</T.PerspectiveCamera>

Notice how this plugin uses TypeScript to augment to possible props this plugin may receive. This is not necessary, but it is a good practice to do so.