
- Part 15 min dose
- Beginner
Turn on JavaScript to run this demo.
Shader Lessons and such
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: 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
The math and the Unity bits every other lesson leans on, so start here.








Series 02
The handful of math tools every shader leans on.






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


