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(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 ; }