From dd249aabbd52c583617501b6f7a6d5377077acdf Mon Sep 17 00:00:00 2001
From: neru <152752583+neeeruuu@users.noreply.github.com>
Date: Fri, 2 Jan 2026 03:37:45 -0300
Subject: [PATCH] feat: increment grass segments, add extra taper at ends
---
src/app/page.tsx | 135 +++++++++++++++++++------------------
src/app/shaders/grass.vert | 28 ++------
2 files changed, 75 insertions(+), 88 deletions(-)
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1673d6f..0c95ddb 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -123,8 +123,6 @@ function Grass({
) {
(materialRef.current.userData.shader as Shader).uniforms.uTime.value =
state.clock.getElapsedTime();
- (materialRef.current.userData.shader as Shader).uniforms.uLOD.value =
- grassLOD;
}
});
@@ -133,37 +131,38 @@ function Grass({
const w = 0.5;
const h = 2;
+ const segments = 4;
- const positions = [
- -w,
- 0,
- 0,
- w,
- 0,
- 0,
- -w,
- h,
- 0,
- w,
- h,
- 0,
- 0,
- 0,
- w,
- 0,
- 0,
- -w,
- 0,
- h,
- w,
- 0,
- h,
- -w
- ];
+ const positions: number[] = [];
+ const uvs: number[] = [];
+ const indices: number[] = [];
- const uvs = [0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1];
+ for (let i = 0; i < 3; i++) {
+ const angle = (Math.PI / 3) * i;
+ const sinA = Math.sin(angle);
+ const cosA = Math.cos(angle);
- const indices = [0, 1, 2, 2, 1, 3, 4, 5, 6, 6, 5, 7];
+ 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',
@@ -187,10 +186,32 @@ function Grass({
const lushColor = new Color('#348a34');
const dryColor = new Color('#556b19');
+ const chunkRadius = size * Math.sqrt(2);
+
+ 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 * 0.1;
+ const falloffEnd = maxDist;
+
+ let fallofFactor = (dist - falloffStart) / (falloffEnd - falloffStart);
+ fallofFactor = Math.max(0, Math.min(1, fallofFactor));
+
+ const density = (1.0 - fallofFactor) * (1.0 - fallofFactor);
+
+ if (Math.random() > density)
+ continue;
+
const localY = getTerrainHeight(
localX,
localZ,
@@ -215,10 +236,8 @@ function Grass({
dummy.scale.set(baseScale, baseScale * heightMult, baseScale);
dummy.updateMatrix();
- meshRef.current.setMatrixAt(i, dummy.matrix);
+ meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
- const globalX = worldXBase + localX;
- const globalZ = worldZBase + localZ;
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (noiseVal + 1) / 2;
@@ -228,8 +247,12 @@ function Grass({
color.lerpColors(dryColor, lushColor, finalT);
- meshRef.current.setColorAt(i, color);
+ meshRef.current.setColorAt(instanceIndex, color);
+
+ instanceIndex++;
}
+ meshRef.current.count = instanceIndex;
+
meshRef.current.instanceMatrix.needsUpdate = true;
if (meshRef.current.instanceColor)
meshRef.current.instanceColor.needsUpdate = true;
@@ -251,11 +274,9 @@ function Grass({
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) {
@@ -374,28 +395,11 @@ function TerrainChunk({
grassSize,
grassLOD
}: TerrainChunkProps) {
- const [showGrass, setShowGrass] = useState(false);
+ const chunkDist = Math.sqrt((x * size) ** 2 + (y * size) ** 2);
+ const shouldRenderGrass = chunkDist < grassLOD + size;
+
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();
@@ -506,7 +510,7 @@ function TerrainChunk({
wireframe={wireframe}
/>
- {!wireframe && showGrass && (
+ {!wireframe && shouldRenderGrass && (
((props, ref) => {
});
return (
-
+
@@ -641,7 +645,6 @@ export default function Home() {
>
{/* */}
- {/* */}
- {/* {
if (mesh && !sealMesh) setSealMesh(mesh);
}}
- /> */}
+ />