diff --git a/src/components/three/world/CloudModel.tsx b/src/components/three/world/CloudModel.tsx
new file mode 100644
index 0000000..e138ae4
--- /dev/null
+++ b/src/components/three/world/CloudModel.tsx
@@ -0,0 +1,53 @@
+import { useMemo } from "react";
+import * as THREE from "three";
+import { useGLTF } from "@react-three/drei";
+import { useThree } from "@react-three/fiber";
+import { CLOUD_CONFIG } from "@/data/world/cloudConfig";
+import { optimizeGLTFSceneTextures } from "@/utils/three/optimizeGLTFScene";
+
+interface CloudModelProps {
+ castShadow?: boolean;
+ receiveShadow?: boolean;
+}
+
+function applyCloudSettings(
+ scene: THREE.Object3D,
+ castShadow: boolean,
+ receiveShadow: boolean,
+): void {
+ scene.traverse((child) => {
+ if (child instanceof THREE.Mesh) {
+ child.castShadow = castShadow;
+ child.receiveShadow = receiveShadow;
+
+ const materials = Array.isArray(child.material)
+ ? child.material
+ : [child.material];
+
+ for (const material of materials) {
+ material.fog = false;
+ }
+ }
+ });
+}
+
+export function CloudModel({
+ castShadow = false,
+ receiveShadow = false,
+}: CloudModelProps): React.JSX.Element {
+ const { scene } = useGLTF(CLOUD_CONFIG.modelPath);
+ const maxAnisotropy = useThree((state) =>
+ state.gl.capabilities.getMaxAnisotropy(),
+ );
+
+ const cloud = useMemo(() => {
+ optimizeGLTFSceneTextures(scene, maxAnisotropy);
+ const model = scene.clone(true);
+ applyCloudSettings(model, castShadow, receiveShadow);
+ return model;
+ }, [castShadow, maxAnisotropy, receiveShadow, scene]);
+
+ return ;
+}
+
+useGLTF.preload(CLOUD_CONFIG.modelPath);
diff --git a/src/data/world/cloudConfig.ts b/src/data/world/cloudConfig.ts
new file mode 100644
index 0000000..07615c3
--- /dev/null
+++ b/src/data/world/cloudConfig.ts
@@ -0,0 +1,34 @@
+import type { Vector3Tuple } from "@/types/three/three";
+
+export const CLOUD_CONFIG = {
+ enabled: true,
+ modelPath: "/models/cloud/model.glb",
+ center: [0, 40, 0] as Vector3Tuple,
+ areaSize: [240, 180] as const,
+ minDriftSpeed: 0.05,
+ wrapPadding: 30,
+};
+
+export const CLOUD_DEFAULTS = {
+ count: 10,
+ minHeight: 25,
+ maxHeight: 55,
+ minScale: 5,
+ maxScale: 13,
+ minRotation: 0,
+ maxRotation: Math.PI * 2,
+ minSpeedMultiplier: 0.4,
+ maxSpeedMultiplier: 1,
+ castShadow: false,
+ receiveShadow: false,
+};
+
+export const CLOUD_BOUNDS = {
+ count: { min: 0, max: 30, step: 1 },
+ height: { min: 10, max: 100, step: 1 },
+ scale: { min: 1, max: 30, step: 0.5 },
+ rotation: { min: -Math.PI * 2, max: Math.PI * 2, step: 0.1 },
+ speedMultiplier: { min: 0, max: 3, step: 0.1 },
+};
+
+export type CloudState = typeof CLOUD_DEFAULTS;
diff --git a/src/hooks/world/useCloudSettings.ts b/src/hooks/world/useCloudSettings.ts
new file mode 100644
index 0000000..ea97912
--- /dev/null
+++ b/src/hooks/world/useCloudSettings.ts
@@ -0,0 +1,10 @@
+import { useWorldSettingsStore } from "@/managers/stores/useWorldSettingsStore";
+import type { CloudState } from "@/data/world/cloudConfig";
+
+export function useCloudSettings(): CloudState {
+ return useWorldSettingsStore((state) => state.clouds);
+}
+
+export function useSetCloudSettings(): (clouds: Partial) => void {
+ return useWorldSettingsStore((state) => state.setClouds);
+}
diff --git a/src/managers/stores/useWorldSettingsStore.ts b/src/managers/stores/useWorldSettingsStore.ts
index dabf773..237674b 100644
--- a/src/managers/stores/useWorldSettingsStore.ts
+++ b/src/managers/stores/useWorldSettingsStore.ts
@@ -1,4 +1,5 @@
import { create } from "zustand";
+import { CLOUD_DEFAULTS, type CloudState } from "@/data/world/cloudConfig";
import { WIND_DEFAULTS, type WindState } from "@/data/world/windConfig";
import {
GRAPHICS_DEFAULTS,
@@ -6,11 +7,13 @@ import {
} from "@/data/world/graphicsConfig";
interface WorldSettingsState {
+ clouds: CloudState;
wind: WindState;
graphics: GraphicsState;
}
interface WorldSettingsActions {
+ setClouds: (clouds: Partial) => void;
setWind: (wind: Partial) => void;
setWindSpeed: (speed: number) => void;
setWindDirection: (direction: number) => void;
@@ -27,6 +30,7 @@ interface WorldSettingsActions {
type WorldSettingsStore = WorldSettingsState & WorldSettingsActions;
const DEFAULT_STATE: WorldSettingsState = {
+ clouds: { ...CLOUD_DEFAULTS },
wind: { ...WIND_DEFAULTS },
graphics: { ...GRAPHICS_DEFAULTS },
};
@@ -34,6 +38,11 @@ const DEFAULT_STATE: WorldSettingsState = {
export const useWorldSettingsStore = create()((set) => ({
...DEFAULT_STATE,
+ setClouds: (cloudsUpdate) =>
+ set((state) => ({
+ clouds: { ...state.clouds, ...cloudsUpdate },
+ })),
+
setWind: (windUpdate) =>
set((state) => ({
wind: { ...state.wind, ...windUpdate },
diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx
index 6935dd5..cfb99db 100644
--- a/src/world/GameMap.tsx
+++ b/src/world/GameMap.tsx
@@ -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({
+
{isMapModelVisible("terrain", { groups, models }) ? (
diff --git a/src/world/clouds/CloudSystem.tsx b/src/world/clouds/CloudSystem.tsx
new file mode 100644
index 0000000..5cbb2d1
--- /dev/null
+++ b/src/world/clouds/CloudSystem.tsx
@@ -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>([]);
+ 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 (
+
+ {clouds.map((cloud, index) => (
+ {
+ refs.current[index] = node;
+ }}
+ position={[cloud.x, cloud.height, cloud.z]}
+ rotation={[0, cloud.rotationY, 0]}
+ scale={cloud.scale}
+ >
+
+
+
+
+ ))}
+
+ );
+}