Turn on JavaScript to run this demo.

Shader Lessons and such

Shaders, taken apart.

Short, cozy lessons on the math inside shaders. Every idea comes with its code and a live demo running exactly that code, so go ahead and poke at it.

sunset.hlsl draws the sky above, 80 lines, open to read
// sunset.hlsl: the home page's dusk. A sky gradient, a striped sun, a grid running to the horizon
// and a heartbeat line along it. Everything is computed per pixel; nothing is an image.
// It paints in sRGB like a picture, so it turns its colors into light once, at the end.

// The sky, from the top of the screen down to the horizon (sRGB).
float3 sky(float t)
{
    float3 c = float3(0.086, 0.059, 0.169);                                  // #160f2b
    c = lerp(c, float3(0.165, 0.090, 0.322), smoothstep(0.00, 0.35, t));   // #2a1752
    c = lerp(c, float3(0.416, 0.247, 0.820), smoothstep(0.35, 0.70, t));   // #6a3fd1
    c = lerp(c, float3(0.784, 0.420, 1.000), smoothstep(0.62, 0.85, t));   // #c86bff
    c = lerp(c, float3(1.000, 0.435, 0.682), smoothstep(0.80, 0.95, t));   // #ff6fae
    return lerp(c, float3(1.000, 0.620, 0.478), smoothstep(0.93, 1.00, t)); // #ff9e7a
}

// A soft bump of width w around a, squared by hand: pow() of a negative number is undefined on a GPU.
float bump(float t, float a, float w)
{
    float x = (t - a) / w;
    return exp(-x * x);
}

// A heartbeat: flat, then a small dip, a tall spike and a dip, once per period.
float ecg(float x)
{
    float t = frac(x);
    return -0.25 * bump(t, 0.46, 0.012) + bump(t, 0.50, 0.010) - 0.40 * bump(t, 0.54, 0.012) + 0.18 * bump(t, 0.66, 0.040);
}

float4 shade(float2 uv)
{
    float aspect = _ScreenParams.x / _ScreenParams.y;
    float px = 1.0 / _ScreenParams.y;
    float horizon = 0.30;
    float2 p = float2(uv.x * aspect, uv.y);

    float3 col = sky(1.0 - (uv.y - horizon) / (1.0 - horizon));

    // The sun: a disc with stripes cut out of its lower half, wider towards the bottom.
    float2 sunC = float2(aspect * 0.70, horizon + 0.21);
    float r = 0.27;
    float d = length(p - sunC) - r;
    float h = (p.y - (sunC.y - r)) / (2.0 * r);
    float band = frac(h * 9.0);
    float cut = (1.0 - smoothstep(0.30, 0.55, h)) * 0.55;
    float stripe = smoothstep(cut - 0.02, cut + 0.02, band) + step(0.55, h);
    float3 sunCol = lerp(float3(1.000, 0.435, 0.682), float3(1.000, 0.886, 0.604), saturate(h * 1.2));
    float sun = saturate(0.5 - d / px) * saturate(stripe);
    col += float3(1.0, 0.45, 0.75) * 0.35 * exp(-6.0 * max(d, 0.0));
    col = lerp(col, sunCol, sun);

    // The ground: a grid in perspective, its rows sliding towards the viewer. The grid's pixel size is
    // measured before the if, since derivatives inside a branch are undefined where it changes.
    float depth = max(horizon - uv.y, 1e-4);
    float z = 0.08 / depth;
    float2 g = float2((p.x - aspect * 0.5) * z * 6.0, z * 2.0 + _Time.y * 0.25);
    float2 w = fwidth(g);
    if (uv.y < horizon)
    {
        float2 f = abs(frac(g) - 0.5);
        float2 lineD = (0.5 - f) / max(w, 1e-4);
        float grid = 1.0 - saturate(min(lineD.x, lineD.y) - 0.5);
        float3 ground = float3(0.114, 0.071, 0.220);
        float fade = exp(-depth * 10.0);
        col = lerp(ground, float3(1.0, 0.43, 0.68), 0.25 * fade);
        col = lerp(col, float3(1.000, 0.700, 0.870), grid * (0.35 + 0.65 * smoothstep(0.0, 0.25, depth)));
    }

    // The heartbeat along the horizon, with a brighter pulse travelling along it.
    float hx = p.x / aspect;
    float hy = horizon + 0.035 + 0.07 * ecg(hx * 1.4 - 0.18);
    float slope = (ecg((hx + 0.001) * 1.4 - 0.18) - ecg((hx - 0.001) * 1.4 - 0.18)) / 0.002 * 0.07 / aspect;
    float dl = abs(uv.y - hy) / sqrt(1.0 + slope * slope) / px;
    float pulse = (frac(_Time.y * 0.12) * 1.4 - hx) * 6.0;
    float travel = exp(-pulse * pulse);
    float lineMask = smoothstep(0.08, 0.2, hx) * (1.0 - smoothstep(0.92, 1.0, hx));
    col += float3(1.0, 0.56, 0.78) * lineMask * (saturate(1.6 - dl) + 0.35 * exp(-dl * 0.25)) * (0.6 + 0.8 * travel);

    return float4(srgbToLinear(saturate(col)), 1.0);
}

Series 01

Basics

The math and the Unity bits every other lesson leans on, so start here.

Series 02

Foundations

The handful of math tools every shader leans on.

Series 03

VRChat tricks

What a shader can find out inside VRChat, and how it can remember things.

Search the lessons

Type any word from the lessons, like normalize or smoothstep. The start of a word is enough.