Master the fundamental concepts of modern graphics apis (low level) through this focused micro-challenge.
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 worksA 3D vertex starts in model space (local to the object). Before rendering, it passes through several spaces, each with a transformation matrix:
The combined transform is v_clip = P * V * M * v_model. Column vectors apply right-to-left: M first, then V, then P.
Clip space coordinates have a homogeneous w component. The GPU divides by w to reach normalized device coordinates in [-1, 1]. Projection matrices encode field of view, aspect ratio, and near/far planes. Distant objects appear smaller because w grows with depth.
glslLoading…
For example, a vertex at model-space (1, 0, 0) transformed by translation +45 on X lands at world (46, 0, 0) before view and projection compress it into clip space.
You will implement 4x4 matrix-vector multiplication and the full MVP chain for a single vertex. This task requires you to print coordinates at each space. Getting multiplication order wrong (PVM vs MVP) is the most common bug on graphics forums, and this exercise forces you to trace the correct sequence.
Implement the vertex shader's classic job: model → world → eye → clip → NDC → window. Build the model matrix from translate/scale/rotate calls, the view matrix from lookAt, and the projection from perspective or ortho, using exactly the OpenGL (gluPerspective/gluLookAt) conventions. Transform vertices through the chain, reject those outside the clip volume, and map the rest to the viewport.
# starts a comment.
cLoading…
All matrices start as the identity.
M = M × T (like glTranslate), so the last call applies to the vertex first. The rotations are right-handed: roty 90 sends +x to -z, rotx turns +y toward +z, and rotz turns +x toward +y.f = normalize(center - eye), s = normalize(f × up), u = s × f. The rows are s, u, -f, with translation (-s·eye, -u·eye, f·eye).f = 1/tan(fovy/2), and the matrix rows are:
[f/aspect 0 0 0][0 f 0 0][0 0 (far+near)/(near-far) 2·far·near/(near-far)][0 0 -1 0]2/(r-l), 2/(t-b), -2/(f-n), and the translation is -(r+l)/(r-l), -(t+b)/(t-b), -(f+n)/(f-n).w ≤ 0, print behind the camera (w <= 0): clipped.|c| > w, print outside the view volume: followed by the failing axes ( x, y, z).window = ((x+1)/2·W, (y+1)/2·H, (z+1)/2).You may use <math.h> (tan, sqrt, …): the task compiles with -lm.
Numbers are printed with %.4f, except that a value with |x| < 0.00005 prints as 0.0000 (never -0.0000):
cLoading…
model:, view:, projection: or mvp = projection * view * model:, followed by 4 indented rows.model: translate X Y Z | scale X Y Z | rotx|roty|rotz DEG | resetlookat: eye and center coincide, lookat: up is parallel to the view directionperspective: 0 < FOVY < 180, ASPECT > 0, 0 < NEAR < FARortho: planes must differviewport: W H > 0 (it resets to 800 600)bad command: LINEInput:
cLoading…
Output:
cLoading…
M × v) and keep the multiplication order explicit. Most bugs in this stage are order bugs.Hidden tests cover composed rotation and scale, lookAt, a wide-screen perspective, points behind the camera and beyond the near/far planes, an orthographic projection, and invalid parameters.