Files
La-Fabrik/src/world/grass/GrassPatch.tsx
T
Tom Boullay 27b4a2c392
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
upatde(fabrik): zone + herbe
2026-06-01 00:14:39 +02:00

266 lines
9.3 KiB
TypeScript

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";
import {
GRASS_BASE_COLOR,
GRASS_COLORS,
GRASS_CONFIG,
} from "@/data/world/grassConfig";
import {
LA_FABRIK_CENTER,
LA_FABRIK_HALF_EXTENTS,
LA_FABRIK_ROTATION_Y,
} from "@/data/world/laFabrikConfig";
import {
grassFragmentShader,
grassVertexShader,
} from "@/world/grass/grassShaders";
import type { TerrainGrassSampler } from "@/world/grass/useTerrainGrassSampler";
interface GrassPatchProps {
density: number;
terrainSampler: TerrainGrassSampler;
}
function random01(seed: number): number {
const value = Math.sin(seed * 12.9898) * 43758.5453;
return value - Math.floor(value);
}
const GRASS_COLOR_VALUES = GRASS_COLORS.map((color) => new THREE.Color(color));
const MARKER_COLOR_VALUES = [0.1, 0, 0, 0, 0, 0.1, 1, 1, 1] as const;
function createGrassGeometry(density: number): THREE.BufferGeometry {
const bladeCount = Math.round(GRASS_CONFIG.bladeCount * density);
const vertexCount = bladeCount * 3;
const positions = new Float32Array(vertexCount * 3);
const markerColorValues = new Float32Array(vertexCount * 3);
const bladeColorValues = new Float32Array(vertexCount * 3);
const uvs = new Float32Array(vertexCount * 2);
const bladeOrigins = new Float32Array(vertexCount * 3);
const yaws = new Float32Array(vertexCount * 3);
const halfPatchSize = GRASS_CONFIG.patchSize * 0.5;
for (let index = 0; index < bladeCount; index++) {
const seed = index * 997;
const originX = random01(seed + 1) * GRASS_CONFIG.patchSize - halfPatchSize;
const originY = 0;
const originZ = random01(seed + 2) * GRASS_CONFIG.patchSize - halfPatchSize;
const yawAngle = random01(seed + 3) * Math.PI * 2;
const yawX = Math.sin(yawAngle);
const yawY = 0;
const yawZ = -Math.cos(yawAngle);
const colorIndex = Math.floor(random01(seed + 4) * GRASS_COLORS.length);
const color = GRASS_COLOR_VALUES[colorIndex] ?? GRASS_COLOR_VALUES[0];
const uvX = originX / GRASS_CONFIG.patchSize + 0.5;
const uvY = originZ / GRASS_CONFIG.patchSize + 0.5;
for (let vertexIndex = 0; vertexIndex < 3; vertexIndex++) {
const vertexOffset = index * 3 + vertexIndex;
const vectorOffset = vertexOffset * 3;
const uvOffset = vertexOffset * 2;
const markerOffset = vertexIndex * 3;
positions[vectorOffset] = originX;
positions[vectorOffset + 1] = originY;
positions[vectorOffset + 2] = originZ;
markerColorValues[vectorOffset] = MARKER_COLOR_VALUES[markerOffset] ?? 1;
markerColorValues[vectorOffset + 1] =
MARKER_COLOR_VALUES[markerOffset + 1] ?? 1;
markerColorValues[vectorOffset + 2] =
MARKER_COLOR_VALUES[markerOffset + 2] ?? 1;
bladeColorValues[vectorOffset] = color?.r ?? 0;
bladeColorValues[vectorOffset + 1] = color?.g ?? 0;
bladeColorValues[vectorOffset + 2] = color?.b ?? 0;
bladeOrigins[vectorOffset] = originX;
bladeOrigins[vectorOffset + 1] = originY;
bladeOrigins[vectorOffset + 2] = originZ;
yaws[vectorOffset] = yawX;
yaws[vectorOffset + 1] = yawY;
yaws[vectorOffset + 2] = yawZ;
uvs[uvOffset] = uvX;
uvs[uvOffset + 1] = uvY;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute(
"color",
new THREE.BufferAttribute(markerColorValues, 3),
);
geometry.setAttribute(
"aBladeColor",
new THREE.BufferAttribute(bladeColorValues, 3),
);
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
geometry.setAttribute(
"aBladeOrigin",
new THREE.BufferAttribute(bladeOrigins, 3),
);
geometry.setAttribute("aYaw", new THREE.BufferAttribute(yaws, 3));
geometry.computeVertexNormals();
geometry.computeBoundingSphere();
return geometry;
}
function createGrassMaterial(
terrainSampler: TerrainGrassSampler,
noiseTexture: THREE.Texture,
grassTexture: THREE.Texture,
): THREE.ShaderMaterial {
return new THREE.ShaderMaterial({
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() },
uBaseBladeColor: { value: new THREE.Color(GRASS_BASE_COLOR) },
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 },
uWindNoiseScale: { value: GRASS_CONFIG.windNoiseScale },
uWindStrength: { value: GRASS_CONFIG.windStrength },
uBaldPatchModifier: { value: GRASS_CONFIG.baldPatchModifier },
uFalloffSharpness: { value: GRASS_CONFIG.falloffSharpness },
uHeightNoiseFrequency: { value: GRASS_CONFIG.heightNoiseFrequency },
uHeightNoiseAmplitude: { value: GRASS_CONFIG.heightNoiseAmplitude },
uClumpFrequency: { value: GRASS_CONFIG.clumpFrequency },
uClumpThreshold: { value: GRASS_CONFIG.clumpThreshold },
uClumpSoftness: { value: GRASS_CONFIG.clumpSoftness },
uZoneFrequency: { value: GRASS_CONFIG.zoneFrequency },
uNoGrassZoneThreshold: { value: GRASS_CONFIG.noGrassZoneThreshold },
uSparseZoneThreshold: { value: GRASS_CONFIG.sparseZoneThreshold },
uMediumZoneThreshold: { value: GRASS_CONFIG.mediumZoneThreshold },
uZoneSoftness: { value: GRASS_CONFIG.zoneSoftness },
uNoGrassZoneHeight: { value: GRASS_CONFIG.noGrassZoneHeight },
uSparseZoneHeight: { value: GRASS_CONFIG.sparseZoneHeight },
uMediumZoneHeight: { value: GRASS_CONFIG.mediumZoneHeight },
uTallZoneHeight: { value: GRASS_CONFIG.tallZoneHeight },
uNoGrassZoneDensity: { value: GRASS_CONFIG.noGrassZoneDensity },
uSparseZoneDensity: { value: GRASS_CONFIG.sparseZoneDensity },
uMediumZoneDensity: { value: GRASS_CONFIG.mediumZoneDensity },
uTallZoneDensity: { value: GRASS_CONFIG.tallZoneDensity },
uMaxBendAngle: { value: GRASS_CONFIG.maxBendAngle },
uMaxBladeHeight: { value: GRASS_CONFIG.maxBladeHeight },
uRandomHeightAmount: { value: GRASS_CONFIG.randomHeightAmount },
uSurfaceOffset: { value: GRASS_CONFIG.surfaceOffset },
uLaFabrikCenter: {
value: new THREE.Vector2(LA_FABRIK_CENTER[0], LA_FABRIK_CENTER[2]),
},
uLaFabrikHalfExtents: {
value: new THREE.Vector2(
LA_FABRIK_HALF_EXTENTS.x,
LA_FABRIK_HALF_EXTENTS.z,
),
},
uLaFabrikRotation: { value: LA_FABRIK_ROTATION_Y },
uLaFabrikNoGrassFeather: { value: 1.4 },
},
});
}
export function GrassPatch({
density,
terrainSampler,
}: 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(density), [density]);
useEffect(() => {
return () => {
grassTextures.grass.dispose();
grassTextures.noise.dispose();
};
}, [grassTextures]);
const material = useMemo(
() =>
createGrassMaterial(
terrainSampler,
grassTextures.noise,
grassTextures.grass,
),
[grassTextures, terrainSampler],
);
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.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.uWindNoiseScale) {
uniforms.uWindNoiseScale.value =
GRASS_CONFIG.windNoiseScale * wind.noiseScale;
}
});
return <mesh geometry={geometry} material={material} frustumCulled={false} />;
}