3 Commits

Author SHA1 Message Date
Tom Boullay bee0c7f223 fix(world): make octree collision proxies solid
🔍 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
2026-06-01 15:15:55 +02:00
Tom Boullay 216d29ae59 docs(three): document sun shadow needsUpdate fix
Update three-debugging.md to reflect that the shadow intermittence
is resolved by explicit sun.shadow.needsUpdate = true at mount and
in useFrame after updateMatrixWorld.
2026-06-01 14:47:29 +02:00
Tom Boullay e13cf1e4c7 fix(world): force per-frame sun shadow refresh
Restore sun.shadow.needsUpdate = true at mount and in useFrame
after updateMatrixWorld. Lost during SHADOW_CONFIG centralization.
Matches develop's belt-and-suspenders pattern; autoUpdate alone
is insufficient because the sun follows the camera (matrix dirty
every frame) and three.js can skip shadow map re-render.
2026-06-01 14:46:57 +02:00
3 changed files with 35 additions and 22 deletions
+22 -18
View File
@@ -41,29 +41,33 @@ The octree visualization reads the live `Octree` instance from `World`. The
mesh uses `depthTest: false` and a high `renderOrder`, so cells stay visible
through opaque geometry.
## Shadow rendering intermittence (open investigation)
## Shadow rendering intermittence
Shadows occasionally fail to render on initial load even though the
`Lighting` configuration runs to completion (verified through diagnostic logs).
The issue is not deterministic across runs with identical config. Suspected
contributors:
Shadows occasionally failed to render on initial load and could disappear
mid-session even though the `Lighting` configuration ran to completion.
- WebGL context restoration timing (`webglcontextrestored` rebinds shadow map
state in `src/pages/page.tsx`).
- First-frame shadow map being rendered before any mesh has its
`castShadow`/`receiveShadow` flag set; `autoUpdate=true` should fix it on the
next frame, but a single dropped frame is still visible at very first paint.
- HMR/state interactions in dev mode that do not occur in production builds.
Root cause: the sun follows the camera (its world matrix is dirty every frame
via `updateMatrixWorld()` inside `Lighting.useFrame`). With `shadow.autoUpdate`
alone, three.js can skip the shadow map re-render on a frame where the matrix
update has happened but the renderer's internal dirty tracking does not pick
it up, leaving the shadow map stale or unrendered.
Mitigations already applied:
Fix in `src/world/Lighting.tsx`: explicit `sun.shadow.needsUpdate = true` in
two places, restoring the belt-and-suspenders pattern from `develop`:
- After `configureSunShadow(...)` in the mount `useEffect`.
- At the end of the `useFrame` block, right after `sun.updateMatrixWorld()`.
Mitigations also in place:
- Shadow config centralized in `src/data/world/lightingConfig.ts`
(`bias=0`, `normalBias=0`, `cameraSize=95`, matching the historically working
values from `develop`).
(`bias=0`, `normalBias=0`, `cameraSize=95`).
- Late-suspension Suspense boundaries in `World.tsx` to prevent global scene
remounts that would re-run shadow setup mid-load.
- `gl.shadowMap.needsUpdate = true` on `onCreated` and on
`webglcontextrestored` in `src/pages/page.tsx`.
If the issue reproduces in production, capture a screenshot plus the
`[diag]`-style logs from `useOctreeGraphNode`, `Lighting`, and `GameMapCollision`
to confirm whether the third configuration pass is happening (which would
indicate a remaining suspending hook outside the existing Suspense boundaries).
If the issue reproduces, capture `[diag]`-style logs from `useOctreeGraphNode`,
`Lighting`, and `GameMapCollision` to confirm there is no extra configuration
pass (which would indicate a remaining suspending hook outside the existing
Suspense boundaries).
+11 -4
View File
@@ -326,10 +326,17 @@ function CollisionModelInstance({
function CollisionBox({ box }: { box: OctreeCollisionBox }): React.JSX.Element {
return (
<mesh position={box.center}>
<boxGeometry args={box.size} />
<meshBasicMaterial />
</mesh>
<group position={box.center}>
<mesh>
<boxGeometry args={box.size} />
<meshBasicMaterial />
</mesh>
{/* Octree ignores material.side, so rotate a second shell for X/Z collisions. */}
<mesh rotation={[0, Math.PI, 0]}>
<boxGeometry args={box.size} />
<meshBasicMaterial />
</mesh>
</group>
);
}
+2
View File
@@ -63,6 +63,7 @@ export function Lighting(): React.JSX.Element {
configureSunShadow(sun.current, sunTarget.current);
configureRendererShadows(gl);
sun.current.shadow.needsUpdate = true;
}, [gl]);
useDebugFolder("Lighting", (folder) => {
@@ -114,6 +115,7 @@ export function Lighting(): React.JSX.Element {
sun.current.color.set(LIGHTING_STATE.sunColor);
sun.current.intensity = LIGHTING_STATE.sunIntensity;
sun.current.updateMatrixWorld();
sun.current.shadow.needsUpdate = true;
}
});