⚡

Shader Code

Efficient GPU Mathematics

🦴💎💡✨🌀⚡
Roadmap topics covered here
Full roadmap →

Shader Code lessons

Interpolation Playground

Every curve maps t=[0,1] to [0,1] but with different motion feel. Linear is robotic. Ease-in accelerates. Ease-out decelerates. Smoothstep and smootherstep have zero derivatives at endpoints. Watch the dots move at different speeds through the same interval:

All curves map t=[0,1] to output=[0,1] but with different "feel". Watch the dots move at different speeds.

Coordinate Space Selection

Spaces
Object -> SDF, mesh ops Tangent -> normal maps View -> SSAO, fog World -> lighting, shadows Clip/NDC -> depth reconstruct, screen UVs
// Reconstruct world position from depth
vec2 uv = gl_FragCoord.xy / uResolution;
float depth = texture(uDepthTex, uv).r * 2.0 - 1.0;
vec4 clipPos = vec4(uv * 2.0 - 1.0, depth, 1.0);
vec4 worldPos = uInvViewProj * clipPos;
worldPos /= worldPos.w;  // perspective divide

FMA and Horner Method

Optimization
Standard: ax^3+bx^2+cx+d = 6 MUL + 3 ADD Horner: ((ax+b)x+c)x+d = 3 FMA -> 2x throughput

Branch Avoidance

GPU warps execute in SIMD lockstep. Divergent branches stall idle lanes. Replace conditionals with branchless math:

// BAD: warp divergence
if (dot(n, l) > 0.0) diffuse = albedo * NdotL;
else diffuse = vec3(0.0);

// GOOD: branchless
float NdotL = max(dot(n, l), 0.0);
vec3 diffuse = albedo * NdotL;

// Patterns: step(), mix(), clamp()
float mask = step(0.0, x);      // x>=0 ? 1 : 0
vec3 result = mix(a, b, mask);   // select

Smoothstep vs Smootherstep

smoothstep is C1 (zero 1st derivative at endpoints). smootherstep is C2 (zero 1st AND 2nd derivatives) -- critical for seamless noise normals:

GPU Warp Divergence Simulator

32 GPU threads execute in lockstep. When a branch causes divergence, non-taken threads idle — wasting cycles. Toggle between branching and branchless to see the cost:

GPU Warp: 32 threads SIMD lockstep
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Step 0: ALU: NdotL = dot(N,L)
Cycles: 7 (serialized) 29% slower

Texture Fetch Latency

Dependent texture reads stall the pipeline — the 2nd fetch needs the 1st result. Optimized code fires textures early, does ALU while waiting, and packs channels:

GPU Pipeline Timeline
TEX: sample(uv)
(stall)
ALU: compute UV2
TEX: sample(uv2)
0400 cycles
Dependent read: 2nd tex needs 1st result. Stall. 400 cycles.
Curves
smoothstep(t) = 3t^2-2t^3 C1 smootherstep(t) = 6t^5-15t^4+10t^3 C2

Precomputed BRDF LUTs

Split-Sum
F = texture(brdfLUT, vec2(NdotV, roughness)) 1 tex fetch replaces ~30 ALU ops

Precision

fp16 vs fp32
fp16: colors, UVs, normals, particles -> 2x bandwidth fp32: world pos, depth, shadows -> full precision Pack: roughness/metallic/AO in one RGBA -> 1 fetch
// inversesqrt = 1 SIMD instruction
vec3 n = v * inversesqrt(dot(v, v)); // fast normalize

// Fire texture early, ALU while waiting
vec4 albedo = texture(uAlbedo, uv);  // fetch
vec3 normal = computeNormal(uv);      // ALU
vec3 color = albedo.rgb * light(normal);

// Channel packing
vec4 orm = texture(uORM, uv);
float ao = orm.r;
float roughness = orm.g;
float metallic = orm.b;
← 🌀 Procedural
Interactive lessonCanvas visualizersMathematical notation