From a766784ce85527cc900466861a44c9faf526e137 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 1 Jun 2026 14:14:32 +0200 Subject: [PATCH] docs: update scene runtime and debug toggles - Drop SceneShadowWarmup section, document centralized shadow config. - Document the localized Suspense boundaries in World.tsx. - Document the new player model and octree debug visualizations. - Open note about intermittent first-load shadow rendering. --- docs/technical/scene-runtime.md | 32 ++++++++++++--------- docs/technical/three-debugging.md | 47 +++++++++++++++++++++++++++++++ src/world/GameMapCollision.tsx | 24 +++++++++------- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/docs/technical/scene-runtime.md b/docs/technical/scene-runtime.md index 230497d..20848ab 100644 --- a/docs/technical/scene-runtime.md +++ b/docs/technical/scene-runtime.md @@ -74,28 +74,32 @@ It tracks: - `gameMapLoaded`: map data and visible map nodes settled - `gameStageLoaded`: Rapier gameplay stage mounted - `showGameStage`: true when the map is ready enough to mount gameplay content -- `shadowsReady`: renderer, shadow lights, and scene matrices have been forced once after the scene is mounted -- `gameplayReady`: true when map, stage, octree, and the shadow warmup are all ready +- `gameplayReady`: true when map, stage, and octree are all ready -The base game-scene readiness condition before the shadow warmup is: +The game-scene readiness condition is: ```ts showGameStage && gameStageLoaded && octree !== null; ``` -After that condition is met, `SceneShadowWarmup` runs one final loading step: +Shadows are configured once when `Lighting` mounts (renderer `shadowMap.enabled`, sun +`shadow.autoUpdate = true`, bias and frustum from `SHADOW_CONFIG` in +`src/data/world/lightingConfig.ts`). The shadow map then refreshes every frame and +follows the player camera through the sun's `target`. The earlier `SceneShadowWarmup` +step has been removed — the visible loading overlay no longer waits for a forced +shadow refresh because `autoUpdate` covers steady-state rendering. -```txt -Activation des ombres -> Ombres prêtes -> Gameplay prêt -``` +### Avoiding global scene remounts -This keeps the loading overlay visible until the renderer shadow map, shadow-casting light, and mounted scene graph have all been explicitly refreshed. - -After the warmup, shadow maps switch back to manual refreshes driven by `Lighting`. -The sun still follows the player camera, but the shadow map is only marked dirty -when the camera has moved enough and a short refresh interval has elapsed. This -keeps shadows present after loading without paying for a full shadow render every -frame across the dense vegetation chunks. +Heavy stage components (`GameStageContent`, `Player`, dialogues) load assets via +`useGLTF`/`useTexture` without preload (e.g. `EbikeSpeedometer` calls `useTexture` +when the bike mounts). To prevent any late suspension from bubbling up to the +root `` boundary in `src/pages/page.tsx` and unmounting the entire +world (which would trigger a redundant octree rebuild and shadow re-config), the +game stage block and the spawn-player block are wrapped in their own +`` boundaries inside `src/world/World.tsx`. Any new +sibling that suspends late should be added inside one of these boundaries or get +its own. The debug physics scene is ready when: diff --git a/docs/technical/three-debugging.md b/docs/technical/three-debugging.md index 1e7c8aa..f32e848 100644 --- a/docs/technical/three-debugging.md +++ b/docs/technical/three-debugging.md @@ -20,3 +20,50 @@ If DevTools still opens a bundled file, stop the dev server, clear Vite's cached rm -rf node_modules/.vite npm run dev:three-debug ``` + +## Visual debug toggles + +The `Debug` folder of the runtime debug GUI exposes inspection toggles backed by +`src/managers/stores/useDebugVisualsStore.ts`: + +- **Show Player Model** — renders the main character GLTF in front of the + current camera (`src/components/debug/DebugPlayerModel.tsx`). The model is + positioned in camera-local space so it stays visible regardless of pitch. +- **Show Octree** — overlays the collision octree as colored line segments, + one wireframe per spatial cell (`src/components/debug/DebugOctreeVisualization.tsx`). + Cells are colored by depth. Use it to inspect collision precision around + doorways or passages. +- **Octree Max Depth** — caps how deep the octree visualization recurses + (default 6). Increase to see leaf-level subdivisions; decrease to keep the + scene readable when the tree is large. + +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) + +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: + +- 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. + +Mitigations already applied: + +- Shadow config centralized in `src/data/world/lightingConfig.ts` + (`bias=0`, `normalBias=0`, `cameraSize=95`, matching the historically working + values from `develop`). +- Late-suspension Suspense boundaries in `World.tsx` to prevent global scene + remounts that would re-run shadow setup mid-load. + +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). diff --git a/src/world/GameMapCollision.tsx b/src/world/GameMapCollision.tsx index 5bce413..0fa1c67 100644 --- a/src/world/GameMapCollision.tsx +++ b/src/world/GameMapCollision.tsx @@ -31,6 +31,7 @@ import { } from "@/data/world/octreeCollisionConfig"; import { getMapModelScaleMultiplier } from "@/data/world/mapInstancingConfig"; import { useCharacterDebugStore } from "@/managers/stores/useCharacterDebugStore"; +import { useGameStore } from "@/managers/stores/useGameStore"; import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision"; import type { MapNode } from "@/types/map/mapScene"; import type { OctreeReadyHandler, Vector3Tuple } from "@/types/three/three"; @@ -125,21 +126,24 @@ export function GameMapCollision({ const settledCollisionNodesRef = useRef(new Set()); const loadedNotifiedRef = useRef(false); const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0); + const mainState = useGameStore((state) => state.mainState); const terrainHeight = useTerrainHeightSampler(); const collisionNodes = nodes.filter(isCollisionNode); + const includeCharacterCollisions = mainState !== "ebike"; + const characterCollisionCount = includeCharacterCollisions + ? CHARACTER_IDS.length + : 0; const collisionSourceCount = - collisionNodes.length + proxyNodes.length + CHARACTER_IDS.length; + collisionNodes.length + proxyNodes.length + characterCollisionCount; const collisionReady = mapReady && settledCollisionNodeCount >= collisionNodes.length; const characterCollisionSignature = useCharacterDebugStore((state) => - CHARACTER_IDS.map((id) => { - const character = state.characters[id]; - return [ - ...character.position, - ...character.rotation, - ...character.scale, - ].join(","); - }).join("|"), + includeCharacterCollisions + ? CHARACTER_IDS.map((id) => { + const character = state.characters[id]; + return [...character.position, ...character.rotation].join(","); + }).join("|") + : "characters-hidden", ); const collisionRebuildKey = collisionReady ? `${collisionNodes.length}:${collisionSourceCount}:${characterCollisionSignature}` @@ -221,7 +225,7 @@ export function GameMapCollision({ /> )) : null} - {mapReady ? ( + {mapReady && includeCharacterCollisions ? ( ) : null} {mapReady