feat: rewrite grass LOD system and visuals

This commit is contained in:
neru
2026-01-02 02:15:34 -03:00
parent 6187dfa6f7
commit 590711db49
3 changed files with 91 additions and 28 deletions
+43 -25
View File
@@ -84,6 +84,7 @@ interface GrassProps {
detailScale: number; detailScale: number;
detailHeight: number; detailHeight: number;
noise2D: (x: number, y: number) => number; noise2D: (x: number, y: number) => number;
grassLOD: number;
enableShadows?: boolean; enableShadows?: boolean;
} }
@@ -98,7 +99,8 @@ function Grass({
hillHeight, hillHeight,
detailScale, detailScale,
detailHeight, detailHeight,
noise2D noise2D,
grassLOD
}: GrassProps) { }: GrassProps) {
const meshRef = useRef<InstancedMesh>(null); const meshRef = useRef<InstancedMesh>(null);
const dummyRef = useRef<Object3D>(new Object3D()); const dummyRef = useRef<Object3D>(new Object3D());
@@ -120,6 +122,8 @@ 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;
} }
}); });
@@ -239,15 +243,18 @@ function Grass({
hillHeight, hillHeight,
detailScale, detailScale,
detailHeight, detailHeight,
noise2D noise2D,
grassLOD
]); ]);
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) {
@@ -309,7 +316,7 @@ function Grass({
materialRef.current.userData.shader = shader; materialRef.current.userData.shader = shader;
} }
}, },
[] [grassLOD]
); );
return ( return (
@@ -366,19 +373,28 @@ function TerrainChunk({
grassSize, grassSize,
grassLOD grassLOD
}: TerrainChunkProps) { }: TerrainChunkProps) {
const distance = Math.sqrt((x * size) ** 2 + (y * size) ** 2); const [showGrass, setShowGrass] = useState(false);
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 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();
@@ -444,8 +460,8 @@ function TerrainChunk({
const colorNoise = noise2D(globalX * 0.02, globalZ * 0.02); const colorNoise = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (colorNoise + 1) / 2; const t = (colorNoise + 1) / 2;
const dryGreen = { r: 0.05, g: 0.1, b: 0.0 }; const dryGreen = { r: 0.01, g: 0.01, b: 0.0 };
const lushGreen = { r: 0.0, g: 0.2, b: 0.0 }; const lushGreen = { r: 0.0, g: 0.05, b: 0.0 };
const r = dryGreen.r + (lushGreen.r - dryGreen.r) * t; const r = dryGreen.r + (lushGreen.r - dryGreen.r) * t;
const g = dryGreen.g + (lushGreen.g - dryGreen.g) * t; const g = dryGreen.g + (lushGreen.g - dryGreen.g) * t;
@@ -489,12 +505,12 @@ function TerrainChunk({
wireframe={wireframe} wireframe={wireframe}
/> />
</mesh> </mesh>
{!wireframe && adjustedGrassCount > 0 && ( {!wireframe && showGrass && (
<Grass <Grass
x={x} x={x}
y={y} y={y}
size={size} size={size}
count={adjustedGrassCount} count={grassCount}
grassSize={grassSize} grassSize={grassSize}
scale={scale} scale={scale}
hillScale={hillScale} hillScale={hillScale}
@@ -502,6 +518,7 @@ function TerrainChunk({
detailScale={detailScale} detailScale={detailScale}
detailHeight={detailHeight} detailHeight={detailHeight}
noise2D={noise2D} noise2D={noise2D}
grassLOD={grassLOD}
/> />
)} )}
</group> </group>
@@ -622,16 +639,17 @@ export default function Home() {
className='canvas' className='canvas'
> >
<EffectComposer> <EffectComposer>
<DepthOfField target={[0, 5, 0]} focalLength={20} bokehScale={5} /> {/* <DepthOfField target={[0, 5, 0]} focalLength={10} bokehScale={5} /> */}
{/* <Pixelation granularity={6} /> */} {/* <Pixelation granularity={6} /> */}
<Vignette /> <Vignette />
<Noise opacity={0.05} /> <Noise opacity={0.05} />
<Bloom <Bloom
intensity={2.2} intensity={2}
luminanceThreshold={0.5} luminanceThreshold={0.5}
luminanceSmoothing={0.1} luminanceSmoothing={0.1}
/> />
{sealMesh ? (
{/* {sealMesh ? (
<GodRays <GodRays
sun={sealMesh} sun={sealMesh}
samples={16} samples={16}
@@ -644,7 +662,7 @@ export default function Home() {
/> />
) : ( ) : (
<></> <></>
)} )} */}
</EffectComposer> </EffectComposer>
<Environment files={'hdr/sky.hdr'} environmentIntensity={1} background /> <Environment files={'hdr/sky.hdr'} environmentIntensity={1} background />
@@ -661,13 +679,13 @@ export default function Home() {
wireframe={false} wireframe={false}
grassCount={8000} grassCount={8000}
grassSize={0.6} grassSize={0.6}
grassLOD={60} grassLOD={80}
/> />
<SealCube {/* <SealCube
ref={(mesh) => { ref={(mesh) => {
if (mesh && !sealMesh) setSealMesh(mesh); if (mesh && !sealMesh) setSealMesh(mesh);
}} }}
/> /> */}
<OrbitControls <OrbitControls
target={[0, 5, 0]} target={[0, 5, 0]}
+17 -3
View File
@@ -1,4 +1,18 @@
vec3 rootColor = diffuseColor.rgb * 0.5; // Fake Ambient Occlusion
vec3 tipColor = diffuseColor.rgb * 1.5 + vec3(0.1, 0.1, 0.0); // Slight yellow tint at tips // Darken root (vGrassUv.y near 0) substantially
float ao = smoothstep(0.0, 0.4, vGrassUv.y);
ao = mix(0.2, 1.0, ao); // Roots are 20% brightness
diffuseColor.rgb = mix(rootColor, tipColor, vGrassUv.y); // Base mixing
vec3 rootColor = diffuseColor.rgb * 0.4;
vec3 tipColor = diffuseColor.rgb * 1.3 + vec3(0.1, 0.1, 0.0);
vec3 grassColor = mix(rootColor, tipColor, vGrassUv.y);
grassColor *= ao;
// Fake Translucency / Backlighting (Simple rim light effect)
// Simulating light passing through blades
float translucency = pow(vGrassUv.y, 2.0) * 0.5;
grassColor += vec3(0.1, 0.2, 0.0) * translucency;
diffuseColor.rgb = grassColor;
+31
View File
@@ -1,5 +1,16 @@
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);
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;
@@ -18,8 +29,28 @@ vec2 windDir = vec2(
); );
windDir = normalize(windDir - 0.5) * 0.8; windDir = normalize(windDir - 0.5) * 0.8;
// Tapering
// Reduce width based on Y (height)
// 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.z *= taper;
// Curvature - Simple quadratic bend
// Bend more as we go up.
float curveAmount = uv.y * uv.y * 2.0;
vec2 curveDir = normalize(vec2(noiseVal, fbm(vec2(worldZ, worldX))) - 0.5); // Randomize curve direction
transformed.x += curveAmount * curveDir.x * 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;