refactor(environment): replace grass with terrain-mesh ghibli shader
This commit is contained in:
+107
-161
@@ -1,32 +1,19 @@
|
||||
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 { 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 { GRASS_COLORS, GRASS_CONFIG } from "@/world/grass/grassConfig";
|
||||
import {
|
||||
grassFragmentShader,
|
||||
grassVertexShader,
|
||||
} from "@/world/grass/grassShaders";
|
||||
import type { TerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
|
||||
|
||||
interface GrassPatchProps {
|
||||
chunkX: number;
|
||||
chunkZ: number;
|
||||
density: number;
|
||||
terrainSurfaceData: TerrainSurfaceData;
|
||||
}
|
||||
|
||||
interface GrassBladeVertexData {
|
||||
color: number[];
|
||||
heightFactor: number;
|
||||
position: number[];
|
||||
terrainSampler: TerrainGrassSampler;
|
||||
}
|
||||
|
||||
function random01(seed: number): number {
|
||||
@@ -34,81 +21,68 @@ function random01(seed: number): number {
|
||||
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 },
|
||||
uPlayerPosition: { value: new THREE.Vector3() },
|
||||
uPatchSize: { value: GRASS_CONFIG.chunkSize },
|
||||
uBladeWidth: { value: GRASS_CONFIG.bladeWidth },
|
||||
uWindDirection: { value: 0 },
|
||||
uWindSpeed: { value: 0 },
|
||||
uWindStrength: { value: 0 },
|
||||
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(
|
||||
positions: number[],
|
||||
colors: number[],
|
||||
bladeColors: number[],
|
||||
bladeBases: number[],
|
||||
heightFactors: number[],
|
||||
windPhases: number[],
|
||||
bladeNormals: number[],
|
||||
sideFactors: number[],
|
||||
tipFactors: number[],
|
||||
randoms: number[],
|
||||
yaws: number[],
|
||||
basePosition: THREE.Vector3,
|
||||
yaw: number,
|
||||
width: number,
|
||||
height: number,
|
||||
baseColor: THREE.Color,
|
||||
tipColor: THREE.Color,
|
||||
windPhase: number,
|
||||
normal: THREE.Vector3,
|
||||
color: THREE.Color,
|
||||
yaw: THREE.Vector3,
|
||||
random: 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,
|
||||
},
|
||||
const vertices = [
|
||||
{ side: 1, tip: 0 },
|
||||
{ side: -1, tip: 0 },
|
||||
{ side: 0, tip: 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,107 +90,84 @@ function createGrassGeometry(
|
||||
chunkX: number,
|
||||
chunkZ: number,
|
||||
density: number,
|
||||
terrainSurfaceData: TerrainSurfaceData,
|
||||
getHeight: (x: number, z: number) => number | null,
|
||||
terrainSampler: TerrainGrassSampler,
|
||||
): THREE.BufferGeometry | null {
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
const bladeColors: number[] = [];
|
||||
const bladeBases: number[] = [];
|
||||
const heightFactors: number[] = [];
|
||||
const windPhases: number[] = [];
|
||||
const baseColor = new THREE.Color(GRASS_CONFIG.baseColor);
|
||||
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 endX = startX + GRASS_CONFIG.chunkSize;
|
||||
const endZ = startZ + GRASS_CONFIG.chunkSize;
|
||||
const bladeBudget = Math.round(GRASS_CONFIG.maxBladesPerChunk * density);
|
||||
let bladeCount = 0;
|
||||
const bladeCount = Math.round(GRASS_CONFIG.baseBladesPerChunk * density);
|
||||
|
||||
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;
|
||||
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 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 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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
addGrassBlade(
|
||||
positions,
|
||||
bladeColors,
|
||||
bladeBases,
|
||||
bladeNormals,
|
||||
sideFactors,
|
||||
tipFactors,
|
||||
randoms,
|
||||
yaws,
|
||||
basePosition,
|
||||
sample.normal,
|
||||
color,
|
||||
yaw,
|
||||
random01(seed + 5),
|
||||
);
|
||||
}
|
||||
|
||||
if (bladeCount === 0) return null;
|
||||
if (positions.length === 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(
|
||||
"aBladeColor",
|
||||
new THREE.Float32BufferAttribute(bladeColors, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aBladeBase",
|
||||
new THREE.Float32BufferAttribute(bladeBases, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aHeightFactor",
|
||||
new THREE.Float32BufferAttribute(heightFactors, 1),
|
||||
"aBladeNormal",
|
||||
new THREE.Float32BufferAttribute(bladeNormals, 3),
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"aWindPhase",
|
||||
new THREE.Float32BufferAttribute(windPhases, 1),
|
||||
"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();
|
||||
|
||||
@@ -227,21 +178,14 @@ export function GrassPatch({
|
||||
chunkX,
|
||||
chunkZ,
|
||||
density,
|
||||
terrainSurfaceData,
|
||||
terrainSampler,
|
||||
}: GrassPatchProps): React.JSX.Element | null {
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const camera = useThree((state) => state.camera);
|
||||
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],
|
||||
() => createGrassGeometry(chunkX, chunkZ, density, terrainSampler),
|
||||
[chunkX, chunkZ, density, terrainSampler],
|
||||
);
|
||||
const material = useMemo(() => createGrassMaterial(), []);
|
||||
|
||||
@@ -265,9 +209,11 @@ export function GrassPatch({
|
||||
|
||||
const uniforms = currentMaterial.uniforms;
|
||||
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.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;
|
||||
@@ -276,5 +222,5 @@ export function GrassPatch({
|
||||
|
||||
if (!geometry) return null;
|
||||
|
||||
return <mesh geometry={geometry} material={material} frustumCulled />;
|
||||
return <mesh geometry={geometry} material={material} frustumCulled={false} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user