From c8bd29e90923cff0698148b90f4ef4e36fb43098 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Tue, 23 Jun 2026 13:38:40 -0400 Subject: [PATCH] revert(viewer): drop distance-based LOD switching from GLB scene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/components/viewer/glb-scene.tsx | 54 ++----------------- packages/viewer/src/hooks/use-gltf-ktx2.tsx | 12 ++--- 2 files changed, 8 insertions(+), 58 deletions(-) diff --git a/packages/viewer/src/components/viewer/glb-scene.tsx b/packages/viewer/src/components/viewer/glb-scene.tsx index 415df5e8..5dd8d28b 100644 --- a/packages/viewer/src/components/viewer/glb-scene.tsx +++ b/packages/viewer/src/components/viewer/glb-scene.tsx @@ -13,7 +13,6 @@ import useViewer from '../../store/use-viewer' /** Vertical gap added per floor in `exploded` level mode (matches LevelSystem). */ const EXPLODED_GAP = 5 -const LOD_FALLBACK_RADIUS = 10 /** A building floor discovered in the baked GLB, ordered bottom-to-top. */ 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. */ type Target = { object: THREE.Object3D; id: string; kind: string; label: string } type HitCandidate = { object: THREE.Object3D; point?: THREE.Vector3 } -type GlbLoadResult = { - scene: THREE.Group - animations: THREE.AnimationClip[] -} function findIdentityAncestor(object: THREE.Object3D): THREE.Object3D | null { let current: THREE.Object3D | null = object @@ -233,49 +228,19 @@ function createZoneWallGeometry(polygon: [number, number][]): THREE.BufferGeomet */ export function GlbScene({ url, - lodUrls, onLevelsChange, onIdentityChange, onHoverChange, }: { url: string - lodUrls?: string[] onLevelsChange?: (levels: GlbLevel[]) => void onIdentityChange?: (identity: GlbIdentity) => void onHoverChange?: (hover: GlbHover) => void }) { - const urls = useMemo(() => (lodUrls && lodUrls.length > 1 ? lodUrls : [url]), [lodUrls, url]) - const loadedGltfs = useGLTFKTX2(urls) as unknown as GlbLoadResult | GlbLoadResult[] - const gltfs = Array.isArray(loadedGltfs) ? loadedGltfs : [loadedGltfs] - 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 gltf = useGLTFKTX2(url) as unknown as { + scene: THREE.Group + animations: THREE.AnimationClip[] + } const rootRef = useRef(null!) const { actions } = useAnimations(gltf.animations, rootRef) const camera = useThree((state) => state.camera) @@ -380,10 +345,7 @@ export function GlbScene({ const built: ZoneFill[] = [] for (const entry of zoneEntries) { const shape = new THREE.Shape() - entry.polygon.forEach(([x, z], i) => { - if (i === 0) shape.moveTo(x, -z) - else shape.lineTo(x, -z) - }) + entry.polygon.forEach(([x, z], i) => (i === 0 ? shape.moveTo(x, -z) : shape.lineTo(x, -z))) shape.closePath() const floorMaterial = createZoneFloorMaterial(entry.color) @@ -706,7 +668,6 @@ export function GlbScene({ return ( - {/* lod0: the interactive level (identity/selection/zones all bind here). */} - {/* 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) => ( - - ))} {/* 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 with the room fill via a CSS transition. */} diff --git a/packages/viewer/src/hooks/use-gltf-ktx2.tsx b/packages/viewer/src/hooks/use-gltf-ktx2.tsx index ec2f8506..000b8ff6 100644 --- a/packages/viewer/src/hooks/use-gltf-ktx2.tsx +++ b/packages/viewer/src/hooks/use-gltf-ktx2.tsx @@ -1,15 +1,9 @@ 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 type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js' import { ensureKtx2Support, ktx2Loader } from '../lib/ktx2-loader' -type GLTFKTX2Path = string | string[] -type GLTFKTX2Result = T extends string[] - ? Array - : GLTF & ObjectMap - -const useGLTFKTX2 = (path: T): GLTFKTX2Result => { +const useGLTFKTX2 = (path: string): ReturnType => { const gl = useThree((state) => state.gl) return useGLTF(path, true, true, (loader) => { @@ -18,7 +12,7 @@ const useGLTFKTX2 = (path: T): GLTFKTX2Result => { loader.setKTX2Loader(ktx2Loader as any) } loader.setMeshoptDecoder(MeshoptDecoder) - }) as unknown as GLTFKTX2Result + }) } export { useGLTFKTX2 }