revert(viewer): drop distance-based LOD switching from GLB scene

Reverts the LOD switching (0469b256). Rendering lod1/lod2 on zoom-out produced
WebGPU validation errors (invalid bindGroup_object) on real-GPU browsers — not
caught earlier because headless Chromium falls back to the WebGL2 backend, so
the WebGPU path was never exercised. The viewer returns to loading the baked
KTX2 lod0 (still the 22MB->12MB win). LOD switching to revisit with a real-GPU
test loop and on-demand loading instead of preload-all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-23 13:38:40 -04:00
co-authored by Claude Opus 4.8
parent 0469b25639
commit c8bd29e909
2 changed files with 8 additions and 58 deletions
@@ -13,7 +13,6 @@ import useViewer from '../../store/use-viewer'
/** Vertical gap added per floor in `exploded` level mode (matches LevelSystem). */ /** Vertical gap added per floor in `exploded` level mode (matches LevelSystem). */
const EXPLODED_GAP = 5 const EXPLODED_GAP = 5
const LOD_FALLBACK_RADIUS = 10
/** A building floor discovered in the baked GLB, ordered bottom-to-top. */ /** A building floor discovered in the baked GLB, ordered bottom-to-top. */
export type GlbLevel = { id: `level_${string}`; label: string } export type GlbLevel = { id: `level_${string}`; label: string }
@@ -49,10 +48,6 @@ type PascalExtras = {
/** The resolved drill target for a raycast hit, given the current selection. */ /** The resolved drill target for a raycast hit, given the current selection. */
type Target = { object: THREE.Object3D; id: string; kind: string; label: string } type Target = { object: THREE.Object3D; id: string; kind: string; label: string }
type HitCandidate = { object: THREE.Object3D; point?: THREE.Vector3 } type HitCandidate = { object: THREE.Object3D; point?: THREE.Vector3 }
type GlbLoadResult = {
scene: THREE.Group
animations: THREE.AnimationClip[]
}
function findIdentityAncestor(object: THREE.Object3D): THREE.Object3D | null { function findIdentityAncestor(object: THREE.Object3D): THREE.Object3D | null {
let current: THREE.Object3D | null = object let current: THREE.Object3D | null = object
@@ -233,49 +228,19 @@ function createZoneWallGeometry(polygon: [number, number][]): THREE.BufferGeomet
*/ */
export function GlbScene({ export function GlbScene({
url, url,
lodUrls,
onLevelsChange, onLevelsChange,
onIdentityChange, onIdentityChange,
onHoverChange, onHoverChange,
}: { }: {
url: string url: string
lodUrls?: string[]
onLevelsChange?: (levels: GlbLevel[]) => void onLevelsChange?: (levels: GlbLevel[]) => void
onIdentityChange?: (identity: GlbIdentity) => void onIdentityChange?: (identity: GlbIdentity) => void
onHoverChange?: (hover: GlbHover) => void onHoverChange?: (hover: GlbHover) => void
}) { }) {
const urls = useMemo(() => (lodUrls && lodUrls.length > 1 ? lodUrls : [url]), [lodUrls, url]) const gltf = useGLTFKTX2(url) as unknown as {
const loadedGltfs = useGLTFKTX2(urls) as unknown as GlbLoadResult | GlbLoadResult[] scene: THREE.Group
const gltfs = Array.isArray(loadedGltfs) ? loadedGltfs : [loadedGltfs] animations: THREE.AnimationClip[]
const gltf = gltfs[0]!
const lodScenes = useMemo(() => gltfs.map((g) => g.scene), [gltfs])
// Distance-based LOD. We toggle scene visibility by camera distance to the
// building's world-space CENTER — `THREE.LOD` measures distance to its own
// origin (0,0,0), which mis-selects levels when the baked scene is offset
// from the origin. Interaction stays bound to lod0 only; the simpler levels
// are visual-only and appear when the camera pulls well past the building
// (so normal + building-view distances keep the interactive lod0).
const lodInfo = useMemo(() => {
if (lodScenes.length <= 1) return null
const box = new THREE.Box3().setFromObject(lodScenes[0]!)
const center = box.getCenter(new THREE.Vector3())
const radius = box.getBoundingSphere(new THREE.Sphere()).radius
const r = Number.isFinite(radius) && radius > 0 ? radius : LOD_FALLBACK_RADIUS
return { center, dist1: r * 2.5, dist2: r * 8, maxLevel: lodScenes.length - 1 }
}, [lodScenes])
useEffect(() => {
lodScenes.forEach((scene, i) => {
scene.visible = i === 0
})
}, [lodScenes])
useFrame(({ camera }) => {
if (!lodInfo) return
const d = camera.position.distanceTo(lodInfo.center)
const level = Math.min(d >= lodInfo.dist2 ? 2 : d >= lodInfo.dist1 ? 1 : 0, lodInfo.maxLevel)
for (let i = 0; i < lodScenes.length; i++) {
lodScenes[i]!.visible = i === level
} }
})
const rootRef = useRef<THREE.Group>(null!) const rootRef = useRef<THREE.Group>(null!)
const { actions } = useAnimations(gltf.animations, rootRef) const { actions } = useAnimations(gltf.animations, rootRef)
const camera = useThree((state) => state.camera) const camera = useThree((state) => state.camera)
@@ -380,10 +345,7 @@ export function GlbScene({
const built: ZoneFill[] = [] const built: ZoneFill[] = []
for (const entry of zoneEntries) { for (const entry of zoneEntries) {
const shape = new THREE.Shape() const shape = new THREE.Shape()
entry.polygon.forEach(([x, z], i) => { entry.polygon.forEach(([x, z], i) => (i === 0 ? shape.moveTo(x, -z) : shape.lineTo(x, -z)))
if (i === 0) shape.moveTo(x, -z)
else shape.lineTo(x, -z)
})
shape.closePath() shape.closePath()
const floorMaterial = createZoneFloorMaterial(entry.color) const floorMaterial = createZoneFloorMaterial(entry.color)
@@ -706,7 +668,6 @@ export function GlbScene({
return ( return (
<group ref={rootRef}> <group ref={rootRef}>
{/* lod0: the interactive level (identity/selection/zones all bind here). */}
<primitive <primitive
object={gltf.scene} object={gltf.scene}
onClick={handleClick} onClick={handleClick}
@@ -714,11 +675,6 @@ export function GlbScene({
onPointerMove={handlePointerMove} onPointerMove={handlePointerMove}
onPointerOut={handlePointerOut} onPointerOut={handlePointerOut}
/> />
{/* Simpler LODs are visual-only; the useFrame above toggles their
visibility by camera distance. They carry no interaction handlers. */}
{gltfs.slice(1).map((g, i) => (
<primitive key={lodScenes[i + 1]?.uuid ?? i} object={g.scene} />
))}
{/* Floating room labels. Each group's matrix is synced to its zone node {/* Floating room labels. Each group's matrix is synced to its zone node
every frame (above) so the label rides level stacking; the div fades every frame (above) so the label rides level stacking; the div fades
with the room fill via a CSS transition. */} with the room fill via a CSS transition. */}
+3 -9
View File
@@ -1,15 +1,9 @@
import { useGLTF } from '@react-three/drei' import { useGLTF } from '@react-three/drei'
import { type ObjectMap, useThree } from '@react-three/fiber' import { useThree } from '@react-three/fiber'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js' import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { ensureKtx2Support, ktx2Loader } from '../lib/ktx2-loader' import { ensureKtx2Support, ktx2Loader } from '../lib/ktx2-loader'
type GLTFKTX2Path = string | string[] const useGLTFKTX2 = (path: string): ReturnType<typeof useGLTF> => {
type GLTFKTX2Result<T extends GLTFKTX2Path> = T extends string[]
? Array<GLTF & ObjectMap>
: GLTF & ObjectMap
const useGLTFKTX2 = <T extends GLTFKTX2Path>(path: T): GLTFKTX2Result<T> => {
const gl = useThree((state) => state.gl) const gl = useThree((state) => state.gl)
return useGLTF(path, true, true, (loader) => { return useGLTF(path, true, true, (loader) => {
@@ -18,7 +12,7 @@ const useGLTFKTX2 = <T extends GLTFKTX2Path>(path: T): GLTFKTX2Result<T> => {
loader.setKTX2Loader(ktx2Loader as any) loader.setKTX2Loader(ktx2Loader as any)
} }
loader.setMeshoptDecoder(MeshoptDecoder) loader.setMeshoptDecoder(MeshoptDecoder)
}) as unknown as GLTFKTX2Result<T> })
} }
export { useGLTFKTX2 } export { useGLTFKTX2 }