Compare commits

...

10 Commits

Author SHA1 Message Date
neru d506071ce2 feat: overhaul grass 2026-06-01 17:31:13 -03:00
neru cad47f07bd feat: randomize grass height 2026-06-01 17:02:05 -03:00
neru 10543bba89 fix: make grass rotation less strong 2026-06-01 17:00:33 -03:00
neru aeee2158ba feat/fix: misc shader changes (fix normals, lighting, uv) 2026-06-01 16:56:33 -03:00
neru 569a4f29fb fix: make fog fit hdr 2026-06-01 16:50:55 -03:00
neru 9a67a800fa feat: add hover anim 2026-06-01 16:49:04 -03:00
neru 719a75d393 feat: change cube lighting to standard mat 2026-06-01 16:49:00 -03:00
neru f583cfdc57 style: formatting 2026-06-01 16:47:44 -03:00
neru 5665804b8f fix: disable AA (already handled by SMAA) 2026-06-01 16:47:37 -03:00
neru 67bf6325fa fix: misc visual changes 2026-06-01 16:47:24 -03:00
5 changed files with 152 additions and 106 deletions
+17 -11
View File
@@ -68,10 +68,10 @@ function Scene() {
grassDryColor: '#495a17',
grassLushColor: '#255825',
grassCount: { value: 8000, min: 1000, max: 30000, step: 500 },
grassSize: { value: 0.65, min: 0.1, max: 2.0, step: 0.05 },
grassSize: { value: 0.85, min: 0.1, max: 2.0, step: 0.05 },
grassLOD: { value: 60, min: 10, max: 200, step: 5 },
grassBlades: { value: 3, min: 1, max: 4, step: 1 },
grassSegments: { value: 3, min: 1, max: 5, step: 1 },
grassBlades: { value: 3, min: 1, max: 5, step: 1 },
grassSegments: { value: 4, min: 1, max: 5, step: 1 },
grassLODStart: { value: 0.15, min: 0.0, max: 0.9, step: 0.05 },
grassLODExponent: { value: 1.8, min: 0.5, max: 3.0, step: 0.1 },
})
@@ -80,11 +80,17 @@ function Scene() {
return (<>
<Environment
files={'niko/hdr/sky.hdr'}
environmentIntensity={1}
environmentIntensity={0.85}
background
/>
<fogExp2 attach='fog' args={[0x9a9a9a, 0.01]} />
<fogExp2 attach='fog' args={[0xa3a5ba, 0.0125]} />
<ambientLight intensity={0.5} />
<directionalLight
position={[15, 25, 15]}
intensity={1}
/>
<Terrain
chunks={chunks}
@@ -123,9 +129,9 @@ function PostProcessing() {
<Vignette />
<Noise opacity={0.05} />
<Bloom
intensity={2}
luminanceThreshold={0.5}
luminanceSmoothing={0.1}
intensity={0.8}
luminanceThreshold={0.4}
luminanceSmoothing={0.5}
/>
<SMAA />
<HueSaturation saturation={0.3} />
@@ -145,11 +151,11 @@ export default function Seal() {
<Canvas
shadows
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
gl={{ antialias: true }}
gl={{ antialias: false, powerPreference: "high-performance" }}
className='canvas'
>
<AmbientSound url="niko/snd/wind.mp3" volume={0.4}/>
<AmbientSound url="niko/snd/birds.mp3" volume={0.1}/>
<AmbientSound url="niko/snd/wind.mp3" volume={0.4} />
<AmbientSound url="niko/snd/birds.mp3" volume={0.1} />
<Scene />
<PostProcessing />
+76 -74
View File
@@ -7,24 +7,24 @@ import grassVert from './shaders/grass.vert';
import grassFrag from './shaders/grass.frag';
interface GrassProps {
x: number;
y: number;
size: number;
count: number;
grassSize: number;
scale: number;
hillScale: number;
hillHeight: number;
detailScale: number;
detailHeight: number;
noise2D: (x: number, y: number) => number;
grassLOD: number;
dryColor: string;
lushColor: string;
grassBlades?: number;
grassSegments?: number;
grassLODStart?: number;
grassLODExponent?: number;
x: number;
y: number;
size: number;
count: number;
grassSize: number;
scale: number;
hillScale: number;
hillHeight: number;
detailScale: number;
detailHeight: number;
noise2D: (x: number, y: number) => number;
grassLOD: number;
dryColor: string;
lushColor: string;
grassBlades?: number;
grassSegments?: number;
grassLODStart?: number;
grassLODExponent?: number;
}
export default function Grass({
@@ -173,24 +173,27 @@ export default function Grass({
dummy.rotation.x = (Math.random() - 0.5) * 0.2;
dummy.rotation.z = (Math.random() - 0.5) * 0.2;
const baseScale = grassSize + Math.random() * grassSize * 0.5;
const heightMult = 0.5 + Math.random() * 1.0;
dummy.scale.set(baseScale, baseScale * heightMult, baseScale);
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (noiseVal + 1) / 2;
const randomInternal = (Math.random() - 0.5) * 0.2;
const finalT = Math.max(0, Math.min(1, t + randomInternal));
color.lerpColors(dryColorObj, lushColorObj, finalT);
meshRef.current.setColorAt(instanceIndex, color);
const heightNoise = noise2D(globalX * 0.08, globalZ * 0.08);
const macroHeight = (heightNoise + 1.0) * 0.5; // 0..1
const microNoise = noise2D(globalX * 0.3, globalZ * 0.3);
const microHeight = (microNoise + 1.0) * 0.25; // 0..0.5
const perBladeRandom = Math.random() * 0.4;
const grassWidth = grassSize * (0.7 + Math.random() * 0.5);
const grassHeight = grassSize * (0.4 + macroHeight * 0.8 + microHeight + perBladeRandom);
dummy.scale.set(grassWidth, grassHeight, grassWidth);
dummy.updateMatrix();
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (noiseVal + 1) / 2;
const randomInternal = (Math.random() - 0.5) * 0.2;
const finalT = Math.max(0, Math.min(1, t + randomInternal));
color.lerpColors(dryColorObj, lushColorObj, finalT);
meshRef.current.setColorAt(instanceIndex, color);
instanceIndex++;
}
meshRef.current.count = instanceIndex;
@@ -222,62 +225,61 @@ export default function Grass({
shader.uniforms.uTime = { value: 0 };
shader.vertexShader = `
uniform float uTime;
varying vec2 vGrassUv;
uniform float uTime;
varying vec2 vGrassUv;
varying vec3 vWorldPos;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for(int i = 0; i < 4; i++) {
value += amplitude * noise(p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return value;
}
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
${shader.vertexShader}
`;
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for(int i = 0; i < 4; i++) {
value += amplitude * noise(p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return value;
}
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
vGrassUv = uv;
${grassVert}
`
#include <begin_vertex>
${grassVert}
`
);
shader.fragmentShader = `
uniform float uTime;
varying vec2 vGrassUv;
${shader.fragmentShader}
`;
uniform float uTime;
varying vec2 vGrassUv;
varying vec3 vWorldPos;
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
'#include <color_fragment>',
`
#include <color_fragment>
${grassFrag}
`
#include <color_fragment>
${grassFrag}
`
);
if (materialRef.current) {
+7 -1
View File
@@ -12,13 +12,19 @@ const SealCube = forwardRef<Mesh>((props, ref) => {
if (meshRef.current) {
meshRef.current.rotation.x += delta * 0.5;
meshRef.current.rotation.y += delta * 0.5;
meshRef.current.position.y = 3 + Math.sin(state.clock.getElapsedTime() * 1) * 0.15;
}
});
return (
<mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow>
<boxGeometry args={[0.85, 0.85, 0.85]} />
<meshBasicMaterial map={texture} depthWrite={true} />
<meshStandardMaterial
map={texture}
roughness={0.4}
metalness={0.1}
envMapIntensity={1.2}
/>
</mesh>
);
});
@@ -1,13 +1,31 @@
float ao = smoothstep(0.0, 0.7, vGrassUv.y);
ao = mix(0.05, 1.0, ao);
ao = mix(0.05, 1.0, pow(ao, 1.6));
vec3 rootColor = diffuseColor.rgb * 0.4;
vec3 tipColor = diffuseColor.rgb * 1.5;
vec3 rootColor = diffuseColor.rgb * 0.15;
vec3 midColor = diffuseColor.rgb;
vec3 tipColor = diffuseColor.rgb * 1.3 + vec3(0.06, 0.08, 0.0);
vec3 grassColor = mix(rootColor, tipColor, vGrassUv.y);
grassColor *= ao;
float heightParam = vGrassUv.y;
vec3 grassColor;
if (heightParam < 0.4) {
float t = smoothstep(0.0, 0.4, heightParam);
grassColor = mix(rootColor, midColor, t);
} else {
float t = smoothstep(0.4, 1.0, heightParam);
grassColor = mix(midColor, tipColor, t);
}
float translucency = pow(vGrassUv.y, 2.0) * 0.5;
grassColor += diffuseColor.rgb * translucency;
vec3 viewDir = normalize(cameraPosition - vWorldPos);
vec3 lightDir = normalize(vec3(15.0, 25.0, 15.0));
diffuseColor.rgb = grassColor;
float VdotL = max(0.0, dot(viewDir, -lightDir));
float sss = pow(VdotL, 3.0) * smoothstep(0.2, 0.9, vGrassUv.y);
vec3 sssColor = diffuseColor.rgb * vec3(0.6, 1.0, 0.15) * 1.8;
grassColor += sssColor * sss * 2.0;
float NdotV = 1.0 - max(0.0, dot(normalize(vNormal), viewDir));
float rim = pow(NdotV, 3.0) * smoothstep(0.3, 1.0, vGrassUv.y) * 0.15;
grassColor += vec3(0.3, 0.5, 0.1) * rim;
diffuseColor.rgb = grassColor * ao;
@@ -1,3 +1,5 @@
vGrassUv = uv;
vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
float gx = worldPos.x;
float gz = worldPos.z;
@@ -8,7 +10,6 @@ float windSpeed = 1.5;
float windTime = uTime * windSpeed;
vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2);
float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2;
float gustNoise = fbm(windSamplePos * 0.4);
float gust = pow(gustNoise, 3.0) * 1.8;
@@ -20,19 +21,32 @@ float spring = sin(uTime * 2.0 + phase) * 0.06 + sin(uTime * 4.5 + phase * 1.5)
float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5;
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
float taperFactor = pow(uv.y, 4.0);
float taper = 1.0 - taperFactor * 0.6;
// taper (fade)
float taperFactor = uv.y * uv.y * uv.y;
float taper = 1.0 - taperFactor * 0.85;
transformed.x *= taper;
transformed.z *= taper;
// curve
float curveVal = fbm(vec2(gx, gz) * 0.5);
float curveStrength = 2.0 + curveVal * 2.0;
float curveStrength = 1.5 + curveVal * 2.5;
float curveAmount = uv.y * uv.y * curveStrength;
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
transformed.x += curveAmount * curveDir.x * 0.5;
transformed.z += curveAmount * curveDir.y * 0.5;
transformed.x += curveAmount * curveDir.x * 0.4;
transformed.z += curveAmount * curveDir.y * 0.4;
// sway
float swayAmount = (totalWind + spring) * uv.y * uv.y;
transformed.x += swayAmount * windDir.x;
transformed.z += swayAmount * windDir.y;
transformed.y -= abs(swayAmount) * 0.2;
// normal comp
vec2 totalBend = curveDir * curveAmount * 0.4 + windDir * swayAmount;
float bendMag = length(totalBend);
vec3 bentNormal = normalize(vec3(-totalBend.x * 0.5, 1.0, -totalBend.y * 0.5));
// normal mix
objectNormal = normalize(mix(vec3(0.0, 1.0, 0.0), bentNormal, uv.y));
vWorldPos = (modelMatrix * instanceMatrix * vec4(transformed, 1.0)).xyz;