Feat/map-environment #6
+12
-42
@@ -1,6 +1,3 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
|
||||||
import * as THREE from "three";
|
|
||||||
import {
|
import {
|
||||||
GAME_SCENE_FALLBACK_BACKGROUND_COLOR,
|
GAME_SCENE_FALLBACK_BACKGROUND_COLOR,
|
||||||
GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
|
GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
|
||||||
@@ -9,46 +6,25 @@ import {
|
|||||||
GAME_SCENE_SKY_MODEL_SCALE,
|
GAME_SCENE_SKY_MODEL_SCALE,
|
||||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||||
} from "@/data/world/environmentConfig";
|
} from "@/data/world/environmentConfig";
|
||||||
import { FOG_LIGHTING_COLOR_MIX } from "@/data/world/fogConfig";
|
|
||||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { useFogSettings } from "@/hooks/world/useFogSettings";
|
|
||||||
import {
|
import {
|
||||||
isMapModelVisible,
|
isMapModelVisible,
|
||||||
useMapPerformanceStore,
|
useMapPerformanceStore,
|
||||||
} from "@/managers/stores/useMapPerformanceStore";
|
} from "@/managers/stores/useMapPerformanceStore";
|
||||||
import { SkyModel } from "@/components/three/world/SkyModel";
|
import { SkyModel } from "@/components/three/world/SkyModel";
|
||||||
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
import { CloudSystem } from "@/world/clouds/CloudSystem";
|
||||||
import { LIGHTING_STATE } from "@/world/lightingState";
|
import { FogSystem } from "@/world/fog/FogSystem";
|
||||||
|
import { GrassSystem } from "@/world/grass/GrassSystem";
|
||||||
const tempSunFogColor = new THREE.Color();
|
import { VegetationSystem } from "@/world/vegetation/VegetationSystem";
|
||||||
|
import { WaterSystem } from "@/world/water/WaterSystem";
|
||||||
function getLightingFogColor(target: THREE.Color): THREE.Color {
|
import { WorldPlane } from "@/world/WorldPlane";
|
||||||
target.set(LIGHTING_STATE.ambientColor);
|
|
||||||
target.multiplyScalar(FOG_LIGHTING_COLOR_MIX.ambient);
|
|
||||||
tempSunFogColor.set(LIGHTING_STATE.sunColor);
|
|
||||||
target.add(tempSunFogColor.multiplyScalar(FOG_LIGHTING_COLOR_MIX.sun));
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Environment(): React.JSX.Element {
|
export function Environment(): React.JSX.Element {
|
||||||
const cameraMode = useCameraMode();
|
|
||||||
const sceneMode = useSceneMode();
|
const sceneMode = useSceneMode();
|
||||||
const fog = useFogSettings();
|
|
||||||
const fogEnabled = useDebugStore((debug) => debug.getFogEnabled());
|
|
||||||
const groups = useMapPerformanceStore((state) => state.groups);
|
const groups = useMapPerformanceStore((state) => state.groups);
|
||||||
const models = useMapPerformanceStore((state) => state.models);
|
const models = useMapPerformanceStore((state) => state.models);
|
||||||
const scene = useThree((state) => state.scene);
|
|
||||||
const fogColor = useMemo(() => getLightingFogColor(new THREE.Color()), []);
|
|
||||||
const showSky = isMapModelVisible("sky", { groups, models });
|
const showSky = isMapModelVisible("sky", { groups, models });
|
||||||
|
|
||||||
useFrame(() => {
|
|
||||||
if (!scene.fog) return;
|
|
||||||
|
|
||||||
getLightingFogColor(scene.fog.color);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sceneMode === "physics") {
|
if (sceneMode === "physics") {
|
||||||
return (
|
return (
|
||||||
<color attach="background" args={[PHYSICS_SCENE_BACKGROUND_COLOR]} />
|
<color attach="background" args={[PHYSICS_SCENE_BACKGROUND_COLOR]} />
|
||||||
@@ -57,18 +33,7 @@ export function Environment(): React.JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{fogEnabled &&
|
<FogSystem />
|
||||||
sceneMode === "game" &&
|
|
||||||
cameraMode === "player" &&
|
|
||||||
fog.mode === "linear" ? (
|
|
||||||
<fog attach="fog" args={[fogColor, fog.near, fog.far]} />
|
|
||||||
) : null}
|
|
||||||
{fogEnabled &&
|
|
||||||
sceneMode === "game" &&
|
|
||||||
cameraMode === "player" &&
|
|
||||||
fog.mode === "exp2" ? (
|
|
||||||
<fogExp2 attach="fog" args={[fogColor, fog.density]} />
|
|
||||||
) : null}
|
|
||||||
{showSky ? (
|
{showSky ? (
|
||||||
<SkyModel
|
<SkyModel
|
||||||
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
||||||
@@ -83,6 +48,11 @@ export function Environment(): React.JSX.Element {
|
|||||||
args={[GAME_SCENE_FALLBACK_BACKGROUND_COLOR]}
|
args={[GAME_SCENE_FALLBACK_BACKGROUND_COLOR]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<WorldPlane />
|
||||||
|
<WaterSystem />
|
||||||
|
<CloudSystem />
|
||||||
|
<GrassSystem />
|
||||||
|
<VegetationSystem />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,9 @@ import {
|
|||||||
} from "@/managers/stores/useMapPerformanceStore";
|
} from "@/managers/stores/useMapPerformanceStore";
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
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 { 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";
|
||||||
@@ -262,10 +258,6 @@ export function GameMap({
|
|||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
<MapInstancingSystem />
|
<MapInstancingSystem />
|
||||||
<WorldPlane />
|
|
||||||
<WaterSystem />
|
|
||||||
<CloudSystem />
|
|
||||||
<VegetationSystem />
|
|
||||||
{isMapModelVisible("terrain", { groups, models }) ? (
|
{isMapModelVisible("terrain", { groups, models }) ? (
|
||||||
terrainNode ? (
|
terrainNode ? (
|
||||||
<TerrainModel
|
<TerrainModel
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { FOG_LIGHTING_COLOR_MIX } from "@/data/world/fogConfig";
|
||||||
|
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
|
import { useDebugStore } from "@/hooks/debug/useDebugStore";
|
||||||
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
|
import { useFogSettings } from "@/hooks/world/useFogSettings";
|
||||||
|
import { LIGHTING_STATE } from "@/world/lightingState";
|
||||||
|
|
||||||
|
const tempSunFogColor = new THREE.Color();
|
||||||
|
|
||||||
|
function getLightingFogColor(target: THREE.Color): THREE.Color {
|
||||||
|
target.set(LIGHTING_STATE.ambientColor);
|
||||||
|
target.multiplyScalar(FOG_LIGHTING_COLOR_MIX.ambient);
|
||||||
|
tempSunFogColor.set(LIGHTING_STATE.sunColor);
|
||||||
|
target.add(tempSunFogColor.multiplyScalar(FOG_LIGHTING_COLOR_MIX.sun));
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FogSystem(): React.JSX.Element | null {
|
||||||
|
const cameraMode = useCameraMode();
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
|
const fog = useFogSettings();
|
||||||
|
const fogEnabled = useDebugStore((debug) => debug.getFogEnabled());
|
||||||
|
const scene = useThree((state) => state.scene);
|
||||||
|
const fogColor = useMemo(() => getLightingFogColor(new THREE.Color()), []);
|
||||||
|
const shouldShowFog =
|
||||||
|
fogEnabled && sceneMode === "game" && cameraMode === "player";
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (!scene.fog) return;
|
||||||
|
|
||||||
|
getLightingFogColor(scene.fog.color);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldShowFog) return null;
|
||||||
|
|
||||||
|
if (fog.mode === "linear") {
|
||||||
|
return <fog attach="fog" args={[fogColor, fog.near, fog.far]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <fogExp2 attach="fog" args={[fogColor, fog.density]} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { TERRAIN_SURFACE_PROJECTION } from "@/data/world/terrainConfig";
|
||||||
|
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
||||||
|
import { useWind } from "@/hooks/world/useWind";
|
||||||
|
import type { TerrainSurfaceData } from "@/types/world/terrainSurface";
|
||||||
|
import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler";
|
||||||
|
import {
|
||||||
|
getGrassTipColor,
|
||||||
|
GRASS_CONFIG,
|
||||||
|
GRASS_SURFACE_KEYS,
|
||||||
|
} from "@/world/grass/grassConfig";
|
||||||
|
import {
|
||||||
|
grassFragmentShader,
|
||||||
|
grassVertexShader,
|
||||||
|
} from "@/world/grass/grassShaders";
|
||||||
|
|
||||||
|
interface GrassPatchProps {
|
||||||
|
chunkX: number;
|
||||||
|
chunkZ: number;
|
||||||
|
density: number;
|
||||||
|
terrainSurfaceData: TerrainSurfaceData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GrassBladeVertexData {
|
||||||
|
color: number[];
|
||||||
|
heightFactor: number;
|
||||||
|
position: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function random01(seed: number): number {
|
||||||
|
const value = Math.sin(seed * 12.9898) * 43758.5453;
|
||||||
|
return value - Math.floor(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lerp(min: number, max: number, ratio: number): number {
|
||||||
|
return min + (max - min) * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGrassMaterial(): THREE.ShaderMaterial {
|
||||||
|
return new THREE.ShaderMaterial({
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
vertexColors: true,
|
||||||
|
vertexShader: grassVertexShader,
|
||||||
|
fragmentShader: grassFragmentShader,
|
||||||
|
uniforms: {
|
||||||
|
uTime: { value: 0 },
|
||||||
|
uWindDirection: { value: 0 },
|
||||||
|
uWindSpeed: { value: 0 },
|
||||||
|
uWindStrength: { value: 0 },
|
||||||
|
uWindNoiseScale: { value: GRASS_CONFIG.windNoiseScale },
|
||||||
|
uBendStrength: { value: GRASS_CONFIG.windBendStrength },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGrassBlade(
|
||||||
|
positions: number[],
|
||||||
|
colors: number[],
|
||||||
|
bladeBases: number[],
|
||||||
|
heightFactors: number[],
|
||||||
|
windPhases: number[],
|
||||||
|
basePosition: THREE.Vector3,
|
||||||
|
yaw: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
baseColor: THREE.Color,
|
||||||
|
tipColor: THREE.Color,
|
||||||
|
windPhase: number,
|
||||||
|
): void {
|
||||||
|
const rightX = Math.cos(yaw) * width * 0.5;
|
||||||
|
const rightZ = Math.sin(yaw) * width * 0.5;
|
||||||
|
const leanX = Math.cos(yaw + Math.PI * 0.5) * width * 0.22;
|
||||||
|
const leanZ = Math.sin(yaw + Math.PI * 0.5) * width * 0.22;
|
||||||
|
const vertexData: GrassBladeVertexData[] = [
|
||||||
|
{
|
||||||
|
position: [
|
||||||
|
basePosition.x - rightX,
|
||||||
|
basePosition.y,
|
||||||
|
basePosition.z - rightZ,
|
||||||
|
],
|
||||||
|
color: [baseColor.r, baseColor.g, baseColor.b],
|
||||||
|
heightFactor: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: [
|
||||||
|
basePosition.x + rightX,
|
||||||
|
basePosition.y,
|
||||||
|
basePosition.z + rightZ,
|
||||||
|
],
|
||||||
|
color: [baseColor.r, baseColor.g, baseColor.b],
|
||||||
|
heightFactor: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: [
|
||||||
|
basePosition.x + leanX,
|
||||||
|
basePosition.y + height,
|
||||||
|
basePosition.z + leanZ,
|
||||||
|
],
|
||||||
|
color: [tipColor.r, tipColor.g, tipColor.b],
|
||||||
|
heightFactor: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const vertex of vertexData) {
|
||||||
|
positions.push(...vertex.position);
|
||||||
|
colors.push(...vertex.color);
|
||||||
|
bladeBases.push(basePosition.x, basePosition.y, basePosition.z);
|
||||||
|
heightFactors.push(vertex.heightFactor);
|
||||||
|
windPhases.push(windPhase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGrassGeometry(
|
||||||
|
chunkX: number,
|
||||||
|
chunkZ: number,
|
||||||
|
density: number,
|
||||||
|
terrainSurfaceData: TerrainSurfaceData,
|
||||||
|
getHeight: (x: number, z: number) => number | null,
|
||||||
|
): THREE.BufferGeometry | null {
|
||||||
|
const positions: number[] = [];
|
||||||
|
const colors: number[] = [];
|
||||||
|
const bladeBases: number[] = [];
|
||||||
|
const heightFactors: number[] = [];
|
||||||
|
const windPhases: number[] = [];
|
||||||
|
const baseColor = new THREE.Color(GRASS_CONFIG.baseColor);
|
||||||
|
const startX = chunkX * GRASS_CONFIG.chunkSize;
|
||||||
|
const startZ = chunkZ * GRASS_CONFIG.chunkSize;
|
||||||
|
const endX = startX + GRASS_CONFIG.chunkSize;
|
||||||
|
const endZ = startZ + GRASS_CONFIG.chunkSize;
|
||||||
|
const bladeBudget = Math.round(GRASS_CONFIG.maxBladesPerChunk * density);
|
||||||
|
let bladeCount = 0;
|
||||||
|
|
||||||
|
for (let x = startX; x < endX; x += GRASS_CONFIG.sampleStep) {
|
||||||
|
for (let z = startZ; z < endZ; z += GRASS_CONFIG.sampleStep) {
|
||||||
|
for (
|
||||||
|
let bladeIndex = 0;
|
||||||
|
bladeIndex < GRASS_CONFIG.bladesPerCell;
|
||||||
|
bladeIndex++
|
||||||
|
) {
|
||||||
|
if (bladeCount >= bladeBudget) break;
|
||||||
|
|
||||||
|
const seed =
|
||||||
|
(chunkX + 101) * 92821 +
|
||||||
|
(chunkZ + 103) * 68917 +
|
||||||
|
Math.round(x * 13) * 193 +
|
||||||
|
Math.round(z * 17) * 389 +
|
||||||
|
bladeIndex * 997;
|
||||||
|
if (random01(seed) > density) continue;
|
||||||
|
|
||||||
|
const sampleX = x + (random01(seed + 1) - 0.5) * GRASS_CONFIG.jitter;
|
||||||
|
const sampleZ = z + (random01(seed + 2) - 0.5) * GRASS_CONFIG.jitter;
|
||||||
|
const sample = sampleTerrainSurfaceAtXZ(
|
||||||
|
terrainSurfaceData.imageData,
|
||||||
|
sampleX,
|
||||||
|
sampleZ,
|
||||||
|
terrainSurfaceData.bounds,
|
||||||
|
TERRAIN_SURFACE_PROJECTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sample.key || !GRASS_SURFACE_KEYS.has(sample.key as never))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const height = getHeight(sampleX, sampleZ);
|
||||||
|
if (height === null) continue;
|
||||||
|
|
||||||
|
const heightRatio = random01(seed + 3);
|
||||||
|
const widthRatio = random01(seed + 4);
|
||||||
|
const tipColor = new THREE.Color(getGrassTipColor(sample.key));
|
||||||
|
const basePosition = new THREE.Vector3(
|
||||||
|
sampleX,
|
||||||
|
height + GRASS_CONFIG.surfaceOffset,
|
||||||
|
sampleZ,
|
||||||
|
);
|
||||||
|
|
||||||
|
addGrassBlade(
|
||||||
|
positions,
|
||||||
|
colors,
|
||||||
|
bladeBases,
|
||||||
|
heightFactors,
|
||||||
|
windPhases,
|
||||||
|
basePosition,
|
||||||
|
random01(seed + 5) * Math.PI * 2,
|
||||||
|
GRASS_CONFIG.bladeWidth * lerp(0.75, 1.25, widthRatio),
|
||||||
|
lerp(
|
||||||
|
GRASS_CONFIG.minBladeHeight,
|
||||||
|
GRASS_CONFIG.maxBladeHeight,
|
||||||
|
heightRatio,
|
||||||
|
),
|
||||||
|
baseColor,
|
||||||
|
tipColor,
|
||||||
|
random01(seed + 6) * Math.PI * 2,
|
||||||
|
);
|
||||||
|
bladeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bladeCount === 0) return null;
|
||||||
|
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
geometry.setAttribute(
|
||||||
|
"position",
|
||||||
|
new THREE.Float32BufferAttribute(positions, 3),
|
||||||
|
);
|
||||||
|
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
||||||
|
geometry.setAttribute(
|
||||||
|
"aBladeBase",
|
||||||
|
new THREE.Float32BufferAttribute(bladeBases, 3),
|
||||||
|
);
|
||||||
|
geometry.setAttribute(
|
||||||
|
"aHeightFactor",
|
||||||
|
new THREE.Float32BufferAttribute(heightFactors, 1),
|
||||||
|
);
|
||||||
|
geometry.setAttribute(
|
||||||
|
"aWindPhase",
|
||||||
|
new THREE.Float32BufferAttribute(windPhases, 1),
|
||||||
|
);
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
geometry.computeBoundingSphere();
|
||||||
|
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GrassPatch({
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
density,
|
||||||
|
terrainSurfaceData,
|
||||||
|
}: GrassPatchProps): React.JSX.Element | null {
|
||||||
|
const terrainHeight = useTerrainHeightSampler();
|
||||||
|
const wind = useWind();
|
||||||
|
const materialRef = useRef<THREE.ShaderMaterial | null>(null);
|
||||||
|
const geometry = useMemo(
|
||||||
|
() =>
|
||||||
|
createGrassGeometry(
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
density,
|
||||||
|
terrainSurfaceData,
|
||||||
|
terrainHeight.getHeight,
|
||||||
|
),
|
||||||
|
[chunkX, chunkZ, density, terrainHeight.getHeight, terrainSurfaceData],
|
||||||
|
);
|
||||||
|
const material = useMemo(() => createGrassMaterial(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
materialRef.current = material;
|
||||||
|
return () => {
|
||||||
|
materialRef.current = null;
|
||||||
|
material.dispose();
|
||||||
|
};
|
||||||
|
}, [material]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
geometry?.dispose();
|
||||||
|
};
|
||||||
|
}, [geometry]);
|
||||||
|
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
const currentMaterial = materialRef.current;
|
||||||
|
if (!currentMaterial) return;
|
||||||
|
|
||||||
|
const uniforms = currentMaterial.uniforms;
|
||||||
|
if (uniforms.uTime) uniforms.uTime.value = clock.elapsedTime;
|
||||||
|
if (uniforms.uWindDirection) uniforms.uWindDirection.value = wind.direction;
|
||||||
|
if (uniforms.uWindSpeed) uniforms.uWindSpeed.value = wind.speed;
|
||||||
|
if (uniforms.uWindStrength) uniforms.uWindStrength.value = wind.strength;
|
||||||
|
if (uniforms.uWindNoiseScale) {
|
||||||
|
uniforms.uWindNoiseScale.value =
|
||||||
|
GRASS_CONFIG.windNoiseScale * wind.noiseScale;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!geometry) return null;
|
||||||
|
|
||||||
|
return <mesh geometry={geometry} material={material} frustumCulled />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
|
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
||||||
|
import {
|
||||||
|
useDynamicGrass,
|
||||||
|
useGrassDensity,
|
||||||
|
} from "@/hooks/world/useGraphicsSettings";
|
||||||
|
import type { TerrainSurfaceBounds } from "@/types/world/terrainSurface";
|
||||||
|
import { GRASS_CONFIG } from "@/world/grass/grassConfig";
|
||||||
|
import { GrassPatch } from "@/world/grass/GrassPatch";
|
||||||
|
|
||||||
|
interface GrassChunk {
|
||||||
|
centerX: number;
|
||||||
|
centerZ: number;
|
||||||
|
key: string;
|
||||||
|
x: number;
|
||||||
|
z: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChunkRange(min: number, max: number): number[] {
|
||||||
|
const start = Math.floor(min / GRASS_CONFIG.chunkSize);
|
||||||
|
const end = Math.floor(max / GRASS_CONFIG.chunkSize);
|
||||||
|
const chunks: number[] = [];
|
||||||
|
|
||||||
|
for (let value = start; value <= end; value++) {
|
||||||
|
chunks.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGrassChunks(bounds: TerrainSurfaceBounds): GrassChunk[] {
|
||||||
|
const chunks: GrassChunk[] = [];
|
||||||
|
const xChunks = getChunkRange(bounds.minX, bounds.maxX);
|
||||||
|
const zChunks = getChunkRange(bounds.minZ, bounds.maxZ);
|
||||||
|
|
||||||
|
for (const x of xChunks) {
|
||||||
|
for (const z of zChunks) {
|
||||||
|
chunks.push({
|
||||||
|
centerX: x * GRASS_CONFIG.chunkSize + GRASS_CONFIG.chunkSize * 0.5,
|
||||||
|
centerZ: z * GRASS_CONFIG.chunkSize + GRASS_CONFIG.chunkSize * 0.5,
|
||||||
|
key: `${x}:${z}`,
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GrassSystem(): React.JSX.Element | null {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const terrainSurfaceData = useTerrainSurfaceData();
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
|
const dynamicGrass = useDynamicGrass();
|
||||||
|
const grassDensity = useGrassDensity();
|
||||||
|
const lastUpdateRef = useRef(-GRASS_CONFIG.updateInterval);
|
||||||
|
const [activeChunkKeys, setActiveChunkKeys] = useState<Set<string>>(
|
||||||
|
() => new Set(),
|
||||||
|
);
|
||||||
|
const density = Math.max(0, grassDensity);
|
||||||
|
const chunks = useMemo(
|
||||||
|
() =>
|
||||||
|
terrainSurfaceData ? createGrassChunks(terrainSurfaceData.bounds) : [],
|
||||||
|
[terrainSurfaceData],
|
||||||
|
);
|
||||||
|
const streamingEnabled = sceneMode === "game";
|
||||||
|
|
||||||
|
const updateActiveChunks = useCallback(() => {
|
||||||
|
const nextKeys = new Set<string>();
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const distance = Math.hypot(
|
||||||
|
chunk.centerX - camera.position.x,
|
||||||
|
chunk.centerZ - camera.position.z,
|
||||||
|
);
|
||||||
|
const wasActive = activeChunkKeys.has(chunk.key);
|
||||||
|
const radius = wasActive
|
||||||
|
? GRASS_CONFIG.unloadRadius
|
||||||
|
: GRASS_CONFIG.loadRadius;
|
||||||
|
|
||||||
|
if (distance <= radius) {
|
||||||
|
nextKeys.add(chunk.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextKeys.size === activeChunkKeys.size &&
|
||||||
|
[...nextKeys].every((key) => activeChunkKeys.has(key))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveChunkKeys(nextKeys);
|
||||||
|
}, [activeChunkKeys, camera, chunks]);
|
||||||
|
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
if (!streamingEnabled) return;
|
||||||
|
|
||||||
|
const now = clock.elapsedTime * 1000;
|
||||||
|
if (now - lastUpdateRef.current < GRASS_CONFIG.updateInterval) return;
|
||||||
|
lastUpdateRef.current = now;
|
||||||
|
|
||||||
|
updateActiveChunks();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!GRASS_CONFIG.enabled ||
|
||||||
|
!dynamicGrass ||
|
||||||
|
density <= 0 ||
|
||||||
|
!terrainSurfaceData
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleChunks = streamingEnabled
|
||||||
|
? chunks.filter((chunk) => {
|
||||||
|
if (activeChunkKeys.size > 0) {
|
||||||
|
return activeChunkKeys.has(chunk.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.hypot(
|
||||||
|
chunk.centerX - camera.position.x,
|
||||||
|
chunk.centerZ - camera.position.z,
|
||||||
|
) <= GRASS_CONFIG.loadRadius
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: chunks;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group name="grass-system">
|
||||||
|
{visibleChunks.map((chunk) => (
|
||||||
|
<Suspense key={chunk.key} fallback={null}>
|
||||||
|
<GrassPatch
|
||||||
|
chunkX={chunk.x}
|
||||||
|
chunkZ={chunk.z}
|
||||||
|
density={density}
|
||||||
|
terrainSurfaceData={terrainSurfaceData}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
||||||
|
|
||||||
|
export const GRASS_CONFIG = {
|
||||||
|
enabled: true,
|
||||||
|
chunkSize: 20,
|
||||||
|
loadRadius: 30,
|
||||||
|
unloadRadius: 34,
|
||||||
|
updateInterval: 250,
|
||||||
|
sampleStep: 1.15,
|
||||||
|
jitter: 0.42,
|
||||||
|
bladesPerCell: 2,
|
||||||
|
maxBladesPerChunk: 720,
|
||||||
|
bladeWidth: 0.12,
|
||||||
|
minBladeHeight: 0.42,
|
||||||
|
maxBladeHeight: 0.82,
|
||||||
|
surfaceOffset: 0.06,
|
||||||
|
baseColor: "#1f3512",
|
||||||
|
windBendStrength: 0.42,
|
||||||
|
windNoiseScale: 0.09,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const GRASS_SURFACE_KEYS = new Set([
|
||||||
|
"grass1",
|
||||||
|
"grass2",
|
||||||
|
"grass3",
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
export function getGrassTipColor(surfaceKey: string | null): string {
|
||||||
|
if (surfaceKey === "grass1") return TERRAIN_COLORS.grass1.grassTipColor;
|
||||||
|
if (surfaceKey === "grass2") return TERRAIN_COLORS.grass2.grassTipColor;
|
||||||
|
if (surfaceKey === "grass3") return TERRAIN_COLORS.grass3.grassTipColor;
|
||||||
|
return TERRAIN_COLORS.grass1.grassTipColor;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
export const grassVertexShader = /* glsl */ `
|
||||||
|
attribute vec3 aColor;
|
||||||
|
attribute vec3 aBladeBase;
|
||||||
|
attribute float aHeightFactor;
|
||||||
|
attribute float aWindPhase;
|
||||||
|
|
||||||
|
varying vec3 vColor;
|
||||||
|
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uWindDirection;
|
||||||
|
uniform float uWindSpeed;
|
||||||
|
uniform float uWindStrength;
|
||||||
|
uniform float uWindNoiseScale;
|
||||||
|
uniform float uBendStrength;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 transformed = position;
|
||||||
|
float topFactor = aHeightFactor * aHeightFactor;
|
||||||
|
vec2 windDirection = normalize(vec2(cos(uWindDirection), sin(uWindDirection)));
|
||||||
|
|
||||||
|
float primaryWind = sin(
|
||||||
|
uTime * max(uWindSpeed, 0.05) +
|
||||||
|
aWindPhase +
|
||||||
|
aBladeBase.x * uWindNoiseScale +
|
||||||
|
aBladeBase.z * uWindNoiseScale
|
||||||
|
);
|
||||||
|
float secondaryWind = sin(
|
||||||
|
uTime * max(uWindSpeed, 0.05) * 1.73 +
|
||||||
|
aWindPhase * 0.71 +
|
||||||
|
aBladeBase.x * uWindNoiseScale * 0.53 -
|
||||||
|
aBladeBase.z * uWindNoiseScale * 0.89
|
||||||
|
) * 0.35;
|
||||||
|
|
||||||
|
float bend = (primaryWind + secondaryWind) * uWindStrength * uBendStrength * topFactor;
|
||||||
|
transformed.xz += windDirection * bend;
|
||||||
|
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const grassFragmentShader = /* glsl */ `
|
||||||
|
varying vec3 vColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(vColor, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user