bvh
A plugin that uses three-mesh-bvh to speed up raycasting and enable
spatial queries against Three.js objects. Any Mesh, BatchedMesh, or Points that are created in the component
and child component where this plugin is called are patched with BVH raycasting methods.
<script lang="ts"> import Scene from './Scene.svelte' import { Canvas } from '@threlte/core' import { type BVHOptions, BVHSplitStrategy } from '@threlte/extras' import { Pane, Checkbox, List, Slider } from 'svelte-tweakpane-ui'
let options = $state<Required<BVHOptions> & { helper: boolean }>({ enabled: true, helper: true, strategy: BVHSplitStrategy.SAH, indirect: false, verbose: false, maxDepth: 20, maxLeafTris: 10, setBoundingBox: true })</script>
<Pane title="bvh" position="fixed"> <Checkbox label="enabled" bind:value={options.enabled} /> <Checkbox label="helper" bind:value={options.helper} /> <Checkbox label="setBoundingBox" bind:value={options.setBoundingBox} /> <List bind:value={options.strategy} label="strategy" options={{ SAH: BVHSplitStrategy.SAH, CENTER: BVHSplitStrategy.CENTER, AVERAGE: BVHSplitStrategy.AVERAGE }} /> <Slider label="maxDepth" bind:value={options.maxDepth} step={1} /> <Slider label="maxLeafTris" bind:value={options.maxLeafTris} step={1} /></Pane>
<Canvas> <Scene {...options} /></Canvas><script lang="ts"> import { OrbitControls, Grid, useGltf, Environment, Wireframe, bvh, interactivity, type BVHOptions } from '@threlte/extras' import { T, useTask } from '@threlte/core' import { BufferAttribute, DynamicDrawUsage, Mesh, Vector3, type Face } from 'three'
let { ...rest }: BVHOptions = $props()
const { raycaster } = interactivity() raycaster.firstHitOnly = true
bvh(() => rest)
const gltf = useGltf('/models/stanford_bunny.glb') const mesh = $derived($gltf ? ($gltf.nodes['Object_2'] as Mesh) : undefined)
$effect(() => { if (mesh) { const array = new Float32Array(3 * mesh.geometry.getAttribute('position').count).fill(1) const attribute = new BufferAttribute(array, 3).setUsage(DynamicDrawUsage) mesh.geometry.setAttribute('color', attribute) } })
const faces = new Set<Face>()
useTask(() => { const attribute = mesh?.geometry.getAttribute('color')
if (!attribute) { return }
for (const face of faces) { let gb = attribute.getY(face.a)
gb += 0.01
if (gb >= 1) { gb = 1 faces.delete(face) }
attribute.setXYZ(face.a, 1, gb, gb) attribute.setXYZ(face.b, 1, gb, gb) attribute.setXYZ(face.c, 1, gb, gb)
attribute.needsUpdate = true } })</script>
<T.PerspectiveCamera makeDefault position.x={-1.3} position.y={1.8} position.z={1.8} fov={50} oncreate={(ref) => ref.lookAt(0, 0.6, 0)}> <OrbitControls enableDamping enableZoom={false} enablePan={false} target={[0, 0.6, 0]} /></T.PerspectiveCamera>
{#if $gltf} <T is={$gltf.nodes['Object_2'] as Mesh} scale={10} rotation.x={-Math.PI / 2} position.y={-0.35} onpointermove={({ face }) => { const attribute = mesh?.geometry.getAttribute('color')
if (face && attribute) { attribute.setXYZ(face.a, 1, 0, 0) attribute.setXYZ(face.b, 1, 0, 0) attribute.setXYZ(face.c, 1, 0, 0) faces.add(face) } }} > <T.MeshStandardMaterial roughness={0.1} metalness={0.4} vertexColors /> <Wireframe /> </T>{/if}
<T.DirectionalLight />
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />
<Grid sectionThickness={1} infiniteGrid cellColor="#dddddd" sectionColor="#ffffff" sectionSize={1} cellSize={0.5} type="circular" fadeOrigin={new Vector3()} fadeDistance={20} fadeStrength={10}/>Basic example
Section titled “Basic example”The plugin can be configured by passing a function that returns an object or $state rune as an argument.
The following options are available and will be set for every Three.js object.
<script lang="ts"> import { T } from '@threlte/core' import { bvh, interactivity, BVHSplitStrategy, type BVHOptions } from '@threlte/extras'
// Usually, you'll also want to call the interactivity plugin. const { raycaster } = interactivity()
// This option is usually set with three-mesh-bvh, // unless you need multiple hits. raycaster.firstHitOnly = true
// These are the default options. const options = $state<BVHOptions>({ enabled: true, helper: false, strategy: BVHSplitStrategy.SAH, indirect: false, verbose: false, maxDepth: 20, maxLeafTris: 10, setBoundingBox: true })
bvh(() => options)</script>Setting options at a per object level is possible with the bvh prop.
<T.Mesh bvh={{ maxDepth: 10 }}> <T.TorusGeometry /> <T.MeshStandardMaterial ></T.Mesh>If you want this prop to be typesafe, you can extend Threlte.UserProps like so:
import type { InteractivityProps, BVHProps } from '@threlte/extras'
declare global { namespace Threlte { interface UserProps extends InteractivityProps, BVHProps {} }}Points
Section titled “Points”The bvh plugin will shapecast against points, attempting to match Three.js’ raycasting behavior with
added three-mesh-bvh optimizations.
<script lang="ts"> import Scene from './Scene.svelte' import { Canvas } from '@threlte/core' import { type BVHOptions, BVHSplitStrategy } from '@threlte/extras' import { Pane, Checkbox, List, Slider } from 'svelte-tweakpane-ui'
const options = $state<Required<BVHOptions> & { helper: boolean; firstHitOnly: boolean }>({ enabled: true, strategy: BVHSplitStrategy.SAH, indirect: false, verbose: false, maxDepth: 40, maxLeafTris: 20, setBoundingBox: true,
firstHitOnly: false, helper: false })</script>
<Pane title="bvh" position="fixed"> <Checkbox label="enabled" bind:value={options.enabled} /> <Checkbox label="helper" bind:value={options.helper} /> <Checkbox label="firstHitOnly" bind:value={options.firstHitOnly} /> <Checkbox label="setBoundingBox" bind:value={options.setBoundingBox} /> <List bind:value={options.strategy} label="strategy" options={{ SAH: BVHSplitStrategy.SAH, CENTER: BVHSplitStrategy.CENTER, AVERAGE: BVHSplitStrategy.AVERAGE }} /> <Slider label="maxDepth" bind:value={options.maxDepth} step={1} /> <Slider label="maxLeafTris" bind:value={options.maxLeafTris} step={1} /></Pane>
<Canvas> <Scene {...options} /></Canvas><script lang="ts"> import { OrbitControls, useGltf, bvh, interactivity, type BVHOptions, PointsMaterial } from '@threlte/extras' import { T, useTask } from '@threlte/core' import { BufferAttribute, DynamicDrawUsage, Points, type Vector3Tuple } from 'three'
let { ...rest }: BVHOptions & { firstHitOnly: boolean } = $props()
const { raycaster } = interactivity() raycaster.params.Points.threshold = 0.5
$effect(() => { raycaster.firstHitOnly = rest.firstHitOnly })
bvh(() => rest)
const gltf = useGltf('/models/stairs.glb')
const points = $derived.by(() => { if (!$gltf) { return }
const results = $gltf.nodes['Object'] as Points const array = new Float32Array(3 * results.geometry.getAttribute('position').count).fill(1) const attribute = new BufferAttribute(array, 3).setUsage(DynamicDrawUsage) results.geometry.setAttribute('color', attribute)
return results })
useTask(() => { if (!points) return
const attribute = points.geometry.getAttribute('color')
const indices = points.userData.indices as Set<number> if (indices.size > 0) { for (const index of indices) { let gb = attribute.getY(index)
gb += 0.005
if (gb >= 1) { gb = 1 indices.delete(index) }
attribute.setXYZ(index, 1, gb, gb) }
attribute.needsUpdate = true } })
let visible = $state(false) let point = $state.raw<Vector3Tuple>([0, 0, 0])</script>
<T.PerspectiveCamera makeDefault position.x={20} position.y={20} position.z={-20} fov={50}> <OrbitControls enableDamping enableZoom={false} enablePan={false} /></T.PerspectiveCamera>
{#if points} <T is={points} rotation.x={-Math.PI / 2} userData.indices={new Set<number>()} onpointerenter={() => { visible = true }} onpointerleave={() => { visible = false }} onpointermove={(event) => { point = event.point.toArray() if (event.index) { points.geometry.getAttribute('color').setXYZ(event.index, 1, 0, 0) points.userData.indices.add(event.index) } }} > <PointsMaterial size={0.2} vertexColors transparent toneMapped={false} opacity={0.75} /> </T>{/if}
<T.Mesh position={point} renderOrder={1} {visible} bvh={{ enabled: false }}> <T.SphereGeometry args={[0.5]} /> <T.MeshBasicMaterial color="red" depthTest={false} transparent opacity={0.5} /></T.Mesh>
<T.DirectionalLight />Limitations
Section titled “Limitations”To avoid unnecessary bounds tree computations, the plugin will not recompute bounds trees when geometry changes occur. The plugin is set to recompute only if a reference to a mesh changes. So, if you want to recompute a bounds tree based on a geometry change, you’ll have to regenerate the mesh as well.
{#key geometry} <T.Mesh> <T is={geometry} /> <T.MeshStandardMaterial /> </T.Mesh>{/key}