From 590711db495e8cfb901d1f8ec558c911e2b2b9d0 Mon Sep 17 00:00:00 2001 From: neru <152752583+neeeruuu@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:15:34 -0300 Subject: [PATCH] feat: rewrite grass LOD system and visuals --- src/app/page.tsx | 68 ++++++++++++++++++++++++-------------- src/app/shaders/grass.frag | 20 +++++++++-- src/app/shaders/grass.vert | 31 +++++++++++++++++ 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0af77e3..4189880 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -84,6 +84,7 @@ interface GrassProps { detailScale: number; detailHeight: number; noise2D: (x: number, y: number) => number; + grassLOD: number; enableShadows?: boolean; } @@ -98,7 +99,8 @@ function Grass({ hillHeight, detailScale, detailHeight, - noise2D + noise2D, + grassLOD }: GrassProps) { const meshRef = useRef(null); const dummyRef = useRef(new Object3D()); @@ -120,6 +122,8 @@ function Grass({ ) { (materialRef.current.userData.shader as Shader).uniforms.uTime.value = state.clock.getElapsedTime(); + (materialRef.current.userData.shader as Shader).uniforms.uLOD.value = + grassLOD; } }); @@ -239,15 +243,18 @@ function Grass({ hillHeight, detailScale, detailHeight, - noise2D + noise2D, + grassLOD ]); const onBeforeCompile = useMemo( () => (shader: Shader) => { shader.uniforms.uTime = { value: 0 }; + shader.uniforms.uLOD = { value: grassLOD }; shader.vertexShader = ` uniform float uTime; + uniform float uLOD; varying vec2 vGrassUv; float hash(vec2 p) { @@ -309,7 +316,7 @@ function Grass({ materialRef.current.userData.shader = shader; } }, - [] + [grassLOD] ); return ( @@ -366,19 +373,28 @@ function TerrainChunk({ grassSize, grassLOD }: TerrainChunkProps) { - const distance = Math.sqrt((x * size) ** 2 + (y * size) ** 2); - - let adjustedGrassCount = grassCount; - if (distance > grassLOD) { - adjustedGrassCount = 0; - } else if (distance > grassLOD * 0.6) { - const fadeStart = grassLOD * 0.6; - const fadeRange = grassLOD * 0.4; - const fadeFactor = 1.0 - (distance - fadeStart) / fadeRange; - adjustedGrassCount = Math.floor(grassCount * fadeFactor * fadeFactor); - } + const [showGrass, setShowGrass] = useState(false); const meshRef = useRef(null); + useFrame((state) => { + const camPos = state.camera.position; + const chunkPosX = x * size; + const chunkPosZ = y * size; + + const dx = camPos.x - chunkPosX; + const dz = camPos.z - chunkPosZ; + const distSq = dx * dx + dz * dz; + + // Use grassLOD as the distance threshold + const threshold = grassLOD * grassLOD; + + if (distSq < threshold) { + if (!showGrass) setShowGrass(true); + } else { + if (showGrass) setShowGrass(false); + } + }); + const geometry = useMemo(() => { const geo = new BufferGeometry(); @@ -444,8 +460,8 @@ function TerrainChunk({ const colorNoise = noise2D(globalX * 0.02, globalZ * 0.02); const t = (colorNoise + 1) / 2; - const dryGreen = { r: 0.05, g: 0.1, b: 0.0 }; - const lushGreen = { r: 0.0, g: 0.2, b: 0.0 }; + const dryGreen = { r: 0.01, g: 0.01, b: 0.0 }; + const lushGreen = { r: 0.0, g: 0.05, b: 0.0 }; const r = dryGreen.r + (lushGreen.r - dryGreen.r) * t; const g = dryGreen.g + (lushGreen.g - dryGreen.g) * t; @@ -489,12 +505,12 @@ function TerrainChunk({ wireframe={wireframe} /> - {!wireframe && adjustedGrassCount > 0 && ( + {!wireframe && showGrass && ( )} @@ -622,16 +639,17 @@ export default function Home() { className='canvas' > - + {/* */} {/* */} - {sealMesh ? ( + + {/* {sealMesh ? ( ) : ( <> - )} + )} */} @@ -661,13 +679,13 @@ export default function Home() { wireframe={false} grassCount={8000} grassSize={0.6} - grassLOD={60} + grassLOD={80} /> - { if (mesh && !sealMesh) setSealMesh(mesh); }} - /> + /> */}