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:
co-authored by
Claude Opus 4.8
parent
0469b25639
commit
c8bd29e909
@@ -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. */}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user