Vectors & spaces
N is the unit surface normal, L points to the light, V points to the camera, and R is the reflected light direction. Use the same coordinate space for every vector and position.
The screenshots are from this project. The equations and GLSL below explain standard shading models; the snippets are illustrative, and cel thresholds are example values.
N is the unit surface normal, L points to the light, V points to the camera, and R is the reflected light direction. Use the same coordinate space for every vector and position.
In world space, transform normals with N = normalize(transpose(inverse(mat3(model))) * normal). The inverse transpose preserves perpendicularity under non-uniform scale. Normalize again after interpolation.
Vertex buffers store positions and normals; index buffers reuse vertices. The vertex shader transforms geometry, and the fragment shader evaluates the lighting. These simplified examples omit attenuation, shadows, and output tone mapping.
A matrix changes every vertex using the same rule. Its columns tell you where the basis vectors e₁ and e₂ go; all other points follow as combinations of those vectors. Edit the target matrix or choose a preset, then scrub Progress to see the grid and model deform together.
The controls define the target matrix M. The animation interpolates its entries from the current matrix to that target. The live determinant measures signed area scale: zero collapses the plane, and a negative value reverses orientation.
Click the grid to place connected vertices. Start a new stroke for separate edges, or close a shape to join its last point to its first. Draw a cube outline, a letter, or any polygon, then watch every point follow the matrix.
Points snap to a quarter unit. Keyboard: arrow keys move the cursor; Enter or Space adds a point.
The cube has three-dimensional vertices. Here the 2 × 2 matrix transforms x and y while z stays unchanged; a fixed oblique projection makes its depth visible. Drawn models lie in the XY plane. The cyan outline keeps the original geometry visible while the pink edges show the current transformed positions.
OpenGL extends this idea with homogeneous coordinates and 4 × 4 matrices. The model matrix places an object in the world, the view matrix expresses it relative to the camera, and the projection matrix maps it into clip space. After the perspective divide, rasterization turns triangles into covered pixels.
With column vectors, the rightmost matrix acts first. A 2 × 2 linear transform fixes the origin; the extra homogeneous coordinate lets a 4 × 4 matrix also encode translation. The visual transition between matrices illustrates their effect and can change scale even between pure rotation targets.
After vertex transformations, rasterization determines which samples a triangle covers. This simplified screen-space demo follows its edges, bounds the area to test, and checks each pixel center. Drag the vertices or edit their coordinates, then use Step to follow coverage or Rasterize to animate it.
For an edge from A to B, E(P) = (B.x − A.x)(P.y − A.y) − (B.y − A.y)(P.x − A.x). After normalizing winding, a covered sample lies on the interior side of all three edges. Boundary samples use a top-left tie rule so adjacent triangles do not both own a shared edge. This demo isolates coverage; depth testing, attribute interpolation, and fragment shading follow separately.
Phong reflection combines ambient, diffuse, and specular light. The sphere shows a continuous transition from light to shadow and a concentrated highlight; the cube shows how the lighting responds to each face normal.
C is the resulting RGB color. A, D, and S are the ambient, diffuse, and specular RGB colors; ka, kd, and ks control their contributions. Products with RGB colors are applied component by component.
vec3 N = normalize(normal);
vec3 L = normalize(lightPos - fragPos);
vec3 V = normalize(cameraPos - fragPos);
vec3 R = reflect(-L, N);
float d = max(dot(N, L), 0.0);
float s = d > 0.0
? pow(max(dot(R, V), 0.0), shininess)
: 0.0;
vec3 color = ka * ambientColor
+ kd * diffuseColor * d
+ ks * specularColor * s;The incident ray passed to reflect is −L because L points from the surface toward the light. Evaluate these terms per fragment using a normalized, interpolated normal for smooth shading.
Adjust each contribution and the light direction to see how the equation builds the final image. Switch between a sphere, cube, and torus; drag any view to rotate all four together. Switch between smooth Phong lighting and stepped cel shading to compare the same lighting terms. This grayscale example uses unit light colors and a directional light.
Cel shading maps a continuous lighting signal into a small set of tones. The sphere makes the stepped bands and sharply bounded highlight easy to see. The cube demonstrates the same stylized treatment on planar faces.
Q is a quantization function: it replaces a smoothly changing diffuse intensity with fixed output levels. A threshold τ turns the Phong specular signal s into a crisp highlight, with the same front-facing-light check as above.
The thresholds select where each band begins; the output values select how bright each band appears. This is one illustrative mapping, not a measurement of the screenshot shader.
// Use the same N, L, V, R, d and s
// calculated in the Phong example.
float q = d < 0.25 ? 0.15
: d < 0.50 ? 0.40
: d < 0.75 ? 0.70 : 1.0;
float h = d > 0.0 ? step(0.5, s) : 0.0;
vec3 color = ka * ambientColor
+ kd * diffuseColor * q
+ ks * specularColor * h;step(0.5, s) returns zero below the example threshold and one at or above it. Adjusting the threshold changes the highlight size; adjusting the diffuse bands changes the visual style.
Cel shading does not require outlines. An outline or rim-light pass can be added separately; neither is assumed by these equations.