Master the fundamental concepts of software rasterizer through this focused micro-challenge.
You have read the whole brief, and the concepts above stay free on every task. Writing and running the code needs a plan.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksWhen multiple triangles overlap on screen, which one is visible? The z-buffer (depth buffer), invented independently by Wolfgang Straßer and Edwin Catmull in 1974, solves this with a second buffer parallel to the color framebuffer.
Algorithm:
The z-buffer operates in screen space after vertex transformation. Each vertex carries (x, y, z) in normalized device coordinates where z typically runs 0 (near) to 1 (far).
cLoading…
For example, a red triangle at z=0.3 and a blue triangle at z=0.7 both covering pixel (5,5): the red triangle wins because 0.3 < 0.7. Z-fighting occurs when two surfaces are nearly equal in depth; limited float precision makes them flicker. Perspective-correct interpolation uses 1/z, not linear z, for accurate results under projection.
You will implement a 10x10 z-buffer with two overlapping triangles and print the final framebuffer colors. This task requires depth testing on every covered pixel. Z-buffering is the visibility algorithm behind Quake through Unreal Engine 5, and z-fighting bugs appear in every graphics programmer's career eventually.
Implement a z-buffer with a realistic twist: depth is stored as an N-bit integer, so it has limited precision. Rasterize triangles in order. Each fragment interpolates its depth exactly, quantizes it to the buffer's format, and passes only if it wins the depth compare. With enough bits, intersecting triangles resolve cleanly. With too few, surfaces that are close together quantize to the same value and the result depends on the compare function: that is z-fighting.
# starts a comment line.
cLoading…
ay == by && bx > ax, left edge by < ay. Triangles of either winding are drawn, and zero area prints NAME: degenerate.Σ wᵢzᵢ / Σwᵢ (a fraction, in 1/10000 units). The stored value is floor(depth × (2^bits - 1) / 10000). The clear value is quantized the same way.q < stored (less), q ≤ stored (lequal) or q > stored (greater). A passing fragment writes its quantized depth and its triangle's letter.q == stored counts as an equal depth, whether it passed or not.depthbits and canvas clear the buffers. Settings persist between draws.cLoading…
draw prints its header, one line per triangle, then the canvas (. where nothing was drawn).fragment(s), equal depth(s).canvas: W (1-64) H (1-32), depthbits: 8, 16, 24 or 32, compare: less|lequal|greater, clear: DEPTH (0-10000)tri: NAME(one letter) X0 Y0 Z0 X1 Y1 Z1 X2 Y2 Z2, tri: depth 0-10000, coordinates within +-1000, tri: at most 8 trianglesprobe: X Y inside the canvasunknown command XInput:
cLoading…
Output:
cLoading…
numerator × (2^bits-1) can exceed 32 bits).Hidden tests cover nearly coplanar surfaces at 8, 16 and 24 bits, lequal resolving ties the other way, a reversed-Z setup (greater with the buffer cleared to 0), probes, and argument errors.