feat: increment grass segments, add extra taper at ends

This commit is contained in:
neru
2026-01-02 03:37:45 -03:00
parent 35f3c74663
commit dd249aabbd
2 changed files with 75 additions and 88 deletions
+70 -65
View File
@@ -123,8 +123,6 @@ function Grass({
) { ) {
(materialRef.current.userData.shader as Shader).uniforms.uTime.value = (materialRef.current.userData.shader as Shader).uniforms.uTime.value =
state.clock.getElapsedTime(); state.clock.getElapsedTime();
(materialRef.current.userData.shader as Shader).uniforms.uLOD.value =
grassLOD;
} }
}); });
@@ -133,37 +131,38 @@ function Grass({
const w = 0.5; const w = 0.5;
const h = 2; const h = 2;
const segments = 4;
const positions = [ const positions: number[] = [];
-w, const uvs: number[] = [];
0, const indices: number[] = [];
0,
w,
0,
0,
-w,
h,
0,
w,
h,
0,
0,
0,
w,
0,
0,
-w,
0,
h,
w,
0,
h,
-w
];
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( geo.setAttribute(
'position', 'position',
@@ -187,10 +186,32 @@ function Grass({
const lushColor = new Color('#348a34'); const lushColor = new Color('#348a34');
const dryColor = new Color('#556b19'); const dryColor = new Color('#556b19');
const chunkRadius = size * Math.sqrt(2);
let instanceIndex = 0;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const localX = (Math.random() - 0.5) * size; const localX = (Math.random() - 0.5) * size;
const localZ = (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( const localY = getTerrainHeight(
localX, localX,
localZ, localZ,
@@ -215,10 +236,8 @@ function Grass({
dummy.scale.set(baseScale, baseScale * heightMult, baseScale); dummy.scale.set(baseScale, baseScale * heightMult, baseScale);
dummy.updateMatrix(); 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 noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (noiseVal + 1) / 2; const t = (noiseVal + 1) / 2;
@@ -228,8 +247,12 @@ function Grass({
color.lerpColors(dryColor, lushColor, finalT); 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; meshRef.current.instanceMatrix.needsUpdate = true;
if (meshRef.current.instanceColor) if (meshRef.current.instanceColor)
meshRef.current.instanceColor.needsUpdate = true; meshRef.current.instanceColor.needsUpdate = true;
@@ -251,11 +274,9 @@ function Grass({
const onBeforeCompile = useMemo( const onBeforeCompile = useMemo(
() => (shader: Shader) => { () => (shader: Shader) => {
shader.uniforms.uTime = { value: 0 }; shader.uniforms.uTime = { value: 0 };
shader.uniforms.uLOD = { value: grassLOD };
shader.vertexShader = ` shader.vertexShader = `
uniform float uTime; uniform float uTime;
uniform float uLOD;
varying vec2 vGrassUv; varying vec2 vGrassUv;
float hash(vec2 p) { float hash(vec2 p) {
@@ -374,28 +395,11 @@ function TerrainChunk({
grassSize, grassSize,
grassLOD grassLOD
}: TerrainChunkProps) { }: TerrainChunkProps) {
const [showGrass, setShowGrass] = useState(false); const chunkDist = Math.sqrt((x * size) ** 2 + (y * size) ** 2);
const shouldRenderGrass = chunkDist < grassLOD + size;
const meshRef = useRef<Mesh>(null); const meshRef = useRef<Mesh>(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 geometry = useMemo(() => {
const geo = new BufferGeometry(); const geo = new BufferGeometry();
@@ -506,7 +510,7 @@ function TerrainChunk({
wireframe={wireframe} wireframe={wireframe}
/> />
</mesh> </mesh>
{!wireframe && showGrass && ( {!wireframe && shouldRenderGrass && (
<Grass <Grass
x={x} x={x}
y={y} y={y}
@@ -604,7 +608,7 @@ const SealCube = forwardRef<Mesh>((props, ref) => {
}); });
return ( return (
<mesh ref={meshRef} position={[0, 5, 0]} castShadow receiveShadow> <mesh ref={meshRef} position={[0, 2, 0]} castShadow receiveShadow>
<boxGeometry args={[0.85, 0.85, 0.85]} /> <boxGeometry args={[0.85, 0.85, 0.85]} />
<meshBasicMaterial map={texture} /> <meshBasicMaterial map={texture} />
</mesh> </mesh>
@@ -641,7 +645,6 @@ export default function Home() {
> >
<EffectComposer> <EffectComposer>
{/* <DepthOfField target={[0, 5, 0]} focalLength={10} bokehScale={5} /> */} {/* <DepthOfField target={[0, 5, 0]} focalLength={10} bokehScale={5} /> */}
{/* <Pixelation granularity={6} /> */}
<Vignette /> <Vignette />
<Noise opacity={0.05} /> <Noise opacity={0.05} />
<Bloom <Bloom
@@ -675,22 +678,24 @@ export default function Home() {
resolution={8.0} resolution={8.0}
scale={1} scale={1}
hillScale={0.1} hillScale={0.1}
hillHeight={6.0} hillHeight={0.0}
// hillHeight={6.0}
detailScale={1.0} detailScale={1.0}
detailHeight={0.1} // detailHeight={0.1}
detailHeight={0.0}
wireframe={false} wireframe={false}
grassCount={8000} grassCount={8000}
grassSize={0.6} grassSize={0.6}
grassLOD={80} grassLOD={60}
/> />
{/* <SealCube <SealCube
ref={(mesh) => { ref={(mesh) => {
if (mesh && !sealMesh) setSealMesh(mesh); if (mesh && !sealMesh) setSealMesh(mesh);
}} }}
/> */} />
<OrbitControls <OrbitControls
target={[0, 5, 0]} target={[0, 2, 0]}
enablePan={false} enablePan={false}
makeDefault makeDefault
minDistance={2} minDistance={2}
+5 -23
View File
@@ -1,16 +1,6 @@
float worldX = instanceMatrix[3][0]; float worldX = instanceMatrix[3][0];
float worldZ = instanceMatrix[3][2]; float worldZ = instanceMatrix[3][2];
float worldY = instanceMatrix[3][1];
vec3 worldPos = vec3(worldX, worldY, worldZ);
float dist = distance(cameraPosition, worldPos);
float fadeStart = uLOD * 0.6;
float fadeRange = uLOD * 0.4;
float distFactor = 1.0 - clamp((dist - fadeStart) / fadeRange, 0.0, 1.0);
float noiseVal = fbm(vec2(worldX, worldZ) * 0.5); float noiseVal = fbm(vec2(worldX, worldZ) * 0.5);
distFactor = clamp(distFactor + (noiseVal - 0.5) * 0.2, 0.0, 1.0);
float windSpeed = 0.3; float windSpeed = 0.3;
float windScale = 0.04; float windScale = 0.04;
@@ -29,28 +19,20 @@ vec2 windDir = vec2(
); );
windDir = normalize(windDir - 0.5) * 0.8; windDir = normalize(windDir - 0.5) * 0.8;
// Tapering float taperFactor = pow(uv.y, 4.0); // Stays near 0 for long, then shoots to 1 at tip
// Reduce width based on Y (height) float taper = 1.0 - taperFactor * 0.6; // Width goes from 1.0 to 0.4 at tip
// As Y increases (from 0 to 1 in UV space roughly), width decreases.
// uv.y is height.
float taper = 1.0 - uv.y;
taper = mix(1.0, taper, 0.5); // Control how sharp the taper is. 0.5 = moderate taper.
transformed.x *= taper; transformed.x *= taper;
transformed.z *= taper; transformed.z *= taper;
// Curvature - Simple quadratic bend float curveStrength = 5.0 + noiseVal * 2.0;
// Bend more as we go up. float curveAmount = uv.y * uv.y * curveStrength;
float curveAmount = uv.y * uv.y * 2.0;
vec2 curveDir = normalize(vec2(noiseVal, fbm(vec2(worldZ, worldX))) - 0.5); // Randomize curve direction vec2 curveDir = normalize(vec2(noiseVal, fbm(vec2(worldZ, worldX))) - 0.5); // Randomize curve direction
transformed.x += curveAmount * curveDir.x * 0.5; transformed.x += curveAmount * curveDir.x * 0.5;
transformed.z += curveAmount * curveDir.y * 0.5; transformed.z += curveAmount * curveDir.y * 0.5;
// Apply wind sway
float swayAmount = (windStrength + turbulence) * uv.y * uv.y; float swayAmount = (windStrength + turbulence) * uv.y * uv.y;
transformed.x += swayAmount * windDir.x; transformed.x += swayAmount * windDir.x;
transformed.z += swayAmount * windDir.y; transformed.z += swayAmount * windDir.y;
transformed.y -= abs(swayAmount) * 0.2; transformed.y -= abs(swayAmount) * 0.2;
// Apply distance scaling
transformed *= distFactor;