Feat/map-environment #6
+107
-161
@@ -1,32 +1,19 @@
|
|||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import * as THREE from "three";
|
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 { useWind } from "@/hooks/world/useWind";
|
||||||
import type { TerrainSurfaceData } from "@/types/world/terrainSurface";
|
import { GRASS_COLORS, GRASS_CONFIG } from "@/world/grass/grassConfig";
|
||||||
import { sampleTerrainSurfaceAtXZ } from "@/utils/world/terrainSurfaceSampler";
|
|
||||||
import {
|
|
||||||
getGrassTipColor,
|
|
||||||
GRASS_CONFIG,
|
|
||||||
GRASS_SURFACE_KEYS,
|
|
||||||
} from "@/world/grass/grassConfig";
|
|
||||||
import {
|
import {
|
||||||
grassFragmentShader,
|
grassFragmentShader,
|
||||||
grassVertexShader,
|
grassVertexShader,
|
||||||
} from "@/world/grass/grassShaders";
|
} from "@/world/grass/grassShaders";
|
||||||
|
import type { TerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
|
||||||
|
|
||||||
interface GrassPatchProps {
|
interface GrassPatchProps {
|
||||||
chunkX: number;
|
chunkX: number;
|
||||||
chunkZ: number;
|
chunkZ: number;
|
||||||
density: number;
|
density: number;
|
||||||
terrainSurfaceData: TerrainSurfaceData;
|
terrainSampler: TerrainGrassSampler;
|
||||||
}
|
|
||||||
|
|
||||||
interface GrassBladeVertexData {
|
|
||||||
color: number[];
|
|
||||||
heightFactor: number;
|
|
||||||
position: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function random01(seed: number): number {
|
function random01(seed: number): number {
|
||||||
@@ -34,81 +21,68 @@ function random01(seed: number): number {
|
|||||||
return value - Math.floor(value);
|
return value - Math.floor(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lerp(min: number, max: number, ratio: number): number {
|
|
||||||
return min + (max - min) * ratio;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createGrassMaterial(): THREE.ShaderMaterial {
|
function createGrassMaterial(): THREE.ShaderMaterial {
|
||||||
return new THREE.ShaderMaterial({
|
return new THREE.ShaderMaterial({
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
vertexColors: true,
|
|
||||||
vertexShader: grassVertexShader,
|
vertexShader: grassVertexShader,
|
||||||
fragmentShader: grassFragmentShader,
|
fragmentShader: grassFragmentShader,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
uTime: { value: 0 },
|
uTime: { value: 0 },
|
||||||
|
uPlayerPosition: { value: new THREE.Vector3() },
|
||||||
|
uPatchSize: { value: GRASS_CONFIG.chunkSize },
|
||||||
|
uBladeWidth: { value: GRASS_CONFIG.bladeWidth },
|
||||||
uWindDirection: { value: 0 },
|
uWindDirection: { value: 0 },
|
||||||
uWindSpeed: { value: 0 },
|
uWindSpeed: { value: 0 },
|
||||||
uWindStrength: { value: 0 },
|
|
||||||
uWindNoiseScale: { value: GRASS_CONFIG.windNoiseScale },
|
uWindNoiseScale: { value: GRASS_CONFIG.windNoiseScale },
|
||||||
uBendStrength: { value: GRASS_CONFIG.windBendStrength },
|
uBaldPatchModifier: { value: GRASS_CONFIG.baldPatchModifier },
|
||||||
|
uFalloffSharpness: { value: GRASS_CONFIG.falloffSharpness },
|
||||||
|
uHeightNoiseFrequency: { value: GRASS_CONFIG.heightNoiseFrequency },
|
||||||
|
uHeightNoiseAmplitude: { value: GRASS_CONFIG.heightNoiseAmplitude },
|
||||||
|
uMaxBendAngle: { value: GRASS_CONFIG.maxBendAngle },
|
||||||
|
uMaxBladeHeight: { value: GRASS_CONFIG.maxBladeHeight },
|
||||||
|
uRandomHeightAmount: { value: GRASS_CONFIG.randomHeightAmount },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pushVector(target: number[], value: THREE.Vector3): void {
|
||||||
|
target.push(value.x, value.y, value.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushColor(target: number[], value: THREE.Color): void {
|
||||||
|
target.push(value.r, value.g, value.b);
|
||||||
|
}
|
||||||
|
|
||||||
function addGrassBlade(
|
function addGrassBlade(
|
||||||
positions: number[],
|
positions: number[],
|
||||||
colors: number[],
|
bladeColors: number[],
|
||||||
bladeBases: number[],
|
bladeBases: number[],
|
||||||
heightFactors: number[],
|
bladeNormals: number[],
|
||||||
windPhases: number[],
|
sideFactors: number[],
|
||||||
|
tipFactors: number[],
|
||||||
|
randoms: number[],
|
||||||
|
yaws: number[],
|
||||||
basePosition: THREE.Vector3,
|
basePosition: THREE.Vector3,
|
||||||
yaw: number,
|
normal: THREE.Vector3,
|
||||||
width: number,
|
color: THREE.Color,
|
||||||
height: number,
|
yaw: THREE.Vector3,
|
||||||
baseColor: THREE.Color,
|
random: number,
|
||||||
tipColor: THREE.Color,
|
|
||||||
windPhase: number,
|
|
||||||
): void {
|
): void {
|
||||||
const rightX = Math.cos(yaw) * width * 0.5;
|
const vertices = [
|
||||||
const rightZ = Math.sin(yaw) * width * 0.5;
|
{ side: 1, tip: 0 },
|
||||||
const leanX = Math.cos(yaw + Math.PI * 0.5) * width * 0.22;
|
{ side: -1, tip: 0 },
|
||||||
const leanZ = Math.sin(yaw + Math.PI * 0.5) * width * 0.22;
|
{ side: 0, tip: 1 },
|
||||||
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) {
|
for (const vertex of vertices) {
|
||||||
positions.push(...vertex.position);
|
pushVector(positions, basePosition);
|
||||||
colors.push(...vertex.color);
|
pushColor(bladeColors, color);
|
||||||
bladeBases.push(basePosition.x, basePosition.y, basePosition.z);
|
pushVector(bladeBases, basePosition);
|
||||||
heightFactors.push(vertex.heightFactor);
|
pushVector(bladeNormals, normal);
|
||||||
windPhases.push(windPhase);
|
pushVector(yaws, yaw);
|
||||||
|
sideFactors.push(vertex.side);
|
||||||
|
tipFactors.push(vertex.tip);
|
||||||
|
randoms.push(random);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,107 +90,84 @@ function createGrassGeometry(
|
|||||||
chunkX: number,
|
chunkX: number,
|
||||||
chunkZ: number,
|
chunkZ: number,
|
||||||
density: number,
|
density: number,
|
||||||
terrainSurfaceData: TerrainSurfaceData,
|
terrainSampler: TerrainGrassSampler,
|
||||||
getHeight: (x: number, z: number) => number | null,
|
|
||||||
): THREE.BufferGeometry | null {
|
): THREE.BufferGeometry | null {
|
||||||
const positions: number[] = [];
|
const positions: number[] = [];
|
||||||
const colors: number[] = [];
|
const bladeColors: number[] = [];
|
||||||
const bladeBases: number[] = [];
|
const bladeBases: number[] = [];
|
||||||
const heightFactors: number[] = [];
|
const bladeNormals: number[] = [];
|
||||||
const windPhases: number[] = [];
|
const sideFactors: number[] = [];
|
||||||
const baseColor = new THREE.Color(GRASS_CONFIG.baseColor);
|
const tipFactors: number[] = [];
|
||||||
|
const randoms: number[] = [];
|
||||||
|
const yaws: number[] = [];
|
||||||
const startX = chunkX * GRASS_CONFIG.chunkSize;
|
const startX = chunkX * GRASS_CONFIG.chunkSize;
|
||||||
const startZ = chunkZ * GRASS_CONFIG.chunkSize;
|
const startZ = chunkZ * GRASS_CONFIG.chunkSize;
|
||||||
const endX = startX + GRASS_CONFIG.chunkSize;
|
const bladeCount = Math.round(GRASS_CONFIG.baseBladesPerChunk * density);
|
||||||
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 index = 0; index < bladeCount; index++) {
|
||||||
for (let z = startZ; z < endZ; z += GRASS_CONFIG.sampleStep) {
|
const seed = (chunkX + 101) * 92821 + (chunkZ + 103) * 68917 + index * 997;
|
||||||
for (
|
const x = startX + random01(seed + 1) * GRASS_CONFIG.chunkSize;
|
||||||
let bladeIndex = 0;
|
const z = startZ + random01(seed + 2) * GRASS_CONFIG.chunkSize;
|
||||||
bladeIndex < GRASS_CONFIG.bladesPerCell;
|
const sample = terrainSampler.sample(x, z);
|
||||||
bladeIndex++
|
if (!sample) continue;
|
||||||
) {
|
|
||||||
if (bladeCount >= bladeBudget) break;
|
|
||||||
|
|
||||||
const seed =
|
const colorIndex = Math.floor(random01(seed + 3) * GRASS_COLORS.length);
|
||||||
(chunkX + 101) * 92821 +
|
const color = new THREE.Color(GRASS_COLORS[colorIndex] ?? GRASS_COLORS[0]);
|
||||||
(chunkZ + 103) * 68917 +
|
const yawAngle = random01(seed + 4) * Math.PI * 2;
|
||||||
Math.round(x * 13) * 193 +
|
const yaw = new THREE.Vector3(Math.sin(yawAngle), 0, -Math.cos(yawAngle));
|
||||||
Math.round(z * 17) * 389 +
|
const basePosition = sample.position
|
||||||
bladeIndex * 997;
|
.clone()
|
||||||
if (random01(seed) > density) continue;
|
.addScaledVector(sample.normal, GRASS_CONFIG.surfaceOffset);
|
||||||
|
|
||||||
const sampleX = x + (random01(seed + 1) - 0.5) * GRASS_CONFIG.jitter;
|
addGrassBlade(
|
||||||
const sampleZ = z + (random01(seed + 2) - 0.5) * GRASS_CONFIG.jitter;
|
positions,
|
||||||
const sample = sampleTerrainSurfaceAtXZ(
|
bladeColors,
|
||||||
terrainSurfaceData.imageData,
|
bladeBases,
|
||||||
sampleX,
|
bladeNormals,
|
||||||
sampleZ,
|
sideFactors,
|
||||||
terrainSurfaceData.bounds,
|
tipFactors,
|
||||||
TERRAIN_SURFACE_PROJECTION,
|
randoms,
|
||||||
);
|
yaws,
|
||||||
|
basePosition,
|
||||||
if (!sample.key || !GRASS_SURFACE_KEYS.has(sample.key as never))
|
sample.normal,
|
||||||
continue;
|
color,
|
||||||
|
yaw,
|
||||||
const height = getHeight(sampleX, sampleZ);
|
random01(seed + 5),
|
||||||
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;
|
if (positions.length === 0) return null;
|
||||||
|
|
||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
geometry.setAttribute(
|
geometry.setAttribute(
|
||||||
"position",
|
"position",
|
||||||
new THREE.Float32BufferAttribute(positions, 3),
|
new THREE.Float32BufferAttribute(positions, 3),
|
||||||
);
|
);
|
||||||
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
geometry.setAttribute(
|
||||||
|
"aBladeColor",
|
||||||
|
new THREE.Float32BufferAttribute(bladeColors, 3),
|
||||||
|
);
|
||||||
geometry.setAttribute(
|
geometry.setAttribute(
|
||||||
"aBladeBase",
|
"aBladeBase",
|
||||||
new THREE.Float32BufferAttribute(bladeBases, 3),
|
new THREE.Float32BufferAttribute(bladeBases, 3),
|
||||||
);
|
);
|
||||||
geometry.setAttribute(
|
geometry.setAttribute(
|
||||||
"aHeightFactor",
|
"aBladeNormal",
|
||||||
new THREE.Float32BufferAttribute(heightFactors, 1),
|
new THREE.Float32BufferAttribute(bladeNormals, 3),
|
||||||
);
|
);
|
||||||
geometry.setAttribute(
|
geometry.setAttribute(
|
||||||
"aWindPhase",
|
"aSideFactor",
|
||||||
new THREE.Float32BufferAttribute(windPhases, 1),
|
new THREE.Float32BufferAttribute(sideFactors, 1),
|
||||||
);
|
);
|
||||||
|
geometry.setAttribute(
|
||||||
|
"aTipFactor",
|
||||||
|
new THREE.Float32BufferAttribute(tipFactors, 1),
|
||||||
|
);
|
||||||
|
geometry.setAttribute(
|
||||||
|
"aRandom",
|
||||||
|
new THREE.Float32BufferAttribute(randoms, 1),
|
||||||
|
);
|
||||||
|
geometry.setAttribute("aYaw", new THREE.Float32BufferAttribute(yaws, 3));
|
||||||
geometry.computeVertexNormals();
|
geometry.computeVertexNormals();
|
||||||
geometry.computeBoundingSphere();
|
geometry.computeBoundingSphere();
|
||||||
|
|
||||||
@@ -227,21 +178,14 @@ export function GrassPatch({
|
|||||||
chunkX,
|
chunkX,
|
||||||
chunkZ,
|
chunkZ,
|
||||||
density,
|
density,
|
||||||
terrainSurfaceData,
|
terrainSampler,
|
||||||
}: GrassPatchProps): React.JSX.Element | null {
|
}: GrassPatchProps): React.JSX.Element | null {
|
||||||
const terrainHeight = useTerrainHeightSampler();
|
const camera = useThree((state) => state.camera);
|
||||||
const wind = useWind();
|
const wind = useWind();
|
||||||
const materialRef = useRef<THREE.ShaderMaterial | null>(null);
|
const materialRef = useRef<THREE.ShaderMaterial | null>(null);
|
||||||
const geometry = useMemo(
|
const geometry = useMemo(
|
||||||
() =>
|
() => createGrassGeometry(chunkX, chunkZ, density, terrainSampler),
|
||||||
createGrassGeometry(
|
[chunkX, chunkZ, density, terrainSampler],
|
||||||
chunkX,
|
|
||||||
chunkZ,
|
|
||||||
density,
|
|
||||||
terrainSurfaceData,
|
|
||||||
terrainHeight.getHeight,
|
|
||||||
),
|
|
||||||
[chunkX, chunkZ, density, terrainHeight.getHeight, terrainSurfaceData],
|
|
||||||
);
|
);
|
||||||
const material = useMemo(() => createGrassMaterial(), []);
|
const material = useMemo(() => createGrassMaterial(), []);
|
||||||
|
|
||||||
@@ -265,9 +209,11 @@ export function GrassPatch({
|
|||||||
|
|
||||||
const uniforms = currentMaterial.uniforms;
|
const uniforms = currentMaterial.uniforms;
|
||||||
if (uniforms.uTime) uniforms.uTime.value = clock.elapsedTime;
|
if (uniforms.uTime) uniforms.uTime.value = clock.elapsedTime;
|
||||||
|
if (uniforms.uPlayerPosition) {
|
||||||
|
uniforms.uPlayerPosition.value.copy(camera.position);
|
||||||
|
}
|
||||||
if (uniforms.uWindDirection) uniforms.uWindDirection.value = wind.direction;
|
if (uniforms.uWindDirection) uniforms.uWindDirection.value = wind.direction;
|
||||||
if (uniforms.uWindSpeed) uniforms.uWindSpeed.value = wind.speed;
|
if (uniforms.uWindSpeed) uniforms.uWindSpeed.value = wind.speed;
|
||||||
if (uniforms.uWindStrength) uniforms.uWindStrength.value = wind.strength;
|
|
||||||
if (uniforms.uWindNoiseScale) {
|
if (uniforms.uWindNoiseScale) {
|
||||||
uniforms.uWindNoiseScale.value =
|
uniforms.uWindNoiseScale.value =
|
||||||
GRASS_CONFIG.windNoiseScale * wind.noiseScale;
|
GRASS_CONFIG.windNoiseScale * wind.noiseScale;
|
||||||
@@ -276,5 +222,5 @@ export function GrassPatch({
|
|||||||
|
|
||||||
if (!geometry) return null;
|
if (!geometry) return null;
|
||||||
|
|
||||||
return <mesh geometry={geometry} material={material} frustumCulled />;
|
return <mesh geometry={geometry} material={material} frustumCulled={false} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Suspense, useCallback, useMemo, useRef, useState } from "react";
|
import { Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { useTerrainSurfaceData } from "@/hooks/world/useTerrainSurfaceData";
|
|
||||||
import {
|
import {
|
||||||
useDynamicGrass,
|
useDynamicGrass,
|
||||||
useGrassDensity,
|
useGrassDensity,
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
import type { TerrainSurfaceBounds } from "@/types/world/terrainSurface";
|
import type { TerrainSurfaceBounds } from "@/types/world/terrainSurface";
|
||||||
import { GRASS_CONFIG } from "@/world/grass/grassConfig";
|
import { GRASS_CONFIG } from "@/world/grass/grassConfig";
|
||||||
import { GrassPatch } from "@/world/grass/GrassPatch";
|
import { GrassPatch } from "@/world/grass/GrassPatch";
|
||||||
|
import { useTerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
|
||||||
|
|
||||||
interface GrassChunk {
|
interface GrassChunk {
|
||||||
centerX: number;
|
centerX: number;
|
||||||
@@ -52,7 +52,7 @@ function createGrassChunks(bounds: TerrainSurfaceBounds): GrassChunk[] {
|
|||||||
|
|
||||||
export function GrassSystem(): React.JSX.Element | null {
|
export function GrassSystem(): React.JSX.Element | null {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const terrainSurfaceData = useTerrainSurfaceData();
|
const terrainSampler = useTerrainGrassSampler();
|
||||||
const sceneMode = useSceneMode();
|
const sceneMode = useSceneMode();
|
||||||
const dynamicGrass = useDynamicGrass();
|
const dynamicGrass = useDynamicGrass();
|
||||||
const grassDensity = useGrassDensity();
|
const grassDensity = useGrassDensity();
|
||||||
@@ -62,9 +62,8 @@ export function GrassSystem(): React.JSX.Element | null {
|
|||||||
);
|
);
|
||||||
const density = Math.max(0, grassDensity);
|
const density = Math.max(0, grassDensity);
|
||||||
const chunks = useMemo(
|
const chunks = useMemo(
|
||||||
() =>
|
() => createGrassChunks(terrainSampler.bounds),
|
||||||
terrainSurfaceData ? createGrassChunks(terrainSurfaceData.bounds) : [],
|
[terrainSampler],
|
||||||
[terrainSurfaceData],
|
|
||||||
);
|
);
|
||||||
const streamingEnabled = sceneMode === "game";
|
const streamingEnabled = sceneMode === "game";
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ export function GrassSystem(): React.JSX.Element | null {
|
|||||||
!GRASS_CONFIG.enabled ||
|
!GRASS_CONFIG.enabled ||
|
||||||
!dynamicGrass ||
|
!dynamicGrass ||
|
||||||
density <= 0 ||
|
density <= 0 ||
|
||||||
!terrainSurfaceData
|
chunks.length === 0
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -138,7 +137,7 @@ export function GrassSystem(): React.JSX.Element | null {
|
|||||||
chunkX={chunk.x}
|
chunkX={chunk.x}
|
||||||
chunkZ={chunk.z}
|
chunkZ={chunk.z}
|
||||||
density={density}
|
density={density}
|
||||||
terrainSurfaceData={terrainSurfaceData}
|
terrainSampler={terrainSampler}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
|
||||||
|
|
||||||
export const GRASS_CONFIG = {
|
export const GRASS_CONFIG = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
chunkSize: 20,
|
chunkSize: 20,
|
||||||
loadRadius: 30,
|
loadRadius: 30,
|
||||||
unloadRadius: 34,
|
unloadRadius: 34,
|
||||||
updateInterval: 250,
|
updateInterval: 250,
|
||||||
sampleStep: 1.15,
|
baseBladesPerChunk: 2600,
|
||||||
jitter: 0.42,
|
bladeWidth: 0.08,
|
||||||
bladesPerCell: 2,
|
maxBladeHeight: 0.36,
|
||||||
maxBladesPerChunk: 720,
|
randomHeightAmount: 0.25,
|
||||||
bladeWidth: 0.12,
|
surfaceOffset: 0.025,
|
||||||
minBladeHeight: 0.42,
|
windNoiseScale: 0.9,
|
||||||
maxBladeHeight: 0.82,
|
baldPatchModifier: 2.5,
|
||||||
surfaceOffset: 0.06,
|
falloffSharpness: 0.35,
|
||||||
baseColor: "#1f3512",
|
heightNoiseFrequency: 12,
|
||||||
windBendStrength: 0.42,
|
heightNoiseAmplitude: 3,
|
||||||
windNoiseScale: 0.09,
|
maxBendAngle: 22,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const GRASS_SURFACE_KEYS = new Set([
|
export const GRASS_COLORS = ["#84C66B", "#67B058", "#A3CA5B"] as const;
|
||||||
"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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,40 +1,98 @@
|
|||||||
export const grassVertexShader = /* glsl */ `
|
export const grassVertexShader = /* glsl */ `
|
||||||
attribute vec3 aColor;
|
attribute vec3 aBladeColor;
|
||||||
|
attribute vec3 aBladeNormal;
|
||||||
attribute vec3 aBladeBase;
|
attribute vec3 aBladeBase;
|
||||||
attribute float aHeightFactor;
|
attribute float aSideFactor;
|
||||||
attribute float aWindPhase;
|
attribute float aTipFactor;
|
||||||
|
attribute float aRandom;
|
||||||
|
attribute vec3 aYaw;
|
||||||
|
|
||||||
varying vec3 vColor;
|
varying vec3 vColor;
|
||||||
|
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
|
uniform vec3 uPlayerPosition;
|
||||||
|
uniform float uPatchSize;
|
||||||
|
uniform float uBladeWidth;
|
||||||
uniform float uWindDirection;
|
uniform float uWindDirection;
|
||||||
uniform float uWindSpeed;
|
uniform float uWindSpeed;
|
||||||
uniform float uWindStrength;
|
|
||||||
uniform float uWindNoiseScale;
|
uniform float uWindNoiseScale;
|
||||||
uniform float uBendStrength;
|
uniform float uBaldPatchModifier;
|
||||||
|
uniform float uFalloffSharpness;
|
||||||
|
uniform float uHeightNoiseFrequency;
|
||||||
|
uniform float uHeightNoiseAmplitude;
|
||||||
|
uniform float uMaxBendAngle;
|
||||||
|
uniform float uMaxBladeHeight;
|
||||||
|
uniform float uRandomHeightAmount;
|
||||||
|
|
||||||
|
float random(vec2 st) {
|
||||||
|
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat3 rotate3d(in vec3 axis, const in float angle) {
|
||||||
|
axis = normalize(axis);
|
||||||
|
float s = sin(angle);
|
||||||
|
float c = cos(angle);
|
||||||
|
float oc = 1.0 - c;
|
||||||
|
return mat3(
|
||||||
|
oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s,
|
||||||
|
oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s,
|
||||||
|
oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float mapValue(float value, float inMin, float inMax, float outMin, float outMax) {
|
||||||
|
return mix(outMin, outMax, (value - inMin) / (inMax - inMin));
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 transformed = position;
|
vec3 origin = aBladeBase;
|
||||||
float topFactor = aHeightFactor * aHeightFactor;
|
vec3 transformed = origin;
|
||||||
vec2 windDirection = normalize(vec2(cos(uWindDirection), sin(uWindDirection)));
|
float halfPatchSize = uPatchSize * 0.5;
|
||||||
|
vec2 uv = vec2(origin.x, origin.z) * 0.05;
|
||||||
|
|
||||||
float primaryWind = sin(
|
float heightNoise =
|
||||||
uTime * max(uWindSpeed, 0.05) +
|
random(floor(uv * uHeightNoiseFrequency) + aRandom) *
|
||||||
aWindPhase +
|
uMaxBladeHeight *
|
||||||
aBladeBase.x * uWindNoiseScale +
|
uHeightNoiseAmplitude;
|
||||||
aBladeBase.z * uWindNoiseScale
|
float heightModifier = heightNoise + random(uv + aRandom) * (uRandomHeightAmount * 0.1);
|
||||||
|
|
||||||
|
float edgeDistanceX = abs(origin.x - uPlayerPosition.x) / halfPatchSize;
|
||||||
|
float edgeDistanceZ = abs(origin.z - uPlayerPosition.z) / halfPatchSize;
|
||||||
|
float edgeFactor = 1.0 - max(edgeDistanceX, edgeDistanceZ);
|
||||||
|
edgeFactor = pow(clamp(edgeFactor, 0.0, 1.0), uFalloffSharpness);
|
||||||
|
|
||||||
|
float baldPatch = random(floor(uv * 3.0)) * (uBaldPatchModifier * (1.0 - edgeFactor));
|
||||||
|
heightModifier = max(0.0, heightModifier - baldPatch);
|
||||||
|
|
||||||
|
float distanceFromCenter = length(origin.xz - uPlayerPosition.xz) / halfPatchSize;
|
||||||
|
float innerCircleFactor = clamp(smoothstep(0.0, 0.5, distanceFromCenter), 0.0, 1.0);
|
||||||
|
heightModifier *= mix(0.25, 1.0, innerCircleFactor);
|
||||||
|
|
||||||
|
vec3 tangent = normalize(aYaw - aBladeNormal * dot(aYaw, aBladeNormal));
|
||||||
|
transformed += tangent * (uBladeWidth * 0.5) * aSideFactor;
|
||||||
|
transformed += aBladeNormal * heightModifier * aTipFactor;
|
||||||
|
|
||||||
|
float noiseScale = uWindNoiseScale * 0.1;
|
||||||
|
vec2 noiseUV = vec2(origin.x * noiseScale, origin.z * noiseScale);
|
||||||
|
mat2 rotation = mat2(
|
||||||
|
cos(uWindDirection), -sin(uWindDirection),
|
||||||
|
sin(uWindDirection), cos(uWindDirection)
|
||||||
);
|
);
|
||||||
float secondaryWind = sin(
|
vec2 rotatedNoiseUV = rotation * noiseUV + uTime * vec2(uWindSpeed);
|
||||||
uTime * max(uWindSpeed, 0.05) * 1.73 +
|
float windA = random(floor(rotatedNoiseUV * 10.0));
|
||||||
aWindPhase * 0.71 +
|
float windB = random(floor(rotatedNoiseUV.yx * 10.0 + 17.0));
|
||||||
aBladeBase.x * uWindNoiseScale * 0.53 -
|
vec3 axis = normalize(vec3(windA, 0.0, windB));
|
||||||
aBladeBase.z * uWindNoiseScale * 0.89
|
float angle = radians(mapValue(windA + windB, 0.0, 2.0, -uMaxBendAngle, uMaxBendAngle)) * aTipFactor;
|
||||||
) * 0.35;
|
mat3 rotationMatrix = rotate3d(axis, angle);
|
||||||
|
|
||||||
float bend = (primaryWind + secondaryWind) * uWindStrength * uBendStrength * topFactor;
|
vec3 relativePosition = transformed - origin;
|
||||||
transformed.xz += windDirection * bend;
|
relativePosition = rotationMatrix * relativePosition;
|
||||||
|
transformed = origin + relativePosition;
|
||||||
|
|
||||||
vColor = aColor;
|
vec3 baseColor = aBladeColor * 0.45;
|
||||||
|
vec3 tipColor = aBladeColor;
|
||||||
|
float shadeNoise = random(floor((uv + uTime * 0.01) * 24.0));
|
||||||
|
vColor = mix(baseColor, tipColor, aTipFactor) * mix(0.72, 1.08, shadeNoise);
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useGLTF } from "@react-three/drei";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { TERRAIN_MODEL_PATH } from "@/data/world/terrainConfig";
|
||||||
|
import type { TerrainSurfaceBounds } from "@/types/world/terrainSurface";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
import { getMapNodesByName } from "@/utils/map/loadMapSceneData";
|
||||||
|
|
||||||
|
const RAYCAST_Y = 500;
|
||||||
|
const RAYCAST_FAR = 1000;
|
||||||
|
const DOWN = new THREE.Vector3(0, -1, 0);
|
||||||
|
const DEFAULT_TERRAIN_POSITION: Vector3Tuple = [0, 0, 0];
|
||||||
|
const DEFAULT_TERRAIN_ROTATION: Vector3Tuple = [0, 0, 0];
|
||||||
|
const DEFAULT_TERRAIN_SCALE: Vector3Tuple = [1, 1, 1];
|
||||||
|
|
||||||
|
export interface TerrainGrassSample {
|
||||||
|
normal: THREE.Vector3;
|
||||||
|
position: THREE.Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerrainGrassSampler {
|
||||||
|
bounds: TerrainSurfaceBounds;
|
||||||
|
sample: (x: number, z: number) => TerrainGrassSample | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFallbackBounds(): TerrainSurfaceBounds {
|
||||||
|
return {
|
||||||
|
minX: -120,
|
||||||
|
maxX: 120,
|
||||||
|
minZ: -120,
|
||||||
|
maxZ: 120,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTerrainMatrix(
|
||||||
|
position: Vector3Tuple,
|
||||||
|
rotation: Vector3Tuple,
|
||||||
|
scale: Vector3Tuple,
|
||||||
|
): THREE.Matrix4 {
|
||||||
|
return new THREE.Matrix4().compose(
|
||||||
|
new THREE.Vector3(...position),
|
||||||
|
new THREE.Quaternion().setFromEuler(new THREE.Euler(...rotation)),
|
||||||
|
new THREE.Vector3(...scale),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTerrainGrassSampler(
|
||||||
|
scene: THREE.Object3D,
|
||||||
|
position: Vector3Tuple,
|
||||||
|
rotation: Vector3Tuple,
|
||||||
|
scale: Vector3Tuple,
|
||||||
|
): TerrainGrassSampler {
|
||||||
|
const meshes: THREE.Mesh[] = [];
|
||||||
|
const terrainMatrix = createTerrainMatrix(position, rotation, scale);
|
||||||
|
const inverseTerrainMatrix = terrainMatrix.clone().invert();
|
||||||
|
const normalMatrix = new THREE.Matrix3().getNormalMatrix(terrainMatrix);
|
||||||
|
const raycaster = new THREE.Raycaster(
|
||||||
|
new THREE.Vector3(),
|
||||||
|
DOWN,
|
||||||
|
0,
|
||||||
|
RAYCAST_FAR,
|
||||||
|
);
|
||||||
|
|
||||||
|
scene.updateMatrixWorld(true);
|
||||||
|
scene.traverse((child) => {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
meshes.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const terrainBounds = new THREE.Box3().setFromObject(scene);
|
||||||
|
if (!terrainBounds.isEmpty()) {
|
||||||
|
terrainBounds.applyMatrix4(terrainMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = terrainBounds.isEmpty()
|
||||||
|
? createFallbackBounds()
|
||||||
|
: {
|
||||||
|
minX: terrainBounds.min.x,
|
||||||
|
maxX: terrainBounds.max.x,
|
||||||
|
minZ: terrainBounds.min.z,
|
||||||
|
maxZ: terrainBounds.max.z,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
bounds,
|
||||||
|
sample: (x, z) => {
|
||||||
|
const localOrigin = new THREE.Vector3(x, RAYCAST_Y, z).applyMatrix4(
|
||||||
|
inverseTerrainMatrix,
|
||||||
|
);
|
||||||
|
const localDirection =
|
||||||
|
DOWN.clone().transformDirection(inverseTerrainMatrix);
|
||||||
|
|
||||||
|
raycaster.set(localOrigin, localDirection);
|
||||||
|
const hit = raycaster.intersectObjects(meshes, false)[0];
|
||||||
|
if (!hit) return null;
|
||||||
|
|
||||||
|
const normal = hit.face?.normal
|
||||||
|
.clone()
|
||||||
|
.transformDirection(hit.object.matrixWorld)
|
||||||
|
.applyMatrix3(normalMatrix)
|
||||||
|
.normalize();
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: hit.point.clone().applyMatrix4(terrainMatrix),
|
||||||
|
normal: normal ?? new THREE.Vector3(0, 1, 0),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTerrainGrassSampler(): TerrainGrassSampler {
|
||||||
|
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
||||||
|
const terrainNode = getMapNodesByName("terrain")[0];
|
||||||
|
const position = terrainNode?.position ?? DEFAULT_TERRAIN_POSITION;
|
||||||
|
const rotation = terrainNode?.rotation ?? DEFAULT_TERRAIN_ROTATION;
|
||||||
|
const scale = terrainNode?.scale ?? DEFAULT_TERRAIN_SCALE;
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => createTerrainGrassSampler(scene, position, rotation, scale),
|
||||||
|
[position, rotation, scale, scene],
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user