feat: rewrite grass LOD system and visuals
This commit is contained in:
+43
-25
@@ -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]}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user