diff --git a/docs/technical/map-lod.md b/docs/technical/map-lod.md index 952a09c..b43e6f5 100644 --- a/docs/technical/map-lod.md +++ b/docs/technical/map-lod.md @@ -25,7 +25,7 @@ Current behavior: | -------- | ------------------: | --- | ------------------------------------- | | `low` | 10m | On | Always use `*-LOD` models | | `medium` | 20m | On | Always use `*-LOD` models | -| `high` | Current default 50m | Off | Regular model up to 10m, then `*-LOD` | +| `high` | 35m | Off | Regular model up to 10m, then `*-LOD` | | `ultra` | 50m | Off | Regular model up to 20m, then `*-LOD` | The unload distance stays slightly larger than the load distance to avoid rapid mount/unmount flickering when the player stands near a boundary. diff --git a/docs/technical/map-performance.md b/docs/technical/map-performance.md index 853b890..5b509b4 100644 --- a/docs/technical/map-performance.md +++ b/docs/technical/map-performance.md @@ -158,9 +158,11 @@ Current runtime values: ```txt chunkSize: 35 -loadRadius: 45 -unloadRadius: 45 -updateInterval: 350ms +low load/unload radius: 10m / 18m +medium load/unload radius: 20m / 30m +high load/unload radius: 35m / 45m +ultra load/unload radius: 50m / 65m +updateInterval: 250ms fog near: 30 fog far: 45 ``` diff --git a/docs/technical/scene-runtime.md b/docs/technical/scene-runtime.md index 08f38e2..230497d 100644 --- a/docs/technical/scene-runtime.md +++ b/docs/technical/scene-runtime.md @@ -91,6 +91,12 @@ Activation des ombres -> Ombres prĂȘtes -> Gameplay prĂȘt 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. + The debug physics scene is ready when: ```ts diff --git a/src/data/world/graphicsConfig.ts b/src/data/world/graphicsConfig.ts index de081fc..be5b1e9 100644 --- a/src/data/world/graphicsConfig.ts +++ b/src/data/world/graphicsConfig.ts @@ -1,5 +1,3 @@ -import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig"; - export const GRAPHICS_PRESET_KEYS = ["low", "medium", "high", "ultra"] as const; export type GraphicsPreset = (typeof GRAPHICS_PRESET_KEYS)[number]; @@ -32,8 +30,8 @@ export const GRAPHICS_PRESETS = { }, high: { label: "High", - chunkLoadRadius: CHUNK_CONFIG.loadRadius, - chunkUnloadRadius: CHUNK_CONFIG.unloadRadius, + chunkLoadRadius: 35, + chunkUnloadRadius: 45, fogEnabled: false, forceLodModels: false, lodHighDetailDistance: 10, diff --git a/src/pages/page.tsx b/src/pages/page.tsx index 6d9d752..c3f9b06 100644 --- a/src/pages/page.tsx +++ b/src/pages/page.tsx @@ -130,7 +130,8 @@ export function HomePage(): React.JSX.Element | null { gl.shadowMap.enabled = true; gl.shadowMap.type = THREE.PCFShadowMap; - gl.shadowMap.autoUpdate = true; + gl.shadowMap.autoUpdate = false; + gl.shadowMap.needsUpdate = true; // The browser hands us a WEBGL_lose_context extension we can use to // ask the GPU to restore the context after a loss. Without this the @@ -147,7 +148,8 @@ export function HomePage(): React.JSX.Element | null { const handleContextRestored = () => { gl.shadowMap.enabled = true; gl.shadowMap.type = THREE.PCFShadowMap; - gl.shadowMap.autoUpdate = true; + gl.shadowMap.autoUpdate = false; + gl.shadowMap.needsUpdate = true; logger.info("WebGL", "Context restored"); }; diff --git a/src/world/Lighting.tsx b/src/world/Lighting.tsx index 9c3cd45..acb1cfb 100644 --- a/src/world/Lighting.tsx +++ b/src/world/Lighting.tsx @@ -1,6 +1,15 @@ import { useEffect, useRef } from "react"; +import type { MutableRefObject } from "react"; import { useFrame, useThree } from "@react-three/fiber"; -import type { AmbientLight, DirectionalLight, Object3D } from "three"; +import { + PCFShadowMap, + Vector3, + type AmbientLight, + type Camera, + type DirectionalLight, + type Object3D, + type WebGLRenderer, +} from "three"; import { AMBIENT_INTENSITY_MAX, AMBIENT_INTENSITY_MIN, @@ -26,29 +35,84 @@ const SHADOW_MAP_SIZE = 2048; const SHADOW_CAMERA_SIZE = 95; const SHADOW_CAMERA_NEAR = 0.5; const SHADOW_CAMERA_FAR = 300; +const SHADOW_REFRESH_INTERVAL_MS = 180; +const SHADOW_REFRESH_DISTANCE = 0.75; +const SHADOW_REFRESH_DISTANCE_SQUARED = + SHADOW_REFRESH_DISTANCE * SHADOW_REFRESH_DISTANCE; + +function configureManualRendererShadows(gl: WebGLRenderer): void { + gl.shadowMap.enabled = true; + gl.shadowMap.type = PCFShadowMap; + gl.shadowMap.autoUpdate = false; + gl.shadowMap.needsUpdate = true; +} + +function configureSunShadow(sun: DirectionalLight, sunTarget: Object3D): void { + sun.target = sunTarget; + sun.shadow.autoUpdate = false; + sun.shadow.needsUpdate = true; + sun.shadow.mapSize.width = SHADOW_MAP_SIZE; + sun.shadow.mapSize.height = SHADOW_MAP_SIZE; + sun.shadow.camera.left = -SHADOW_CAMERA_SIZE; + sun.shadow.camera.right = SHADOW_CAMERA_SIZE; + sun.shadow.camera.top = SHADOW_CAMERA_SIZE; + sun.shadow.camera.bottom = -SHADOW_CAMERA_SIZE; + sun.shadow.camera.near = SHADOW_CAMERA_NEAR; + sun.shadow.camera.far = SHADOW_CAMERA_FAR; + sun.shadow.camera.updateProjectionMatrix(); +} + +function requestSunShadowRefresh({ + camera, + elapsedMs, + gl, + lastCameraPosition, + lastRefreshMs, + shadowHasInitialPosition, + sun, +}: { + camera: Camera; + elapsedMs: number; + gl: WebGLRenderer; + lastCameraPosition: Vector3; + lastRefreshMs: MutableRefObject; + shadowHasInitialPosition: MutableRefObject; + sun: DirectionalLight; +}): void { + if (elapsedMs - lastRefreshMs.current < SHADOW_REFRESH_INTERVAL_MS) { + return; + } + + const cameraMovedEnough = + !shadowHasInitialPosition.current || + lastCameraPosition.distanceToSquared(camera.position) >= + SHADOW_REFRESH_DISTANCE_SQUARED; + + if (!cameraMovedEnough) return; + + configureManualRendererShadows(gl); + sun.shadow.needsUpdate = true; + lastCameraPosition.copy(camera.position); + lastRefreshMs.current = elapsedMs; + shadowHasInitialPosition.current = true; +} export function Lighting(): React.JSX.Element { const camera = useThree((state) => state.camera); + const gl = useThree((state) => state.gl); const ambient = useRef(null); const sun = useRef(null); const sunTarget = useRef(null); + const lastShadowRefreshMs = useRef(-SHADOW_REFRESH_INTERVAL_MS); + const lastShadowCameraPosition = useRef(new Vector3()); + const shadowHasInitialPosition = useRef(false); useEffect(() => { if (!sun.current || !sunTarget.current) return; - sun.current.target = sunTarget.current; - sun.current.shadow.autoUpdate = true; - sun.current.shadow.needsUpdate = true; - sun.current.shadow.mapSize.width = SHADOW_MAP_SIZE; - sun.current.shadow.mapSize.height = SHADOW_MAP_SIZE; - sun.current.shadow.camera.left = -SHADOW_CAMERA_SIZE; - sun.current.shadow.camera.right = SHADOW_CAMERA_SIZE; - sun.current.shadow.camera.top = SHADOW_CAMERA_SIZE; - sun.current.shadow.camera.bottom = -SHADOW_CAMERA_SIZE; - sun.current.shadow.camera.near = SHADOW_CAMERA_NEAR; - sun.current.shadow.camera.far = SHADOW_CAMERA_FAR; - sun.current.shadow.camera.updateProjectionMatrix(); - }, []); + configureSunShadow(sun.current, sunTarget.current); + configureManualRendererShadows(gl); + }, [gl]); useDebugFolder("Lighting", (folder) => { folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color"); @@ -82,7 +146,7 @@ export function Lighting(): React.JSX.Element { .name("Sun Z"); }); - useFrame(() => { + useFrame(({ clock }) => { if (ambient.current) { ambient.current.color.set(LIGHTING_STATE.ambientColor); ambient.current.intensity = LIGHTING_STATE.ambientIntensity; @@ -99,7 +163,15 @@ 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; + requestSunShadowRefresh({ + camera, + elapsedMs: clock.elapsedTime * 1000, + gl, + lastCameraPosition: lastShadowCameraPosition.current, + lastRefreshMs: lastShadowRefreshMs, + shadowHasInitialPosition, + sun: sun.current, + }); } }); diff --git a/src/world/SceneShadowWarmup.tsx b/src/world/SceneShadowWarmup.tsx index 4f994bd..d3a4897 100644 --- a/src/world/SceneShadowWarmup.tsx +++ b/src/world/SceneShadowWarmup.tsx @@ -45,6 +45,11 @@ function forceSceneShadowPass( }); } +function restoreManualShadowUpdates(gl: THREE.WebGLRenderer): void { + gl.shadowMap.autoUpdate = false; + gl.shadowMap.needsUpdate = true; +} + export function SceneShadowWarmup({ active, onReady, @@ -77,6 +82,7 @@ export function SceneShadowWarmup({ secondFrame = window.requestAnimationFrame(() => { forceSceneShadowPass(gl, scene); + restoreManualShadowUpdates(gl); invalidate(); onReady(); });