feat(environment): add terrain water shader
This commit is contained in:
@@ -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],
|
||||
},
|
||||
];
|
||||
@@ -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({
|
||||
))}
|
||||
</group>
|
||||
<MapInstancingSystem />
|
||||
<WaterSystem />
|
||||
<VegetationSystem />
|
||||
{isMapModelVisible("terrain", { groups, models }) ? (
|
||||
<TerrainModel />
|
||||
|
||||
@@ -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<THREE.ShaderMaterial>(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 (
|
||||
<mesh position={position} rotation={[-Math.PI / 2, 0, 0]} renderOrder={1}>
|
||||
<planeGeometry args={size} />
|
||||
<shaderMaterial
|
||||
ref={materialRef}
|
||||
attach="material"
|
||||
depthWrite={false}
|
||||
fragmentShader={WATER_FRAGMENT_SHADER}
|
||||
side={THREE.FrontSide}
|
||||
transparent
|
||||
uniforms={uniforms}
|
||||
vertexShader={WATER_VERTEX_SHADER}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
@@ -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) => (
|
||||
<WaterSurface key={index} {...surface} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user