Guard viewer BVH against empty geometries

This commit is contained in:
sudhir
2026-05-11 11:24:44 +05:30
parent 4956de4910
commit afadfb2ea8
2 changed files with 140 additions and 3 deletions
@@ -1,6 +1,5 @@
'use client'
import { Bvh } from '@react-three/drei'
import { Canvas, extend, type ThreeToJSXElements, useFrame, useThree } from '@react-three/fiber'
import { useEffect, useMemo, useRef } from 'react'
import * as THREE from 'three/webgpu'
@@ -28,6 +27,7 @@ import FrameLimiter from './frame-limiter'
import { Lights } from './lights'
import { PerfMonitor } from './perf-monitor'
import PostProcessing, { DEFAULT_HOVER_STYLES, type HoverStyles } from './post-processing'
import { SceneBvh } from './scene-bvh'
import { SelectionManager } from './selection-manager'
import { ViewerCamera } from './viewer-camera'
@@ -216,9 +216,9 @@ const Viewer: React.FC<ViewerProps> = ({
{/* <directionalLight position={[10, 10, 5]} intensity={0.5} castShadow
/> */}
<Lights />
<Bvh>
<SceneBvh>
<SceneRenderer />
</Bvh>
</SceneBvh>
{/* Default Systems */}
<LevelSystem />
@@ -0,0 +1,137 @@
import { useThree } from '@react-three/fiber'
import {
type ReactNode,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
} from 'react'
import { Group, Mesh, type BufferGeometry } from 'three'
import {
SAH,
acceleratedRaycast,
computeBoundsTree,
disposeBoundsTree,
type SplitStrategy,
} from 'three-mesh-bvh'
type SceneBvhProps = {
children?: ReactNode
enabled?: boolean
firstHitOnly?: boolean
strategy?: SplitStrategy
verbose?: boolean
setBoundingBox?: boolean
maxDepth?: number
maxLeafSize?: number
indirect?: boolean
}
const isMesh = (object: unknown): object is Mesh =>
!!object && typeof object === 'object' && (object as Mesh).isMesh === true
const hasBvhCompatibleGeometry = (geometry?: BufferGeometry | null) => {
if (!geometry) return false
const position = geometry.getAttribute('position')
if (!position) return false
const vertexCount = geometry.getIndex()?.count ?? position.count
return vertexCount >= 3
}
export const SceneBvh = forwardRef<Group, SceneBvhProps>(
(
{
children,
enabled = true,
firstHitOnly = false,
strategy = SAH,
verbose = false,
setBoundingBox = true,
maxDepth = 40,
maxLeafSize = 10,
indirect = false,
},
forwardedRef,
) => {
const ref = useRef<Group>(null)
const raycaster = useThree((state) => state.raycaster)
useImperativeHandle(forwardedRef, () => ref.current!, [])
useEffect(() => {
if (!enabled || !ref.current) return
const options = {
strategy,
verbose,
setBoundingBox,
maxDepth,
maxLeafSize,
indirect,
}
const group = ref.current
const acceleratedMeshes = new Set<Mesh>()
const computedGeometries = new Set<BufferGeometry>()
;(raycaster as any).firstHitOnly = firstHitOnly
group.traverse((child) => {
if (!isMesh(child)) return
if (child.raycast === Mesh.prototype.raycast) {
child.raycast = acceleratedRaycast
acceleratedMeshes.add(child)
}
if (child.raycast !== acceleratedRaycast) return
const geometry = child.geometry
if (geometry.boundsTree || !hasBvhCompatibleGeometry(geometry)) return
try {
geometry.computeBoundsTree = computeBoundsTree
geometry.disposeBoundsTree = disposeBoundsTree
geometry.computeBoundsTree(options)
computedGeometries.add(geometry)
} catch (error) {
console.warn('[viewer] Skipping BVH for incompatible mesh geometry.', {
mesh: child.name || child.type,
error,
})
}
})
return () => {
delete (raycaster as any).firstHitOnly
for (const geometry of computedGeometries) {
if (geometry.boundsTree) {
geometry.disposeBoundsTree()
}
}
for (const mesh of acceleratedMeshes) {
if (mesh.raycast === acceleratedRaycast) {
mesh.raycast = Mesh.prototype.raycast
}
}
}
}, [
enabled,
firstHitOnly,
strategy,
verbose,
setBoundingBox,
maxDepth,
maxLeafSize,
indirect,
raycaster,
])
return <group ref={ref}>{children}</group>
},
)
SceneBvh.displayName = 'SceneBvh'