Output values as color
Write any value to the output to see it, and read what the colors tell you.
How it works
A fragment shaderThe GPU program that runs once for every pixel a triangle covers and decides its color. Fragment shader The GPU program that runs once for every pixel a triangle covers and decides its color.print. It runs millions of times per frame, on the GPU,
and the only thing that comes back is a color per pixel. So the color is the debugger: when a value looks wrong, write
it to the output and look at it. Every step preview on this site’s cards is exactly that, one variable shown as color.
One value, as gray
A single float between 0 and 1 reads best as gray: black is 0, white is 1. Put the same value in all three channels:
float d = length(p); // the value you want to inspect
fragColor = vec4(vec3(d), 1.0); // black at the center, brighter outward
A UV coordinates A 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.vec2 fits in red and green, as with UVA 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.uv = gl_FragCoord.xy / resolutionvec3 fills all three channels.
To read one component of a vector, isolate it: fragColor = vec4(vec3(n.y), 1.0); shows only y as gray.
Values outside 0 to 1
The display clamps the output, so anything below 0 shows as black and anything above 1 as white. A whole region of flat black can hide a value of -0.01 or of -500. Remap first:
- A range of -1 to 1, such as a direction or a normalA unit vector perpendicular to a surface, pointing the way it faces. Lighting and Fresnel effects compare it with other directions.Fundamental
Normal
A unit vector perpendicular to a surface, pointing the way it faces. Lighting and Fresnel effects compare it with other directions.
Sources:v * 0.5 + 0.5maps it to 0..1. Facing +x turns pinkish red, +y light green, +z light blue; a zero vector maps to 0.5 in every channel. - A large range, such as a distance in world units or a time in seconds: divide by the largest value you expect,
or show
fract(v), which repeats every 1 unit and draws contour lines across the field. - A sign that matters, such as a signed distance: give negative and positive values their own color. Mapping values to arbitrary colors to make a property visible is called false color.
// red where v is negative, green where it is positive, black at zero
vec3 signedView(float v) {
return vec3(max(-v, 0.0), max(v, 0.0), 0.0);
}
// thin bands every 0.1 units: equal values share a band
float contours(float v) {
return step(0.9, fract(v * 10.0));
}
Reading exact numbers
The eye is bad at judging brightness, and on this site the frame encodes the output to sRGB, so a value of 0.5 shows
noticeably lighter than middle gray. To check a number, compare it instead of eyeballing it: step(0.5, v) turns
white exactly where v reaches 0.5, and a split view shows two candidates side by side.
vec3 a = vec3(smoothstep(0.2, 0.8, x));
vec3 b = vec3(x * x * (3.0 - 2.0 * x));
vec3 color = uv.x < 0.5 ? a : b; // left half: a, right half: b
NaN and infinity
A division by zero, the square root of a negative number or normalize of a zero vector can produce NaN (not a
number) or infinity. NaN spreads through every operation that touches it, and different GPUs display it differently:
black, white, or random. GLSL ES 3.00 has isnan and isinf, so you can paint the bad pixels a color nothing else
uses:
if (isnan(v) || isinf(v)) {
fragColor = vec4(1.0, 0.0, 1.0, 1.0); // magenta: find where this starts
return;
}
Some compilers optimize on the assumption that NaN never happens, so a clean result is not a proof. Once a pixel is flagged, walk backward through the chain and show each earlier value until the magenta disappears.
Walk the chain
Most shaders are a chain: coordinates, then a distance, then a mask, then a color. When the final image is wrong, show the first link, then the next, and stop at the first one that looks wrong. That one, or the input to it, holds the bug. The step previews on the cards follow the same order for the same reason.
In an engine, make sure the value reaches the screen unchanged. In Godot, writing it to ALBEDO lights and shades it;
add render_mode unshaded; while you debug, and ALBEDO = value; then shows the raw value. Writing it to EMISSION
with ALBEDO = vec3(0.0); is not enough: the default SPECULAR of 0.5 still adds light and sky reflections on top,
so set SPECULAR = 0.0; too if you take that route.