Master the fundamental concepts of modern graphics apis (low level) 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 worksForward rendering draws each object and lights it in the same pass. With 100 lights and 1000 objects, you risk 100,000 lighting calculations, most wasted because each light affects only a small screen region.
Deferred rendering (Michael Deering, 1988; popularized by Killzone 2) splits work into two passes:
Lighting cost depends on screen resolution and light count, not triangle count. A million-triangle scene with few lights runs as fast as a hundred-triangle scene.
cLoading…
For example, a dynamic point light touches maybe 5% of screen pixels; deferred shades only those instead of every object surface. Tradeoffs include high G-buffer bandwidth, difficult MSAA, and transparency requiring a forward pass afterward. Modern engines add tiled deferred to cull lights per screen tile.
You will simulate the geometry pass by populating a tiny G-buffer from vertex data. This task asks you to write position, normal, and albedo per pixel without computing lighting. The G-buffer layout you build here is what RenderDoc shows when debugging deferred renderers in Unreal or id Tech.
Implement deferred shading in two passes:
Report the numbers that make deferred shading worthwhile: fragments rasterized, pixels covered, overdraw, and light evaluations compared with forward shading, which lights every fragment, including hidden ones.
# starts a comment.
cLoading…
ay == by && bx > ax) or a left edge (by < ay).z = round_half_up(Σ wᵢzᵢ / Σwᵢ). The fragment is kept if z < the stored depth, which starts at 1000. Every covered pixel centre counts as a fragment.geometry pass: T triangle(s), F fragment(s), P pixel(s) covered, plus , overdraw F/P to 2 decimals (half up) when P > 0.For each covered pixel, with its triangle's normal n and albedo a:
sum = ambient × 10000 + Σ max(0, n·l) × intensity over the lights, with no normalization (the vectors are assumed unit ×100);lit = min(255, a × sum / 1000000), using integer division.lighting pass: P pixel(s) x L light(s) = P·L evaluations (forward shading: F·L).show prints the G-buffer, one row per line, each ending with |:
. for empty;'0' + z/100;" .:-=+*#%@"[lit × 10 / 256] (empty pixels have lit 0).cLoading…
probe prints pixel (x, y): empty or the line above.
Errors:
gbuffer: W (1-24) H (1-16) (a valid resize requires a new render)ambient: PERCENT (0-100)light: DX DY DZ INTENSITY%, light: at most 4 lightstri: X0 Y0 Z0 X1 Y1 Z1 X2 Y2 Z2 NX NY NZ ALBEDO, tri: at most 16 trianglesshow: id|depth|albedo|lit, show: render firstprobe: X Y inside the G-buffer, probe: render firstunknown command XInput:
cLoading…
Output:
cLoading…
Hidden tests cover three lights including a back-facing one, saturation at 255, surfaces facing away from all lights (ambient only), depth ordering, the depth and albedo views, an empty scene, and argument errors.