Files
neru.rip/src/app/niko/scene-components/grass.tsx
T
2026-06-01 17:31:13 -03:00

312 lines
7.7 KiB
TypeScript

import { useFrame, useLoader } from "@react-three/fiber";
import { useLayoutEffect, useMemo, useRef } from "react";
import { BufferAttribute, BufferGeometry, Color, DoubleSide, InstancedMesh, MeshStandardMaterial, Object3D, TextureLoader } from "three";
import { getTerrainHeight, Shader } from "./helpers";
import grassVert from './shaders/grass.vert';
import grassFrag from './shaders/grass.frag';
interface GrassProps {
x: number;
y: number;
size: number;
count: number;
grassSize: number;
scale: number;
hillScale: number;
hillHeight: number;
detailScale: number;
detailHeight: number;
noise2D: (x: number, y: number) => number;
grassLOD: number;
dryColor: string;
lushColor: string;
grassBlades?: number;
grassSegments?: number;
grassLODStart?: number;
grassLODExponent?: number;
}
export default function Grass({
x,
y,
size,
count,
grassSize,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D,
grassLOD,
dryColor = '#556b19',
lushColor = '#348a34',
grassBlades = 3,
grassSegments = 4,
grassLODStart = 0.5,
grassLODExponent = 1.0
}: GrassProps) {
const meshRef = useRef<InstancedMesh>(null);
const dummyRef = useRef<Object3D>(new Object3D());
const [alphaMap, normalMap] = useLoader(TextureLoader, [
'niko/img/grass_alpha.png',
'niko/img/grass_normal.png'
]);
const materialRef = useRef<
MeshStandardMaterial & { userData: { shader: Shader } }
>(null);
useFrame((state) => {
if (
materialRef.current &&
materialRef.current.userData &&
materialRef.current.userData.shader
) {
(materialRef.current.userData.shader as Shader).uniforms.uTime.value =
state.clock.getElapsedTime();
}
});
const geometry = useMemo(() => {
const geo = new BufferGeometry();
const w = 0.5;
const h = 2;
const segments = grassSegments;
const bladesCount = grassBlades;
const positions: number[] = [];
const uvs: number[] = [];
const indices: number[] = [];
for (let i = 0; i < bladesCount; i++) {
const angle = (Math.PI / bladesCount) * i;
const sinA = Math.sin(angle);
const cosA = Math.cos(angle);
const offset = positions.length / 3;
for (let y = 0; y <= segments; y++) {
const v = y / segments;
const yPos = v * h;
positions.push(-w * cosA, yPos, -w * sinA);
uvs.push(0, v);
positions.push(w * cosA, yPos, w * sinA);
uvs.push(1, v);
}
for (let y = 0; y < segments; y++) {
const row1 = offset + y * 2;
const row2 = offset + (y + 1) * 2;
indices.push(row1, row1 + 1, row2);
indices.push(row1 + 1, row2 + 1, row2);
}
}
geo.setAttribute(
'position',
new BufferAttribute(new Float32Array(positions), 3)
);
geo.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
}, [grassBlades, grassSegments]);
useLayoutEffect(() => {
if (!meshRef.current) return;
const dummy = dummyRef.current;
const worldXBase = x * size;
const worldZBase = y * size;
const color = new Color();
const lushColorObj = new Color(lushColor);
const dryColorObj = new Color(dryColor);
let instanceIndex = 0;
for (let i = 0; i < count; i++) {
const localX = (Math.random() - 0.5) * size;
const localZ = (Math.random() - 0.5) * size;
const globalX = worldXBase + localX;
const globalZ = worldZBase + localZ;
const dist = Math.sqrt(globalX * globalX + globalZ * globalZ);
const maxDist = grassLOD;
const falloffStart = maxDist * grassLODStart;
const falloffEnd = maxDist;
let fallofFactor = (dist - falloffStart) / (falloffEnd - falloffStart);
fallofFactor = Math.max(0, Math.min(1, fallofFactor));
const density = Math.pow(1.0 - fallofFactor, grassLODExponent);
if (Math.random() > density) continue;
const localY = getTerrainHeight(
localX,
localZ,
worldXBase,
worldZBase,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D
);
dummy.position.set(localX, localY, localZ);
dummy.rotation.y = Math.random() * Math.PI * 2;
dummy.rotation.x = (Math.random() - 0.5) * 0.2;
dummy.rotation.z = (Math.random() - 0.5) * 0.2;
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (noiseVal + 1) / 2;
const randomInternal = (Math.random() - 0.5) * 0.2;
const finalT = Math.max(0, Math.min(1, t + randomInternal));
color.lerpColors(dryColorObj, lushColorObj, finalT);
meshRef.current.setColorAt(instanceIndex, color);
const heightNoise = noise2D(globalX * 0.08, globalZ * 0.08);
const macroHeight = (heightNoise + 1.0) * 0.5; // 0..1
const microNoise = noise2D(globalX * 0.3, globalZ * 0.3);
const microHeight = (microNoise + 1.0) * 0.25; // 0..0.5
const perBladeRandom = Math.random() * 0.4;
const grassWidth = grassSize * (0.7 + Math.random() * 0.5);
const grassHeight = grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom);
dummy.scale.set(grassWidth, grassHeight, grassWidth);
dummy.updateMatrix();
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
instanceIndex++;
}
meshRef.current.count = instanceIndex;
meshRef.current.instanceMatrix.needsUpdate = true;
if (meshRef.current.instanceColor)
meshRef.current.instanceColor.needsUpdate = true;
}, [
x,
y,
size,
count,
grassSize,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D,
grassLOD,
dryColor,
lushColor,
grassLODStart,
grassLODExponent
]);
const onBeforeCompile = useMemo(
() => (shader: Shader) => {
shader.uniforms.uTime = { value: 0 };
shader.vertexShader = `
uniform float uTime;
varying vec2 vGrassUv;
varying vec3 vWorldPos;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for(int i = 0; i < 4; i++) {
value += amplitude * noise(p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return value;
}
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
${grassVert}
`
);
shader.fragmentShader = `
uniform float uTime;
varying vec2 vGrassUv;
varying vec3 vWorldPos;
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
'#include <color_fragment>',
`
#include <color_fragment>
${grassFrag}
`
);
if (materialRef.current) {
materialRef.current.userData.shader = shader;
}
},
[]
);
return (
<instancedMesh
ref={meshRef}
args={[undefined, undefined, count]}
position={[x * size, 0, y * size]}
geometry={geometry}
>
<meshStandardMaterial
ref={materialRef}
color='#ffffff'
side={DoubleSide}
alphaMap={alphaMap}
alphaTest={0.5}
normalMap={normalMap}
roughness={0.8}
metalness={0.1}
onBeforeCompile={onBeforeCompile}
/>
</instancedMesh>
);
}