Shader Lab
16.Truchet tiles

Truchet tiles

Arc 3 · Procedural Patterns · Lesson 16 of 35
coordinate signal color
0.10
0.06

Purpose

A single simple tile, placed in a grid and rotated randomly in each cell, produces a richly connected global pattern that looks nothing like its parts. Randomness at the cell level, structure across the whole.

Key insight

Earlier noise lessons generated complexity from continuous randomness — smooth, flowing noise fields. Truchet is the opposite: randomness in discrete choices, one per tile. Take one tile with a simple motif (commonly two quarter-arcs connecting opposite corners). Lay it in a grid. For each cell, hash the cell's integer coordinates into one of two rotations. Draw the motif rotated by that choice.

The arcs line up at tile edges regardless of which rotation was chosen, so every junction connects — producing a single unbroken network of arcs winding through the whole grid. Almost all the visual interest comes not from any one tile but from the accidental connections between them. Turn the randomness off and the pattern collapses into something trivial; turn it on and the same tile suddenly reads as a maze, a circuit, an Islamic lattice.

Break it

1. Press re-seed a dozen times in rapid succession. Each press snaps to a completely different maze while the tile, grid, and cell-size never change. Teaches: Truchet is a generator, not a texture. What you are authoring is the tile and the rule, not any specific image.

2. Watch what happens when the re-seed lands on a "boring" looking pattern, then press again. You can't un-press it — there's no undo. Teaches: every seed is one sample from the distribution. The pattern space is huge; the viewer is browsing it.

Direct Claude

"denser / tighter maze" cell-size down "bolder / simpler pattern" cell-size up "different maze same style" re-seed "thicker lines" motif stroke width up (signal layer) "thinner lines" motif stroke width down "curved / flowing maze" quarter-arc motif "angular / circuit-board maze" diagonal-line motif "more orientation variety" expand rotation set (2 → 4 options; or hex grid)
Meta-phrase you gain here: "per-cell hash picks the orientation" — naming this pattern means you can ask for Truchet-style variation on any tiled motif, including ones that don't exist yet.
Combines with: Lesson 8 (the per-cell hash lifts the hashing idea straight from noise), Lesson 15 (both are grid + per-cell randomness, but Voronoi randomizes position while Truchet randomizes orientation), Lesson 34 (palette cycling animates the whole maze without changing geometry).
the fragment shader running above
uniform float u_cellSize;  // coordinate: size of one tile in canvas units
uniform float u_seed;      // coordinate: which sample from the distribution
uniform float u_stroke;    // signal: arc line thickness

float hash(vec2 p) {
  return fract(sin(dot(p, vec2(127.1, 311.7)) + u_seed) * 43758.5453);
}

void main() {
  // Aspect-correct UV in canvas units
  vec2 uv = gl_FragCoord.xy / u_resolution.y;

  // COORDINATE LAYER — tile the plane; i is the cell ID, f is the local [0,1] coord
  vec2 scaled = uv / u_cellSize;
  vec2 i = floor(scaled);
  vec2 f = fract(scaled);

  // Per-cell hash → binary choice of orientation.
  // Flip the tile's X inside the cell for half the cells.
  if (hash(i) > 0.5) f.x = 1.0 - f.x;

  // SIGNAL LAYER — distance to two quarter-arcs meeting the corners (0,0) and (1,1)
  float arc1 = abs(length(f - vec2(0.0, 0.0)) - 0.5);
  float arc2 = abs(length(f - vec2(1.0, 1.0)) - 0.5);
  float d = min(arc1, arc2);

  // Ink width — controlled by stroke slider
  float w = u_stroke;
  float ink = 1.0 - smoothstep(w, w + 0.02, d);

  // COLOR LAYER — ink on paper
  vec3 paper = vec3(0.95, 0.92, 0.86);
  vec3 inkC  = vec3(0.14, 0.12, 0.18);
  vec3 col = mix(paper, inkC, ink);

  gl_FragColor = vec4(col, 1.0);
}