fix(world): warm up map shadows from environment
🔍 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
🔍 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
This commit is contained in:
Binary file not shown.
@@ -72,14 +72,23 @@ It tracks:
|
|||||||
- `gameMapLoaded`: map data and visible map nodes settled
|
- `gameMapLoaded`: map data and visible map nodes settled
|
||||||
- `gameStageLoaded`: Rapier gameplay stage mounted
|
- `gameStageLoaded`: Rapier gameplay stage mounted
|
||||||
- `showGameStage`: true when the map is ready enough to mount gameplay content
|
- `showGameStage`: true when the map is ready enough to mount gameplay content
|
||||||
- `gameplayReady`: true when map, stage, and octree are all ready
|
- `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
|
||||||
|
|
||||||
The final game-scene readiness condition is:
|
The base game-scene readiness condition before the shadow warmup is:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
showGameStage && gameStageLoaded && octree !== null;
|
showGameStage && gameStageLoaded && octree !== null;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
After that condition is met, `SceneShadowWarmup` runs one final loading step:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
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.
|
||||||
|
|
||||||
The debug physics scene is ready when:
|
The debug physics scene is ready when:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ interface UseWorldSceneLoadingOptions {
|
|||||||
interface UseWorldSceneLoadingResult {
|
interface UseWorldSceneLoadingResult {
|
||||||
octree: Octree | null;
|
octree: Octree | null;
|
||||||
gameplayReady: boolean;
|
gameplayReady: boolean;
|
||||||
|
shouldWarmUpShadows: boolean;
|
||||||
showGameStage: boolean;
|
showGameStage: boolean;
|
||||||
handleGameStageLoaded: () => void;
|
handleGameStageLoaded: () => void;
|
||||||
handleGameMapLoaded: () => void;
|
handleGameMapLoaded: () => void;
|
||||||
handleOctreeReady: (octree: Octree) => void;
|
handleOctreeReady: (octree: Octree) => void;
|
||||||
|
handleShadowWarmupReady: () => void;
|
||||||
|
handleShadowWarmupStarted: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorldSceneLoading({
|
export function useWorldSceneLoading({
|
||||||
@@ -24,13 +27,19 @@ export function useWorldSceneLoading({
|
|||||||
const [octree, setOctree] = useState<Octree | null>(null);
|
const [octree, setOctree] = useState<Octree | null>(null);
|
||||||
const [gameMapLoaded, setGameMapLoaded] = useState(false);
|
const [gameMapLoaded, setGameMapLoaded] = useState(false);
|
||||||
const [gameStageLoaded, setGameStageLoaded] = useState(false);
|
const [gameStageLoaded, setGameStageLoaded] = useState(false);
|
||||||
|
const [shadowsReady, setShadowsReady] = useState(false);
|
||||||
const showGameStage = sceneMode === "game" && gameMapLoaded;
|
const showGameStage = sceneMode === "game" && gameMapLoaded;
|
||||||
const gameplayReady = showGameStage && gameStageLoaded && octree !== null;
|
const gameSceneReadyForShadows =
|
||||||
|
showGameStage && gameStageLoaded && octree !== null;
|
||||||
|
const shadowWarmupReady = sceneMode === "game" && gameSceneReadyForShadows;
|
||||||
|
const shouldWarmUpShadows = shadowWarmupReady && !shadowsReady;
|
||||||
|
const gameplayReady = gameSceneReadyForShadows && shadowsReady;
|
||||||
const sceneReady =
|
const sceneReady =
|
||||||
(sceneMode === "game" && gameplayReady) ||
|
(sceneMode === "game" && gameplayReady) ||
|
||||||
(sceneMode === "physics" && octree !== null);
|
(sceneMode === "physics" && octree !== null);
|
||||||
|
|
||||||
const handleGameMapLoaded = useCallback(() => {
|
const handleGameMapLoaded = useCallback(() => {
|
||||||
|
setShadowsReady(false);
|
||||||
setGameMapLoaded(true);
|
setGameMapLoaded(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -45,6 +54,7 @@ export function useWorldSceneLoading({
|
|||||||
|
|
||||||
const handleOctreeReady = useCallback(
|
const handleOctreeReady = useCallback(
|
||||||
(nextOctree: Octree) => {
|
(nextOctree: Octree) => {
|
||||||
|
setShadowsReady(false);
|
||||||
setOctree(nextOctree);
|
setOctree(nextOctree);
|
||||||
onLoadingStateChange?.({
|
onLoadingStateChange?.({
|
||||||
currentStep: "Collision prête",
|
currentStep: "Collision prête",
|
||||||
@@ -55,6 +65,23 @@ export function useWorldSceneLoading({
|
|||||||
[onLoadingStateChange],
|
[onLoadingStateChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleShadowWarmupStarted = useCallback(() => {
|
||||||
|
onLoadingStateChange?.({
|
||||||
|
currentStep: "Activation des ombres",
|
||||||
|
progress: 0.97,
|
||||||
|
status: "loading",
|
||||||
|
});
|
||||||
|
}, [onLoadingStateChange]);
|
||||||
|
|
||||||
|
const handleShadowWarmupReady = useCallback(() => {
|
||||||
|
setShadowsReady(true);
|
||||||
|
onLoadingStateChange?.({
|
||||||
|
currentStep: "Ombres prêtes",
|
||||||
|
progress: 0.99,
|
||||||
|
status: "loading",
|
||||||
|
});
|
||||||
|
}, [onLoadingStateChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoadingStateChange?.({
|
onLoadingStateChange?.({
|
||||||
currentStep: "Initialisation du jeu",
|
currentStep: "Initialisation du jeu",
|
||||||
@@ -88,9 +115,12 @@ export function useWorldSceneLoading({
|
|||||||
return {
|
return {
|
||||||
octree,
|
octree,
|
||||||
gameplayReady,
|
gameplayReady,
|
||||||
|
shouldWarmUpShadows,
|
||||||
showGameStage,
|
showGameStage,
|
||||||
handleGameStageLoaded,
|
handleGameStageLoaded,
|
||||||
handleGameMapLoaded,
|
handleGameMapLoaded,
|
||||||
handleOctreeReady,
|
handleOctreeReady,
|
||||||
|
handleShadowWarmupReady,
|
||||||
|
handleShadowWarmupStarted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,24 @@ import { SkyModel } from "@/components/three/world/SkyModel";
|
|||||||
import { CloudSystem } from "@/world/clouds/CloudSystem";
|
import { CloudSystem } from "@/world/clouds/CloudSystem";
|
||||||
import { FogSystem } from "@/world/fog/FogSystem";
|
import { FogSystem } from "@/world/fog/FogSystem";
|
||||||
import { GrassSystem } from "@/world/grass/GrassSystem";
|
import { GrassSystem } from "@/world/grass/GrassSystem";
|
||||||
|
import { SceneShadowWarmup } from "@/world/SceneShadowWarmup";
|
||||||
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
||||||
import { WaterSystem } from "@/world/water/WaterSystem";
|
import { WaterSystem } from "@/world/water/WaterSystem";
|
||||||
import { WorldPlane } from "@/world/WorldPlane";
|
import { WorldPlane } from "@/world/WorldPlane";
|
||||||
|
|
||||||
export function Environment(): React.JSX.Element {
|
interface ShadowWarmupConfig {
|
||||||
|
active: boolean;
|
||||||
|
onReady: () => void;
|
||||||
|
onStarted: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnvironmentProps {
|
||||||
|
shadowWarmup?: ShadowWarmupConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment({
|
||||||
|
shadowWarmup,
|
||||||
|
}: EnvironmentProps): React.JSX.Element {
|
||||||
const sceneMode = useSceneMode();
|
const sceneMode = useSceneMode();
|
||||||
const groups = useMapPerformanceStore((state) => state.groups);
|
const groups = useMapPerformanceStore((state) => state.groups);
|
||||||
const models = useMapPerformanceStore((state) => state.models);
|
const models = useMapPerformanceStore((state) => state.models);
|
||||||
@@ -34,6 +47,13 @@ export function Environment(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FogSystem />
|
<FogSystem />
|
||||||
|
{shadowWarmup ? (
|
||||||
|
<SceneShadowWarmup
|
||||||
|
active={shadowWarmup.active}
|
||||||
|
onReady={shadowWarmup.onReady}
|
||||||
|
onStarted={shadowWarmup.onStarted}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{showSky ? (
|
{showSky ? (
|
||||||
<SkyModel
|
<SkyModel
|
||||||
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
interface SceneShadowWarmupProps {
|
||||||
|
active: boolean;
|
||||||
|
onReady: () => void;
|
||||||
|
onStarted: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markShadowLightForUpdate(object: THREE.Object3D): void {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
object instanceof THREE.DirectionalLight ||
|
||||||
|
object instanceof THREE.PointLight ||
|
||||||
|
object instanceof THREE.SpotLight
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!object.castShadow) return;
|
||||||
|
|
||||||
|
object.updateMatrixWorld(true);
|
||||||
|
object.shadow.camera.updateProjectionMatrix();
|
||||||
|
object.shadow.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceSceneShadowPass(
|
||||||
|
gl: THREE.WebGLRenderer,
|
||||||
|
scene: THREE.Scene,
|
||||||
|
): void {
|
||||||
|
gl.shadowMap.enabled = true;
|
||||||
|
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||||
|
gl.shadowMap.autoUpdate = true;
|
||||||
|
gl.shadowMap.needsUpdate = true;
|
||||||
|
|
||||||
|
scene.updateMatrixWorld(true);
|
||||||
|
scene.traverse((object) => {
|
||||||
|
if (object instanceof THREE.Mesh) {
|
||||||
|
object.updateMatrixWorld(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
markShadowLightForUpdate(object);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SceneShadowWarmup({
|
||||||
|
active,
|
||||||
|
onReady,
|
||||||
|
onStarted,
|
||||||
|
}: SceneShadowWarmupProps): null {
|
||||||
|
const gl = useThree((state) => state.gl);
|
||||||
|
const scene = useThree((state) => state.scene);
|
||||||
|
const invalidate = useThree((state) => state.invalidate);
|
||||||
|
const isRunningRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) {
|
||||||
|
isRunningRef.current = false;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunningRef.current) return undefined;
|
||||||
|
|
||||||
|
isRunningRef.current = true;
|
||||||
|
onStarted();
|
||||||
|
forceSceneShadowPass(gl, scene);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
let firstFrame = 0;
|
||||||
|
let secondFrame = 0;
|
||||||
|
|
||||||
|
firstFrame = window.requestAnimationFrame(() => {
|
||||||
|
forceSceneShadowPass(gl, scene);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
secondFrame = window.requestAnimationFrame(() => {
|
||||||
|
forceSceneShadowPass(gl, scene);
|
||||||
|
invalidate();
|
||||||
|
onReady();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(firstFrame);
|
||||||
|
window.cancelAnimationFrame(secondFrame);
|
||||||
|
};
|
||||||
|
}, [active, gl, invalidate, onReady, onStarted, scene]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+10
-1
@@ -47,6 +47,9 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
|||||||
handleGameStageLoaded,
|
handleGameStageLoaded,
|
||||||
handleGameMapLoaded,
|
handleGameMapLoaded,
|
||||||
handleOctreeReady,
|
handleOctreeReady,
|
||||||
|
handleShadowWarmupReady,
|
||||||
|
handleShadowWarmupStarted,
|
||||||
|
shouldWarmUpShadows,
|
||||||
} = useWorldSceneLoading({ sceneMode, onLoadingStateChange });
|
} = useWorldSceneLoading({ sceneMode, onLoadingStateChange });
|
||||||
const playerSpawnPosition =
|
const playerSpawnPosition =
|
||||||
sceneMode === "game"
|
sceneMode === "game"
|
||||||
@@ -61,7 +64,13 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Environment />
|
<Environment
|
||||||
|
shadowWarmup={{
|
||||||
|
active: shouldWarmUpShadows,
|
||||||
|
onReady: handleShadowWarmupReady,
|
||||||
|
onStarted: handleShadowWarmupStarted,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Lighting />
|
<Lighting />
|
||||||
<DebugHelpers />
|
<DebugHelpers />
|
||||||
{showHandTrackingGloves ? (
|
{showHandTrackingGloves ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user