Feat/polish-mission1 #12
@@ -25,7 +25,7 @@ Current behavior:
|
|||||||
| -------- | ------------------: | --- | ------------------------------------- |
|
| -------- | ------------------: | --- | ------------------------------------- |
|
||||||
| `low` | 10m | On | Always use `*-LOD` models |
|
| `low` | 10m | On | Always use `*-LOD` models |
|
||||||
| `medium` | 20m | 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` |
|
| `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.
|
The unload distance stays slightly larger than the load distance to avoid rapid mount/unmount flickering when the player stands near a boundary.
|
||||||
|
|||||||
@@ -158,9 +158,11 @@ Current runtime values:
|
|||||||
|
|
||||||
```txt
|
```txt
|
||||||
chunkSize: 35
|
chunkSize: 35
|
||||||
loadRadius: 45
|
low load/unload radius: 10m / 18m
|
||||||
unloadRadius: 45
|
medium load/unload radius: 20m / 30m
|
||||||
updateInterval: 350ms
|
high load/unload radius: 35m / 45m
|
||||||
|
ultra load/unload radius: 50m / 65m
|
||||||
|
updateInterval: 250ms
|
||||||
fog near: 30
|
fog near: 30
|
||||||
fog far: 45
|
fog far: 45
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
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:
|
The debug physics scene is ready when:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
|
|
||||||
|
|
||||||
export const GRAPHICS_PRESET_KEYS = ["low", "medium", "high", "ultra"] as const;
|
export const GRAPHICS_PRESET_KEYS = ["low", "medium", "high", "ultra"] as const;
|
||||||
|
|
||||||
export type GraphicsPreset = (typeof GRAPHICS_PRESET_KEYS)[number];
|
export type GraphicsPreset = (typeof GRAPHICS_PRESET_KEYS)[number];
|
||||||
@@ -32,8 +30,8 @@ export const GRAPHICS_PRESETS = {
|
|||||||
},
|
},
|
||||||
high: {
|
high: {
|
||||||
label: "High",
|
label: "High",
|
||||||
chunkLoadRadius: CHUNK_CONFIG.loadRadius,
|
chunkLoadRadius: 35,
|
||||||
chunkUnloadRadius: CHUNK_CONFIG.unloadRadius,
|
chunkUnloadRadius: 45,
|
||||||
fogEnabled: false,
|
fogEnabled: false,
|
||||||
forceLodModels: false,
|
forceLodModels: false,
|
||||||
lodHighDetailDistance: 10,
|
lodHighDetailDistance: 10,
|
||||||
|
|||||||
+4
-2
@@ -130,7 +130,8 @@ export function HomePage(): React.JSX.Element | null {
|
|||||||
|
|
||||||
gl.shadowMap.enabled = true;
|
gl.shadowMap.enabled = true;
|
||||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
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
|
// 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
|
// 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 = () => {
|
const handleContextRestored = () => {
|
||||||
gl.shadowMap.enabled = true;
|
gl.shadowMap.enabled = true;
|
||||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||||
gl.shadowMap.autoUpdate = true;
|
gl.shadowMap.autoUpdate = false;
|
||||||
|
gl.shadowMap.needsUpdate = true;
|
||||||
logger.info("WebGL", "Context restored");
|
logger.info("WebGL", "Context restored");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+88
-16
@@ -1,6 +1,15 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import type { MutableRefObject } from "react";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
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 {
|
import {
|
||||||
AMBIENT_INTENSITY_MAX,
|
AMBIENT_INTENSITY_MAX,
|
||||||
AMBIENT_INTENSITY_MIN,
|
AMBIENT_INTENSITY_MIN,
|
||||||
@@ -26,29 +35,84 @@ const SHADOW_MAP_SIZE = 2048;
|
|||||||
const SHADOW_CAMERA_SIZE = 95;
|
const SHADOW_CAMERA_SIZE = 95;
|
||||||
const SHADOW_CAMERA_NEAR = 0.5;
|
const SHADOW_CAMERA_NEAR = 0.5;
|
||||||
const SHADOW_CAMERA_FAR = 300;
|
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<number>;
|
||||||
|
shadowHasInitialPosition: MutableRefObject<boolean>;
|
||||||
|
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 {
|
export function Lighting(): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
const gl = useThree((state) => state.gl);
|
||||||
const ambient = useRef<AmbientLight>(null);
|
const ambient = useRef<AmbientLight>(null);
|
||||||
const sun = useRef<DirectionalLight>(null);
|
const sun = useRef<DirectionalLight>(null);
|
||||||
const sunTarget = useRef<Object3D>(null);
|
const sunTarget = useRef<Object3D>(null);
|
||||||
|
const lastShadowRefreshMs = useRef(-SHADOW_REFRESH_INTERVAL_MS);
|
||||||
|
const lastShadowCameraPosition = useRef(new Vector3());
|
||||||
|
const shadowHasInitialPosition = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sun.current || !sunTarget.current) return;
|
if (!sun.current || !sunTarget.current) return;
|
||||||
|
|
||||||
sun.current.target = sunTarget.current;
|
configureSunShadow(sun.current, sunTarget.current);
|
||||||
sun.current.shadow.autoUpdate = true;
|
configureManualRendererShadows(gl);
|
||||||
sun.current.shadow.needsUpdate = true;
|
}, [gl]);
|
||||||
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();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useDebugFolder("Lighting", (folder) => {
|
useDebugFolder("Lighting", (folder) => {
|
||||||
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");
|
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");
|
||||||
@@ -82,7 +146,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
.name("Sun Z");
|
.name("Sun Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(({ clock }) => {
|
||||||
if (ambient.current) {
|
if (ambient.current) {
|
||||||
ambient.current.color.set(LIGHTING_STATE.ambientColor);
|
ambient.current.color.set(LIGHTING_STATE.ambientColor);
|
||||||
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
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.color.set(LIGHTING_STATE.sunColor);
|
||||||
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||||
sun.current.updateMatrixWorld();
|
sun.current.updateMatrixWorld();
|
||||||
sun.current.shadow.needsUpdate = true;
|
requestSunShadowRefresh({
|
||||||
|
camera,
|
||||||
|
elapsedMs: clock.elapsedTime * 1000,
|
||||||
|
gl,
|
||||||
|
lastCameraPosition: lastShadowCameraPosition.current,
|
||||||
|
lastRefreshMs: lastShadowRefreshMs,
|
||||||
|
shadowHasInitialPosition,
|
||||||
|
sun: sun.current,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ function forceSceneShadowPass(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreManualShadowUpdates(gl: THREE.WebGLRenderer): void {
|
||||||
|
gl.shadowMap.autoUpdate = false;
|
||||||
|
gl.shadowMap.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
export function SceneShadowWarmup({
|
export function SceneShadowWarmup({
|
||||||
active,
|
active,
|
||||||
onReady,
|
onReady,
|
||||||
@@ -77,6 +82,7 @@ export function SceneShadowWarmup({
|
|||||||
|
|
||||||
secondFrame = window.requestAnimationFrame(() => {
|
secondFrame = window.requestAnimationFrame(() => {
|
||||||
forceSceneShadowPass(gl, scene);
|
forceSceneShadowPass(gl, scene);
|
||||||
|
restoreManualShadowUpdates(gl);
|
||||||
invalidate();
|
invalidate();
|
||||||
onReady();
|
onReady();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user