diff --git a/src/data/world/waterConfig.ts b/src/data/world/waterConfig.ts new file mode 100644 index 0000000..8cacc50 --- /dev/null +++ b/src/data/world/waterConfig.ts @@ -0,0 +1,35 @@ +import { TERRAIN_WATER_HEIGHT } from "@/data/world/terrainConfig"; +import type { Vector3Tuple } from "@/types/three/three"; + +export interface WaterSurfaceConfig { + position: Vector3Tuple; + size: [number, number]; +} + +export const WATER_SHADER_CONFIG = { + enabled: true, + height: TERRAIN_WATER_HEIGHT, + scale: 0.3, + 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_SURFACES: WaterSurfaceConfig[] = [ + { + position: [62, TERRAIN_WATER_HEIGHT, -82], + size: [75, 42], + }, +]; diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index aa579b7..d62872e 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -25,6 +25,7 @@ import { isGeneratedMapModelName } from "@/world/map-generated/generatedMapModel import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem"; import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig"; import { VegetationSystem } from "@/world/vegetation/VegetationSystem"; +import { WaterSystem } from "@/world/water/WaterSystem"; import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading"; import { logger } from "@/utils/core/Logger"; import { loadMapSceneData } from "@/utils/map/loadMapSceneData"; @@ -257,6 +258,7 @@ export function GameMap({ ))} + {isMapModelVisible("terrain", { groups, models }) ? ( diff --git a/src/world/water/WaterSurface.tsx b/src/world/water/WaterSurface.tsx new file mode 100644 index 0000000..93c039c --- /dev/null +++ b/src/world/water/WaterSurface.tsx @@ -0,0 +1,63 @@ +import { useMemo, useRef } from "react"; +import * as THREE from "three"; +import { useFrame } from "@react-three/fiber"; +import { WATER_SHADER_CONFIG } from "@/data/world/waterConfig"; +import type { WaterSurfaceConfig } from "@/data/world/waterConfig"; +import { + WATER_FRAGMENT_SHADER, + WATER_VERTEX_SHADER, +} from "@/world/water/waterShaders"; + +export function WaterSurface({ + position, + size, +}: WaterSurfaceConfig): React.JSX.Element { + const materialRef = useRef(null); + 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 }, + 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 }, + }), + [], + ); + + useFrame(({ clock }) => { + const uniform = materialRef.current?.uniforms.uTime; + if (uniform) { + uniform.value = clock.getElapsedTime(); + } + }); + + return ( + + + + + ); +} diff --git a/src/world/water/WaterSystem.tsx b/src/world/water/WaterSystem.tsx new file mode 100644 index 0000000..07195a3 --- /dev/null +++ b/src/world/water/WaterSystem.tsx @@ -0,0 +1,16 @@ +import { WATER_SHADER_CONFIG, WATER_SURFACES } from "@/data/world/waterConfig"; +import { WaterSurface } from "@/world/water/WaterSurface"; + +export function WaterSystem(): React.JSX.Element | null { + if (!WATER_SHADER_CONFIG.enabled) { + return null; + } + + return ( + <> + {WATER_SURFACES.map((surface, index) => ( + + ))} + + ); +} diff --git a/src/world/water/waterShaders.ts b/src/world/water/waterShaders.ts new file mode 100644 index 0000000..2035c9d --- /dev/null +++ b/src/world/water/waterShaders.ts @@ -0,0 +1,132 @@ +export const WATER_VERTEX_SHADER = /* glsl */ ` + varying vec2 vWorldPos; + + void main() { + vec4 worldPosition = modelMatrix * vec4(position, 1.0); + 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 vec3 uDeepColor; + uniform vec3 uMidColor; + uniform float uMidPos; + uniform vec3 uHighlight; + uniform float uOpacity; + uniform float uDeepOpacity; + + varying vec2 vWorldPos; + + 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; + + gl_FragColor = vec4(color, alpha); + } +`;