feat(environment): add wind-driven cloud system

This commit is contained in:
Tom Boullay
2026-05-27 00:33:53 +02:00
parent a6787a7ecb
commit 4ebb5b8c25
6 changed files with 250 additions and 0 deletions
+2
View File
@@ -20,6 +20,7 @@ import {
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
import { GameMapCollision } from "@/world/GameMapCollision";
import { CloudSystem } from "@/world/clouds/CloudSystem";
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig";
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
@@ -261,6 +262,7 @@ export function GameMap({
<MapInstancingSystem />
<WorldPlane />
<WaterSystem />
<CloudSystem />
<VegetationSystem />
{isMapModelVisible("terrain", { groups, models }) ? (
<TerrainModel />
+142
View File
@@ -0,0 +1,142 @@
import { Suspense, useMemo, useRef } from "react";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { CLOUD_CONFIG } from "@/data/world/cloudConfig";
import { getWindVector } from "@/data/world/windConfig";
import { useDynamicClouds } from "@/hooks/world/useGraphicsSettings";
import { useCloudSettings } from "@/hooks/world/useCloudSettings";
import { useWind } from "@/hooks/world/useWind";
import { CloudModel } from "@/components/three/world/CloudModel";
import type { CloudState } from "@/data/world/cloudConfig";
interface CloudInstance {
height: number;
rotationY: number;
scale: number;
speedMultiplier: number;
x: number;
z: number;
}
function lerp(min: number, max: number, ratio: number): number {
return min + (max - min) * ratio;
}
function createCloudInstances(cloudSettings: CloudState): CloudInstance[] {
const instances: CloudInstance[] = [];
const [areaWidth, areaDepth] = CLOUD_CONFIG.areaSize;
const count = Math.max(0, Math.round(cloudSettings.count));
const columns = Math.ceil(Math.sqrt(count));
const rows = columns > 0 ? Math.ceil(count / columns) : 0;
for (let index = 0; index < count; index++) {
const column = index % columns;
const row = Math.floor(index / columns);
const columnRatio = columns <= 1 ? 0.5 : column / (columns - 1);
const rowRatio = rows <= 1 ? 0.5 : row / (rows - 1);
const variation = ((index * 37) % 100) / 100;
instances.push({
height: lerp(cloudSettings.minHeight, cloudSettings.maxHeight, variation),
rotationY: lerp(
cloudSettings.minRotation,
cloudSettings.maxRotation,
variation,
),
scale: lerp(cloudSettings.minScale, cloudSettings.maxScale, variation),
speedMultiplier: lerp(
cloudSettings.minSpeedMultiplier,
cloudSettings.maxSpeedMultiplier,
((index * 53) % 100) / 100,
),
x: CLOUD_CONFIG.center[0] + (columnRatio - 0.5) * areaWidth,
z: CLOUD_CONFIG.center[2] + (rowRatio - 0.5) * areaDepth,
});
}
return instances;
}
function wrapAxis(
value: number,
center: number,
size: number,
padding: number,
): number {
const min = center - size / 2 - padding;
const max = center + size / 2 + padding;
const range = max - min;
if (value < min) return value + range;
if (value > max) return value - range;
return value;
}
export function CloudSystem(): React.JSX.Element | null {
const cloudSettings = useCloudSettings();
const dynamicClouds = useDynamicClouds();
const wind = useWind();
const refs = useRef<Array<THREE.Group | null>>([]);
const clouds = useMemo(
() => createCloudInstances(cloudSettings),
[cloudSettings],
);
useFrame((_, delta) => {
const windVector = getWindVector(wind);
const windLength = Math.hypot(windVector.x, windVector.z);
const safeWindLength = Math.max(windLength, CLOUD_CONFIG.minDriftSpeed);
const directionX =
windLength > 0 ? windVector.x / windLength : Math.cos(wind.direction);
const directionZ =
windLength > 0 ? windVector.z / windLength : Math.sin(wind.direction);
refs.current.forEach((cloud, index) => {
if (!cloud) return;
const instance = clouds[index];
if (!instance) return;
const distance = safeWindLength * instance.speedMultiplier * delta;
cloud.position.x = wrapAxis(
cloud.position.x + directionX * distance,
CLOUD_CONFIG.center[0],
CLOUD_CONFIG.areaSize[0],
CLOUD_CONFIG.wrapPadding,
);
cloud.position.z = wrapAxis(
cloud.position.z + directionZ * distance,
CLOUD_CONFIG.center[2],
CLOUD_CONFIG.areaSize[1],
CLOUD_CONFIG.wrapPadding,
);
});
});
if (!CLOUD_CONFIG.enabled || !dynamicClouds) {
return null;
}
return (
<group name="cloud-system">
{clouds.map((cloud, index) => (
<group
key={index}
ref={(node) => {
refs.current[index] = node;
}}
position={[cloud.x, cloud.height, cloud.z]}
rotation={[0, cloud.rotationY, 0]}
scale={cloud.scale}
>
<Suspense fallback={null}>
<CloudModel
castShadow={cloudSettings.castShadow}
receiveShadow={cloudSettings.receiveShadow}
/>
</Suspense>
</group>
))}
</group>
);
}