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.