Articles
WebGL Interview Questions: Coordinate Spaces, Buffers, and Draw Calls
Practice WebGL interview questions with coordinate-space traces, vertex buffer byte layouts, indexed draw calls, blank-canvas debugging, and context recovery.
A WebGL draw call can execute without an obvious API error and still produce a blank canvas. The interesting interview question is where the data disappeared: outside the clip volume, inside a misread vertex buffer, behind the depth buffer, or before a usable shader program existed.
This guide prepares you to trace that path with numbers. You will work through homogeneous coordinates, an interleaved vertex layout, indexed drawing, and a recovery plan for a lost context. The central habit is to identify the input and output contract of each stage before changing rendering code.
Evidence boundary: Official WebGL rules come from the Khronos WebGL specification and the linked browser API documentation. Worked examples and debugging priorities below are original exercises and engineering inference. No candidate reports are used, and these exercises do not establish any employer's interview format.
For a targeted starting point, practice graphics transformation reasoning and explain every intermediate coordinate aloud.
How do model, view, and projection coordinates differ?
An answer that says “multiply by the MVP matrix” leaves the main reasoning unfinished. Start with a vertex in the object's local coordinate system. A model transform places that object in the world. A view transform expresses the world relative to the camera. A projection transform produces the homogeneous clip coordinates written to gl_Position.
For the column-vector convention, the familiar expression is:
clip = projection * view * model * position
The rightmost transform acts first. A library using a different vector convention needs a correspondingly different expression; mixing conventions silently changes the result. Storage order and multiplication convention are related implementation choices, but saying “column-major” alone does not explain which transformation acts first.
Official documentation: MDN's model, view, and projection guide describes the transformation stages and division by the homogeneous component. WebGL does not supply your camera or scene graph. Your application or library constructs those transforms.
A useful follow-up is: “If moving the camera right makes everything move right on screen, what might be wrong?” Under a conventional view transform, you express the world in the camera's coordinate frame. Using the camera's world transform directly where its inverse is needed can reverse the intended motion. That diagnosis is an engineering hypothesis, not proof: check the actual matrix convention and a simple point before rewriting the camera.
Trace a clip-space point to a canvas pixel
Original coordinate ledger: Suppose the vertex shader outputs (1, -0.5, 0, 2). Use a viewport whose origin is (0, 0) and dimensions are 800 × 600 drawing-buffer pixels.
| Stage | Computation | Result |
|---|---|---|
| Clip coordinates | Preserve all four components | (1, -0.5, 0, 2) |
| Normalized device coordinates | Divide x, y, z by w | (0.5, -0.25, 0) |
| Window x | (0.5 + 1) × 800 / 2 | 600 |
| Window y | (-0.25 + 1) × 600 / 2 | 225 |
| DOM-oriented y, same dimensions | 600 - 225 | 375 |
The last row changes the coordinate convention for a DOM-oriented location; it does not change the rendered vertex. WebGL window coordinates use the lower-left convention. Pointer events typically refer to a different origin and may use CSS pixels. Account for the canvas's bounding rectangle and the ratio of drawing-buffer size to displayed size when mapping a pointer into the buffer.
Two mistakes deserve separate explanations. First, treating clip (x, y, z) as already normalized ignores w. Second, comparing clip x against [-1, 1] before division uses the wrong bound. In the ordinary positive-w case, the clip volume uses -w ≤ x, y, z ≤ w. Here the point survives. Changing x to 3 with w still 2 puts that point outside the x boundary.
Do not claim that one outside vertex makes an entire triangle disappear. A primitive crossing a clipping boundary may be clipped into a visible portion. Also distinguish x/y viewport mapping from depth-range mapping: the default depth range maps normalized z from [-1, 1] to [0, 1].
A strong answer gives the numbers, the origin, and the units. “Somewhere near the right side” cannot reveal a missing divide or a CSS-size mismatch.
What does vertexAttribPointer capture?
Official API behavior: vertexAttribPointer describes how an attribute reads the buffer currently bound to ARRAY_BUFFER when the pointer is specified. Later binding another buffer to ARRAY_BUFFER does not automatically retarget that previously configured attribute.
This matters in a renderer that alternates meshes. Binding mesh B's buffer and calling drawArrays is insufficient if the attribute configuration still refers to mesh A's buffer. You must configure the appropriate attribute state, or bind a vertex array object that already holds it. Vertex array objects are core in WebGL 2; WebGL 1 applications can use the relevant extension when available. Name the context version instead of assuming the same API everywhere.
Original byte-layout exercise: Each vertex contains six Float32 components: position x/y/z followed by color r/g/b. Each component occupies four bytes, so each vertex occupies 24 bytes.
| Vertex attribute | Components | Stride | Offset within vertex | First byte for vertex 1 |
|---|---|---|---|---|
| Position | 3 floats | 24 bytes | 0 bytes | 24 |
| Color | 3 floats | 24 bytes | 12 bytes | 36 |
Here “vertex 1” means the second vertex, using zero-based indexing. For attribute offset o, vertex index i, and stride s, its first byte is o + i × s.
// posLoc and colorLoc are active attribute locations.
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 24, 0);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 24, 12);
gl.enableVertexAttribArray(colorLoc);
Using stride 6 mistakes a component count for a byte count. Using color offset 3 makes the same mistake and violates the alignment needed for float components. Using stride 0 means tightly packed data for that attribute; it does not tell the API to infer the six-component interleaved record.
The normalized flag is another useful probe. It controls conversion of integer attribute components to normalized floating-point values in this API. It does not normalize the length of a three-dimensional vector. For these float components, it does not perform that geometric operation.
What does an indexed draw count actually count?
An element buffer stores indices that select vertices. It does not contain the position vectors themselves. With the same four-vertex quad, indices [0, 1, 2, 0, 2, 3] describe two triangles under TRIANGLES.
Official API behavior: drawElements takes a count of indices, an index component type, and an offset measured in bytes into the element buffer. The type must match how the uploaded index bytes are interpreted, and the offset must satisfy that type's alignment.
For a Uint16Array index buffer:
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
Six means six indices, not six bytes and not six distinct vertices. The example consumes twelve bytes of index data. An offset of two skips one unsigned-short index; an offset of one is misaligned. If you skip the first index, you must also reconsider the available count and the intended triangle grouping. Merely advancing the offset can change both safety and geometry.
UNSIGNED_INT element indices are core in WebGL 2; WebGL 1 requires the appropriate extension. A version-aware answer checks that support before choosing a 32-bit index array.
Do not confuse index reuse with automatic correctness. An index can select beyond the available attribute data, and a valid set of indices can still describe triangles with unwanted winding. Attribute layouts, element interpretation, and topology must agree.
Diagnose a blank canvas in a reproducible order
Engineering inference: Reduce the scene to one known triangle and one constant fragment color. This removes textures, lighting, and application data from the first diagnostic pass. It is a proposed debugging strategy, not a guaranteed order for every failure.
- Verify that a context exists and record whether it is WebGL 1 or 2. Check shader compile status and logs, then program link status and logs. MDN documents program parameter checks; a created program object is not evidence of a successful link.
- Confirm the active program and active attribute locations. An optimized-out attribute may return
-1; blindly using that as a real attribute index hides the wrong assumption. - Trace one vertex through its buffer bytes and transform. Use identity transforms first if that creates an independently understood result. Inspect actual numbers rather than changing several matrices at once.
- Check drawing-buffer dimensions and viewport. Resizing the canvas requires updating the intended viewport; CSS dimensions alone do not define the GPU's rendering resolution.
- Examine visibility state: culling and winding, depth testing and depth clearing, scissor bounds, blending, and color writes. A valid primitive can be discarded or produce an invisible contribution.
After each change, keep a small record of the hypothesis and observed result. For example, a triangle appearing after disabling culling implicates winding or culling state more directly than shader compilation. It still does not prove which upstream transform flipped the winding.
For a production failure that appears only after switching meshes, capture the attribute and element state around that transition. For one that appears only after resizing, compare drawing-buffer size, viewport, and projection aspect ratio. These triggers guide the next measurement.
What survives context loss?
Official lifecycle behavior: The webglcontextlost event signals a lost rendering context. A recovery-capable application handles that event, prevents its default behavior to permit restoration, and rebuilds its GPU resources after the restoration event. Restoration is not a promise that old buffers, textures, shaders, or programs remain usable.
Engineering inference: Keep CPU-side descriptions of assets and renderer configuration, or a reliable way to reload them. Stop the rendering loop while the context is lost. On restoration, recreate resources, re-establish state, and then resume one rendering loop. Avoid attaching a second animation loop while the original callback chain still exists.
A useful test deliberately loses the context through WEBGL_lose_context when the extension is available. Verify recovery after an asset upload, during resizing, and while the scene is idle. Assert that the recreated scene renders correctly and that input handlers and animation callbacks are not duplicated.
The numerical fixtures accompanying this article check coordinate mapping, byte offsets, and index consumption. They do not execute shaders or establish cross-browser rendering correctness. A real application needs browser/GPU integration checks, including context restoration.