Checkerboard
Two colors alternating on a grid, from floor and mod alone.
- 01GoalPattern
- 02TechniqueGrids & tiling
- 03ExecutionCheckerboard
Parameters
Copy for your platform
- GLSLCopy
- GodotCopy
Node chain
Built step by step, in GLSL style, with its node names.
Built step by step, in Godot style, with its node names.
No Unreal Engine 5 version of this card yet, so the nodes show the GLSL names.
No Unity version of this card yet, so the nodes show the GLSL names.
No Blender version of this card yet, so the nodes show the GLSL names.
floorFloorfloorfloorfloor UV01 Number the cells
modRemaindermodmodmod In02 Even or odd
mixMixmixmixmix In03 Pick the color
How it works
A checkerboard is the smallest useful grid: number the cells, and color each one by whether its number is even or odd.
Step by step
- Number the cells.
i.uvruns from 0 to 1 across the quad. Multiplying it by the column and row counts stretches that range to 0–16 across and 0–10 up, andfloordrops the fractional part, so every pixel in a cell gets the same whole-number pair, from(0, 0)at the bottom left to(15, 9). Two counts rather than one let the cells stay square on a quad that isn’t: the preview is 16:10. Dividing by the counts brings the pair back to 0–1, the corner of each cell, which the preview shows as a red and green staircase. This is the 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.FundamentalUV 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.
uv = gl_FragCoord.xy / resolutionSourcessplit from the fract and floor fundamental, using only the cell half of it.- gl_FragCoord (opens in a new tab)Reference
- UV mapping (opens in a new tab)Reference
- Even or odd.
dot(cell, counts)undoes the divide and adds the two parts: column plus row. Moving one cell right or up adds 1 to that sum, so neighbors always differ by one, andmod(…, 2.0)turns it into 0 for even cells and 1 for odd ones. Diagonal neighbors add 2 and keep the same color, which is exactly the checker. The divide and multiply leave the sum a hair off a whole number, soroundsnaps it back beforemodreads it. - Pick the color.
mix(p_even, p_odd, parity)with aparityof exactly 0 or 1 is a two-way choice without a branch: it returns one color or the other, never a blend.
floor matters at step 1. int() truncates toward zero, so -0.5 and 0.5 would both land in cell 0, and an offset or
tiled UV would show one cell twice as wide where it crosses zero. floor rounds toward negative infinity and keeps
every cell the same size.
Production notes
Cost. Seven operations per pixel: one multiply, one floor, one divide, one dot, one round, one mod and one mix. A version that skips the visible 0–1 step needs only five (multiply, floor, add, mod, mix). No texture reads, one pass. It is about the cheapest pattern there is, and it costs no memory, so it can cover a whole level.
Aliasing at a distance. The edges are hard: each pixel samples the pattern once, at its center, and gets one color
or the other. Up close that is perfectly crisp. Push the columns and rows past a thousand in the preview, or put the
material on a floor that runs to the horizon, and the cells shrink below a pixel. The samples then land on cells
almost at random, and the board breaks into false bands and a false, coarser board (moiré); on a curved or receding
surface those become curves, and on a moving camera, shimmer: a textbook case of aliasingThe false pattern that appears when a signal changes faster than it is sampled. In a shader, detail finer than a pixel turns into jagged edges, shimmer and moiré, because each pixel takes one sample from its center. Aliasing The false pattern that appears when a signal changes faster than it is sampled. In a shader, detail finer than a pixel turns into jagged edges, shimmer and moiré, because each pixel takes one sample from its center.
The fix is to average the pattern over each pixel’s footprint. fwidth(i.uv * counts) says how many cells one pixel
spans, and the filterable procedurals article in the sources derives the exact box-filtered checker from it, which fades
the board smoothly to the average gray as the cells shrink. Screen-space derivatives come later in the learning order;
returning to this card to filter it is the exercise there.
When to write your own. In a node editor, the built-in checker is the same math. Write it by hand when the grid has to do something the node doesn’t: number the cells for random tints, offset every other row for bricks, or filter the edges.
Make it in GLSL
The copy button gives a complete WebGL2 fragment shader: #version 300 es, the uniforms, the Inputs struct and a
main() that fills it and calls effect(). To use it in your own WebGL2 code, draw a quad (or a full-screen triangle)
whose vertex shader writes v_uv from 0 to 1, plus v_position, v_normal and v_view (this card reads only the UV, so
constants will do), compile the pair, and set the uniforms:
p_columnsandp_rowsas floats. Keep their ratio equal to the canvas’s (16 and 10 for an 800 × 500 canvas) and the cells stay square.p_evenandp_oddas linear colors. A color from a picker is sRGB; decode it on the CPU with the sRGB curve (orMath.pow(c, 2.2)per channel, close enough) before uploading.
The shader writes the final color already encoded to sRGB, so it goes straight to the canvas. In a pipeline that
encodes later (rendering to a linear target first), drop the encode at the end of main().
On a 3D mesh the same effect() works on the mesh’s UV0. Use it as a UV checker: square cells mean the UVs aren’t
stretched, and cells that grow, shrink or skew show you where they are.
Make it in Godot
VisualShader has no checker node. The node graph below follows the steps above, but sums the whole-number cell coordinates with a VectorDecompose and an Add instead of dividing and taking a dot product; the parity comes out the same with fewer nodes.
The copy button gives a canvas_item shader. In a 2D scene, add a ColorRect (or any Control or Sprite2D), give it a
new ShaderMaterial, and paste the code into a new Shader on that material. The board fills the node’s rectangle:
columns and rows are in the inspector under Shader Parameters, and their ratio should match the node’s size (16 and
10 for a 320 × 200 rect) for square cells. even and odd are color pickers.
On a 3D mesh. The same effect() works in a spatial shader, on the mesh’s UV. Start a new Shader with
shader_type spatial; and render_mode unshaded;, paste the uniforms and effect(), and write a fragment() that
fills Inputs from UV and sets ALBEDO = effect(i).rgb;. Spatial shaders decode source_color uniforms to linear
and encode the output themselves, so drop the u_toLinear calls there. Drop unshaded for a floor the scene’s lights
should shade.
As a node graph. In a VisualShader set to CanvasItem mode: the UV input into a Multiply (Vector2) with a Vector2Parameter for the cell counts, then Floor, then VectorDecompose, then Add (x + y), then Remainder by 2.0, and that result as the weight of a Mix between two ColorParameters, into the Color output.
No Unreal Engine 5 tutorial for this card yet.
No Unity tutorial for this card yet.
No Blender tutorial for this card yet.
Code
#version 300 es// Grids & tiling → Checkerboard, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_columns; // Columns: 16.0 (1.0 to 2000.0)uniform float p_rows; // Rows: 10.0 (1.0 to 1250.0)uniform vec3 p_even; // Even cells: vec3(0.93, 0.91, 0.86), sRGB; upload it decoded to linearuniform vec3 p_odd; // Odd cells: vec3(0.16, 0.17, 0.2), sRGB; upload it decoded to linearin vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}vec4 effect(Inputs i) { vec2 counts = vec2(p_columns, p_rows); // floor, not int(): int() truncates toward zero, so when the UV is offset or tiled past 0..1 the two cells on // either side of 0 would become one double-width cell. Dividing by the counts keeps each cell's corner in 0..1 vec2 cell = floor(i.uv * counts) / counts; // dot(cell, counts) is column + row as whole numbers; round absorbs the float error the divide left float parity = mod(round(dot(cell, counts)), 2.0); vec3 color = mix(p_even, p_odd, parity); return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Grids & tiling → Checkerboard, Godot 4. From Creative Center, MIT license.shader_type canvas_item;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform float columns : hint_range(1.0, 2000.0, 1.0) = 16.0;uniform float rows : hint_range(1.0, 1250.0, 1.0) = 10.0;uniform vec3 even : source_color = vec3(0.93, 0.91, 0.86);uniform vec3 odd : source_color = vec3(0.16, 0.17, 0.2);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// Godot's 2D renderer works in sRGB (HDR 2D off, the default), so the frame decodes color params and sRGB// textures to linear itself and encodes the result. With HDR 2D on, make these return c unchanged.vec3 u_toLinear(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}vec4 effect(Inputs i) { vec2 counts = vec2(columns, rows); // floor, not int(): int() truncates toward zero, so when the UV is offset or tiled past 0..1 the two cells on // either side of 0 would become one double-width cell. Dividing by the counts keeps each cell's corner in 0..1 vec2 cell = floor(i.uv * counts) / counts; // dot(cell, counts) is column + row as whole numbers; round absorbs the float error the divide left float parity = mod(round(dot(cell, counts)), 2.0); vec3 color = mix(u_toLinear(even), u_toLinear(odd), parity); return vec4(color, 1.0);}void fragment() { Inputs i; // UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // A flat quad facing the viewer, the same as the site's 2D preview i.position = vec3(i.uv * 2.0 - 1.0, 0.0); i.normal = vec3(0.0, 0.0, 1.0); i.view = vec3(0.0, 0.0, 1.0); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = 1.0 / SCREEN_PIXEL_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); COLOR = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}No Unreal Engine 5 code for this card yet.
No Unity code for this card yet.
No Blender code for this card yet.