refactor(environment): use player-centered ghibli grass patch
This commit is contained in:
Binary file not shown.
Binary file not shown.
+150
-144
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useTexture } from "@react-three/drei";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import { useWind } from "@/hooks/world/useWind";
|
||||
@@ -10,8 +11,6 @@ import {
|
||||
import type { TerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
|
||||
|
||||
interface GrassPatchProps {
|
||||
chunkX: number;
|
||||
chunkZ: number;
|
||||
density: number;
|
||||
terrainSampler: TerrainGrassSampler;
|
||||
}
|
||||
@@ -21,15 +20,126 @@ function random01(seed: number): number {
|
||||
return value - Math.floor(value);
|
||||
}
|
||||
|
||||
function createGrassMaterial(): THREE.ShaderMaterial {
|
||||
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 createGrassGeometry(density: number): THREE.BufferGeometry {
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
const uvs: number[] = [];
|
||||
const bladeOrigins: number[] = [];
|
||||
const yaws: number[] = [];
|
||||
const bladeCount = Math.round(GRASS_CONFIG.bladeCount * density);
|
||||
const halfPatchSize = GRASS_CONFIG.patchSize * 0.5;
|
||||
|
||||
for (let index = 0; index < bladeCount; index++) {
|
||||
const seed = index * 997;
|
||||
const origin = new THREE.Vector3(
|
||||
random01(seed + 1) * GRASS_CONFIG.patchSize - halfPatchSize,
|
||||
0,
|
||||
random01(seed + 2) * GRASS_CONFIG.patchSize - halfPatchSize,
|
||||
);
|
||||
const yawAngle = random01(seed + 3) * Math.PI * 2;
|
||||
const yaw = new THREE.Vector3(Math.sin(yawAngle), 0, -Math.cos(yawAngle));
|
||||
const colorIndex = Math.floor(random01(seed + 4) * GRASS_COLORS.length);
|
||||
const color = new THREE.Color(GRASS_COLORS[colorIndex] ?? GRASS_COLORS[0]);
|
||||
const markerColors = [
|
||||
new THREE.Color(0.1, 0, 0),
|
||||
new THREE.Color(0, 0, 0.1),
|
||||
new THREE.Color(1, 1, 1),
|
||||
] as const;
|
||||
const uv = new THREE.Vector2(
|
||||
origin.x / GRASS_CONFIG.patchSize + 0.5,
|
||||
origin.z / GRASS_CONFIG.patchSize + 0.5,
|
||||
);
|
||||
|
||||
for (let vertexIndex = 0; vertexIndex < 3; vertexIndex++) {
|
||||
pushVector(positions, origin);
|
||||
pushColor(colors, markerColors[vertexIndex] ?? markerColors[2]);
|
||||
pushVector(bladeOrigins, origin);
|
||||
pushVector(yaws, yaw);
|
||||
pushColor(colors, color);
|
||||
uvs.push(uv.x, uv.y);
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const markerColorValues: number[] = [];
|
||||
const bladeColorValues: number[] = [];
|
||||
|
||||
for (let index = 0; index < colors.length; index += 6) {
|
||||
markerColorValues.push(
|
||||
colors[index] ?? 0,
|
||||
colors[index + 1] ?? 0,
|
||||
colors[index + 2] ?? 0,
|
||||
);
|
||||
bladeColorValues.push(
|
||||
colors[index + 3] ?? 0,
|
||||
colors[index + 4] ?? 0,
|
||||
colors[index + 5] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(positions, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"color",
|
||||
new THREE.Float32BufferAttribute(markerColorValues, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aBladeColor",
|
||||
new THREE.Float32BufferAttribute(bladeColorValues, 3),
|
||||
);
|
||||
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
|
||||
geometry.setAttribute(
|
||||
"aBladeOrigin",
|
||||
new THREE.Float32BufferAttribute(bladeOrigins, 3),
|
||||
);
|
||||
geometry.setAttribute("aYaw", new THREE.Float32BufferAttribute(yaws, 3));
|
||||
geometry.computeVertexNormals();
|
||||
geometry.computeBoundingSphere();
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function createGrassMaterial(
|
||||
terrainSampler: TerrainGrassSampler,
|
||||
noiseTexture: THREE.Texture,
|
||||
grassTexture: THREE.Texture,
|
||||
): THREE.ShaderMaterial {
|
||||
return new THREE.ShaderMaterial({
|
||||
side: THREE.DoubleSide,
|
||||
vertexShader: grassVertexShader,
|
||||
fragmentShader: grassFragmentShader,
|
||||
vertexColors: true,
|
||||
side: THREE.DoubleSide,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uNoiseTexture: { value: noiseTexture },
|
||||
uDiffuseMap: { value: grassTexture },
|
||||
uHeightMap: { value: terrainSampler.heightTexture },
|
||||
uPlayerPosition: { value: new THREE.Vector3() },
|
||||
uPatchSize: { value: GRASS_CONFIG.chunkSize },
|
||||
uBoundingBoxMin: {
|
||||
value: new THREE.Vector3(
|
||||
terrainSampler.bounds.minX,
|
||||
terrainSampler.minHeight,
|
||||
terrainSampler.bounds.minZ,
|
||||
),
|
||||
},
|
||||
uBoundingBoxMax: {
|
||||
value: new THREE.Vector3(
|
||||
terrainSampler.bounds.maxX,
|
||||
terrainSampler.maxHeight,
|
||||
terrainSampler.bounds.maxZ,
|
||||
),
|
||||
},
|
||||
uPatchSize: { value: GRASS_CONFIG.patchSize },
|
||||
uBladeWidth: { value: GRASS_CONFIG.bladeWidth },
|
||||
uWindDirection: { value: 0 },
|
||||
uWindSpeed: { value: 0 },
|
||||
@@ -41,153 +151,51 @@ function createGrassMaterial(): THREE.ShaderMaterial {
|
||||
uMaxBendAngle: { value: GRASS_CONFIG.maxBendAngle },
|
||||
uMaxBladeHeight: { value: GRASS_CONFIG.maxBladeHeight },
|
||||
uRandomHeightAmount: { value: GRASS_CONFIG.randomHeightAmount },
|
||||
uSurfaceOffset: { value: GRASS_CONFIG.surfaceOffset },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
positions: number[],
|
||||
bladeColors: number[],
|
||||
bladeBases: number[],
|
||||
bladeNormals: number[],
|
||||
sideFactors: number[],
|
||||
tipFactors: number[],
|
||||
randoms: number[],
|
||||
yaws: number[],
|
||||
basePosition: THREE.Vector3,
|
||||
normal: THREE.Vector3,
|
||||
color: THREE.Color,
|
||||
yaw: THREE.Vector3,
|
||||
random: number,
|
||||
): void {
|
||||
const vertices = [
|
||||
{ side: 1, tip: 0 },
|
||||
{ side: -1, tip: 0 },
|
||||
{ side: 0, tip: 1 },
|
||||
];
|
||||
|
||||
for (const vertex of vertices) {
|
||||
pushVector(positions, basePosition);
|
||||
pushColor(bladeColors, color);
|
||||
pushVector(bladeBases, basePosition);
|
||||
pushVector(bladeNormals, normal);
|
||||
pushVector(yaws, yaw);
|
||||
sideFactors.push(vertex.side);
|
||||
tipFactors.push(vertex.tip);
|
||||
randoms.push(random);
|
||||
}
|
||||
}
|
||||
|
||||
function createGrassGeometry(
|
||||
chunkX: number,
|
||||
chunkZ: number,
|
||||
density: number,
|
||||
terrainSampler: TerrainGrassSampler,
|
||||
): THREE.BufferGeometry | null {
|
||||
const positions: number[] = [];
|
||||
const bladeColors: number[] = [];
|
||||
const bladeBases: number[] = [];
|
||||
const bladeNormals: number[] = [];
|
||||
const sideFactors: number[] = [];
|
||||
const tipFactors: number[] = [];
|
||||
const randoms: number[] = [];
|
||||
const yaws: number[] = [];
|
||||
const startX = chunkX * GRASS_CONFIG.chunkSize;
|
||||
const startZ = chunkZ * GRASS_CONFIG.chunkSize;
|
||||
const bladeCount = Math.round(GRASS_CONFIG.baseBladesPerChunk * density);
|
||||
|
||||
for (let index = 0; index < bladeCount; index++) {
|
||||
const seed = (chunkX + 101) * 92821 + (chunkZ + 103) * 68917 + index * 997;
|
||||
const x = startX + random01(seed + 1) * GRASS_CONFIG.chunkSize;
|
||||
const z = startZ + random01(seed + 2) * GRASS_CONFIG.chunkSize;
|
||||
const sample = terrainSampler.sample(x, z);
|
||||
if (!sample) continue;
|
||||
|
||||
const colorIndex = Math.floor(random01(seed + 3) * GRASS_COLORS.length);
|
||||
const color = new THREE.Color(GRASS_COLORS[colorIndex] ?? GRASS_COLORS[0]);
|
||||
const yawAngle = random01(seed + 4) * Math.PI * 2;
|
||||
const yaw = new THREE.Vector3(Math.sin(yawAngle), 0, -Math.cos(yawAngle));
|
||||
const basePosition = sample.position
|
||||
.clone()
|
||||
.addScaledVector(sample.normal, GRASS_CONFIG.surfaceOffset);
|
||||
|
||||
addGrassBlade(
|
||||
positions,
|
||||
bladeColors,
|
||||
bladeBases,
|
||||
bladeNormals,
|
||||
sideFactors,
|
||||
tipFactors,
|
||||
randoms,
|
||||
yaws,
|
||||
basePosition,
|
||||
sample.normal,
|
||||
color,
|
||||
yaw,
|
||||
random01(seed + 5),
|
||||
);
|
||||
}
|
||||
|
||||
if (positions.length === 0) return null;
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(positions, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aBladeColor",
|
||||
new THREE.Float32BufferAttribute(bladeColors, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aBladeBase",
|
||||
new THREE.Float32BufferAttribute(bladeBases, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aBladeNormal",
|
||||
new THREE.Float32BufferAttribute(bladeNormals, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aSideFactor",
|
||||
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.computeBoundingSphere();
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
export function GrassPatch({
|
||||
chunkX,
|
||||
chunkZ,
|
||||
density,
|
||||
terrainSampler,
|
||||
}: GrassPatchProps): React.JSX.Element | null {
|
||||
}: GrassPatchProps): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const wind = useWind();
|
||||
const [noiseTexture, grassTexture] = useTexture([
|
||||
"/textures/grass/noise.png",
|
||||
"/textures/grass/grass.jpg",
|
||||
]) as [THREE.Texture, THREE.Texture];
|
||||
const grassTextures = useMemo(() => {
|
||||
const noise = noiseTexture.clone();
|
||||
const grass = grassTexture.clone();
|
||||
|
||||
noise.wrapS = noise.wrapT = THREE.RepeatWrapping;
|
||||
grass.wrapS = grass.wrapT = THREE.MirroredRepeatWrapping;
|
||||
noise.needsUpdate = true;
|
||||
grass.needsUpdate = true;
|
||||
|
||||
return { grass, noise };
|
||||
}, [grassTexture, noiseTexture]);
|
||||
const materialRef = useRef<THREE.ShaderMaterial | null>(null);
|
||||
const geometry = useMemo(
|
||||
() => createGrassGeometry(chunkX, chunkZ, density, terrainSampler),
|
||||
[chunkX, chunkZ, density, terrainSampler],
|
||||
const geometry = useMemo(() => createGrassGeometry(density), [density]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
grassTextures.grass.dispose();
|
||||
grassTextures.noise.dispose();
|
||||
};
|
||||
}, [grassTextures]);
|
||||
|
||||
const material = useMemo(
|
||||
() =>
|
||||
createGrassMaterial(
|
||||
terrainSampler,
|
||||
grassTextures.noise,
|
||||
grassTextures.grass,
|
||||
),
|
||||
[grassTextures, terrainSampler],
|
||||
);
|
||||
const material = useMemo(() => createGrassMaterial(), []);
|
||||
|
||||
useEffect(() => {
|
||||
materialRef.current = material;
|
||||
@@ -199,7 +207,7 @@ export function GrassPatch({
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
geometry?.dispose();
|
||||
geometry.dispose();
|
||||
};
|
||||
}, [geometry]);
|
||||
|
||||
@@ -220,7 +228,5 @@ export function GrassPatch({
|
||||
}
|
||||
});
|
||||
|
||||
if (!geometry) return null;
|
||||
|
||||
return <mesh geometry={geometry} material={material} frustumCulled={false} />;
|
||||
}
|
||||
|
||||
@@ -1,146 +1,25 @@
|
||||
import { Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
import { Suspense } from "react";
|
||||
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";
|
||||
import { useTerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
|
||||
|
||||
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 terrainSampler = useTerrainGrassSampler();
|
||||
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(
|
||||
() => createGrassChunks(terrainSampler.bounds),
|
||||
[terrainSampler],
|
||||
);
|
||||
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 ||
|
||||
chunks.length === 0
|
||||
) {
|
||||
if (!GRASS_CONFIG.enabled || !dynamicGrass || density <= 0) {
|
||||
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}
|
||||
terrainSampler={terrainSampler}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
</group>
|
||||
<Suspense fallback={null}>
|
||||
<GrassPatch density={density} terrainSampler={terrainSampler} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
export const GRASS_CONFIG = {
|
||||
enabled: true,
|
||||
chunkSize: 20,
|
||||
loadRadius: 30,
|
||||
unloadRadius: 34,
|
||||
updateInterval: 250,
|
||||
baseBladesPerChunk: 420,
|
||||
patchSize: 30,
|
||||
bladeCount: 18000,
|
||||
bladeWidth: 0.08,
|
||||
maxBladeHeight: 0.36,
|
||||
randomHeightAmount: 0.25,
|
||||
surfaceOffset: 0.025,
|
||||
heightTextureSize: 128,
|
||||
windNoiseScale: 0.9,
|
||||
baldPatchModifier: 2.5,
|
||||
falloffSharpness: 0.35,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
export const grassVertexShader = /* glsl */ `
|
||||
attribute vec3 aBladeColor;
|
||||
attribute vec3 aBladeNormal;
|
||||
attribute vec3 aBladeBase;
|
||||
attribute float aSideFactor;
|
||||
attribute float aTipFactor;
|
||||
attribute float aRandom;
|
||||
attribute vec3 aYaw;
|
||||
attribute vec3 aBladeOrigin;
|
||||
attribute vec3 aBladeColor;
|
||||
|
||||
varying vec3 vColor;
|
||||
|
||||
uniform float uTime;
|
||||
uniform vec3 uPlayerPosition;
|
||||
uniform sampler2D uHeightMap;
|
||||
uniform sampler2D uDiffuseMap;
|
||||
uniform sampler2D uNoiseTexture;
|
||||
uniform vec3 uBoundingBoxMin;
|
||||
uniform vec3 uBoundingBoxMax;
|
||||
uniform float uPatchSize;
|
||||
uniform float uBladeWidth;
|
||||
uniform float uWindDirection;
|
||||
@@ -23,6 +24,7 @@ export const grassVertexShader = /* glsl */ `
|
||||
uniform float uMaxBendAngle;
|
||||
uniform float uMaxBladeHeight;
|
||||
uniform float uRandomHeightAmount;
|
||||
uniform float uSurfaceOffset;
|
||||
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
@@ -45,33 +47,59 @@ export const grassVertexShader = /* glsl */ `
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 origin = aBladeBase;
|
||||
vec3 transformed = origin;
|
||||
vec3 transformed = position;
|
||||
vec3 origin = aBladeOrigin;
|
||||
float halfPatchSize = uPatchSize * 0.5;
|
||||
vec2 uv = vec2(origin.x, origin.z) * 0.05;
|
||||
|
||||
float heightNoise =
|
||||
random(floor(uv * uHeightNoiseFrequency) + aRandom) *
|
||||
uMaxBladeHeight *
|
||||
uHeightNoiseAmplitude;
|
||||
float heightModifier = heightNoise + random(uv + aRandom) * (uRandomHeightAmount * 0.1);
|
||||
origin.x = mod(origin.x - uPlayerPosition.x + halfPatchSize, uPatchSize) - halfPatchSize;
|
||||
origin.z = mod(origin.z - uPlayerPosition.z + halfPatchSize, uPatchSize) - halfPatchSize;
|
||||
|
||||
float edgeDistanceX = abs(origin.x - uPlayerPosition.x) / halfPatchSize;
|
||||
float edgeDistanceZ = abs(origin.z - uPlayerPosition.z) / halfPatchSize;
|
||||
vec3 worldPos = vec3(uPlayerPosition.x + origin.x, 0.0, uPlayerPosition.z + origin.z);
|
||||
transformed.x = worldPos.x;
|
||||
transformed.z = worldPos.z;
|
||||
|
||||
vec2 terrainUv = vec2(
|
||||
mapValue(worldPos.x, uBoundingBoxMin.x, uBoundingBoxMax.x, 0.0, 1.0),
|
||||
mapValue(worldPos.z, uBoundingBoxMin.z, uBoundingBoxMax.z, 0.0, 1.0)
|
||||
);
|
||||
terrainUv = clamp(terrainUv, 0.0, 1.0);
|
||||
|
||||
float terrainHeightRatio = texture2D(uHeightMap, terrainUv).r;
|
||||
float terrainHeight = mix(uBoundingBoxMin.y, uBoundingBoxMax.y, terrainHeightRatio);
|
||||
transformed.y = terrainHeight + uSurfaceOffset;
|
||||
|
||||
vec3 heightNoise = texture2D(uNoiseTexture, terrainUv.yx * vec2(uHeightNoiseFrequency)).rgb;
|
||||
float heightModifier = ((heightNoise.r + heightNoise.g + heightNoise.b) * uMaxBladeHeight) * uHeightNoiseAmplitude;
|
||||
heightModifier += random(terrainUv) * (uRandomHeightAmount * 0.1);
|
||||
|
||||
float edgeDistanceX = abs(origin.x) / halfPatchSize;
|
||||
float edgeDistanceZ = abs(origin.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 baldPatchOffset = heightNoise.r * (uBaldPatchModifier * (1.0 - edgeFactor));
|
||||
heightModifier -= baldPatchOffset;
|
||||
|
||||
float distanceFromCenter = length(origin.xz - uPlayerPosition.xz) / halfPatchSize;
|
||||
float edgeFade =
|
||||
smoothstep(uBoundingBoxMin.x, uBoundingBoxMin.x + 2.0, worldPos.x) *
|
||||
smoothstep(uBoundingBoxMax.x, uBoundingBoxMax.x - 2.0, worldPos.x) *
|
||||
smoothstep(uBoundingBoxMin.z, uBoundingBoxMin.z + 2.0, worldPos.z) *
|
||||
smoothstep(uBoundingBoxMax.z, uBoundingBoxMax.z - 2.0, worldPos.z);
|
||||
heightModifier *= edgeFade;
|
||||
|
||||
float sideFactor = (color.r == 0.1) ? 1.0 : (color.b == 0.1) ? -1.0 : 0.0;
|
||||
float tipFactor = color.g;
|
||||
float width = smoothstep(0.5, 1.0, heightModifier * 2.0) * uBladeWidth;
|
||||
transformed += aYaw * (width / 2.0) * sideFactor;
|
||||
|
||||
vec3 textureColor = texture2D(uDiffuseMap, terrainUv * 10.0).rgb;
|
||||
vec3 colorNoise = texture2D(uNoiseTexture, terrainUv.yx * vec2(uHeightNoiseFrequency) + (uTime * 0.1)).rgb;
|
||||
vColor = mix(aBladeColor * 0.55, aBladeColor, tipFactor) * textureColor * mix(vec3(0.75), vec3(1.15), colorNoise);
|
||||
|
||||
float distanceFromCenter = length(origin.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(
|
||||
@@ -79,20 +107,18 @@ export const grassVertexShader = /* glsl */ `
|
||||
sin(uWindDirection), cos(uWindDirection)
|
||||
);
|
||||
vec2 rotatedNoiseUV = rotation * noiseUV + uTime * vec2(uWindSpeed);
|
||||
float windA = random(floor(rotatedNoiseUV * 10.0));
|
||||
float windB = random(floor(rotatedNoiseUV.yx * 10.0 + 17.0));
|
||||
vec3 axis = normalize(vec3(windA, 0.0, windB));
|
||||
float angle = radians(mapValue(windA + windB, 0.0, 2.0, -uMaxBendAngle, uMaxBendAngle)) * aTipFactor;
|
||||
vec3 windNoise = texture2D(uNoiseTexture, rotatedNoiseUV).rgb;
|
||||
|
||||
vec3 axis = vec3(windNoise.g, 0.0, windNoise.b);
|
||||
float angle = radians(mapValue(windNoise.g + windNoise.b, 0.0, 2.0, -uMaxBendAngle, uMaxBendAngle)) * tipFactor;
|
||||
mat3 rotationMatrix = rotate3d(axis, angle);
|
||||
|
||||
vec3 relativePosition = transformed - origin;
|
||||
vec3 basePosition = vec3(transformed.x, transformed.y - heightModifier, transformed.z);
|
||||
vec3 relativePosition = transformed - basePosition;
|
||||
relativePosition = rotationMatrix * relativePosition;
|
||||
transformed = origin + relativePosition;
|
||||
transformed = basePosition + relativePosition;
|
||||
transformed.y += heightModifier * tipFactor;
|
||||
|
||||
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);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -5,6 +5,7 @@ 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";
|
||||
import { GRASS_CONFIG } from "@/world/grass/grassConfig";
|
||||
|
||||
const RAYCAST_Y = 500;
|
||||
const RAYCAST_FAR = 1000;
|
||||
@@ -20,6 +21,9 @@ export interface TerrainGrassSample {
|
||||
|
||||
export interface TerrainGrassSampler {
|
||||
bounds: TerrainSurfaceBounds;
|
||||
heightTexture: THREE.DataTexture;
|
||||
maxHeight: number;
|
||||
minHeight: number;
|
||||
sample: (x: number, z: number) => TerrainGrassSample | null;
|
||||
}
|
||||
|
||||
@@ -82,33 +86,98 @@ function createTerrainGrassSampler(
|
||||
maxZ: terrainBounds.max.z,
|
||||
};
|
||||
|
||||
const sample = (x: number, z: number): TerrainGrassSample | null => {
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
const { heightTexture, maxHeight, minHeight } = createTerrainHeightTexture(
|
||||
bounds,
|
||||
sample,
|
||||
);
|
||||
|
||||
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),
|
||||
};
|
||||
},
|
||||
heightTexture,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
sample,
|
||||
};
|
||||
}
|
||||
|
||||
function createTerrainHeightTexture(
|
||||
bounds: TerrainSurfaceBounds,
|
||||
sample: (x: number, z: number) => TerrainGrassSample | null,
|
||||
): { heightTexture: THREE.DataTexture; maxHeight: number; minHeight: number } {
|
||||
const size = GRASS_CONFIG.heightTextureSize;
|
||||
const heights = new Float32Array(size * size);
|
||||
let minHeight = Number.POSITIVE_INFINITY;
|
||||
let maxHeight = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (let zIndex = 0; zIndex < size; zIndex++) {
|
||||
for (let xIndex = 0; xIndex < size; xIndex++) {
|
||||
const xRatio = size <= 1 ? 0 : xIndex / (size - 1);
|
||||
const zRatio = size <= 1 ? 0 : zIndex / (size - 1);
|
||||
const x = bounds.minX + (bounds.maxX - bounds.minX) * xRatio;
|
||||
const z = bounds.minZ + (bounds.maxZ - bounds.minZ) * zRatio;
|
||||
const terrainSample = sample(x, z);
|
||||
const height = terrainSample?.position.y ?? 0;
|
||||
const index = zIndex * size + xIndex;
|
||||
|
||||
heights[index] = height;
|
||||
minHeight = Math.min(minHeight, height);
|
||||
maxHeight = Math.max(maxHeight, height);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(minHeight) || !Number.isFinite(maxHeight)) {
|
||||
minHeight = 0;
|
||||
maxHeight = 1;
|
||||
}
|
||||
|
||||
const range = Math.max(maxHeight - minHeight, 0.0001);
|
||||
const data = new Uint8Array(size * size);
|
||||
|
||||
for (let index = 0; index < heights.length; index++) {
|
||||
data[index] = Math.round(
|
||||
(((heights[index] ?? minHeight) - minHeight) / range) * 255,
|
||||
);
|
||||
}
|
||||
|
||||
const heightTexture = new THREE.DataTexture(
|
||||
data,
|
||||
size,
|
||||
size,
|
||||
THREE.RedFormat,
|
||||
THREE.UnsignedByteType,
|
||||
);
|
||||
heightTexture.magFilter = THREE.LinearFilter;
|
||||
heightTexture.minFilter = THREE.LinearFilter;
|
||||
heightTexture.wrapS = THREE.ClampToEdgeWrapping;
|
||||
heightTexture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
heightTexture.needsUpdate = true;
|
||||
|
||||
return { heightTexture, maxHeight, minHeight };
|
||||
}
|
||||
|
||||
export function useTerrainGrassSampler(): TerrainGrassSampler {
|
||||
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
||||
const terrainNode = getMapNodesByName("terrain")[0];
|
||||
|
||||
Reference in New Issue
Block a user