Symptoms
Start at the bug, not at lesson one.
30 things graphics code does wrong, in the words people use when it happens. Every row ends in a link that does not describe the bug — it reproduces it, on your screen, with the arithmetic that predicted it printed beside it.
Two flat surfaces flicker and tear into each other where they touch.
You think: The two surfaces are too close together. I need to push them apart.
Depth precision is spent by the near plane, not by the model. At near = 0.02 the smallest gap the buffer can resolve 80 units out is 1.9e-2, and the panels are 0.002 apart. Pull near out to 1 and that gap shrinks fiftyfold, with nothing in the scene touched.
I switched to a linear workflow and everything went washed-out and grey.
You think: sRGB and linear are two spellings of the same numbers, so the conversion is cosmetic.
The number 0.5 in an sRGB image is about a fifth of the light. Treating it as half is the whole error, and it moves every midtone in the frame.
Reproduce itLab 8, Colour & Gamma
My lighting goes wrong the moment I scale the object.
You think: A normal is a direction, so the model matrix moves it the way it moves the vertices.
An uneven scale tilts normals off the surface they belong to. They transform by the inverse-transpose of the upper-left 3x3, which is a different matrix from the one the positions use.
Reproduce itLab 4, Light & Normals
My texture turns into shimmering static in the distance.
You think: The texture is too low-resolution. A bigger one will settle it down.
Out there one pixel covers dozens of texels. Point-sampling picks whichever one the pixel centre landed in, so the answer changes every time the camera moves a fraction of a pixel — a bigger texture makes it worse.
My frame rate collapses and my triangle count is tiny.
You think: The GPU is the bottleneck. I need fewer triangles.
Ten thousand cubes are 120,000 triangles whether you draw them in one call or ten thousand. The cost that moved is on the CPU, and it counts calls.
My object orbits the origin instead of spinning where it stands.
You think: I rotate and then translate, in the order I wrote the two lines.
A matrix chain reads right to left, so the last one written is the first one applied. T·R turns the object where it stands; R·T moves it first and then swings the whole thing round the origin.
Reproduce itLab 1, The Model Matrix
My transparent panes disappear when I look at them from a different angle.
You think: Transparency is a blend mode. Switch it on and the hardware handles the rest.
With depth writing on, whichever pane draws first stamps the depth buffer and everything behind it is discarded before it can blend. The panes vanish by array order, not by where they are.
Things vanish outright when the camera gets close, instead of fading.
You think: Near and far are a visibility range, like fog.
They are a clip test. A triangle crossing the near plane is sliced flat where it crosses, and one entirely inside it is simply gone — there is no partial credit at either end.
My shader gives me a black screen and no error anywhere.
You think: It compiled, so the problem must be in my geometry or my uniforms.
The driver does report it, with a message and a line number. Nothing in WebGL shows it to you unless you ask for the info log, so a compile failure and a working shader that draws black look identical.
Reproduce itLab 10, Write a Shader
I pulled the far plane in and the z-fighting is still there.
You think: The depth range is too big. Shrinking it from the far end will buy precision back.
Drag the far plane from 400 to 1000 and the smallest resolvable gap moves by 0.008 per cent. It is the control everyone reaches for first and very close to the least effective one available.
My specular highlight covers half the object and the whole thing looks like plastic.
You think: The highlight is too strong. I should turn the specular colour down.
Strength and size are two different numbers. Brightness is the specular term; size is the exponent, and at a shininess of 4 the highlight spreads across the entire sphere however dim you make it.
Reproduce itLab 4, Light & Normals
My texture is a smear of blurry squares when the camera is right up against it.
You think: Linear filtering is the better setting, so it should be on everywhere.
Magnification and minification are two different failures. Up close one texel covers many pixels, and linear filtering spends that whole span fading between four texels — nearest keeps the edge instead.
There is a visible band across my ground where the texture suddenly changes sharpness.
You think: Something is wrong with how my mipmaps were generated.
That is the handover between two mip levels, and it is exactly where a mip chain shows its seams. Blending across the boundary is what the second “linear” in trilinear buys.
My ground texture goes soft as soon as I look along it rather than down at it.
You think: Mipmapping is over-eager and I should bias the level selection.
At a shallow angle the pixel footprint is long and thin. One mip level has to cover both axes, so it is chosen for the long one and the short one is blurred far more than it needed to be. That gap is what anisotropic filtering buys back.
The edge of my texture is smeared into long streaks across the surface.
You think: My UVs are broken outside 0 to 1.
The UVs are fine; the wrap mode decides what is out there. Under clamp every coordinate past the edge returns the edge texel, stretched to wherever the coordinate asked.
My smooth mesh renders as flat facets, or the highlight jumps between vertices.
You think: The normals in my mesh are wrong.
They may be fine. Flat, Gouraud and Phong differ only in where the lighting equation runs — per face, per vertex, per pixel — and a highlight smaller than a triangle exists only in the last one.
Reproduce itLab 4, Light & Normals
Half my object is pure black and the line between lit and unlit is razor sharp.
You think: The light is not reaching round far enough.
Diffuse is a clamped cosine. It reaches zero at ninety degrees and stays there, so the line is the terminator and with no ambient there is nothing underneath it.
Reproduce itLab 4, Light & Normals
Turning the light up blows my highlights to flat white with no shape left in them.
You think: Doubling the light value doubles the light.
Not on encoded numbers. In the wrong space the error changes sign with intensity: too dark at the low end, clipped to white at the high end, while the same scene done in linear light still has shape in the highlight.
Reproduce itLab 8, Colour & Gamma
My lighting is dark and muddy through the midtones and I cannot say why.
You think: Multiplying a texture colour by a light value is multiplying light.
Those numbers are sRGB-encoded, so the multiply lands on the encoding rather than on the light, and the error is largest exactly through the midtones where the encoding curve is steepest.
Reproduce itLab 8, Colour & Gamma
Objects at the edge of my screen look stretched and wrong, like a fisheye lens.
You think: Field of view is a zoom control.
It is the width of the frustum. Widen it and the same object at the edge is projected across far more screen than the one in the centre — the stretch is the perspective divide doing precisely what it should.
Nothing in my scene gets smaller with distance. It looks like a blueprint.
You think: The projection matrix is what makes distant things small.
The matrix only loads the distance into w. The divide by w happens in hardware afterwards, and an orthographic matrix leaves w at 1, so there is nothing there to divide by.
I mirrored my model and now it renders inside out, or disappears entirely.
You think: A negative scale is a scale like any other.
It is a reflection, and it is the one transform that changes which way a face points. Winding order flips with it, so back-face culling starts throwing away the front.
Reproduce itLab 1, The Model Matrix
My picture is vertically flipped, or my mouse coordinates are.
You think: Screen space and clip space disagree about y for some historical reason.
They disagree because a window counts rows down from the top and the maths counts up from the bottom. The viewport transform is the one step that performs the flip, and it is the last one.
A vertex lands somewhere impossible and I cannot tell which matrix did it.
You think: The matrices are one lump. If the picture is wrong, one of them is wrong.
They hand over at named boundaries, and the numbers are readable at each one. Clip space is where w stops being 1 — before the divide, after the projection — which is the boundary most bugs sit on.
My translation is in the wrong three floats and I cannot tell if my matrices are row- or column-major.
You think: The layout in memory is the layout it is printed in.
Printed as a matrix the translation reads across the bottom row; in memory it is elements 12, 13 and 14, because the four columns are stored end to end. Same sixteen floats, two different pictures of them.
Reproduce itLab 1, The Model Matrix
I turned depth writes off and my transparency is still wrong from some angles.
You think: Depth writing was the problem. That was the fix.
That was half of it. Blending is order-dependent, so the translucent surfaces still have to be drawn back to front — and which one is at the back changes every time the camera moves.
Instancing did not help, because every object needs its own uniform data.
You think: Per-object data means a buffer and a bind per object.
It means one buffer, one bind group, and a byte offset that moves per draw. The offsets are 256-byte aligned, so the packing is coarser than the data — and it is still one bind.
Stepping a hundred thousand particles on the CPU costs more than drawing them.
You think: The simulation has to run on the CPU because that is where the loop is.
A compute shader is a stage that draws nothing and writes memory the GPU already owns. The particles never touch the CPU, which then issues one dispatch and one draw whatever the count is.
The last few items in my buffer never update, or I get garbage past the end of it.
You think: A dispatch of ceil(n / 64) workgroups runs n invocations.
It runs a multiple of 64. Ask for 100,001 particles and the dispatch is 1,563 workgroups covering 100,032 invocations — the 31 extra must return early, or they read and write past the end.
I passed a value into my shader and every pixel got the same one.
You think: A uniform varies per pixel, the way the coordinate does.
It does not, which is what uniform means. Every invocation in the draw is handed the identical value; the coordinate is the only thing that differs, and everything per-pixel has to be computed from it.
Reproduce itLab 10, Write a Shader
Why the links carry state
Every lab on this site keeps its controls in the address bar, so any configuration of any figure is a URL. That is what makes the reproduce links possible: one opens the depth lab with the near plane already at 0.02, another opens the shading lab with the normals already transformed by the wrong matrix. The failure is on screen before you have read a word of the essay around it.
The 10 labs these 30 rows point into are essays with the figures built into them, and each one is written to repair a single wrong prediction rather than to cover a topic. If none of the sentences above is yours, the reading path puts them in an order, the labs are the full list, and the glossary is the vocabulary underneath both.