Feat/map-environment #6

Merged
math-pixel merged 116 commits from feat/map-environment into develop 2026-05-29 00:00:51 +00:00
5 changed files with 248 additions and 0 deletions
Showing only changes of commit fbe8c0c854 - Show all commits
+35
View File
@@ -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],
},
];
+2
View File
@@ -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 />
+63
View File
@@ -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>
);
}
+16
View File
@@ -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} />
))}
</>
);
}
+132
View File
@@ -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);
}
`;