Quick-reference cheat sheet for the math used throughout this guide. Keep this open while coding.
Vector: v = (x, y, z)
Addition: a + b = (a.x + b.x, a.y + b.y, a.z + b.z)
Subtraction: a - b = (a.x - b.x, a.y - b.y, a.z - b.z)
Scalar multiply: s * v = (s*v.x, s*v.y, s*v.z)
Negation: -v = (-v.x, -v.y, -v.z)
|v| = sqrt(v.x^2 + v.y^2 + v.z^2)
// 2D
|v| = sqrt(v.x^2 + v.y^2)
v_hat = v / |v|
// Result has magnitude 1. Direction is preserved.
// WARNING: check |v| > 0 before dividing!
a . b = a.x*b.x + a.y*b.y + a.z*b.z
// Geometric form:
a . b = |a| * |b| * cos(theta)
// Properties:
a . b = b . a (commutative)
a . b = 0 when a perp to b (orthogonality test)
a . a = |a|^2 (length squared)
Common uses:
- Lighting:
diffuse = max(0, N . L)where N is surface normal, L is light direction - Projection: length of a along b =
(a . b) / |b| - Angle between vectors:
cos(theta) = (a . b) / (|a| * |b|) - Front/back test:
N . V > 0means surface faces camera
a x b = (a.y*b.z - a.z*b.y,
a.z*b.x - a.x*b.z,
a.x*b.y - a.y*b.x)
// Geometric form:
|a x b| = |a| * |b| * sin(theta)
// Properties:
a x b = -(b x a) (anti-commutative!)
a x b is perpendicular to both a and b
|a x b| = area of parallelogram formed by a and b
Common uses:
- Surface normal:
N = normalize((v1 - v0) x (v2 - v0)) - Winding order: sign of cross product z-component (2D)
- Tangent frame construction
dist(a, b) = |b - a| = sqrt((b.x-a.x)^2 + (b.y-a.y)^2 + (b.z-a.z)^2)
// Often compare squared distances to avoid sqrt:
distSq(a, b) = (b.x-a.x)^2 + (b.y-a.y)^2 + (b.z-a.z)^2
// Reflect vector I around normal N (N must be unit length)
R = I - 2 * (I . N) * N
// For light reflection: if L points TOWARD the surface,
// the reflected direction is:
R = 2 * (N . L) * N - L
n1 * sin(theta1) = n2 * sin(theta2)
// Refracted direction (I and N are unit vectors, eta = n1/n2):
k = 1 - eta^2 * (1 - (N . I)^2)
if k < 0: total internal reflection (no refraction)
T = eta * I - (eta * (N . I) + sqrt(k)) * N
lerp(a, b, t) = a + t * (b - a) = (1 - t) * a + t * b
// t = 0 → a
// t = 1 → b
// t = 0.5 → midpoint
smoothstep(edge0, edge1, x):
t = clamp((x - edge0) / (edge1 - edge0), 0, 1)
return t * t * (3 - 2 * t)
// S-shaped curve: smooth transition from 0 to 1
// Derivative is 0 at both endpoints
All matrices are 4x4 for homogeneous coordinates. Points use w=1, directions use w=0.
T(tx, ty, tz) = | 1 0 0 tx |
| 0 1 0 ty |
| 0 0 1 tz |
| 0 0 0 1 |
S(sx, sy, sz) = | sx 0 0 0 |
| 0 sy 0 0 |
| 0 0 sz 0 |
| 0 0 0 1 |
R(theta) = | cos(theta) -sin(theta) |
| sin(theta) cos(theta) |
Rx(theta) = | 1 0 0 0 |
| 0 cos(theta) -sin(theta) 0 |
| 0 sin(theta) cos(theta) 0 |
| 0 0 0 1 |
Ry(theta) = | cos(theta) 0 sin(theta) 0 |
| 0 1 0 0 |
| -sin(theta) 0 cos(theta) 0 |
| 0 0 0 1 |
Rz(theta) = | cos(theta) -sin(theta) 0 0 |
| sin(theta) cos(theta) 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
// Rotate by angle theta around unit axis k = (kx, ky, kz):
R = cos(theta) * I + (1 - cos(theta)) * (k * k^T) + sin(theta) * K
where K = | 0 -kz ky | (skew-symmetric matrix of k)
| kz 0 -kx |
| -ky kx 0 |
// Camera at position eye, looking at target, with up vector
forward = normalize(target - eye)
right = normalize(forward x up)
newUp = right x forward
View = | right.x right.y right.z -(right . eye) |
| newUp.x newUp.y newUp.z -(newUp . eye) |
| -forward.x -forward.y -forward.z (forward . eye) |
| 0 0 0 1 |
// fov = vertical field of view in radians
// aspect = width / height
// n = near plane, f = far plane
f = 1 / tan(fov / 2)
P = | f/aspect 0 0 0 |
| 0 f 0 0 |
| 0 0 (f+n)/(n-f) 2*f*n/(n-f) |
| 0 0 -1 0 |
// After multiplying by P and dividing by w:
// x_ndc in [-1, 1]
// y_ndc in [-1, 1]
// z_ndc in [-1, 1]
// l, r = left, right
// b, t = bottom, top
// n, f = near, far
P = | 2/(r-l) 0 0 -(r+l)/(r-l) |
| 0 2/(t-b) 0 -(t+b)/(t-b) |
| 0 0 -2/(f-n) -(f+n)/(f-n) |
| 0 0 0 1 |
clip_pos = Projection * View * Model * local_pos
// Then perspective divide:
ndc = clip_pos.xyz / clip_pos.w
// Then viewport transform:
screen_x = (ndc.x + 1) / 2 * width
screen_y = (ndc.y + 1) / 2 * height
For a point P inside triangle (A, B, C):
P = u*A + v*B + w*C
where u + v + w = 1 and u,v,w >= 0
// Compute from areas:
u = area(P, B, C) / area(A, B, C)
v = area(A, P, C) / area(A, B, C)
w = 1 - u - v
// Using cross products (2D version):
area(A, B, C) = 0.5 * |(B-A) x (C-A)|
// Edge function method (efficient for rasterization):
edge01(P) = (P.x - v0.x) * (v1.y - v0.y) - (P.y - v0.y) * (v1.x - v0.x)
Uses: interpolating vertex attributes (color, normals, UVs, depth) across a triangle during rasterization.
P(t) = O + t * D where t >= 0
O = ray origin
D = ray direction (usually normalized)
Sphere: center C, radius r
Substituting into |P - C|^2 = r^2:
a = D . D (= 1 if D is normalized)
b = 2 * D . (O - C)
c = (O - C) . (O - C) - r^2
discriminant = b^2 - 4*a*c
if discriminant < 0: no intersection
if discriminant = 0: one intersection (tangent)
if discriminant > 0: two intersections
t = (-b +/- sqrt(discriminant)) / (2*a)
Use the smaller positive t for the nearest hit.
Hit point: P = O + t * D
Normal at hit: N = normalize(P - C)
Plane: N . P + d = 0 (or equivalently, N . P = d)
N = plane normal, d = distance from origin
t = -(N . O + d) / (N . D)
if N . D = 0: ray is parallel to plane (no intersection)
if t < 0: intersection is behind the ray
Triangle vertices: v0, v1, v2
Edge vectors: e1 = v1 - v0, e2 = v2 - v0
h = D x e2
a = e1 . h
if |a| < epsilon: ray parallel to triangle
f = 1 / a
s = O - v0
u = f * (s . h)
if u < 0 or u > 1: miss
q = s x e1
v = f * (D . q)
if v < 0 or u + v > 1: miss
t = f * (e2 . q)
if t < 0: miss (behind ray)
// Hit! Barycentric coordinates are (1-u-v, u, v)
For each axis i (x, y, z):
t_near_i = (box.min[i] - O[i]) / D[i]
t_far_i = (box.max[i] - O[i]) / D[i]
if D[i] < 0: swap(t_near_i, t_far_i)
t_enter = max(t_near_x, t_near_y, t_near_z)
t_exit = min(t_far_x, t_far_y, t_far_z)
if t_exit < t_enter: no intersection
if t_exit < 0: box is behind ray
Quick reference for the functions you'll use most in shaders.
abs(x) // absolute value
sign(x) // -1, 0, or 1
floor(x) // round down
ceil(x) // round up
fract(x) // x - floor(x), fractional part
mod(x, y) // x - y * floor(x/y)
min(a, b) // minimum
max(a, b) // maximum
clamp(x, lo, hi) // min(max(x, lo), hi)
mix(a, b, t) // a*(1-t) + b*t (same as lerp)
step(edge, x) // 0 if x < edge, else 1
smoothstep(e0,e1,x) // smooth Hermite interpolation
pow(x, y) // x^y
exp(x) // e^x
log(x) // ln(x)
sqrt(x) // square root
inversesqrt(x) // 1 / sqrt(x)length(v) // magnitude: sqrt(v.x^2 + v.y^2 + v.z^2)
distance(a, b) // length(b - a)
dot(a, b) // dot product
cross(a, b) // cross product (vec3 only)
normalize(v) // v / length(v)
reflect(I, N) // I - 2*dot(N,I)*N
refract(I, N, eta) // Snell's law refraction
faceforward(N,I,Nref) // N if dot(Nref,I)<0, else -Nsin(x), cos(x), tan(x)
asin(x), acos(x), atan(y, x)
radians(degrees) // degrees to radians
degrees(radians) // radians to degreestexture(sampler, uv) // sample 2D texture
textureLod(sampler, uv, lod) // sample at specific mip level
textureGrad(sampler, uv, ddx, ddy) // sample with explicit gradients
texelFetch(sampler, ivec2, lod) // fetch exact texel (no filtering)
textureSize(sampler, lod) // get texture dimensionssin^2(x) + cos^2(x) = 1
1 + tan^2(x) = sec^2(x)
sin(2x) = 2 * sin(x) * cos(x)
cos(2x) = cos^2(x) - sin^2(x)
= 2*cos^2(x) - 1
= 1 - 2*sin^2(x)
sin^2(x/2) = (1 - cos(x)) / 2
cos^2(x/2) = (1 + cos(x)) / 2
sin(a +/- b) = sin(a)*cos(b) +/- cos(a)*sin(b)
cos(a +/- b) = cos(a)*cos(b) -/+ sin(a)*sin(b)
sin(x) ≈ x
cos(x) ≈ 1 - x^2/2
tan(x) ≈ x
sin(x) = x - x^3/3! + x^5/5! - x^7/7! + ...
cos(x) = 1 - x^2/2! + x^4/4! - x^6/6! + ...
e^x = 1 + x + x^2/2! + x^3/3! + ...
if linear <= 0.0031308:
sRGB = 12.92 * linear
else:
sRGB = 1.055 * linear^(1/2.4) - 0.055
// Quick approximation: sRGB ≈ linear^(1/2.2)
if sRGB <= 0.04045:
linear = sRGB / 12.92
else:
linear = ((sRGB + 0.055) / 1.055)^2.4
// Quick approximation: linear ≈ sRGB^2.2
H in [0, 360), S in [0, 1], V in [0, 1]
C = V * S
X = C * (1 - |((H/60) mod 2) - 1|)
m = V - C
(R', G', B') based on H sector:
0-60: (C, X, 0)
60-120: (X, C, 0)
120-180: (0, C, X)
180-240: (0, X, C)
240-300: (X, 0, C)
300-360: (C, 0, X)
(R, G, B) = (R'+m, G'+m, B'+m)
pi = 3.14159265358979...
2*pi = 6.28318530717959... (full circle in radians)
pi/2 = 1.57079632679490... (90 degrees)
pi/4 = 0.78539816339745... (45 degrees)
1/pi = 0.31830988618379...
1/(2*pi) = 0.15915494309190...
sqrt(2) = 1.41421356237310...
1/sqrt(2) = 0.70710678118655...
e = 2.71828182845905...
golden = 1.61803398874989... (golden ratio)
// Degrees ↔ Radians
degrees = radians * 180 / pi
radians = degrees * pi / 180
F(theta) = F0 + (1 - F0) * (1 - cos(theta))^5
// F0 = reflectance at normal incidence
// Common F0 values:
// Water: 0.02
// Glass: 0.04
// Plastic: 0.04
// Diamond: 0.17
// Iron: (0.56, 0.57, 0.58)
// Gold: (1.0, 0.71, 0.29)
// Silver: (0.95, 0.93, 0.88)
D(h) = alpha^2 / (pi * ((N.H)^2 * (alpha^2 - 1) + 1)^2)
// alpha = roughness^2
// N = surface normal
// H = half vector = normalize(L + V)
G(N,V,L) = G1(N,V) * G1(N,L)
G1(N,X) = 2*(N.X) / ((N.X) + sqrt(alpha^2 + (1-alpha^2)*(N.X)^2))
f_spec = D * F * G / (4 * (N.L) * (N.V))
f = (1 - F) * (1 - metallic) * albedo/pi // diffuse (Lambert)
+ D * F * G / (4 * (N.L) * (N.V)) // specular (Cook-Torrance)
L_out = f * L_in * max(0, N.L)