Documentation

Shader Example: Moving Field

Procedural terrain and camera effects.

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);
}