Shader Example: Moving Field
Single-pass fragment shader mimicing a windy grass field.
Live Demo
Fragment Shader
Copy paste from below to see its effect; Use the Divooka version when using with Shader Lab in Divooka.
Snippets
WebGL
precision mediump float;
uniform vec2 u_Resolution;
uniform float u_Time;
uniform vec3 u_Mouse; // x, y, pressed
// Mouse = look around a bit.
// Time animates wind and slow camera drift.
#define FAR 140.0
#define PI 3.14159265359
mat2 rot(float a)
{
float c = cos(a), s = sin(a);
return mat2(c,-s,s,c);
}
float hash11(float p)
{
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash12(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float noise(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f = f*f*(3.0 - 2.0*f);
float a = hash12(i + vec2(0,0));
float b = hash12(i + vec2(1,0));
float c = hash12(i + vec2(0,1));
float d = hash12(i + vec2(1,1));
return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);
}
float fbm(vec2 p)
{
float s = 0.0;
float a = 0.5;
for (int i = 0; i < 6; i++)
{
s += a * noise(p);
p *= 2.02;
a *= 0.5;
}
return s;
}
float terrainHeight(vec2 xz)
{
vec2 p = xz * 0.055;
float h = 0.0;
h += fbm(p * 0.8) * 2.8;
h += fbm(p * 2.2 + 17.0) * 0.7;
h += sin(xz.x * 0.03) * 0.8;
h += cos(xz.y * 0.025) * 0.6;
return h;
}
float windField(vec2 xz, float t)
{
float w = 0.0;
w += sin(xz.x * 0.18 + t * 1.8);
w += sin(xz.y * 0.13 + t * 1.3 + 1.7);
w += sin((xz.x + xz.y) * 0.09 + t * 2.2 + 4.0);
return w / 3.0;
}
float mapTerrain(vec3 p)
{
return p.y - terrainHeight(p.xz);
}
vec3 terrainNormal(vec3 p)
{
float e = 0.12;
float h = mapTerrain(p);
vec2 k = vec2(e, 0.0);
vec3 n = vec3(
mapTerrain(p + vec3(k.x,0,k.y)) - h,
e,
mapTerrain(p + vec3(k.y,0,k.x)) - h
);
return normalize(n);
}
bool traceTerrain(vec3 ro, vec3 rd, out vec3 pos, out float t)
{
t = 0.0;
for (int i = 0; i < 180; i++)
{
pos = ro + rd * t;
float d = mapTerrain(pos);
if (d < 0.002 * max(1.0, t * 0.08))
return true;
t += max(0.04, d * 0.55);
if (t > FAR) break;
}
return false;
}
vec3 skyColor(vec3 rd, vec3 sunDir)
{
float sunAmt = max(dot(rd, sunDir), 0.0);
float h = max(rd.y, -0.2);
vec3 skyHorizon = vec3(0.72, 0.83, 0.95);
vec3 skyZenith = vec3(0.18, 0.38, 0.75);
vec3 col = mix(skyHorizon, skyZenith, smoothstep(-0.15, 0.65, h));
col += vec3(1.0, 0.85, 0.65) * pow(sunAmt, 64.0) * 0.45;
col += vec3(1.0, 0.92, 0.80) * pow(sunAmt, 512.0) * 0.25;
float low = smoothstep(-0.2, 0.15, rd.y);
col = mix(col * 0.9, col, low);
return col;
}
float bladeMask(vec2 xz, vec2 local, float dist)
{
// Blade distribution per cell
vec2 gv = fract(xz) - 0.5;
vec2 id = floor(xz);
vec2 jitter = hash22(id) - 0.5;
vec2 c = jitter * 0.35;
vec2 p = gv - c;
// Wind bend in projected local space
float width = mix(0.014, 0.035, hash12(id + 11.7));
float height = mix(0.6, 1.3, hash12(id + 7.3));
float taper = mix(1.0, 0.18, clamp(local.y / height, 0.0, 1.0));
float bend = sin(u_Time * 2.6 + id.x * 0.7 + id.y * 0.9) * 0.10;
p.x -= bend * smoothstep(0.0, height, local.y);
float m = 1.0 - smoothstep(width * taper, width * taper + 0.02 + dist * 0.0015, abs(p.x));
m *= smoothstep(0.0, 0.08, local.y);
m *= 1.0 - smoothstep(height - 0.08, height, local.y);
return m;
}
vec3 grassAlbedo(vec3 p, vec3 n, vec3 rd, vec3 sunDir, float dist)
{
vec2 xz = p.xz;
float t = u_Time;
float wind = windField(xz, t);
float large = fbm(xz * 0.12);
float medium = fbm(xz * 0.65 + wind * 0.4);
float fine = noise(xz * 7.0 + wind);
vec3 baseA = vec3(0.16, 0.30, 0.08);
vec3 baseB = vec3(0.30, 0.46, 0.14);
vec3 baseC = vec3(0.45, 0.58, 0.18);
vec3 col = mix(baseA, baseB, smoothstep(0.2, 0.8, large));
col = mix(col, baseC, smoothstep(0.45, 1.0, medium) * 0.45);
// Near-field blade-like striping in tangent space
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 tx = normalize(cross(up, n) + vec3(0.001,0.0,0.0));
vec3 tz = cross(n, tx);
vec2 local = vec2(dot(p, tx), dot(p, tz));
float bladeLayer = 0.0;
if (dist < 28.0)
{
vec2 cell = xz * 5.0;
float h1 = bladeMask(cell, vec2(fract(local.x * 3.0), fract(local.y * 2.0)), dist);
float h2 = bladeMask(cell + 17.37, vec2(fract(local.x * 2.1), fract(local.y * 2.4)), dist);
bladeLayer = max(h1, h2 * 0.8);
}
col *= 0.85 + fine * 0.30;
col += vec3(0.12, 0.18, 0.04) * bladeLayer;
// Yellowish dry variation
float dry = smoothstep(0.62, 1.0, noise(xz * 0.22 + 50.0));
col = mix(col, col * vec3(1.15, 1.02, 0.75), dry * 0.22);
// Wind sheen
float sheen = smoothstep(0.2, 1.0, wind * 0.5 + 0.5) * smoothstep(0.0, 0.8, fine);
col += vec3(0.08, 0.10, 0.04) * sheen * 0.4;
// Forward scattering style tint
float fwd = pow(max(dot(normalize(sunDir - rd * 0.4), n), 0.0), 4.0);
col += vec3(0.18, 0.20, 0.08) * fwd * 0.25;
return col;
}
float softShadow(vec3 ro, vec3 rd)
{
float res = 1.0;
float t = 0.1;
for (int i = 0; i < 28; i++)
{
vec3 p = ro + rd * t;
float h = mapTerrain(p);
res = min(res, 10.0 * h / t);
t += clamp(h, 0.08, 1.4);
if (h < 0.001 || t > 40.0) break;
}
return clamp(res, 0.0, 1.0);
}
vec3 render(vec3 ro, vec3 rd)
{
vec3 sunDir = normalize(vec3(0.55, 0.50, 0.35));
vec3 sky = skyColor(rd, sunDir);
vec3 pos;
float t;
if (!traceTerrain(ro, rd, pos, t))
return sky;
vec3 n = terrainNormal(pos);
float dist = length(pos - ro);
vec3 albedo = grassAlbedo(pos, n, rd, sunDir, dist);
float NoL = max(dot(n, sunDir), 0.0);
float shadow = softShadow(pos + n * 0.05, sunDir);
vec3 ambient = vec3(0.20, 0.28, 0.18) * (0.5 + 0.5 * n.y);
vec3 bounce = vec3(0.10, 0.09, 0.05) * smoothstep(-0.2, 0.3, n.y);
vec3 diff = vec3(1.15, 1.05, 0.90) * NoL * shadow;
// Specular glints for grass
vec3 h = normalize(sunDir - rd);
float spec = pow(max(dot(n, h), 0.0), 32.0);
float sparkle = noise(pos.xz * 18.0 + u_Time * 0.5);
spec *= smoothstep(0.82, 1.0, sparkle) * 0.18 * shadow;
vec3 col = albedo * (ambient + bounce + diff);
col += vec3(1.0, 0.95, 0.80) * spec;
// Distant field desaturation / aerial perspective
float haze = 1.0 - exp(-dist * 0.018);
col = mix(col, sky, haze * 0.75);
// Ground occlusion in creases
float occ = clamp(0.55 + 0.45 * n.y, 0.0, 1.0);
col *= occ;
return col;
}
void main()
{
vec2 uv = (gl_FragCoord.xy - 0.5 * u_Resolution.xy) / u_Resolution.y;
float t = u_Time * 0.22;
// Camera
vec3 ro = vec3(0.0, 3.4, -8.0 + t * 6.0);
ro.x += sin(t * 0.7) * 4.0;
vec3 ta = ro + vec3(sin(t * 0.35) * 1.5, -0.5, 8.0);
// Mouse look
vec2 m = iMouse.xy / max(u_Resolution.xy, vec2(1.0));
float yaw = (iMouse.z > 0.0) ? (m.x - 0.5) * 2.4 : 0.18 * sin(u_Time * 0.12);
float pit = (iMouse.z > 0.0) ? (m.y - 0.5) * 1.0 : -0.08;
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(vec3(0,1,0), ww));
vec3 vv = cross(ww, uu);
vec3 rd = normalize(uu * uv.x + vv * uv.y + ww * 1.8);
rd.xz *= rot(yaw);
rd.yz *= rot(pit);
vec3 col = render(ro, rd);
// Subtle vignette
float vig = 1.0 - dot(uv, uv) * 0.22;
col *= vig;
// Tonemap + gamma
col = col / (1.0 + col);
col = pow(col, vec3(0.4545));
gl_FragColor = vec4(col, 1.0);
}
Shadertoy
// Mouse = look around a bit.
// Time animates wind and slow camera drift.
#define FAR 140.0
#define PI 3.14159265359
mat2 rot(float a)
{
float c = cos(a), s = sin(a);
return mat2(c,-s,s,c);
}
float hash11(float p)
{
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash12(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float noise(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f = f*f*(3.0 - 2.0*f);
float a = hash12(i + vec2(0,0));
float b = hash12(i + vec2(1,0));
float c = hash12(i + vec2(0,1));
float d = hash12(i + vec2(1,1));
return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);
}
float fbm(vec2 p)
{
float s = 0.0;
float a = 0.5;
for (int i = 0; i < 6; i++)
{
s += a * noise(p);
p *= 2.02;
a *= 0.5;
}
return s;
}
float terrainHeight(vec2 xz)
{
vec2 p = xz * 0.055;
float h = 0.0;
h += fbm(p * 0.8) * 2.8;
h += fbm(p * 2.2 + 17.0) * 0.7;
h += sin(xz.x * 0.03) * 0.8;
h += cos(xz.y * 0.025) * 0.6;
return h;
}
float windField(vec2 xz, float t)
{
float w = 0.0;
w += sin(xz.x * 0.18 + t * 1.8);
w += sin(xz.y * 0.13 + t * 1.3 + 1.7);
w += sin((xz.x + xz.y) * 0.09 + t * 2.2 + 4.0);
return w / 3.0;
}
float mapTerrain(vec3 p)
{
return p.y - terrainHeight(p.xz);
}
vec3 terrainNormal(vec3 p)
{
float e = 0.12;
float h = mapTerrain(p);
vec2 k = vec2(e, 0.0);
vec3 n = vec3(
mapTerrain(p + vec3(k.x,0,k.y)) - h,
e,
mapTerrain(p + vec3(k.y,0,k.x)) - h
);
return normalize(n);
}
bool traceTerrain(vec3 ro, vec3 rd, out vec3 pos, out float t)
{
t = 0.0;
for (int i = 0; i < 180; i++)
{
pos = ro + rd * t;
float d = mapTerrain(pos);
if (d < 0.002 * max(1.0, t * 0.08))
return true;
t += max(0.04, d * 0.55);
if (t > FAR) break;
}
return false;
}
vec3 skyColor(vec3 rd, vec3 sunDir)
{
float sunAmt = max(dot(rd, sunDir), 0.0);
float h = max(rd.y, -0.2);
vec3 skyHorizon = vec3(0.72, 0.83, 0.95);
vec3 skyZenith = vec3(0.18, 0.38, 0.75);
vec3 col = mix(skyHorizon, skyZenith, smoothstep(-0.15, 0.65, h));
col += vec3(1.0, 0.85, 0.65) * pow(sunAmt, 64.0) * 0.45;
col += vec3(1.0, 0.92, 0.80) * pow(sunAmt, 512.0) * 0.25;
float low = smoothstep(-0.2, 0.15, rd.y);
col = mix(col * 0.9, col, low);
return col;
}
float bladeMask(vec2 xz, vec2 local, float dist)
{
// Blade distribution per cell
vec2 gv = fract(xz) - 0.5;
vec2 id = floor(xz);
vec2 jitter = hash22(id) - 0.5;
vec2 c = jitter * 0.35;
vec2 p = gv - c;
// Wind bend in projected local space
float width = mix(0.014, 0.035, hash12(id + 11.7));
float height = mix(0.6, 1.3, hash12(id + 7.3));
float taper = mix(1.0, 0.18, clamp(local.y / height, 0.0, 1.0));
float bend = sin(iTime * 2.6 + id.x * 0.7 + id.y * 0.9) * 0.10;
p.x -= bend * smoothstep(0.0, height, local.y);
float m = 1.0 - smoothstep(width * taper, width * taper + 0.02 + dist * 0.0015, abs(p.x));
m *= smoothstep(0.0, 0.08, local.y);
m *= 1.0 - smoothstep(height - 0.08, height, local.y);
return m;
}
vec3 grassAlbedo(vec3 p, vec3 n, vec3 rd, vec3 sunDir, float dist)
{
vec2 xz = p.xz;
float t = iTime;
float wind = windField(xz, t);
float large = fbm(xz * 0.12);
float medium = fbm(xz * 0.65 + wind * 0.4);
float fine = noise(xz * 7.0 + wind);
vec3 baseA = vec3(0.16, 0.30, 0.08);
vec3 baseB = vec3(0.30, 0.46, 0.14);
vec3 baseC = vec3(0.45, 0.58, 0.18);
vec3 col = mix(baseA, baseB, smoothstep(0.2, 0.8, large));
col = mix(col, baseC, smoothstep(0.45, 1.0, medium) * 0.45);
// Near-field blade-like striping in tangent space
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 tx = normalize(cross(up, n) + vec3(0.001,0.0,0.0));
vec3 tz = cross(n, tx);
vec2 local = vec2(dot(p, tx), dot(p, tz));
float bladeLayer = 0.0;
if (dist < 28.0)
{
vec2 cell = xz * 5.0;
float h1 = bladeMask(cell, vec2(fract(local.x * 3.0), fract(local.y * 2.0)), dist);
float h2 = bladeMask(cell + 17.37, vec2(fract(local.x * 2.1), fract(local.y * 2.4)), dist);
bladeLayer = max(h1, h2 * 0.8);
}
col *= 0.85 + fine * 0.30;
col += vec3(0.12, 0.18, 0.04) * bladeLayer;
// Yellowish dry variation
float dry = smoothstep(0.62, 1.0, noise(xz * 0.22 + 50.0));
col = mix(col, col * vec3(1.15, 1.02, 0.75), dry * 0.22);
// Wind sheen
float sheen = smoothstep(0.2, 1.0, wind * 0.5 + 0.5) * smoothstep(0.0, 0.8, fine);
col += vec3(0.08, 0.10, 0.04) * sheen * 0.4;
// Forward scattering style tint
float fwd = pow(max(dot(normalize(sunDir - rd * 0.4), n), 0.0), 4.0);
col += vec3(0.18, 0.20, 0.08) * fwd * 0.25;
return col;
}
float softShadow(vec3 ro, vec3 rd)
{
float res = 1.0;
float t = 0.1;
for (int i = 0; i < 28; i++)
{
vec3 p = ro + rd * t;
float h = mapTerrain(p);
res = min(res, 10.0 * h / t);
t += clamp(h, 0.08, 1.4);
if (h < 0.001 || t > 40.0) break;
}
return clamp(res, 0.0, 1.0);
}
vec3 render(vec3 ro, vec3 rd)
{
vec3 sunDir = normalize(vec3(0.55, 0.50, 0.35));
vec3 sky = skyColor(rd, sunDir);
vec3 pos;
float t;
if (!traceTerrain(ro, rd, pos, t))
return sky;
vec3 n = terrainNormal(pos);
float dist = length(pos - ro);
vec3 albedo = grassAlbedo(pos, n, rd, sunDir, dist);
float NoL = max(dot(n, sunDir), 0.0);
float shadow = softShadow(pos + n * 0.05, sunDir);
vec3 ambient = vec3(0.20, 0.28, 0.18) * (0.5 + 0.5 * n.y);
vec3 bounce = vec3(0.10, 0.09, 0.05) * smoothstep(-0.2, 0.3, n.y);
vec3 diff = vec3(1.15, 1.05, 0.90) * NoL * shadow;
// Specular glints for grass
vec3 h = normalize(sunDir - rd);
float spec = pow(max(dot(n, h), 0.0), 32.0);
float sparkle = noise(pos.xz * 18.0 + iTime * 0.5);
spec *= smoothstep(0.82, 1.0, sparkle) * 0.18 * shadow;
vec3 col = albedo * (ambient + bounce + diff);
col += vec3(1.0, 0.95, 0.80) * spec;
// Distant field desaturation / aerial perspective
float haze = 1.0 - exp(-dist * 0.018);
col = mix(col, sky, haze * 0.75);
// Ground occlusion in creases
float occ = clamp(0.55 + 0.45 * n.y, 0.0, 1.0);
col *= occ;
return col;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float t = iTime * 0.22;
// Camera
vec3 ro = vec3(0.0, 3.4, -8.0 + t * 6.0);
ro.x += sin(t * 0.7) * 4.0;
vec3 ta = ro + vec3(sin(t * 0.35) * 1.5, -0.5, 8.0);
// Mouse look
vec2 m = iMouse.xy / max(iResolution.xy, vec2(1.0));
float yaw = (iMouse.z > 0.0) ? (m.x - 0.5) * 2.4 : 0.18 * sin(iTime * 0.12);
float pit = (iMouse.z > 0.0) ? (m.y - 0.5) * 1.0 : -0.08;
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(vec3(0,1,0), ww));
vec3 vv = cross(ww, uu);
vec3 rd = normalize(uu * uv.x + vv * uv.y + ww * 1.8);
rd.xz *= rot(yaw);
rd.yz *= rot(pit);
vec3 col = render(ro, rd);
// Subtle vignette
float vig = 1.0 - dot(uv, uv) * 0.22;
col *= vig;
// Tonemap + gamma
col = col / (1.0 + col);
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}
Variations
Variation 1
ShaderToy
// Grass field with actual raymarched blade clumps
// Single-pass fragment shader.
//
// Notes:
// - This is substantially heavier than the previous version.
// - It raymarches terrain first, then raymarches local grass clumps in the near/mid field.
// - Distant grass is shaded procedurally for performance.
// - Mouse drag = look around.
//
// Recommended:
// 1) Run at 1/2 res on slower GPUs
// 2) Set iFrame<2 accumulation off if benchmarking
//
// Tuning:
// GRASS_DIST = near grass raymarch distance
// CLUMP_STEPS = quality / speed
// TERR_STEPS = terrain stability
//
#define PI 3.14159265359
#define FAR 160.0
#define TERR_STEPS 180
#define CLUMP_STEPS 72
#define GRASS_DIST 22.0
mat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); }
float hash11(float p){
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash12(vec2 p){
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p){
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031,0.1030,0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float noise(vec2 p){
vec2 i = floor(p);
vec2 f = fract(p);
f = f*f*(3.0-2.0*f);
float a = hash12(i + vec2(0.0,0.0));
float b = hash12(i + vec2(1.0,0.0));
float c = hash12(i + vec2(0.0,1.0));
float d = hash12(i + vec2(1.0,1.0));
return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);
}
float fbm(vec2 p){
float v = 0.0;
float a = 0.5;
for(int i=0;i<6;i++){
v += a * noise(p);
p *= 2.02;
a *= 0.5;
}
return v;
}
float terrainHeight(vec2 xz){
vec2 p = xz * 0.05;
float h = 0.0;
h += fbm(p * 0.90) * 2.8;
h += fbm(p * 2.20 + 11.3) * 0.8;
h += sin(xz.x * 0.030) * 0.7;
h += cos(xz.y * 0.023) * 0.6;
return h;
}
float mapTerrain(vec3 p){
return p.y - terrainHeight(p.xz);
}
vec3 terrainNormal(vec3 p){
float e = 0.1;
float h = mapTerrain(p);
vec2 k = vec2(e,0.0);
return normalize(vec3(
mapTerrain(p + vec3(k.x,0.0,k.y)) - h,
e,
mapTerrain(p + vec3(k.y,0.0,k.x)) - h
));
}
bool traceTerrain(vec3 ro, vec3 rd, out vec3 pos, out float t){
t = 0.0;
for(int i=0;i<TERR_STEPS;i++){
pos = ro + rd * t;
float d = mapTerrain(pos);
if(d < 0.0015 * max(1.0, t * 0.08)) return true;
t += max(0.04, d * 0.55);
if(t > FAR) break;
}
return false;
}
float windField(vec2 xz, float t){
float w = 0.0;
w += sin(xz.x * 0.22 + t * 1.8);
w += sin(xz.y * 0.17 + t * 1.4 + 1.2);
w += sin((xz.x + xz.y) * 0.09 + t * 2.3 + 4.0);
return w / 3.0;
}
vec3 skyColor(vec3 rd, vec3 sunDir){
float sunAmt = max(dot(rd, sunDir), 0.0);
vec3 horizon = vec3(0.72, 0.83, 0.95);
vec3 zenith = vec3(0.16, 0.37, 0.74);
vec3 col = mix(horizon, zenith, smoothstep(-0.18, 0.7, rd.y));
col += vec3(1.0,0.86,0.66) * pow(sunAmt, 64.0) * 0.42;
col += vec3(1.0,0.93,0.85) * pow(sunAmt, 512.0) * 0.22;
return col;
}
float softShadowTerrain(vec3 ro, vec3 rd){
float res = 1.0;
float t = 0.08;
for(int i=0;i<26;i++){
vec3 p = ro + rd * t;
float h = mapTerrain(p);
res = min(res, 10.0 * h / t);
t += clamp(h, 0.06, 1.2);
if(h < 0.0005 || t > 40.0) break;
}
return clamp(res, 0.0, 1.0);
}
// ------------------------------------------------------------
// Grass blade SDF
// ------------------------------------------------------------
struct GrassHit {
float d;
vec3 col;
float bladeId;
};
float sdSegment2(vec2 p, vec2 a, vec2 b){
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa,ba)/dot(ba,ba), 0.0, 1.0);
return length(pa - ba*h);
}
// local coords: x = width direction, y = height, z = thickness direction
float bladeSDF(vec3 p, float h, float wBase, float thick, float bend, float lean, float wav, out float vCoord)
{
// Curved centerline in x/y, with a little z flutter.
float y = clamp(p.y / h, 0.0, 1.0);
vec2 a = vec2(0.0, 0.0);
vec2 b = vec2(lean * 0.25, h * 0.33);
vec2 c = vec2(lean * 0.55 + bend * 0.25, h * 0.66);
vec2 d = vec2(lean + bend, h);
// cubic Bezier centerline sample by piecewise search
float best = 1e9;
float bestT = 0.0;
vec2 q = vec2(p.x, p.y);
vec2 prev = a;
for(int i=1;i<=12;i++){
float t = float(i) / 12.0;
float it = 1.0 - t;
vec2 cur =
it*it*it*a +
3.0*it*it*t*b +
3.0*it*t*t*c +
t*t*t*d;
float ds = sdSegment2(q, prev, cur);
if(ds < best){
best = ds;
bestT = t;
}
prev = cur;
}
vCoord = bestT;
float width = mix(wBase, wBase * 0.10, pow(bestT, 0.85));
float thickness = mix(thick, thick * 0.22, bestT);
// z flutter / torsion
float zOff = sin(bestT * 8.0 + wav) * 0.012 * (1.0 - bestT);
vec2 d2 = vec2(best, abs(p.z - zOff));
// Flattened elliptical ribbon
float ribbon = length(d2 / vec2(width, thickness)) - 1.0;
// Base burying into ground a little
ribbon = max(ribbon, -(p.y + 0.01));
// Sharper pointed tip
ribbon = max(ribbon, bestT - 1.02);
return ribbon * min(width, thickness) * 1.4;
}
GrassHit mapGrassClump(vec3 p, vec3 cellOrigin, vec2 cellId, float time, vec3 terrainN)
{
GrassHit res;
res.d = 1e9;
res.col = vec3(0.25,0.4,0.1);
res.bladeId = -1.0;
float clumpRnd = hash12(cellId + 1.7);
int bladeCount = 5 + int(floor(clumpRnd * 5.0)); // 5..9 blades
float wind = windField(cellOrigin.xz, time);
float slopeInfluence = 1.0 - terrainN.y;
for(int i=0;i<9;i++){
if(i >= bladeCount) break;
float fi = float(i);
vec2 rr = hash22(cellId * 7.13 + fi * 3.17);
float ang = rr.x * 2.0 * PI;
float rad = sqrt(rr.y) * 0.09;
vec3 bp = vec3(cos(ang)*rad, 0.0, sin(ang)*rad);
// local coords around base point
vec3 q = p - bp;
float yaw = ang + hash11(fi + clumpRnd * 10.0) * 0.7 - 0.35;
q.xz *= mat2(cos(yaw), -sin(yaw), sin(yaw), cos(yaw));
float h = mix(0.45, 1.15, hash11(fi*11.1 + clumpRnd*13.7));
float w = mix(0.010, 0.022, hash11(fi*4.7 + 7.0));
float thick = w * mix(0.24, 0.42, hash11(fi*8.1 + 2.4));
float bend = wind * mix(0.10, 0.28, hash11(fi*5.2 + 0.5));
bend += slopeInfluence * 0.10;
float lean = mix(-0.05, 0.22, hash11(fi*9.3 + 4.0));
lean += wind * 0.06;
float wav = fi * 1.7 + time * 1.8;
float vCoord;
float d = bladeSDF(q, h, w, thick, bend, lean, wav, vCoord);
if(d < res.d){
float dry = hash11(fi*2.3 + clumpRnd*19.0);
vec3 greenA = vec3(0.17, 0.32, 0.08);
vec3 greenB = vec3(0.30, 0.49, 0.13);
vec3 tipTint = vec3(0.64, 0.74, 0.22);
vec3 dryTint = vec3(0.58, 0.54, 0.22);
vec3 col = mix(greenA, greenB, 0.55 + 0.45 * hash11(fi + 1.2));
col = mix(col, tipTint, smoothstep(0.55, 1.0, vCoord) * 0.35);
col = mix(col, dryTint, smoothstep(0.7, 1.0, dry) * 0.22);
res.d = d;
res.col = col;
res.bladeId = fi;
}
}
return res;
}
GrassHit mapGrassPatch(vec3 p, float time)
{
GrassHit outHit;
outHit.d = 1e9;
outHit.col = vec3(0.25);
outHit.bladeId = -1.0;
// Project onto terrain local neighborhood.
vec2 cid = floor(p.xz * 2.5); // cell size ~0.4
for(int j=-1;j<=1;j++){
for(int i=-1;i<=1;i++){
vec2 id = cid + vec2(float(i), float(j));
vec2 centerXZ = (id + 0.5) / 2.5;
float th = terrainHeight(centerXZ);
vec3 base = vec3(centerXZ.x, th, centerXZ.y);
vec3 terrainN = terrainNormal(base);
// Small chance of sparse patch
float presence = hash12(id + 2.1);
if(presence < 0.18) continue;
vec3 lp = p - base;
GrassHit gh = mapGrassClump(lp, base, id, time, terrainN);
if(gh.d < outHit.d){
outHit = gh;
}
}
}
return outHit;
}
vec3 grassNormal(vec3 p, float time)
{
float e = 0.003;
GrassHit h0 = mapGrassPatch(p, time);
float dx = mapGrassPatch(p + vec3(e,0,0), time).d - h0.d;
float dy = mapGrassPatch(p + vec3(0,e,0), time).d - h0.d;
float dz = mapGrassPatch(p + vec3(0,0,e), time).d - h0.d;
return normalize(vec3(dx,dy,dz));
}
bool traceGrass(vec3 ro, vec3 rd, float tMin, float tMax, float time,
out vec3 hitPos, out vec3 hitCol, out vec3 hitN, out float tHit)
{
tHit = tMin;
for(int i=0;i<CLUMP_STEPS;i++){
vec3 p = ro + rd * tHit;
// Cull anything too high above terrain quickly
float terr = terrainHeight(p.xz);
if(p.y < terr - 0.05){
tHit += 0.06;
continue;
}
if(p.y > terr + 1.4){
tHit += 0.08;
continue;
}
GrassHit gh = mapGrassPatch(p, time);
if(gh.d < 0.0012){
hitPos = p;
hitCol = gh.col;
hitN = grassNormal(p, time);
return true;
}
tHit += clamp(gh.d * 0.7, 0.01, 0.18);
if(tHit > tMax) break;
}
return false;
}
float grassShadowApprox(vec3 ro, vec3 sunDir, float time)
{
float t = 0.03;
float occ = 1.0;
for(int i=0;i<18;i++){
vec3 p = ro + sunDir * t;
float terr = terrainHeight(p.xz);
if(p.y < terr) return 0.0;
if(t < 2.8){
GrassHit gh = mapGrassPatch(p, time);
occ = min(occ, 14.0 * gh.d / t);
}
float terrD = p.y - terr;
t += clamp(min(terrD, 0.25), 0.03, 0.25);
}
return clamp(occ, 0.0, 1.0);
}
// Distant field fallback
vec3 distantGrassAlbedo(vec3 p, vec3 n, vec3 rd, vec3 sunDir)
{
vec2 xz = p.xz;
float wind = windField(xz, iTime);
float a = fbm(xz * 0.12);
float b = fbm(xz * 0.65 + wind * 0.35);
float c = noise(xz * 8.0 + wind);
vec3 col = mix(vec3(0.16,0.28,0.08), vec3(0.29,0.45,0.13), smoothstep(0.2,0.8,a));
col = mix(col, vec3(0.46,0.60,0.20), smoothstep(0.55,1.0,b) * 0.35);
col *= 0.85 + 0.30 * c;
float fwd = pow(max(dot(normalize(sunDir - rd*0.5), n), 0.0), 4.0);
col += vec3(0.12,0.14,0.05) * fwd * 0.25;
return col;
}
vec3 shadeGrass(vec3 p, vec3 n, vec3 baseCol, vec3 rd, vec3 sunDir, float dist, float time)
{
float NoL = max(dot(n, sunDir), 0.0);
float back = max(dot(-n, sunDir), 0.0);
float sh = softShadowTerrain(p + n*0.01, sunDir);
float gsh = grassShadowApprox(p + n*0.01, sunDir, time);
sh *= gsh;
vec3 skyAmb = vec3(0.24,0.30,0.22) * (0.5 + 0.5 * clamp(n.y,0.0,1.0));
vec3 bounce = vec3(0.08,0.07,0.04);
// thin translucent blade look
vec3 trans = vec3(0.55,0.62,0.18) * back * 0.7;
vec3 h = normalize(sunDir - rd);
float spec = pow(max(dot(n,h), 0.0), 40.0) * 0.18;
vec3 col = baseCol * (skyAmb + bounce + NoL * sh * vec3(1.12,1.05,0.92));
col += trans * sh;
col += vec3(1.0,0.95,0.82) * spec * sh;
return col;
}
vec3 render(vec3 ro, vec3 rd)
{
vec3 sunDir = normalize(vec3(0.58, 0.52, 0.27));
vec3 sky = skyColor(rd, sunDir);
vec3 tPos;
float tTerr;
if(!traceTerrain(ro, rd, tPos, tTerr)){
return sky;
}
vec3 terrN = terrainNormal(tPos);
float grassMax = min(tTerr, GRASS_DIST);
vec3 gPos, gCol, gN;
float tGrass;
bool hitGrass = false;
if(grassMax > 0.0){
hitGrass = traceGrass(ro, rd, max(0.0, tTerr - 2.5), grassMax, iTime, gPos, gCol, gN, tGrass);
}
vec3 col;
if(hitGrass){
float dist = length(gPos - ro);
col = shadeGrass(gPos, gN, gCol, rd, sunDir, dist, iTime);
float haze = 1.0 - exp(-dist * 0.02);
col = mix(col, sky, haze * 0.55);
}
else{
vec3 p = tPos;
vec3 n = terrN;
float dist = length(p - ro);
vec3 albedo = distantGrassAlbedo(p, n, rd, sunDir);
float NoL = max(dot(n, sunDir), 0.0);
float sh = softShadowTerrain(p + n*0.03, sunDir);
vec3 amb = vec3(0.20,0.27,0.17) * (0.5 + 0.5*n.y);
vec3 diff = vec3(1.10,1.04,0.92) * NoL * sh;
col = albedo * (amb + diff);
float haze = 1.0 - exp(-dist * 0.018);
col = mix(col, sky, haze * 0.72);
}
return col;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float t = iTime * 0.22;
vec3 ro = vec3(0.0, 2.7, -7.0 + t * 5.5);
ro.x += sin(t * 0.8) * 3.0;
vec3 ta = ro + vec3(sin(t * 0.35) * 1.2, -0.25, 8.0);
vec2 m = iMouse.xy / max(iResolution.xy, vec2(1.0));
float yaw = (iMouse.z > 0.0) ? (m.x - 0.5) * 2.6 : 0.16 * sin(iTime * 0.15);
float pit = (iMouse.z > 0.0) ? (m.y - 0.5) * 1.1 : -0.10;
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(vec3(0,1,0), ww));
vec3 vv = cross(ww, uu);
vec3 rd = normalize(uu * uv.x + vv * uv.y + ww * 1.9);
rd.xz *= rot(yaw);
rd.yz *= rot(pit);
vec3 col = render(ro, rd);
float vig = 1.0 - dot(uv,uv) * 0.22;
col *= vig;
col = col / (1.0 + col);
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}