From 25e0f7e062d8d0087114dd635142fa3dd1a229f5 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Wed, 27 May 2026 00:54:17 +0200 Subject: [PATCH] feat(environment): add adaptive atmospheric fog --- src/data/world/fogConfig.ts | 32 ++++++++++++--- src/hooks/debug/useEnvironmentDebug.ts | 30 +++++++++++++- src/hooks/debug/useMapPerformanceDebug.ts | 3 -- src/hooks/world/useFogSettings.ts | 10 +++++ src/managers/stores/useWorldSettingsStore.ts | 14 +++++++ src/world/Environment.tsx | 42 +++++++++++++++++--- src/world/Lighting.tsx | 14 +------ src/world/lightingState.ts | 13 ++++++ src/world/water/WaterSurface.tsx | 10 +++++ src/world/water/waterShaders.ts | 8 ++++ 10 files changed, 148 insertions(+), 28 deletions(-) create mode 100644 src/hooks/world/useFogSettings.ts create mode 100644 src/world/lightingState.ts diff --git a/src/data/world/fogConfig.ts b/src/data/world/fogConfig.ts index 67a017c..93a5d59 100644 --- a/src/data/world/fogConfig.ts +++ b/src/data/world/fogConfig.ts @@ -1,17 +1,39 @@ import { TERRAIN_COLORS } from "@/data/world/terrainConfig"; +export type FogMode = "linear" | "exp2"; + export const FOG_CONFIG = { enabled: true, - color: "#dce8df", - near: 38, - far: 45, + mode: "exp2" as FogMode, + color: "#dfe7d8", + near: 32, + far: 48, + density: 0.032, }; +export const FOG_LIGHTING_COLOR_MIX = { + ambient: 0.3, + sun: 0.7, +}; + +export const FOG_BOUNDS = { + near: { min: 0, max: 100, step: 1 }, + far: { min: 1, max: 160, step: 1 }, + density: { min: 0.001, max: 0.05, step: 0.001 }, +}; + +export interface FogState { + density: number; + far: number; + mode: FogMode; + near: number; +} + export const CHUNK_CONFIG = { enabled: true, chunkSize: 35, - loadRadius: 45, - unloadRadius: 45, + loadRadius: 65, + unloadRadius: 75, updateInterval: 350, }; diff --git a/src/hooks/debug/useEnvironmentDebug.ts b/src/hooks/debug/useEnvironmentDebug.ts index 6fe0863..7bbc6c3 100644 --- a/src/hooks/debug/useEnvironmentDebug.ts +++ b/src/hooks/debug/useEnvironmentDebug.ts @@ -1,7 +1,9 @@ import { CLOUD_BOUNDS } from "@/data/world/cloudConfig"; +import { FOG_BOUNDS, type FogMode } from "@/data/world/fogConfig"; import { WIND_BOUNDS } from "@/data/world/windConfig"; import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; import { useWorldSettingsStore } from "@/managers/stores/useWorldSettingsStore"; +import { Debug } from "@/utils/debug/Debug"; export function useEnvironmentDebug(): void { useDebugFolder("Dynamic Wind", (folder) => { @@ -49,13 +51,39 @@ export function useEnvironmentDebug(): void { }); useDebugFolder("Environment", (folder) => { - const { clouds, graphics, setClouds, setDynamicClouds } = + Debug.getInstance().addFogControl(folder); + + const { clouds, fog, graphics, setClouds, setDynamicClouds, setFog } = useWorldSettingsStore.getState(); const controls = { ...clouds, + ...fog, dynamicClouds: graphics.dynamicClouds, }; + folder + .add(controls, "mode", { Linear: "linear", Exp2: "exp2" }) + .name("Fog mode") + .onChange((mode: FogMode) => setFog({ mode })); + + folder + .add(controls, "near", FOG_BOUNDS.near.min, FOG_BOUNDS.near.max) + .step(FOG_BOUNDS.near.step) + .name("Fog near") + .onChange((near: number) => setFog({ near })); + + folder + .add(controls, "far", FOG_BOUNDS.far.min, FOG_BOUNDS.far.max) + .step(FOG_BOUNDS.far.step) + .name("Fog far") + .onChange((far: number) => setFog({ far })); + + folder + .add(controls, "density", FOG_BOUNDS.density.min, FOG_BOUNDS.density.max) + .step(FOG_BOUNDS.density.step) + .name("Fog density") + .onChange((density: number) => setFog({ density })); + folder .add(controls, "dynamicClouds") .name("Clouds") diff --git a/src/hooks/debug/useMapPerformanceDebug.ts b/src/hooks/debug/useMapPerformanceDebug.ts index f52b7f1..1978cb4 100644 --- a/src/hooks/debug/useMapPerformanceDebug.ts +++ b/src/hooks/debug/useMapPerformanceDebug.ts @@ -1,5 +1,4 @@ import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; -import { Debug } from "@/utils/debug/Debug"; import { MAP_PERFORMANCE_GROUP_NAMES, MAP_PERFORMANCE_MODEL_NAMES, @@ -16,8 +15,6 @@ function toLabel(value: string): string { export function useMapPerformanceDebug(): void { useDebugFolder("Map", (folder) => { - Debug.getInstance().addFogControl(folder); - const { groups, models, diff --git a/src/hooks/world/useFogSettings.ts b/src/hooks/world/useFogSettings.ts new file mode 100644 index 0000000..9446180 --- /dev/null +++ b/src/hooks/world/useFogSettings.ts @@ -0,0 +1,10 @@ +import { useWorldSettingsStore } from "@/managers/stores/useWorldSettingsStore"; +import type { FogState } from "@/data/world/fogConfig"; + +export function useFogSettings(): FogState { + return useWorldSettingsStore((state) => state.fog); +} + +export function useSetFogSettings(): (fog: Partial) => void { + return useWorldSettingsStore((state) => state.setFog); +} diff --git a/src/managers/stores/useWorldSettingsStore.ts b/src/managers/stores/useWorldSettingsStore.ts index 237674b..59d20dd 100644 --- a/src/managers/stores/useWorldSettingsStore.ts +++ b/src/managers/stores/useWorldSettingsStore.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { CLOUD_DEFAULTS, type CloudState } from "@/data/world/cloudConfig"; +import { FOG_CONFIG, type FogState } from "@/data/world/fogConfig"; import { WIND_DEFAULTS, type WindState } from "@/data/world/windConfig"; import { GRAPHICS_DEFAULTS, @@ -8,12 +9,14 @@ import { interface WorldSettingsState { clouds: CloudState; + fog: FogState; wind: WindState; graphics: GraphicsState; } interface WorldSettingsActions { setClouds: (clouds: Partial) => void; + setFog: (fog: Partial) => void; setWind: (wind: Partial) => void; setWindSpeed: (speed: number) => void; setWindDirection: (direction: number) => void; @@ -31,6 +34,12 @@ type WorldSettingsStore = WorldSettingsState & WorldSettingsActions; const DEFAULT_STATE: WorldSettingsState = { clouds: { ...CLOUD_DEFAULTS }, + fog: { + density: FOG_CONFIG.density, + far: FOG_CONFIG.far, + mode: FOG_CONFIG.mode, + near: FOG_CONFIG.near, + }, wind: { ...WIND_DEFAULTS }, graphics: { ...GRAPHICS_DEFAULTS }, }; @@ -43,6 +52,11 @@ export const useWorldSettingsStore = create()((set) => ({ clouds: { ...state.clouds, ...cloudsUpdate }, })), + setFog: (fogUpdate) => + set((state) => ({ + fog: { ...state.fog, ...fogUpdate }, + })), + setWind: (windUpdate) => set((state) => ({ wind: { ...state.wind, ...windUpdate }, diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx index e789104..3679c45 100644 --- a/src/world/Environment.tsx +++ b/src/world/Environment.tsx @@ -1,3 +1,6 @@ +import { useMemo } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import * as THREE from "three"; import { GAME_SCENE_FALLBACK_BACKGROUND_COLOR, GAME_SCENE_FALLBACK_SKY_MODEL_PATH, @@ -6,24 +9,46 @@ import { GAME_SCENE_SKY_MODEL_SCALE, PHYSICS_SCENE_BACKGROUND_COLOR, } from "@/data/world/environmentConfig"; -import { FOG_CONFIG } from "@/data/world/fogConfig"; +import { FOG_CONFIG, FOG_LIGHTING_COLOR_MIX } from "@/data/world/fogConfig"; import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; +import { useFogSettings } from "@/hooks/world/useFogSettings"; import { isMapModelVisible, useMapPerformanceStore, } from "@/managers/stores/useMapPerformanceStore"; import { SkyModel } from "@/components/three/world/SkyModel"; import { useDebugStore } from "@/hooks/debug/useDebugStore"; +import { LIGHTING_STATE } from "@/world/lightingState"; + +const tempSunFogColor = new THREE.Color(); + +function getLightingFogColor(target: THREE.Color): THREE.Color { + target.set(LIGHTING_STATE.ambientColor); + target.multiplyScalar(FOG_LIGHTING_COLOR_MIX.ambient); + tempSunFogColor.set(LIGHTING_STATE.sunColor); + target.add(tempSunFogColor.multiplyScalar(FOG_LIGHTING_COLOR_MIX.sun)); + + return target; +} export function Environment(): React.JSX.Element { const cameraMode = useCameraMode(); const sceneMode = useSceneMode(); + const fog = useFogSettings(); const fogEnabled = useDebugStore((debug) => debug.getFogEnabled()); const groups = useMapPerformanceStore((state) => state.groups); const models = useMapPerformanceStore((state) => state.models); + const scene = useThree((state) => state.scene); + const fogColor = useMemo(() => getLightingFogColor(new THREE.Color()), []); const showSky = isMapModelVisible("sky", { groups, models }); + useFrame(() => { + if (!scene.fog) return; + + getLightingFogColor(scene.fog.color); + }); + if (sceneMode === "physics") { return ( @@ -35,11 +60,16 @@ export function Environment(): React.JSX.Element { {FOG_CONFIG.enabled && fogEnabled && sceneMode === "game" && - cameraMode === "player" ? ( - + cameraMode === "player" && + fog.mode === "linear" ? ( + + ) : null} + {FOG_CONFIG.enabled && + fogEnabled && + sceneMode === "game" && + cameraMode === "player" && + fog.mode === "exp2" ? ( + ) : null} {showSky ? ( (null); const sun = useRef(null); diff --git a/src/world/lightingState.ts b/src/world/lightingState.ts new file mode 100644 index 0000000..c5c36a9 --- /dev/null +++ b/src/world/lightingState.ts @@ -0,0 +1,13 @@ +import { LIGHTING_DEFAULTS } from "@/data/world/lightingConfig"; + +export interface LightingState { + ambientColor: string; + ambientIntensity: number; + sunColor: string; + sunIntensity: number; + sunX: number; + sunY: number; + sunZ: number; +} + +export const LIGHTING_STATE: LightingState = { ...LIGHTING_DEFAULTS }; diff --git a/src/world/water/WaterSurface.tsx b/src/world/water/WaterSurface.tsx index a96079d..b448903 100644 --- a/src/world/water/WaterSurface.tsx +++ b/src/world/water/WaterSurface.tsx @@ -44,8 +44,10 @@ export function WaterSurface({ uOpacity: { value: WATER_SHADER_CONFIG.opacity }, uDeepOpacity: { value: WATER_SHADER_CONFIG.deepOpacity }, uFogEnabled: { value: 0 }, + uFogMode: { value: 0 }, uFogNear: { value: FOG_CONFIG.near }, uFogFar: { value: FOG_CONFIG.far }, + uFogDensity: { value: FOG_CONFIG.density }, uFogColor: { value: new THREE.Color(FOG_CONFIG.color) }, }), [], @@ -61,8 +63,10 @@ export function WaterSurface({ uFlowX, uFlowZ, uFogColor, + uFogDensity, uFogEnabled, uFogFar, + uFogMode, uFogNear, uNoiseScale, uTime, @@ -77,9 +81,15 @@ export function WaterSurface({ if (scene.fog instanceof THREE.Fog) { if (uFogEnabled) uFogEnabled.value = 1; + if (uFogMode) uFogMode.value = 0; if (uFogNear) uFogNear.value = scene.fog.near; if (uFogFar) uFogFar.value = scene.fog.far; if (uFogColor) uFogColor.value.copy(scene.fog.color); + } else if (scene.fog instanceof THREE.FogExp2) { + if (uFogEnabled) uFogEnabled.value = 1; + if (uFogMode) uFogMode.value = 1; + if (uFogDensity) uFogDensity.value = scene.fog.density; + if (uFogColor) uFogColor.value.copy(scene.fog.color); } else if (uFogEnabled) { uFogEnabled.value = 0; } diff --git a/src/world/water/waterShaders.ts b/src/world/water/waterShaders.ts index d31cebc..cb12cd5 100644 --- a/src/world/water/waterShaders.ts +++ b/src/world/water/waterShaders.ts @@ -33,8 +33,10 @@ export const WATER_FRAGMENT_SHADER = /* glsl */ ` uniform float uOpacity; uniform float uDeepOpacity; uniform float uFogEnabled; + uniform float uFogMode; uniform float uFogNear; uniform float uFogFar; + uniform float uFogDensity; uniform vec3 uFogColor; varying vec2 vUv; @@ -152,6 +154,12 @@ export const WATER_FRAGMENT_SHADER = /* glsl */ ` if (uFogEnabled > 0.5) { float fogDistance = distance(cameraPosition, vWorldPosition); float fogFactor = smoothstep(uFogNear, uFogFar, fogDistance); + + if (uFogMode > 0.5) { + fogFactor = 1.0 - exp(-uFogDensity * uFogDensity * fogDistance * fogDistance); + } + + fogFactor = clamp(fogFactor, 0.0, 1.0); color = mix(color, uFogColor, fogFactor); alpha *= 1.0 - fogFactor; }