TA logoThe Technical Artist
Home/Shaders/Debugging Shaders
shaders - working
debugging
method beats memory

Debugging Shaders That Look Wrong

Workingtutorial~4 min readreviewed 2026-07-05

You can't printf a pixel - but you can make the pixel show you its variables. Shader debugging is three techniques and a catalogue of known culprits. Learn the techniques; the catalogue accumulates on its own.

Technique 1 - Output The Suspect

The pixel shader's one superpower as a debugger: it already displays a value on screen every frame - you just choose which value. Suspect the mask? Wire the mask straight into Emissive/Base Color and look at it. The screen becomes your inspector:

HLSL - visualizing intermediates
return float4(mask.xxx, 1); // grayscale: is the mask what I think?
return float4(uv, 0, 1); // UVs as red/green - the classic
return float4(normalWS * 0.5 + 0.5, 1); // normals, remapped to visible range
return float4(frac(worldPos / 100), 1); // world position as repeating bands

Read the diagnostics like an X-ray: a UV view that isn't a smooth red/green gradient means broken UVs, not a broken shader. A normal view with hard facets means the mesh normals are the problem, four layers before your fresnel. Half of "shader bugs" turn out to be input bugs, and this technique finds them in seconds. (Values can exceed the visible 0-1: multiply down or frac them to inspect big ranges.)

Technique 2 - Bisect The Graph

When the material is a hundred nodes and something's wrong somewhere: cut it in half. Output the value at the midpoint (technique 1). Looks right? The bug is downstream. Wrong? Upstream. Repeat. Seven checks find one bad node in a hundred - the same binary search from the interview bank story, because it's genuinely how working TAs debug. Corollary: build complex materials with checkpoints - named reroute nodes or function boundaries at sane midpoints make future bisection instant.

Technique 3 - The Minimal Repro

Bug survives bisection ambiguity, or appears only "sometimes"? Rebuild the smallest material that shows it: new material, the suspect feature alone, default cube. If the bug vanishes, the problem is an interaction - add pieces back one at a time. If it persists, you now hold a five-node repro instead of a hundred-node haystack - which is also precisely the artifact a rendering engineer needs if the answer turns out to be "engine bug." Attach it to the ticket; be the TA whose reports engineers love.

The Catalogue Of Classic Wrong-Looks

SymptomPrime suspectThe fix
Washed-out or too-dark data (roughness feels wrong, masks mushy)sRGB on a data texture - gamma silently appliedTexture settings -> sRGB off; use linear formats for data. (The one-breath answer from the interview bank.)
Bumps look pushed in, not outNormal map green channel convention (GL vs DX)Flip Green Channel on import - verify with the PBR Map Generator's lit preview
Black splotches that spread - sometimes across the whole screen once bloom grabs itNaN: sqrt/pow of a negative, or divide by zerosaturate() before sqrt/pow; max(d, 0.0001) on divisors. NaN spreads through every op that touches it, so guard at the source
Lighting has faceted seams along UV islandsTangent space: mismatched tangents/binormals between bake and engine (or missing tangent import)Re-import with correct tangent settings; bake and render must agree on tangent basis (MikkTSpace everywhere is the modern peace treaty)
Fine patterns shimmer/crawl at distanceNo mips (or aliasing high-frequency detail in the shader)Enable mips; move detail into textures (which mip) rather than procedural high-frequency math (which doesn't)
Banding in gradients, especially dark ones8-bit precision, often via a low-bit render path or texture formatHigher-precision format for the gradient source, or break the band edges with a hair of noise/dither
Works on PC, broken on mobile/QuestHalf-precision floats: mobile runs half where PC ran floatBig numbers (world pos, time) overflow half - frac time down, localize world coordinates before the math
Flickers only when the camera movesZ-fighting (coplanar faces) or temporal AA eating a hard-edged effectSeparate the surfaces; for TAA shimmer, ease hard thresholds with smoothstep and mind dithered opacity

When It's Not Wrong, Just Slow

Different discipline, same instrumentation instinct. In Unreal: Shader Complexity view (Alt+8) for the heat map - green is fine, red means this material or its overdraw needs a conversation; material stats (instruction counts, samplers, and the permutation count from the previous page); and the GPU profiler to confirm the material actually matters - a 300-instruction shader on a distant prop is noise, the same shader on fullscreen water is the frame. The order is always: see the cost, then optimize. The materials performance guide takes it from here.

From the trenches

The best shader bug I ever met: a character's eyes went black, but only at dusk, only on one console. Weeks of on-and-off hunting, until someone output the lighting term to base color - negative values, arriving from a cloud shadow function that could, at exactly the sun angle dusk produced, return -0.02. A pow() downstream turned that into NaN; the console's bloom pass smeared it across the iris. One saturate() fixed it. The lesson outlived the project: don't debug by staring at the final image - make the pixel show you its homework.