The Universe in a Grid Square
In Chapter 4, you learned how mathematical functions create waves and patterns. Now we'll take this further: using simple rules to generate infinite complexity.
Before reading further, look at your fingerprint. See those swirls? Now look at a fern leaf, a snowflake, honeycomb, or zebra stripes.
These patterns share a secret: they're not drawn, they're grown. Simple rules, applied repeatedly in space, create infinite complexity. This chapter teaches you to grow patterns, not draw them.
The Aha Moment That Changes Everything
Here's the breakthrough: When you write fract(uv * 5)
, you're not just "repeating a pattern 5 times." You're creating 5×5 parallel universes, each unaware of the others, each computing the same rules in isolation.
This is procedural generation:
- One rule → Applied everywhere → Emergent complexity
- Local behavior → Global patterns
- Simple math → Organic results
Concept Introduction: The Three Pillars of Procedural Magic
Pillar 1: Space Duplication with fract()
float2 grid = fract(uv * 10.0);
This single line creates 100 copies of your coordinate system. But here's the beautiful part - you can still access which copy you're in:
float2 grid = fract(uv * 10.0); // Local: Where am I in my cell? (0-1)
float2 cellID = floor(uv * 10.0); // Global: Which cell am I in? (0-9, 0-9)
Pillar 2: The Random Seed Magic
Each cell needs unique randomness, but shaders can't use random(). The solution? A beautiful hack:
float random(float2 st) {
return fract(sin(dot(st, float2(12.9898, 78.233))) * 43758.5453);
}
This isn't really random - it's deterministic chaos. Same input always gives same output, but the output looks random.
Pillar 3: Pattern Transformation
Before repetition, transform. After repetition, combine. This order matters:
// Different results!
float2 rotated_then_grid = fract(rotate(uv) * 5.0); // Rotated grid
float2 grid_then_rotated = rotate(fract(uv * 5.0)); // Each cell rotated
Your First Living Pattern
Let's build something that makes people say "How is this just math?"
[[ stitchable ]] half4 livingMosaic(float2 position, half4 color, float2 size, float time) {
// Convert to UV coordinates (see Chapter 2 for detailed explanation)
float2 uv = position / size;
// Create grid
float gridSize = 8.0;
float2 grid = fract(uv * gridSize);
float2 cellID = floor(uv * gridSize);
// Unique random per cell
float cellRandom = fract(sin(dot(cellID, float2(12.9898, 78.233))) * 43758.5453);
// Each cell has different behavior based on its random value
float pattern = 0.0;
if (cellRandom < 0.33) {
// Circles that pulse
float2 center = grid - 0.5;
float pulse = sin(time * 2.0 + cellRandom * 6.28) * 0.1 + 0.3;
pattern = 1.0 - smoothstep(pulse - 0.05, pulse, length(center));
}
else if (cellRandom < 0.66) {
// Rotating crosses
float angle = time + cellRandom * 6.28;
float2 rotGrid = float2(
grid.x * cos(angle) - grid.y * sin(angle),
grid.x * sin(angle) + grid.y * cos(angle)
);
float cross = min(
smoothstep(0.4, 0.45, abs(rotGrid.x - 0.5)),
smoothstep(0.4, 0.45, abs(rotGrid.y - 0.5))
);
pattern = 1.0 - cross;
}
else {
// Diagonal stripes
float stripes = sin((grid.x + grid.y) * 10.0 + time * 3.0) * 0.5 + 0.5;
pattern = stripes;
}
// Color based on cell position
half3 cellColor = half3(
sin(cellID.x * 0.3 + 1.0),
sin(cellID.y * 0.3 + 2.0),
sin(cellID.x * cellID.y * 0.1 + 3.0)
) * 0.5 + 0.5;
return half4(pattern * cellColor.r, pattern * cellColor.g, pattern * cellColor.b, 1.0);
}
The Patterns That Broke My Brain (First Time I Saw Them)
Truchet Tiles: Order from Chaos
[[ stitchable ]] half4 truchetMagic(float2 position, half4 color, float2 size) {
float2 uv = position / size;
float2 grid = fract(uv * 8.0);
float2 cellID = floor(uv * 8.0);
// Random rotation per cell
float rand = fract(sin(dot(cellID, float2(12.9898, 78.233))) * 43758.5453);
// Rotate grid coordinates for some cells
if (rand > 0.5) {
grid = float2(1.0 - grid.y, grid.x);
}
// Draw quarter circles in opposite corners
// Arc from bottom-left corner
float arc1 = length(grid);
// Arc from top-right corner
float arc2 = length(grid - float2(1.0, 1.0));
// Create the line pattern (where the arcs should be)
float lineWidth = 0.03;
float line = 0.0;
// First arc (radius 0.5, centered at 0,0)
if (abs(arc1 - 0.5) < lineWidth) {
line = 1.0;
}
// Second arc (radius 0.5, centered at 1,1)
if (abs(arc2 - 0.5) < lineWidth) {
line = 1.0;
}
return half4(line, line, line, 1.0);
}
Why this is mind-blowing: Random tiles create continuous paths! The arcs always connect because math guarantees it.
Voronoi: Nature's Favorite Pattern
[[ stitchable ]] half4 voronoiOrganic(float2 position, half4 color, float2 size, float time) {
float2 uv = position / size;
float2 scaledUV = uv * 5.0;
float2 gridID = floor(scaledUV);
float2 gridUV = fract(scaledUV);
float minDist1 = 10.0; // Closest
float minDist2 = 10.0; // Second closest
float2 closestID;
// Check 3x3 grid of neighbors
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
float2 neighbor = float2(x, y);
float2 cellID = gridID + neighbor;
// Animated random point in each cell
float2 pointOffset = float2(
fract(sin(dot(cellID, float2(12.9898, 78.233))) * 43758.5453),
fract(sin(dot(cellID, float2(78.233, 12.9898))) * 43758.5453)
);
// Add subtle animation
pointOffset += sin(time * 2.0 + pointOffset * 6.28) * 0.1;
float2 point = neighbor + pointOffset;
float dist = length(point - gridUV);
if (dist < minDist1) {
minDist2 = minDist1;
minDist1 = dist;
closestID = cellID;
} else if (dist < minDist2) {
minDist2 = dist;
}
}
}
// Color based on cell
half3 cellColor = half3(
fract(sin(closestID.x * 12.9898) * 43758.5453),
fract(sin(closestID.y * 78.233) * 43758.5453),
fract(sin(dot(closestID, float2(45.678, 98.765))) * 43758.5453)
);
return half4(cellColor, 1.0);
}
Advanced Techniques: When Simple Rules Create Complex Beauty
Hexagonal Grids (Why Bees Are Mathematicians)
[[ stitchable ]] half4 hexagonalHoney(float2 position, half4 color, float2 size) {
float2 uv = position / size;
// Aspect correction
float aspect = size.x / size.y;
uv.x *= aspect;
// Scale
float2 p = uv * 10.0;
// Hexagonal grid math
float2 hex = float2(1.0, 1.73205); // 1, sqrt(3)
float2 a = fmod(p, hex) - hex * 0.5;
float2 b = fmod(p - hex * 0.5, hex) - hex * 0.5;
// Find closest hexagon center
float2 gv = (dot(a, a) < dot(b, b)) ? a : b;
// Proper hexagon distance (not circular!)
float2 absGv = abs(gv);
float d = max(dot(absGv, float2(0.866025, 0.5)), absGv.x);
// Create honeycomb walls
float walls = smoothstep(0.45, 0.40, d);
// Honey color inside cells, dark walls
half3 honeyColor = half3(1.0, 0.85, 0.0);
half3 wallColor = half3(0.4, 0.2, 0.0);
half3 finalColor = mix(wallColor, honeyColor, walls);
return half4(finalColor.r, finalColor.g, finalColor.b, 1.0);
}
Domain Warping (The Secret to Organic Patterns)
[[ stitchable ]] half4 domainWarpPattern(float2 position, half4 color, float2 size, float time) {
float2 uv = position / size;
// First pattern to warp space
float2 q = float2(
sin(uv.x * 4.0 + time * 0.3),
cos(uv.y * 4.0 + time * 0.2)
) * 0.1;
// Warp the coordinates
float2 warpedUV = uv + q;
// Apply pattern to warped space
float2 grid = fract(warpedUV * 8.0);
float2 center = grid - 0.5;
float circles = 1.0 - smoothstep(0.2, 0.25, length(center));
// Second layer of warping for organic feel
float warp2 = sin(length(uv - 0.5) * 10.0 - time) * 0.5 + 0.5;
float r = circles * warp2;
float g = circles * warp2 * 0.7;
float b = circles * warp2 * 0.5;
return half4(r, g, b, 1.0);
}
Pattern Philosophy: Why This Matters
Procedural patterns aren't just "cool effects." They represent a fundamental principle:
Complex systems emerge from simple rules + local interactions
This applies to:
- Biology (cell division, plant growth)
- Physics (crystal formation, fluid dynamics)
- Society (traffic patterns, market behavior)
- Art (Islamic geometry, Celtic knots)
When you master procedural patterns, you're learning to think like nature itself.
Common Pitfalls (And Their Fixes)
Pitfall 1: The Aspect Ratio Trap
// WRONG - Squares become rectangles!
float2 grid = fract(uv * 10.0);
// CORRECT - Perfect squares regardless of screen shape
float aspectRatio = size.x / size.y;
float2 grid = fract(float2(uv.x * aspectRatio, uv.y) * 10.0);
Pitfall 2: The Random Correlation Bug
// WRONG - X and Y randomness correlated
float randX = fract(sin(cellID.x * 12.9898) * 43758.5453);
float randY = fract(sin(cellID.y * 12.9898) * 43758.5453); // Same multiplier!
// CORRECT - Independent randomness
float randX = fract(sin(cellID.x * 12.9898) * 43758.5453);
float randY = fract(sin(cellID.y * 78.233) * 43758.5453); // Different multiplier
Pitfall 3: Performance Death by Neighbors
// WRONG - Checking too many cells kills performance
for (int y = -5; y <= 5; y++) {
for (int x = -5; x <= 5; x++) { // 121 iterations per pixel!
// CORRECT - Minimum necessary neighbors
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) { // Only 9 iterations
Challenges
Challenge 1: Checkerboard Plus
Create an animated checkerboard where white squares have rotating crosses and black squares have pulsing circles.
Challenge 2: Islamic Geometric Pattern
Create a pattern with 8-fold symmetry using rotated and mirrored grids. Think mosque tiles.
Challenge 3: Animated Ball Along Path
Draw a zigzag (triangular sine) path and a circular ball animated along the path.
Challenge 4: Reaction-Diffusion Approximation
Simulate organic growth patterns by combining multiple scales of voronoi noise.
Challenge 5: Quasicrystal Pattern
Create a Penrose-like pattern with 5-fold symmetry - seemingly impossible with regular grids!
Want the Challenges Solutions?
Get the full Xcode project with solutions to all challenges, bonus examples, and clean, runnable code.
Get the Full Project →Validation Questions
- What's the mathematical relationship between
fract()
andfmod()
?
fract(x)
is equivalent to fmod(x, 1.0)
- both return the fractional part (0.0-1.0). However, fract()
is specifically optimized for this common graphics operation and handles negative numbers consistently.
- How do you ensure each grid cell has unique but deterministic randomness?
Use the cell ID (from floor(uv * gridSize)
) as input to a hash function like fract(sin(dot(cellID, float2(12.9898, 78.233))) * 43758.5453)
. Same cell ID always produces the same "random" value, but different cells get different values.
- Why do truchet tiles create continuous patterns despite being randomly oriented?
Because the tile designs (quarter circles) are mathematically designed to connect properly regardless of rotation. When rotated 90°, the arc endpoints still align with neighboring tiles' endpoints, guaranteeing continuous paths.
- What's the key insight behind voronoi patterns?
Find the closest point (seed) to each pixel by checking distance to random points in the current cell and all neighboring cells. Color each pixel based on which seed is closest, creating natural-looking cellular regions like those found in biology.
- How does domain warping create organic-looking patterns?
Apply one pattern to distort the coordinate space, then apply a second pattern to the warped coordinates. This breaks the mechanical regularity of grid-based patterns, mimicking how forces in nature (wind, gravity, growth) distort underlying structures.
- Why are hexagonal grids optimal for certain natural patterns?
Hexagons provide the most efficient packing (maximum area with minimum perimeter) and have 6-fold symmetry. This efficiency appears in nature (honeycomb, crystal structures) because it minimizes energy while maximizing utility.
Debug Visualization Tricks
// Visualize your grid structure
float2 cellID = floor(uv * 10.0);
float debugColor = fract(sin(dot(cellID, float2(12.9898, 78.233))) * 43758.5453);
return half4(debugColor, fract(debugColor * 3.14), fract(debugColor * 7.0), 1.0);
// Show grid boundaries
float2 grid = fract(uv * 10.0);
float boundary = step(0.95, grid.x) + step(0.95, grid.y);
return half4(boundary, 0.0, 0.0, 1.0);
Further Exploration
- Penrose Tilings: True mathematical aperiodicity
- Wang Tiles: Computer science meets art
- L-Systems: Recursive pattern generation
- Cellular Automata: Conway's Game of Life in shaders
Next Chapter Preview: You've mastered deterministic patterns. But nature isn't perfect - it has variation, randomness, texture. Chapter 6 introduces noise functions, the secret sauce that transforms mechanical patterns into organic beauty. You'll learn why Perlin noise won a Technical Oscar and how to create everything from clouds to marble to alien landscapes.