fix(world): restore multi-frame shadow warmup and unblock fabrik doorway
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled

- Lighting: replace single-frame needsUpdate with a 3-rAF warmup that forces
  scene.updateMatrixWorld + sun.shadow.needsUpdate + gl.shadowMap.needsUpdate.
  This restores the SceneShadowWarmup behaviour (deleted in 777e51e) inline,
  so shadows survive Physics Suspense remounts and webglcontextrestored.
- octreeCollisionConfig: remove (comment out) the thin LA_FABRIK interior box
  at x=-6.93 that was sealing the doorway despite the mesh hole; fabrik mesh
  octree already provides surrounding wall collision.
- DebugOctreeVisualization: add Fabrik-only filter to inspect interior
  collisions/non-collisions in isolation.
This commit is contained in:
Tom Boullay
2026-06-01 22:41:45 +02:00
parent 1b57a25e5f
commit 69c720b86b
5 changed files with 89 additions and 9 deletions
+37 -3
View File
@@ -53,6 +53,18 @@ function configureSunShadow(sun: DirectionalLight, sunTarget: Object3D): void {
sun.shadow.camera.updateProjectionMatrix();
}
function forceShadowPass(
gl: WebGLRenderer,
scene: Scene,
sun: DirectionalLight,
): void {
scene.updateMatrixWorld(true);
sun.updateMatrixWorld(true);
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.needsUpdate = true;
gl.shadowMap.needsUpdate = true;
}
// [diag] temporary helper: count shadow-casting/receiving meshes in the scene
function snapshotShadowMeshes(scene: Scene): {
meshCount: number;
@@ -76,6 +88,7 @@ export function Lighting(): React.JSX.Element {
const camera = useThree((state) => state.camera);
const gl = useThree((state) => state.gl);
const scene = useThree((state) => state.scene);
const invalidate = useThree((state) => state.invalidate);
const ambient = useRef<AmbientLight>(null);
const sun = useRef<DirectionalLight>(null);
const sunTarget = useRef<Object3D>(null);
@@ -86,7 +99,25 @@ export function Lighting(): React.JSX.Element {
configureSunShadow(sun.current, sunTarget.current);
configureRendererShadows(gl);
sun.current.shadow.needsUpdate = true;
// Multi-frame shadow warmup: forces the shadow pass over 3 consecutive
// frames so newly mounted meshes (loaded asynchronously by Suspense) get
// their world matrices and shadow map properly allocated. Without this,
// shadows can fail to render after a Physics Suspense remount.
let raf1 = 0;
let raf2 = 0;
forceShadowPass(gl, scene, sun.current);
invalidate();
raf1 = window.requestAnimationFrame(() => {
if (!sun.current) return;
forceShadowPass(gl, scene, sun.current);
invalidate();
raf2 = window.requestAnimationFrame(() => {
if (!sun.current) return;
forceShadowPass(gl, scene, sun.current);
invalidate();
});
});
// [diag] one-shot scene snapshot to count shadow casters/receivers
const counts = snapshotShadowMeshes(scene);
@@ -110,16 +141,19 @@ export function Lighting(): React.JSX.Element {
timestamp: performance.now().toFixed(0),
});
if (sun.current) {
sun.current.shadow.needsUpdate = true;
forceShadowPass(gl, scene, sun.current);
invalidate();
}
};
canvas.addEventListener("webglcontextlost", handleContextLost);
canvas.addEventListener("webglcontextrestored", handleContextRestored);
return () => {
window.cancelAnimationFrame(raf1);
window.cancelAnimationFrame(raf2);
canvas.removeEventListener("webglcontextlost", handleContextLost);
canvas.removeEventListener("webglcontextrestored", handleContextRestored);
};
}, [gl, scene]);
}, [gl, invalidate, scene]);
useDebugFolder("Lighting", (folder) => {
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");