feat: increment grass segments, add extra taper at ends
This commit is contained in:
+70
-65
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user