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); }} - /> */} + />