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

This commit is contained in:
Tom Boullay
2026-05-31 11:00:40 +02:00
parent bff8a16290
commit e073fc375b
6 changed files with 165 additions and 5 deletions
+31 -1
View File
@@ -11,10 +11,13 @@ interface UseWorldSceneLoadingOptions {
interface UseWorldSceneLoadingResult {
octree: Octree | null;
gameplayReady: boolean;
shouldWarmUpShadows: boolean;
showGameStage: boolean;
handleGameStageLoaded: () => void;
handleGameMapLoaded: () => void;
handleOctreeReady: (octree: Octree) => void;
handleShadowWarmupReady: () => void;
handleShadowWarmupStarted: () => void;
}
export function useWorldSceneLoading({
@@ -24,13 +27,19 @@ export function useWorldSceneLoading({
const [octree, setOctree] = useState<Octree | null>(null);
const [gameMapLoaded, setGameMapLoaded] = useState(false);
const [gameStageLoaded, setGameStageLoaded] = useState(false);
const [shadowsReady, setShadowsReady] = useState(false);
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 =
(sceneMode === "game" && gameplayReady) ||
(sceneMode === "physics" && octree !== null);
const handleGameMapLoaded = useCallback(() => {
setShadowsReady(false);
setGameMapLoaded(true);
}, []);
@@ -45,6 +54,7 @@ export function useWorldSceneLoading({
const handleOctreeReady = useCallback(
(nextOctree: Octree) => {
setShadowsReady(false);
setOctree(nextOctree);
onLoadingStateChange?.({
currentStep: "Collision prête",
@@ -55,6 +65,23 @@ export function useWorldSceneLoading({
[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(() => {
onLoadingStateChange?.({
currentStep: "Initialisation du jeu",
@@ -88,9 +115,12 @@ export function useWorldSceneLoading({
return {
octree,
gameplayReady,
shouldWarmUpShadows,
showGameStage,
handleGameStageLoaded,
handleGameMapLoaded,
handleOctreeReady,
handleShadowWarmupReady,
handleShadowWarmupStarted,
};
}
+21 -1
View File
@@ -15,11 +15,24 @@ import { SkyModel } from "@/components/three/world/SkyModel";
import { CloudSystem } from "@/world/clouds/CloudSystem";
import { FogSystem } from "@/world/fog/FogSystem";
import { GrassSystem } from "@/world/grass/GrassSystem";
import { SceneShadowWarmup } from "@/world/SceneShadowWarmup";
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
import { WaterSystem } from "@/world/water/WaterSystem";
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 groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models);
@@ -34,6 +47,13 @@ export function Environment(): React.JSX.Element {
return (
<>
<FogSystem />
{shadowWarmup ? (
<SceneShadowWarmup
active={shadowWarmup.active}
onReady={shadowWarmup.onReady}
onStarted={shadowWarmup.onStarted}
/>
) : null}
{showSky ? (
<SkyModel
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
+92
View File
@@ -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
View File
@@ -47,6 +47,9 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
handleGameStageLoaded,
handleGameMapLoaded,
handleOctreeReady,
handleShadowWarmupReady,
handleShadowWarmupStarted,
shouldWarmUpShadows,
} = useWorldSceneLoading({ sceneMode, onLoadingStateChange });
const playerSpawnPosition =
sceneMode === "game"
@@ -61,7 +64,13 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
return (
<>
<Environment />
<Environment
shadowWarmup={{
active: shouldWarmUpShadows,
onReady: handleShadowWarmupReady,
onStarted: handleShadowWarmupStarted,
}}
/>
<Lighting />
<DebugHelpers />
{showHandTrackingGloves ? (