Compare commits
16 Commits
6d178dc59e
...
ab3943eef3
| Author | SHA1 | Date | |
|---|---|---|---|
| ab3943eef3 | |||
| 4ebb5b8c25 | |||
| a6787a7ecb | |||
| d816e4b07e | |||
| 665d9f9702 | |||
| 0696ca2ae3 | |||
| d6d3d5b685 | |||
| 1c27d55e5a | |||
| fd558db034 | |||
| fbe8c0c854 | |||
| 88b6db6166 | |||
| 2b08665508 | |||
| 4f8355e934 | |||
| 417afdc1d5 | |||
| 235a38f67b | |||
| f54e71fc03 |
@@ -6,12 +6,14 @@ import {
|
|||||||
DEBUG_GRID_SIZE,
|
DEBUG_GRID_SIZE,
|
||||||
DEBUG_GRID_Y,
|
DEBUG_GRID_Y,
|
||||||
} from "@/data/debug/debugConfig";
|
} from "@/data/debug/debugConfig";
|
||||||
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
export function DebugHelpers(): React.JSX.Element | null {
|
export function DebugHelpers(): React.JSX.Element | null {
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
|
|
||||||
if (!debug.active) {
|
if (!debug.active || sceneMode === "game") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 <primitive object={cloud} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(CLOUD_CONFIG.modelPath);
|
||||||
@@ -25,6 +25,23 @@ function applyTerrainMaterialSettings(
|
|||||||
scene.traverse((child) => {
|
scene.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
child.receiveShadow = receiveShadow;
|
child.receiveShadow = receiveShadow;
|
||||||
|
|
||||||
|
const materials = Array.isArray(child.material)
|
||||||
|
? child.material
|
||||||
|
: [child.material];
|
||||||
|
|
||||||
|
for (const material of materials) {
|
||||||
|
const materialWithAlphaMap = material as THREE.Material & {
|
||||||
|
alphaMap?: THREE.Texture | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
material.depthTest = true;
|
||||||
|
material.depthWrite = true;
|
||||||
|
|
||||||
|
if (material.opacity >= 1 && !materialWithAlphaMap.alphaMap) {
|
||||||
|
material.transparent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const PLAYER_GRAVITY = 30;
|
|||||||
export const PLAYER_MAX_DELTA = 0.05;
|
export const PLAYER_MAX_DELTA = 0.05;
|
||||||
export const PLAYER_ACCELERATION_MULTIPLIER = 9;
|
export const PLAYER_ACCELERATION_MULTIPLIER = 9;
|
||||||
export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
||||||
|
export const PLAYER_FALL_RESPAWN_Y = -20;
|
||||||
|
export const PLAYER_FALL_RESPAWN_DELAY = 3;
|
||||||
|
|
||||||
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 50, 0];
|
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 50, 0];
|
||||||
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
export const AMBIENT_LIGHT_COLOR = "#dbeafe";
|
export const AMBIENT_LIGHT_COLOR = "#dfe7d8";
|
||||||
export const SUN_LIGHT_COLOR = "#fff7ed";
|
export const SUN_LIGHT_COLOR = "#ffe2bf";
|
||||||
|
|
||||||
export const LIGHTING_DEFAULTS = {
|
export const LIGHTING_DEFAULTS = {
|
||||||
ambientIntensity: 1.8,
|
ambientColor: AMBIENT_LIGHT_COLOR,
|
||||||
sunIntensity: 2.8,
|
ambientIntensity: 0.9,
|
||||||
sunX: 60,
|
sunColor: SUN_LIGHT_COLOR,
|
||||||
sunY: 80,
|
sunIntensity: 2.2,
|
||||||
sunZ: 30,
|
sunX: 70,
|
||||||
|
sunY: 45,
|
||||||
|
sunZ: 35,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AMBIENT_INTENSITY_MIN = 0;
|
export const AMBIENT_INTENSITY_MIN = 0;
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import type { TerrainSurfaceColorConfig } from "@/types/world/terrainSurface";
|
|||||||
|
|
||||||
export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||||
export const TERRAIN_SURFACE_COLOR_TOLERANCE = 15;
|
export const TERRAIN_SURFACE_COLOR_TOLERANCE = 15;
|
||||||
export const TERRAIN_WATER_HEIGHT = 0;
|
export const TERRAIN_SURFACE_PROJECTION = {
|
||||||
|
flipX: false,
|
||||||
|
flipZ: true,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetZ: 0,
|
||||||
|
};
|
||||||
|
export const TERRAIN_WATER_HEIGHT = 0.8;
|
||||||
export const TERRAIN_TILE_SIZE = 1;
|
export const TERRAIN_TILE_SIZE = 1;
|
||||||
export const GRASS_BASE_COLOR = "#1a3a1a";
|
export const GRASS_BASE_COLOR = "#1a3a1a";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { TERRAIN_WATER_HEIGHT } from "@/data/world/terrainConfig";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
export interface WaterSurfaceConfig {
|
||||||
|
position: Vector3Tuple;
|
||||||
|
rotation: Vector3Tuple;
|
||||||
|
size: [number, number];
|
||||||
|
renderOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WATER_SHADER_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
height: TERRAIN_WATER_HEIGHT,
|
||||||
|
depthOffset: -0.04,
|
||||||
|
borderRadius: 0.18,
|
||||||
|
borderSoftness: 0.035,
|
||||||
|
scale: 0.4,
|
||||||
|
smoothness: 0.55,
|
||||||
|
edgeThreshold: 0.067,
|
||||||
|
edgeSoftness: 0.01,
|
||||||
|
flowX: 0,
|
||||||
|
flowZ: 0.05,
|
||||||
|
cellSpeed: 0.3,
|
||||||
|
noiseScale: 1.52,
|
||||||
|
noiseFlowSpeed: 0.2,
|
||||||
|
distortAmount: 0.3,
|
||||||
|
deepColor: "#1a3a5c",
|
||||||
|
midColor: "#59c0e8",
|
||||||
|
midPos: 0.084,
|
||||||
|
highlightColor: "#ffffff",
|
||||||
|
opacity: 0.88,
|
||||||
|
deepOpacity: 0.45,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WATER_STREAMING_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
loadDistance: 40,
|
||||||
|
unloadDistance: 48,
|
||||||
|
updateInterval: 350,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WATER_SURFACES: WaterSurfaceConfig[] = [
|
||||||
|
{
|
||||||
|
position: [40, TERRAIN_WATER_HEIGHT, -102],
|
||||||
|
rotation: [0, 0, 0],
|
||||||
|
size: [75, 45],
|
||||||
|
renderOrder: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
export const WIND_DEFAULTS = {
|
export const WIND_DEFAULTS = {
|
||||||
speed: 0.3,
|
speed: 1.5,
|
||||||
direction: Math.PI * 0.25,
|
direction: 0.5584,
|
||||||
strength: 1.0,
|
strength: 1.5,
|
||||||
noiseScale: 0.9,
|
noiseScale: 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WIND_BOUNDS = {
|
export const WIND_BOUNDS = {
|
||||||
@@ -13,3 +13,12 @@ export const WIND_BOUNDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WindState = typeof WIND_DEFAULTS;
|
export type WindState = typeof WIND_DEFAULTS;
|
||||||
|
|
||||||
|
export function getWindVector(wind: WindState): { x: number; z: number } {
|
||||||
|
const intensity = wind.speed * wind.strength;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.cos(wind.direction) * intensity,
|
||||||
|
z: Math.sin(wind.direction) * intensity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
|
export const WORLD_BOUNDS_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
center: [0, 0, 0] as Vector3Tuple,
|
||||||
|
planeColor: TERRAIN_COLORS.grass1.hex,
|
||||||
|
planeY: -0.04,
|
||||||
|
planeCollisionThickness: 1,
|
||||||
|
size: [270, 260] as const,
|
||||||
|
wallHeight: 28,
|
||||||
|
wallThickness: 4,
|
||||||
|
};
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { CLOUD_BOUNDS } from "@/data/world/cloudConfig";
|
||||||
|
import { WIND_BOUNDS } from "@/data/world/windConfig";
|
||||||
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
import { useWorldSettingsStore } from "@/managers/stores/useWorldSettingsStore";
|
||||||
|
|
||||||
|
export function useEnvironmentDebug(): void {
|
||||||
|
useDebugFolder("Dynamic Wind", (folder) => {
|
||||||
|
const { setWind, wind } = useWorldSettingsStore.getState();
|
||||||
|
const controls = { ...wind };
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(controls, "speed", WIND_BOUNDS.speed.min, WIND_BOUNDS.speed.max)
|
||||||
|
.step(WIND_BOUNDS.speed.step)
|
||||||
|
.name("Wind speed")
|
||||||
|
.onChange((speed: number) => setWind({ speed }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"direction",
|
||||||
|
WIND_BOUNDS.direction.min,
|
||||||
|
WIND_BOUNDS.direction.max,
|
||||||
|
)
|
||||||
|
.step(WIND_BOUNDS.direction.step)
|
||||||
|
.name("Wind direction")
|
||||||
|
.onChange((direction: number) => setWind({ direction }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"strength",
|
||||||
|
WIND_BOUNDS.strength.min,
|
||||||
|
WIND_BOUNDS.strength.max,
|
||||||
|
)
|
||||||
|
.step(WIND_BOUNDS.strength.step)
|
||||||
|
.name("Wind strength")
|
||||||
|
.onChange((strength: number) => setWind({ strength }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"noiseScale",
|
||||||
|
WIND_BOUNDS.noiseScale.min,
|
||||||
|
WIND_BOUNDS.noiseScale.max,
|
||||||
|
)
|
||||||
|
.step(WIND_BOUNDS.noiseScale.step)
|
||||||
|
.name("Wind noise scale")
|
||||||
|
.onChange((noiseScale: number) => setWind({ noiseScale }));
|
||||||
|
});
|
||||||
|
|
||||||
|
useDebugFolder("Environment", (folder) => {
|
||||||
|
const { clouds, graphics, setClouds, setDynamicClouds } =
|
||||||
|
useWorldSettingsStore.getState();
|
||||||
|
const controls = {
|
||||||
|
...clouds,
|
||||||
|
dynamicClouds: graphics.dynamicClouds,
|
||||||
|
};
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(controls, "dynamicClouds")
|
||||||
|
.name("Clouds")
|
||||||
|
.onChange((dynamicClouds: boolean) => setDynamicClouds(dynamicClouds));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(controls, "count", CLOUD_BOUNDS.count.min, CLOUD_BOUNDS.count.max)
|
||||||
|
.step(CLOUD_BOUNDS.count.step)
|
||||||
|
.name("Cloud count")
|
||||||
|
.onChange((count: number) => setClouds({ count }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(controls, "minScale", CLOUD_BOUNDS.scale.min, CLOUD_BOUNDS.scale.max)
|
||||||
|
.step(CLOUD_BOUNDS.scale.step)
|
||||||
|
.name("Cloud min scale")
|
||||||
|
.onChange((minScale: number) => setClouds({ minScale }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(controls, "maxScale", CLOUD_BOUNDS.scale.min, CLOUD_BOUNDS.scale.max)
|
||||||
|
.step(CLOUD_BOUNDS.scale.step)
|
||||||
|
.name("Cloud max scale")
|
||||||
|
.onChange((maxScale: number) => setClouds({ maxScale }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"minRotation",
|
||||||
|
CLOUD_BOUNDS.rotation.min,
|
||||||
|
CLOUD_BOUNDS.rotation.max,
|
||||||
|
)
|
||||||
|
.step(CLOUD_BOUNDS.rotation.step)
|
||||||
|
.name("Cloud min rotation")
|
||||||
|
.onChange((minRotation: number) => setClouds({ minRotation }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"maxRotation",
|
||||||
|
CLOUD_BOUNDS.rotation.min,
|
||||||
|
CLOUD_BOUNDS.rotation.max,
|
||||||
|
)
|
||||||
|
.step(CLOUD_BOUNDS.rotation.step)
|
||||||
|
.name("Cloud max rotation")
|
||||||
|
.onChange((maxRotation: number) => setClouds({ maxRotation }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"minHeight",
|
||||||
|
CLOUD_BOUNDS.height.min,
|
||||||
|
CLOUD_BOUNDS.height.max,
|
||||||
|
)
|
||||||
|
.step(CLOUD_BOUNDS.height.step)
|
||||||
|
.name("Cloud min height")
|
||||||
|
.onChange((minHeight: number) => setClouds({ minHeight }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"maxHeight",
|
||||||
|
CLOUD_BOUNDS.height.min,
|
||||||
|
CLOUD_BOUNDS.height.max,
|
||||||
|
)
|
||||||
|
.step(CLOUD_BOUNDS.height.step)
|
||||||
|
.name("Cloud max height")
|
||||||
|
.onChange((maxHeight: number) => setClouds({ maxHeight }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"minSpeedMultiplier",
|
||||||
|
CLOUD_BOUNDS.speedMultiplier.min,
|
||||||
|
CLOUD_BOUNDS.speedMultiplier.max,
|
||||||
|
)
|
||||||
|
.step(CLOUD_BOUNDS.speedMultiplier.step)
|
||||||
|
.name("Cloud min speed")
|
||||||
|
.onChange((minSpeedMultiplier: number) =>
|
||||||
|
setClouds({ minSpeedMultiplier }),
|
||||||
|
);
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(
|
||||||
|
controls,
|
||||||
|
"maxSpeedMultiplier",
|
||||||
|
CLOUD_BOUNDS.speedMultiplier.min,
|
||||||
|
CLOUD_BOUNDS.speedMultiplier.max,
|
||||||
|
)
|
||||||
|
.step(CLOUD_BOUNDS.speedMultiplier.step)
|
||||||
|
.name("Cloud max speed")
|
||||||
|
.onChange((maxSpeedMultiplier: number) =>
|
||||||
|
setClouds({ maxSpeedMultiplier }),
|
||||||
|
);
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(controls, "castShadow")
|
||||||
|
.name("Cloud cast shadow")
|
||||||
|
.onChange((castShadow: boolean) => setClouds({ castShadow }));
|
||||||
|
|
||||||
|
folder
|
||||||
|
.add(controls, "receiveShadow")
|
||||||
|
.name("Cloud receive shadow")
|
||||||
|
.onChange((receiveShadow: boolean) => setClouds({ receiveShadow }));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<CloudState>) => void {
|
||||||
|
return useWorldSettingsStore((state) => state.setClouds);
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ export function useTerrainSurfaceData(): TerrainSurfaceData | null {
|
|||||||
return {
|
return {
|
||||||
bounds: createTerrainSurfaceBounds(scene),
|
bounds: createTerrainSurfaceBounds(scene),
|
||||||
imageData,
|
imageData,
|
||||||
|
raycastTarget: scene,
|
||||||
};
|
};
|
||||||
}, [scene]);
|
}, [scene]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { CLOUD_DEFAULTS, type CloudState } from "@/data/world/cloudConfig";
|
||||||
import { WIND_DEFAULTS, type WindState } from "@/data/world/windConfig";
|
import { WIND_DEFAULTS, type WindState } from "@/data/world/windConfig";
|
||||||
import {
|
import {
|
||||||
GRAPHICS_DEFAULTS,
|
GRAPHICS_DEFAULTS,
|
||||||
@@ -6,11 +7,13 @@ import {
|
|||||||
} from "@/data/world/graphicsConfig";
|
} from "@/data/world/graphicsConfig";
|
||||||
|
|
||||||
interface WorldSettingsState {
|
interface WorldSettingsState {
|
||||||
|
clouds: CloudState;
|
||||||
wind: WindState;
|
wind: WindState;
|
||||||
graphics: GraphicsState;
|
graphics: GraphicsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorldSettingsActions {
|
interface WorldSettingsActions {
|
||||||
|
setClouds: (clouds: Partial<CloudState>) => void;
|
||||||
setWind: (wind: Partial<WindState>) => void;
|
setWind: (wind: Partial<WindState>) => void;
|
||||||
setWindSpeed: (speed: number) => void;
|
setWindSpeed: (speed: number) => void;
|
||||||
setWindDirection: (direction: number) => void;
|
setWindDirection: (direction: number) => void;
|
||||||
@@ -27,6 +30,7 @@ interface WorldSettingsActions {
|
|||||||
type WorldSettingsStore = WorldSettingsState & WorldSettingsActions;
|
type WorldSettingsStore = WorldSettingsState & WorldSettingsActions;
|
||||||
|
|
||||||
const DEFAULT_STATE: WorldSettingsState = {
|
const DEFAULT_STATE: WorldSettingsState = {
|
||||||
|
clouds: { ...CLOUD_DEFAULTS },
|
||||||
wind: { ...WIND_DEFAULTS },
|
wind: { ...WIND_DEFAULTS },
|
||||||
graphics: { ...GRAPHICS_DEFAULTS },
|
graphics: { ...GRAPHICS_DEFAULTS },
|
||||||
};
|
};
|
||||||
@@ -34,6 +38,11 @@ const DEFAULT_STATE: WorldSettingsState = {
|
|||||||
export const useWorldSettingsStore = create<WorldSettingsStore>()((set) => ({
|
export const useWorldSettingsStore = create<WorldSettingsStore>()((set) => ({
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
|
|
||||||
|
setClouds: (cloudsUpdate) =>
|
||||||
|
set((state) => ({
|
||||||
|
clouds: { ...state.clouds, ...cloudsUpdate },
|
||||||
|
})),
|
||||||
|
|
||||||
setWind: (windUpdate) =>
|
setWind: (windUpdate) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
wind: { ...state.wind, ...windUpdate },
|
wind: { ...state.wind, ...windUpdate },
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type * as THREE from "three";
|
||||||
|
|
||||||
export type TerrainSurfaceKind =
|
export type TerrainSurfaceKind =
|
||||||
| "grass"
|
| "grass"
|
||||||
| "path"
|
| "path"
|
||||||
@@ -20,6 +22,13 @@ export interface TerrainSurfaceBounds {
|
|||||||
maxZ: number;
|
maxZ: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TerrainSurfaceProjectionConfig {
|
||||||
|
flipX: boolean;
|
||||||
|
flipZ: boolean;
|
||||||
|
offsetX: number;
|
||||||
|
offsetZ: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TerrainSurfaceColorConfig {
|
export interface TerrainSurfaceColorConfig {
|
||||||
hex: string;
|
hex: string;
|
||||||
rgb: TerrainSurfaceRgb;
|
rgb: TerrainSurfaceRgb;
|
||||||
@@ -38,4 +47,5 @@ export interface TerrainSurfaceSample {
|
|||||||
export interface TerrainSurfaceData {
|
export interface TerrainSurfaceData {
|
||||||
bounds: TerrainSurfaceBounds;
|
bounds: TerrainSurfaceBounds;
|
||||||
imageData: ImageData;
|
imageData: ImageData;
|
||||||
|
raycastTarget: THREE.Object3D;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ interface DebugEvents {
|
|||||||
|
|
||||||
const DEBUG_FOLDER_ORDER = [
|
const DEBUG_FOLDER_ORDER = [
|
||||||
"Lighting",
|
"Lighting",
|
||||||
|
"Dynamic Wind",
|
||||||
|
"Environment",
|
||||||
"Game",
|
"Game",
|
||||||
"Interaction",
|
"Interaction",
|
||||||
"Hand Tracking",
|
"Hand Tracking",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||||
import type {
|
import type {
|
||||||
TerrainSurfaceBounds,
|
TerrainSurfaceBounds,
|
||||||
|
TerrainSurfaceProjectionConfig,
|
||||||
TerrainSurfaceRgb,
|
TerrainSurfaceRgb,
|
||||||
TerrainSurfaceSample,
|
TerrainSurfaceSample,
|
||||||
TerrainSurfaceUv,
|
TerrainSurfaceUv,
|
||||||
@@ -14,11 +15,14 @@ type TerrainSurfaceImageSource =
|
|||||||
| ImageBitmap;
|
| ImageBitmap;
|
||||||
|
|
||||||
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
const imageDataCache = new WeakMap<TerrainSurfaceImageSource, ImageData>();
|
||||||
|
|
||||||
function clamp01(value: number): number {
|
function clamp01(value: number): number {
|
||||||
return Math.min(Math.max(value, 0), 1);
|
return Math.min(Math.max(value, 0), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrap01(value: number): number {
|
||||||
|
return ((value % 1) + 1) % 1;
|
||||||
|
}
|
||||||
|
|
||||||
function isTerrainSurfaceImageSource(
|
function isTerrainSurfaceImageSource(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): value is TerrainSurfaceImageSource {
|
): value is TerrainSurfaceImageSource {
|
||||||
@@ -83,13 +87,27 @@ export function terrainSurfaceUvFromXZ(
|
|||||||
x: number,
|
x: number,
|
||||||
z: number,
|
z: number,
|
||||||
bounds: TerrainSurfaceBounds,
|
bounds: TerrainSurfaceBounds,
|
||||||
|
projection?: TerrainSurfaceProjectionConfig,
|
||||||
): TerrainSurfaceUv {
|
): TerrainSurfaceUv {
|
||||||
const width = bounds.maxX - bounds.minX;
|
const width = bounds.maxX - bounds.minX;
|
||||||
const depth = bounds.maxZ - bounds.minZ;
|
const depth = bounds.maxZ - bounds.minZ;
|
||||||
|
let u = width === 0 ? 0 : x / width + 0.5;
|
||||||
|
let v = depth === 0 ? 0 : z / depth + 0.5;
|
||||||
|
|
||||||
|
if (projection?.flipX) {
|
||||||
|
u = 1 - u;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projection?.flipZ) {
|
||||||
|
v = 1 - v;
|
||||||
|
}
|
||||||
|
|
||||||
|
u = wrap01(u + (projection?.offsetX ?? 0));
|
||||||
|
v = wrap01(v + (projection?.offsetZ ?? 0));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
u: width === 0 ? 0 : (x - bounds.minX) / width,
|
u,
|
||||||
v: depth === 0 ? 0 : (z - bounds.minZ) / depth,
|
v,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +116,10 @@ export function sampleTerrainSurfaceAtXZ(
|
|||||||
x: number,
|
x: number,
|
||||||
z: number,
|
z: number,
|
||||||
bounds: TerrainSurfaceBounds,
|
bounds: TerrainSurfaceBounds,
|
||||||
|
projection?: TerrainSurfaceProjectionConfig,
|
||||||
): TerrainSurfaceSample {
|
): TerrainSurfaceSample {
|
||||||
return sampleTerrainSurfaceAtUv(
|
return sampleTerrainSurfaceAtUv(
|
||||||
imageData,
|
imageData,
|
||||||
terrainSurfaceUvFromXZ(x, z, bounds),
|
terrainSurfaceUvFromXZ(x, z, bounds, projection),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ import {
|
|||||||
useMapPerformanceStore,
|
useMapPerformanceStore,
|
||||||
} from "@/managers/stores/useMapPerformanceStore";
|
} from "@/managers/stores/useMapPerformanceStore";
|
||||||
import { GameMapCollision } from "@/world/GameMapCollision";
|
import { GameMapCollision } from "@/world/GameMapCollision";
|
||||||
|
import { CloudSystem } from "@/world/clouds/CloudSystem";
|
||||||
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance";
|
||||||
import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig";
|
import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModelConfig";
|
||||||
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem";
|
||||||
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
||||||
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
||||||
|
import { WaterSystem } from "@/world/water/WaterSystem";
|
||||||
|
import { WorldPlane } from "@/world/WorldPlane";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
import { logger } from "@/utils/core/Logger";
|
import { logger } from "@/utils/core/Logger";
|
||||||
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
||||||
@@ -257,6 +260,9 @@ export function GameMap({
|
|||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
<MapInstancingSystem />
|
<MapInstancingSystem />
|
||||||
|
<WorldPlane />
|
||||||
|
<WaterSystem />
|
||||||
|
<CloudSystem />
|
||||||
<VegetationSystem />
|
<VegetationSystem />
|
||||||
{isMapModelVisible("terrain", { groups, models }) ? (
|
{isMapModelVisible("terrain", { groups, models }) ? (
|
||||||
<TerrainModel />
|
<TerrainModel />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import * as THREE from "three";
|
|||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
||||||
|
import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision";
|
||||||
import type { MapNode } from "@/types/editor/editor";
|
import type { MapNode } from "@/types/editor/editor";
|
||||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
import type { OctreeReadyHandler } from "@/types/three/three";
|
||||||
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
|
||||||
@@ -173,6 +174,7 @@ export function GameMapCollision({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={groupRef} visible={false}>
|
<group ref={groupRef} visible={false}>
|
||||||
|
{mapReady ? <WorldBoundsCollision /> : null}
|
||||||
{mapReady
|
{mapReady
|
||||||
? collisionNodes.map((mapNode, index) => (
|
? collisionNodes.map((mapNode, index) => (
|
||||||
<CollisionErrorBoundary
|
<CollisionErrorBoundary
|
||||||
|
|||||||
+10
-6
@@ -5,12 +5,10 @@ import {
|
|||||||
AMBIENT_INTENSITY_MAX,
|
AMBIENT_INTENSITY_MAX,
|
||||||
AMBIENT_INTENSITY_MIN,
|
AMBIENT_INTENSITY_MIN,
|
||||||
AMBIENT_INTENSITY_STEP,
|
AMBIENT_INTENSITY_STEP,
|
||||||
AMBIENT_LIGHT_COLOR,
|
|
||||||
LIGHTING_DEFAULTS,
|
LIGHTING_DEFAULTS,
|
||||||
SUN_INTENSITY_MAX,
|
SUN_INTENSITY_MAX,
|
||||||
SUN_INTENSITY_MIN,
|
SUN_INTENSITY_MIN,
|
||||||
SUN_INTENSITY_STEP,
|
SUN_INTENSITY_STEP,
|
||||||
SUN_LIGHT_COLOR,
|
|
||||||
SUN_X_MAX,
|
SUN_X_MAX,
|
||||||
SUN_X_MIN,
|
SUN_X_MIN,
|
||||||
SUN_X_STEP,
|
SUN_X_STEP,
|
||||||
@@ -24,12 +22,14 @@ import {
|
|||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
|
||||||
const SHADOW_MAP_SIZE = 2048;
|
const SHADOW_MAP_SIZE = 2048;
|
||||||
const SHADOW_CAMERA_SIZE = 100;
|
const SHADOW_CAMERA_SIZE = 170;
|
||||||
const SHADOW_CAMERA_NEAR = 0.5;
|
const SHADOW_CAMERA_NEAR = 0.5;
|
||||||
const SHADOW_CAMERA_FAR = 200;
|
const SHADOW_CAMERA_FAR = 300;
|
||||||
|
|
||||||
type LightingState = {
|
type LightingState = {
|
||||||
|
ambientColor: string;
|
||||||
ambientIntensity: number;
|
ambientIntensity: number;
|
||||||
|
sunColor: string;
|
||||||
sunIntensity: number;
|
sunIntensity: number;
|
||||||
sunX: number;
|
sunX: number;
|
||||||
sunY: number;
|
sunY: number;
|
||||||
@@ -57,6 +57,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useDebugFolder("Lighting", (folder) => {
|
useDebugFolder("Lighting", (folder) => {
|
||||||
|
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");
|
||||||
folder
|
folder
|
||||||
.add(
|
.add(
|
||||||
LIGHTING_STATE,
|
LIGHTING_STATE,
|
||||||
@@ -75,6 +76,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
SUN_INTENSITY_STEP,
|
SUN_INTENSITY_STEP,
|
||||||
)
|
)
|
||||||
.name("Sun Intensity");
|
.name("Sun Intensity");
|
||||||
|
folder.addColor(LIGHTING_STATE, "sunColor").name("Sun Color");
|
||||||
folder
|
folder
|
||||||
.add(LIGHTING_STATE, "sunX", SUN_X_MIN, SUN_X_MAX, SUN_X_STEP)
|
.add(LIGHTING_STATE, "sunX", SUN_X_MIN, SUN_X_MAX, SUN_X_STEP)
|
||||||
.name("Sun X");
|
.name("Sun X");
|
||||||
@@ -88,6 +90,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
if (ambient.current) {
|
if (ambient.current) {
|
||||||
|
ambient.current.color.set(LIGHTING_STATE.ambientColor);
|
||||||
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +100,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
LIGHTING_STATE.sunY,
|
LIGHTING_STATE.sunY,
|
||||||
LIGHTING_STATE.sunZ,
|
LIGHTING_STATE.sunZ,
|
||||||
);
|
);
|
||||||
|
sun.current.color.set(LIGHTING_STATE.sunColor);
|
||||||
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -106,7 +110,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
<ambientLight
|
<ambientLight
|
||||||
ref={ambient}
|
ref={ambient}
|
||||||
intensity={LIGHTING_STATE.ambientIntensity}
|
intensity={LIGHTING_STATE.ambientIntensity}
|
||||||
color={AMBIENT_LIGHT_COLOR}
|
color={LIGHTING_STATE.ambientColor}
|
||||||
/>
|
/>
|
||||||
<directionalLight
|
<directionalLight
|
||||||
ref={sun}
|
ref={sun}
|
||||||
@@ -116,7 +120,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
LIGHTING_STATE.sunZ,
|
LIGHTING_STATE.sunZ,
|
||||||
]}
|
]}
|
||||||
intensity={LIGHTING_STATE.sunIntensity}
|
intensity={LIGHTING_STATE.sunIntensity}
|
||||||
color={SUN_LIGHT_COLOR}
|
color={LIGHTING_STATE.sunColor}
|
||||||
castShadow
|
castShadow
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
PLAYER_SPAWN_POSITION_PHYSICS,
|
PLAYER_SPAWN_POSITION_PHYSICS,
|
||||||
} from "@/data/player/playerConfig";
|
} from "@/data/player/playerConfig";
|
||||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
|
import { useEnvironmentDebug } from "@/hooks/debug/useEnvironmentDebug";
|
||||||
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
|
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
|
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
|
||||||
@@ -36,6 +37,7 @@ interface WorldProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
||||||
|
useEnvironmentDebug();
|
||||||
useMapPerformanceDebug();
|
useMapPerformanceDebug();
|
||||||
|
|
||||||
const cameraMode = useCameraMode();
|
const cameraMode = useCameraMode();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig";
|
||||||
|
|
||||||
|
export function WorldPlane(): React.JSX.Element | null {
|
||||||
|
if (!WORLD_BOUNDS_CONFIG.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { center, planeColor, planeY, size } = WORLD_BOUNDS_CONFIG;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
name="world-plane"
|
||||||
|
position={[center[0], planeY, center[2]]}
|
||||||
|
receiveShadow
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
>
|
||||||
|
<planeGeometry args={size} />
|
||||||
|
<meshStandardMaterial color={planeColor} roughness={1} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { WorldPlaneCollision } from "@/world/collision/WorldPlaneCollision";
|
||||||
|
import { WorldWallsCollision } from "@/world/collision/WorldWallsCollision";
|
||||||
|
|
||||||
|
export function WorldBoundsCollision(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<group name="world-bounds-collision">
|
||||||
|
<WorldPlaneCollision />
|
||||||
|
<WorldWallsCollision />
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig";
|
||||||
|
|
||||||
|
export function WorldPlaneCollision(): React.JSX.Element | null {
|
||||||
|
if (!WORLD_BOUNDS_CONFIG.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { center, planeCollisionThickness, planeY, size } = WORLD_BOUNDS_CONFIG;
|
||||||
|
const [width, depth] = size;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
name="world-plane-collision"
|
||||||
|
position={[center[0], planeY - planeCollisionThickness / 2, center[2]]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[width, planeCollisionThickness, depth]} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig";
|
||||||
|
|
||||||
|
export function WorldWallsCollision(): React.JSX.Element | null {
|
||||||
|
if (!WORLD_BOUNDS_CONFIG.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { center, size, wallHeight, wallThickness } = WORLD_BOUNDS_CONFIG;
|
||||||
|
const [width, depth] = size;
|
||||||
|
const wallY = center[1] + wallHeight / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfDepth = depth / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group name="world-walls-collision">
|
||||||
|
<mesh position={[center[0], wallY, center[2] - halfDepth]}>
|
||||||
|
<boxGeometry
|
||||||
|
args={[width + wallThickness * 2, wallHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[center[0], wallY, center[2] + halfDepth]}>
|
||||||
|
<boxGeometry
|
||||||
|
args={[width + wallThickness * 2, wallHeight, wallThickness]}
|
||||||
|
/>
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[center[0] - halfWidth, wallY, center[2]]}>
|
||||||
|
<boxGeometry args={[wallThickness, wallHeight, depth]} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[center[0] + halfWidth, wallY, center[2]]}>
|
||||||
|
<boxGeometry args={[wallThickness, wallHeight, depth]} />
|
||||||
|
<meshBasicMaterial />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
|
||||||
|
import {
|
||||||
|
PATH_DEBUG_PREVIEW_ENABLED,
|
||||||
|
PATH_TILE_RENDER_ENABLED,
|
||||||
|
PATH_TILE_MODEL_PATH,
|
||||||
|
} from "@/world/paths/pathConfig";
|
||||||
|
import { usePathTileData } from "@/world/paths/usePathTileData";
|
||||||
|
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
||||||
|
|
||||||
|
export function PathSystem(): React.JSX.Element | null {
|
||||||
|
if (!PATH_DEBUG_PREVIEW_ENABLED && !PATH_TILE_RENDER_ENABLED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PathTiles />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PathTiles(): React.JSX.Element | null {
|
||||||
|
const pathTiles = usePathTileData();
|
||||||
|
|
||||||
|
if (pathTiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PATH_DEBUG_PREVIEW_ENABLED) {
|
||||||
|
return <PathDebugPreview instances={pathTiles} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PATH_TILE_RENDER_ENABLED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstancedMapAsset
|
||||||
|
castShadow={false}
|
||||||
|
instances={pathTiles}
|
||||||
|
modelPath={PATH_TILE_MODEL_PATH}
|
||||||
|
receiveShadow
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PathDebugPreview({
|
||||||
|
instances,
|
||||||
|
}: {
|
||||||
|
instances: MapAssetInstance[];
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const instancedMeshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const instancedMesh = instancedMeshRef.current;
|
||||||
|
if (!instancedMesh) return;
|
||||||
|
|
||||||
|
const matrix = new THREE.Matrix4();
|
||||||
|
const position = new THREE.Vector3();
|
||||||
|
const quaternion = new THREE.Quaternion();
|
||||||
|
const scale = new THREE.Vector3(1, 1, 1);
|
||||||
|
|
||||||
|
for (let i = 0; i < instances.length; i++) {
|
||||||
|
const instance = instances[i];
|
||||||
|
if (!instance) continue;
|
||||||
|
|
||||||
|
position.set(
|
||||||
|
instance.position[0],
|
||||||
|
instance.position[1] + 0.08,
|
||||||
|
instance.position[2],
|
||||||
|
);
|
||||||
|
matrix.compose(position, quaternion, scale);
|
||||||
|
instancedMesh.setMatrixAt(i, matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||||
|
instancedMesh.computeBoundingSphere();
|
||||||
|
}, [instances]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh
|
||||||
|
ref={instancedMeshRef}
|
||||||
|
args={[undefined, undefined, instances.length]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[0.35, 0.08, 0.35]} />
|
||||||
|
<meshBasicMaterial color="#ff00ff" />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { TERRAIN_COLORS, TERRAIN_TILE_SIZE } from "@/data/world/terrainConfig";
|
||||||
|
|
||||||
|
export const PATH_SURFACE_KEY = "chemin";
|
||||||
|
export const PATH_DEBUG_PREVIEW_ENABLED = false;
|
||||||
|
export const PATH_TILE_RENDER_ENABLED = false;
|
||||||
|
export const PATH_TILE_MODEL_PATH = TERRAIN_COLORS.chemin.modelPath;
|
||||||
|
export const PATH_TILE_SIZE =
|
||||||
|
TERRAIN_COLORS.chemin.tileSize ?? TERRAIN_TILE_SIZE;
|
||||||
|
export const PATH_TILE_SAMPLE_STEP = 2;
|
||||||
|
export const PATH_TILE_MAX_COUNT = 1500;
|
||||||
|
export const PATH_TILE_ROTATION = [0, 0, 0] as const;
|
||||||
|
export const PATH_TILE_SCALE = [1, 1, 1] as const;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { TERRAIN_SURFACE_PROJECTION } from "@/data/world/terrainConfig";
|
||||||
|
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
||||||
|
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler";
|
||||||
|
import type { MapAssetInstance } from "@/world/map-instancing/useMapInstancingData";
|
||||||
|
import {
|
||||||
|
PATH_TILE_MAX_COUNT,
|
||||||
|
PATH_SURFACE_KEY,
|
||||||
|
PATH_TILE_ROTATION,
|
||||||
|
PATH_TILE_SAMPLE_STEP,
|
||||||
|
PATH_TILE_SCALE,
|
||||||
|
} from "@/world/paths/pathConfig";
|
||||||
|
|
||||||
|
function createSampleCenters(min: number, max: number, step: number): number[] {
|
||||||
|
const start = Math.ceil(min / step) * step + step * 0.5;
|
||||||
|
const centers: number[] = [];
|
||||||
|
|
||||||
|
for (let value = start; value <= max; value += step) {
|
||||||
|
centers.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return centers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePathTileData(): MapAssetInstance[] {
|
||||||
|
const terrainSurfaceData = useTerrainSurfaceData();
|
||||||
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!terrainSurfaceData) return [];
|
||||||
|
|
||||||
|
const instances: MapAssetInstance[] = [];
|
||||||
|
const xCenters = createSampleCenters(
|
||||||
|
terrainSurfaceData.bounds.minX,
|
||||||
|
terrainSurfaceData.bounds.maxX,
|
||||||
|
PATH_TILE_SAMPLE_STEP,
|
||||||
|
);
|
||||||
|
const zCenters = createSampleCenters(
|
||||||
|
terrainSurfaceData.bounds.minZ,
|
||||||
|
terrainSurfaceData.bounds.maxZ,
|
||||||
|
PATH_TILE_SAMPLE_STEP,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const x of xCenters) {
|
||||||
|
for (const z of zCenters) {
|
||||||
|
if (instances.length >= PATH_TILE_MAX_COUNT) return instances;
|
||||||
|
|
||||||
|
const sample = sampleTerrainSurfaceAtXZ(
|
||||||
|
terrainSurfaceData.imageData,
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
terrainSurfaceData.bounds,
|
||||||
|
TERRAIN_SURFACE_PROJECTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sample.key !== PATH_SURFACE_KEY) continue;
|
||||||
|
|
||||||
|
const height = terrainHeight.getHeight(x, z) ?? 0;
|
||||||
|
|
||||||
|
instances.push({
|
||||||
|
position: [x, height, z],
|
||||||
|
rotation: [...PATH_TILE_ROTATION] as Vector3Tuple,
|
||||||
|
scale: [...PATH_TILE_SCALE] as Vector3Tuple,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances;
|
||||||
|
}, [terrainHeight, terrainSurfaceData]);
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
PLAYER_AIR_CONTROL_FACTOR,
|
PLAYER_AIR_CONTROL_FACTOR,
|
||||||
PLAYER_CAPSULE_RADIUS,
|
PLAYER_CAPSULE_RADIUS,
|
||||||
PLAYER_EYE_HEIGHT,
|
PLAYER_EYE_HEIGHT,
|
||||||
|
PLAYER_FALL_RESPAWN_DELAY,
|
||||||
|
PLAYER_FALL_RESPAWN_Y,
|
||||||
PLAYER_GRAVITY,
|
PLAYER_GRAVITY,
|
||||||
PLAYER_JUMP_SPEED,
|
PLAYER_JUMP_SPEED,
|
||||||
PLAYER_MAX_DELTA,
|
PLAYER_MAX_DELTA,
|
||||||
@@ -57,6 +59,22 @@ const _up = new THREE.Vector3(0, 1, 0);
|
|||||||
const _translateVec = new THREE.Vector3();
|
const _translateVec = new THREE.Vector3();
|
||||||
const _collisionCorrection = new THREE.Vector3();
|
const _collisionCorrection = new THREE.Vector3();
|
||||||
|
|
||||||
|
function resetPlayerCapsule(
|
||||||
|
capsule: Capsule,
|
||||||
|
spawnPosition: Vector3Tuple,
|
||||||
|
camera: THREE.Camera,
|
||||||
|
velocity: THREE.Vector3,
|
||||||
|
): void {
|
||||||
|
capsule.start.set(
|
||||||
|
spawnPosition[0],
|
||||||
|
spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
|
||||||
|
spawnPosition[2],
|
||||||
|
);
|
||||||
|
capsule.end.set(...spawnPosition);
|
||||||
|
velocity.set(0, 0, 0);
|
||||||
|
camera.position.copy(capsule.end);
|
||||||
|
}
|
||||||
|
|
||||||
function createSpawnCapsule(spawnPosition: Vector3Tuple): Capsule {
|
function createSpawnCapsule(spawnPosition: Vector3Tuple): Capsule {
|
||||||
return new Capsule(
|
return new Capsule(
|
||||||
new THREE.Vector3(
|
new THREE.Vector3(
|
||||||
@@ -104,6 +122,7 @@ export function PlayerController({
|
|||||||
const movementLockedRef = useRef(movementLocked);
|
const movementLockedRef = useRef(movementLocked);
|
||||||
const keys = useRef<Keys>({ ...DEFAULT_KEYS });
|
const keys = useRef<Keys>({ ...DEFAULT_KEYS });
|
||||||
const velocity = useRef(new THREE.Vector3());
|
const velocity = useRef(new THREE.Vector3());
|
||||||
|
const fallDuration = useRef(0);
|
||||||
const onFloor = useRef(false);
|
const onFloor = useRef(false);
|
||||||
const wantsJump = useRef(false);
|
const wantsJump = useRef(false);
|
||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
@@ -112,16 +131,15 @@ export function PlayerController({
|
|||||||
const capsule = useRef(createSpawnCapsule(spawnPosition));
|
const capsule = useRef(createSpawnCapsule(spawnPosition));
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
capsule.current.start.set(
|
resetPlayerCapsule(
|
||||||
spawnPosition[0],
|
capsule.current,
|
||||||
spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
|
spawnPosition,
|
||||||
spawnPosition[2],
|
camera,
|
||||||
|
velocity.current,
|
||||||
);
|
);
|
||||||
capsule.current.end.set(...spawnPosition);
|
fallDuration.current = 0;
|
||||||
velocity.current.set(0, 0, 0);
|
|
||||||
onFloor.current = false;
|
onFloor.current = false;
|
||||||
wantsJump.current = false;
|
wantsJump.current = false;
|
||||||
camera.position.copy(capsule.current.end);
|
|
||||||
initializedRef.current = true;
|
initializedRef.current = true;
|
||||||
}, [camera, spawnPosition]);
|
}, [camera, spawnPosition]);
|
||||||
|
|
||||||
@@ -211,6 +229,27 @@ export function PlayerController({
|
|||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
if (!initializedRef.current) return;
|
if (!initializedRef.current) return;
|
||||||
|
|
||||||
|
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
||||||
|
|
||||||
|
if (capsule.current.end.y < PLAYER_FALL_RESPAWN_Y) {
|
||||||
|
fallDuration.current += dt;
|
||||||
|
|
||||||
|
if (fallDuration.current >= PLAYER_FALL_RESPAWN_DELAY) {
|
||||||
|
resetPlayerCapsule(
|
||||||
|
capsule.current,
|
||||||
|
spawnPosition,
|
||||||
|
camera,
|
||||||
|
velocity.current,
|
||||||
|
);
|
||||||
|
fallDuration.current = 0;
|
||||||
|
onFloor.current = false;
|
||||||
|
wantsJump.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fallDuration.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (isPlayerInputLocked() || !canMove) {
|
if (isPlayerInputLocked() || !canMove) {
|
||||||
keys.current = { ...DEFAULT_KEYS };
|
keys.current = { ...DEFAULT_KEYS };
|
||||||
velocity.current.set(0, 0, 0);
|
velocity.current.set(0, 0, 0);
|
||||||
@@ -218,8 +257,6 @@ export function PlayerController({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
|
||||||
|
|
||||||
camera.getWorldDirection(_forward);
|
camera.getWorldDirection(_forward);
|
||||||
_forward.setY(0);
|
_forward.setY(0);
|
||||||
if (_forward.lengthSq() > 0) {
|
if (_forward.lengthSq() > 0) {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { useMemo, useRef } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { FOG_CONFIG } from "@/data/world/fogConfig";
|
||||||
|
import { getWindVector } from "@/data/world/windConfig";
|
||||||
|
import { WATER_SHADER_CONFIG } from "@/data/world/waterConfig";
|
||||||
|
import type { WaterSurfaceConfig } from "@/data/world/waterConfig";
|
||||||
|
import { useWind } from "@/hooks/world/useWind";
|
||||||
|
import {
|
||||||
|
WATER_FRAGMENT_SHADER,
|
||||||
|
WATER_VERTEX_SHADER,
|
||||||
|
} from "@/world/water/waterShaders";
|
||||||
|
|
||||||
|
export function WaterSurface({
|
||||||
|
position,
|
||||||
|
renderOrder,
|
||||||
|
rotation,
|
||||||
|
size,
|
||||||
|
}: WaterSurfaceConfig): React.JSX.Element {
|
||||||
|
const scene = useThree((state) => state.scene);
|
||||||
|
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
||||||
|
const wind = useWind();
|
||||||
|
const uniforms = useMemo(
|
||||||
|
() => ({
|
||||||
|
uTime: { value: 0 },
|
||||||
|
uScale: { value: WATER_SHADER_CONFIG.scale },
|
||||||
|
uSmoothness: { value: WATER_SHADER_CONFIG.smoothness },
|
||||||
|
uEdgeThreshold: { value: WATER_SHADER_CONFIG.edgeThreshold },
|
||||||
|
uEdgeSoftness: { value: WATER_SHADER_CONFIG.edgeSoftness },
|
||||||
|
uFlowX: { value: WATER_SHADER_CONFIG.flowX },
|
||||||
|
uFlowZ: { value: WATER_SHADER_CONFIG.flowZ },
|
||||||
|
uCellSpeed: { value: WATER_SHADER_CONFIG.cellSpeed },
|
||||||
|
uNoiseScale: { value: WATER_SHADER_CONFIG.noiseScale },
|
||||||
|
uNoiseFlowSpeed: { value: WATER_SHADER_CONFIG.noiseFlowSpeed },
|
||||||
|
uDistortAmount: { value: WATER_SHADER_CONFIG.distortAmount },
|
||||||
|
uBorderRadius: { value: WATER_SHADER_CONFIG.borderRadius },
|
||||||
|
uBorderSoftness: { value: WATER_SHADER_CONFIG.borderSoftness },
|
||||||
|
uDeepColor: { value: new THREE.Color(WATER_SHADER_CONFIG.deepColor) },
|
||||||
|
uMidColor: { value: new THREE.Color(WATER_SHADER_CONFIG.midColor) },
|
||||||
|
uMidPos: { value: WATER_SHADER_CONFIG.midPos },
|
||||||
|
uHighlight: {
|
||||||
|
value: new THREE.Color(WATER_SHADER_CONFIG.highlightColor),
|
||||||
|
},
|
||||||
|
uOpacity: { value: WATER_SHADER_CONFIG.opacity },
|
||||||
|
uDeepOpacity: { value: WATER_SHADER_CONFIG.deepOpacity },
|
||||||
|
uFogEnabled: { value: 0 },
|
||||||
|
uFogNear: { value: FOG_CONFIG.near },
|
||||||
|
uFogFar: { value: FOG_CONFIG.far },
|
||||||
|
uFogColor: { value: new THREE.Color(FOG_CONFIG.color) },
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
const material = materialRef.current;
|
||||||
|
if (!material) return;
|
||||||
|
|
||||||
|
const windVector = getWindVector(wind);
|
||||||
|
|
||||||
|
const {
|
||||||
|
uFlowX,
|
||||||
|
uFlowZ,
|
||||||
|
uFogColor,
|
||||||
|
uFogEnabled,
|
||||||
|
uFogFar,
|
||||||
|
uFogNear,
|
||||||
|
uNoiseScale,
|
||||||
|
uTime,
|
||||||
|
} = material.uniforms;
|
||||||
|
|
||||||
|
if (uTime) uTime.value = clock.getElapsedTime();
|
||||||
|
if (uFlowX) uFlowX.value = WATER_SHADER_CONFIG.flowX + windVector.x;
|
||||||
|
if (uFlowZ) uFlowZ.value = WATER_SHADER_CONFIG.flowZ + windVector.z;
|
||||||
|
if (uNoiseScale) {
|
||||||
|
uNoiseScale.value = WATER_SHADER_CONFIG.noiseScale * wind.noiseScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.fog instanceof THREE.Fog) {
|
||||||
|
if (uFogEnabled) uFogEnabled.value = 1;
|
||||||
|
if (uFogNear) uFogNear.value = scene.fog.near;
|
||||||
|
if (uFogFar) uFogFar.value = scene.fog.far;
|
||||||
|
if (uFogColor) uFogColor.value.copy(scene.fog.color);
|
||||||
|
} else if (uFogEnabled) {
|
||||||
|
uFogEnabled.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
position={[
|
||||||
|
position[0],
|
||||||
|
position[1] + WATER_SHADER_CONFIG.depthOffset,
|
||||||
|
position[2],
|
||||||
|
]}
|
||||||
|
rotation={[-Math.PI / 2 + rotation[0], rotation[1], rotation[2]]}
|
||||||
|
renderOrder={renderOrder}
|
||||||
|
>
|
||||||
|
<planeGeometry args={size} />
|
||||||
|
<shaderMaterial
|
||||||
|
ref={materialRef}
|
||||||
|
attach="material"
|
||||||
|
depthTest
|
||||||
|
depthWrite={false}
|
||||||
|
fragmentShader={WATER_FRAGMENT_SHADER}
|
||||||
|
side={THREE.FrontSide}
|
||||||
|
transparent
|
||||||
|
uniforms={uniforms}
|
||||||
|
vertexShader={WATER_VERTEX_SHADER}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import {
|
||||||
|
WATER_SHADER_CONFIG,
|
||||||
|
WATER_STREAMING_CONFIG,
|
||||||
|
WATER_SURFACES,
|
||||||
|
} from "@/data/world/waterConfig";
|
||||||
|
import type { WaterSurfaceConfig } from "@/data/world/waterConfig";
|
||||||
|
import { WaterSurface } from "@/world/water/WaterSurface";
|
||||||
|
|
||||||
|
function getDistanceToWaterSurface(
|
||||||
|
surface: WaterSurfaceConfig,
|
||||||
|
x: number,
|
||||||
|
z: number,
|
||||||
|
): number {
|
||||||
|
const halfWidth = surface.size[0] / 2;
|
||||||
|
const halfDepth = surface.size[1] / 2;
|
||||||
|
const distanceX = Math.max(Math.abs(x - surface.position[0]) - halfWidth, 0);
|
||||||
|
const distanceZ = Math.max(Math.abs(z - surface.position[2]) - halfDepth, 0);
|
||||||
|
|
||||||
|
return Math.hypot(distanceX, distanceZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WaterSystem(): React.JSX.Element | null {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const lastUpdateRef = useRef(-WATER_STREAMING_CONFIG.updateInterval);
|
||||||
|
const [activeSurfaceIndexes, setActiveSurfaceIndexes] = useState<Set<number>>(
|
||||||
|
() => new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateActiveSurfaces = useCallback(() => {
|
||||||
|
const nextIndexes = new Set<number>();
|
||||||
|
const cameraX = camera.position.x;
|
||||||
|
const cameraZ = camera.position.z;
|
||||||
|
|
||||||
|
WATER_SURFACES.forEach((surface, index) => {
|
||||||
|
const distance = getDistanceToWaterSurface(surface, cameraX, cameraZ);
|
||||||
|
const wasActive = activeSurfaceIndexes.has(index);
|
||||||
|
const radius = wasActive
|
||||||
|
? WATER_STREAMING_CONFIG.unloadDistance
|
||||||
|
: WATER_STREAMING_CONFIG.loadDistance;
|
||||||
|
|
||||||
|
if (distance <= radius) {
|
||||||
|
nextIndexes.add(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextIndexes.size === activeSurfaceIndexes.size &&
|
||||||
|
[...nextIndexes].every((index) => activeSurfaceIndexes.has(index))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSurfaceIndexes(nextIndexes);
|
||||||
|
}, [activeSurfaceIndexes, camera]);
|
||||||
|
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
if (!WATER_STREAMING_CONFIG.enabled) return;
|
||||||
|
|
||||||
|
const now = clock.elapsedTime * 1000;
|
||||||
|
if (now - lastUpdateRef.current < WATER_STREAMING_CONFIG.updateInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdateRef.current = now;
|
||||||
|
updateActiveSurfaces();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!WATER_SHADER_CONFIG.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleSurfaces = WATER_SURFACES.map((surface, index) => ({
|
||||||
|
index,
|
||||||
|
surface,
|
||||||
|
})).filter(({ index, surface }) => {
|
||||||
|
if (!WATER_STREAMING_CONFIG.enabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSurfaceIndexes.size > 0) {
|
||||||
|
return activeSurfaceIndexes.has(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
getDistanceToWaterSurface(
|
||||||
|
surface,
|
||||||
|
camera.position.x,
|
||||||
|
camera.position.z,
|
||||||
|
) <= WATER_STREAMING_CONFIG.loadDistance
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group name="water-system">
|
||||||
|
{visibleSurfaces.map(({ index, surface }) => (
|
||||||
|
<WaterSurface key={index} {...surface} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
export const WATER_VERTEX_SHADER = /* glsl */ `
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
varying vec2 vWorldPos;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
||||||
|
vWorldPosition = worldPosition.xyz;
|
||||||
|
vWorldPos = worldPosition.xz;
|
||||||
|
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WATER_FRAGMENT_SHADER = /* glsl */ `
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uScale;
|
||||||
|
uniform float uSmoothness;
|
||||||
|
uniform float uEdgeThreshold;
|
||||||
|
uniform float uEdgeSoftness;
|
||||||
|
uniform float uFlowX;
|
||||||
|
uniform float uFlowZ;
|
||||||
|
uniform float uCellSpeed;
|
||||||
|
uniform float uNoiseScale;
|
||||||
|
uniform float uNoiseFlowSpeed;
|
||||||
|
uniform float uDistortAmount;
|
||||||
|
uniform float uBorderRadius;
|
||||||
|
uniform float uBorderSoftness;
|
||||||
|
uniform vec3 uDeepColor;
|
||||||
|
uniform vec3 uMidColor;
|
||||||
|
uniform float uMidPos;
|
||||||
|
uniform vec3 uHighlight;
|
||||||
|
uniform float uOpacity;
|
||||||
|
uniform float uDeepOpacity;
|
||||||
|
uniform float uFogEnabled;
|
||||||
|
uniform float uFogNear;
|
||||||
|
uniform float uFogFar;
|
||||||
|
uniform vec3 uFogColor;
|
||||||
|
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying vec3 vWorldPosition;
|
||||||
|
varying vec2 vWorldPos;
|
||||||
|
|
||||||
|
float roundedBoxMask(vec2 uv, float radius, float softness) {
|
||||||
|
vec2 centeredUv = uv * 2.0 - 1.0;
|
||||||
|
vec2 boxSize = vec2(1.0 - radius);
|
||||||
|
vec2 distanceToEdge = abs(centeredUv) - boxSize;
|
||||||
|
float outsideDistance = length(max(distanceToEdge, 0.0)) - radius;
|
||||||
|
|
||||||
|
return 1.0 - smoothstep(-softness, softness, outsideDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 hash2(vec2 p) {
|
||||||
|
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
|
||||||
|
return fract(sin(p) * 43758.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
float smin(float a, float b, float k) {
|
||||||
|
float h = max(k - abs(a - b), 0.0) / k;
|
||||||
|
return min(a, b) - h * h * h * k / 6.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 cellPoint(vec2 seed) {
|
||||||
|
return 0.5 + 0.5 * sin(uTime * uCellSpeed + 6.2831 * seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
float voronoiF1(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
float nearest = 8.0;
|
||||||
|
|
||||||
|
for (int y = -1; y <= 1; y++) {
|
||||||
|
for (int x = -1; x <= 1; x++) {
|
||||||
|
vec2 neighbor = vec2(float(x), float(y));
|
||||||
|
vec2 point = cellPoint(hash2(i + neighbor));
|
||||||
|
nearest = min(nearest, length(neighbor + point - f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
float voronoiSmoothF1(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
float result = 8.0;
|
||||||
|
|
||||||
|
for (int y = -1; y <= 1; y++) {
|
||||||
|
for (int x = -1; x <= 1; x++) {
|
||||||
|
vec2 neighbor = vec2(float(x), float(y));
|
||||||
|
vec2 point = cellPoint(hash2(i + neighbor));
|
||||||
|
result = smin(result, length(neighbor + point - f), uSmoothness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
float noiseHash(vec2 p) {
|
||||||
|
p = fract(p * vec2(127.1, 311.7));
|
||||||
|
p += dot(p, p + 45.32);
|
||||||
|
return fract(p.x * p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float valueNoise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
f = f * f * (3.0 - 2.0 * f);
|
||||||
|
|
||||||
|
return mix(
|
||||||
|
mix(noiseHash(i), noiseHash(i + vec2(1.0, 0.0)), f.x),
|
||||||
|
mix(noiseHash(i + vec2(0.0, 1.0)), noiseHash(i + vec2(1.0, 1.0)), f.x),
|
||||||
|
f.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float fbm(vec2 p) {
|
||||||
|
float value = 0.0;
|
||||||
|
float amplitude = 0.5;
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
value += amplitude * valueNoise(p);
|
||||||
|
p *= 2.0;
|
||||||
|
amplitude *= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 noiseUv = vWorldPos * uNoiseScale + vec2(uTime * uNoiseFlowSpeed, 0.0);
|
||||||
|
float noiseFactor = fbm(noiseUv);
|
||||||
|
vec2 distortion = vec2(noiseFactor - 0.5) * uDistortAmount;
|
||||||
|
vec2 uv = vWorldPos * uScale + vec2(uFlowX, uFlowZ) * uTime + distortion;
|
||||||
|
|
||||||
|
float f1 = voronoiF1(uv);
|
||||||
|
float smoothF1 = voronoiSmoothF1(uv);
|
||||||
|
float edge = f1 - smoothF1;
|
||||||
|
float ramp = smoothstep(uEdgeThreshold - uEdgeSoftness, uEdgeThreshold + uEdgeSoftness, edge);
|
||||||
|
float safeMidPosition = max(uMidPos, 0.0001);
|
||||||
|
float firstSegment = clamp(ramp / safeMidPosition, 0.0, 1.0);
|
||||||
|
float secondSegment = clamp((ramp - safeMidPosition) / max(1.0 - safeMidPosition, 0.0001), 0.0, 1.0);
|
||||||
|
float inSecondSegment = step(safeMidPosition, ramp);
|
||||||
|
vec3 color = mix(
|
||||||
|
mix(uDeepColor, uMidColor, firstSegment),
|
||||||
|
mix(uMidColor, uHighlight, secondSegment),
|
||||||
|
inSecondSegment
|
||||||
|
);
|
||||||
|
float alpha = mix(uDeepOpacity, 1.0, ramp) * uOpacity;
|
||||||
|
alpha *= roundedBoxMask(vUv, uBorderRadius, uBorderSoftness);
|
||||||
|
|
||||||
|
if (uFogEnabled > 0.5) {
|
||||||
|
float fogDistance = distance(cameraPosition, vWorldPosition);
|
||||||
|
float fogFactor = smoothstep(uFogNear, uFogFar, fogDistance);
|
||||||
|
color = mix(color, uFogColor, fogFactor);
|
||||||
|
alpha *= 1.0 - fogFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alpha < 0.01) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_FragColor = vec4(color, alpha);
|
||||||
|
}
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user