16 KiB
2D Procedural Patterns — Detailed Reference
This document is a complete supplement to SKILL.md, containing prerequisites, detailed explanations for each step, variant descriptions, in-depth performance analysis, and combination example code.
Prerequisites
- GLSL Basic Syntax: uniform, varying, built-in functions
- Vector Math:
dot,length,normalize,atan - Coordinate Space Concepts: UV normalization, aspect ratio correction
- Basic Math Functions:
sin/cos,fract/floor/mod,smoothstep,pow - Polar Coordinates:
atan(y,x)returns angle,lengthreturns radial distance
Core Principles in Detail
The essence of 2D procedural patterns is the combination of domain transforms + distance fields + color mapping:
- Domain Repetition: use
fract()/mod()to fold an infinite plane into finite cells, each cell independently rendering the same (or variant) pattern - Cell Identification: use
floor()to extract the integer coordinates of the current cell as a hash seed to generate pseudo-random numbers, driving independent variations per cell - Distance Fields (SDF): use mathematical functions to compute the distance from a pixel to geometric shapes (circles, hexagons, line segments, arcs), converting to crisp or soft edges via
smoothstep - Color Mapping: Cosine palette
a + b*cos(2pi(c*t+d))or HSV mapping, converting scalar values to rich colors - Layered Compositing: results from multiple loops or multi-layer passes are combined through addition, multiplication, or
mixto build visual complexity
Implementation Steps in Detail
Step 1: UV Coordinate Normalization and Aspect Ratio Correction
What: Convert pixel coordinates to normalized coordinates centered on the screen with Y-axis range [-1, 1]
Why: A unified coordinate system ensures patterns don't distort with resolution changes; using Y-axis as reference maintains square pixels
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
Step 2: Domain Repetition — Dividing Space into Repeating Cells
What: Scale UV coordinates and take the fractional part to generate repeating local coordinates; simultaneously extract cell IDs using floor
Why: fract() folds an infinite plane into a repeating [0,1) space, floor() provides a unique cell identifier for subsequent randomization. Subtracting 0.5 centers the origin
#define SCALE 4.0 // Tunable: repetition density, higher = more cells
vec2 cell_uv = fract(uv * SCALE) - 0.5;
vec2 cell_id = floor(uv * SCALE);
For hexagonal grids, domain repetition requires special handling (two offset rectangular grids, taking the nearest):
const vec2 s = vec2(1, 1.7320508); // 1 and sqrt(3)
vec4 hC = floor(vec4(p, p - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
vec4 h = vec4(p - hC.xy * s, p - (hC.zw + 0.5) * s);
// Take the nearest hexagonal center
vec4 hex_data = dot(h.xy, h.xy) < dot(h.zw, h.zw)
? vec4(h.xy, hC.xy)
: vec4(h.zw, hC.zw + vec2(0.5, 1.0));
Step 3: Cell Randomization
What: Use cell IDs to generate pseudo-random numbers, giving each cell different attributes (size, position, color offset)
Why: Pure repetition looks mechanical; randomization gives patterns a "procedural yet lively" quality
float hash21(vec2 p) {
return fract(sin(dot(p, vec2(141.173, 289.927))) * 43758.5453);
}
float rnd = hash21(cell_id);
float radius = 0.15 + 0.1 * rnd; // Tunable: base radius and random range
Step 4: Distance Field Shape Rendering
What: Compute the distance from the pixel to the target shape, then convert to visualization using smoothstep
Why: SDF is the cornerstone of procedural graphics — a single scalar value simultaneously encodes shape, edges, and glow effects
// Circle SDF
float d = length(cell_uv) - radius;
// Hexagon SDF
float hex_sdf(vec2 p) {
p = abs(p);
return max(dot(p, vec2(0.5, 0.866025)), p.x);
}
// Line segment SDF (for networks/grid lines)
float line_sdf(vec2 a, vec2 b, vec2 p) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
// Anti-aliased rendering with smoothstep
float shape = 1.0 - smoothstep(radius - 0.008, radius + 0.008, length(cell_uv));
Step 5: Polar Coordinate Conversion and Ring/Arc Patterns
What: Convert Cartesian coordinates to polar coordinates, using radial distance to draw concentric rings and angle to draw sectors/arc segments
Why: Polar coordinates are naturally suited for radar sweeps, concentric circles, spirals, and other radially symmetric patterns
vec2 polar = vec2(length(uv), atan(uv.y, uv.x));
float ring_id = floor(polar.x * NUM_RINGS + 0.5) / NUM_RINGS; // Tunable: NUM_RINGS ring count
// Concentric rings
float ring = 1.0 - pow(abs(sin(polar.x * 3.14159 * NUM_RINGS)) * 1.25, 2.5);
// Arc segment clipping
float arc_end = polar.y + sin(iTime + ring_id * 5.5) * 1.52 - 1.5;
ring *= smoothstep(0.0, 0.05, arc_end);
Step 6: Cosine Palette
What: Generate a continuous rainbow color mapping function using four vec3 parameters
Why: A single line of code generates infinite smooth color schemes, more flexible and GPU-friendly than lookup tables
vec3 palette(float t) {
// Tunable: modify a/b/c/d to change color scheme
vec3 a = vec3(0.5, 0.5, 0.5); // Brightness offset
vec3 b = vec3(0.5, 0.5, 0.5); // Amplitude
vec3 c = vec3(1.0, 1.0, 1.0); // Frequency
vec3 d = vec3(0.263, 0.416, 0.557); // Phase offset
return a + b * cos(6.28318 * (c * t + d));
}
Step 7: Iterative Stacking and Glow Effects
What: Repeatedly perform domain repetition + distance field calculation in a loop, accumulating color; use pow(1/d) to produce glow
Why: A single layer pattern is too simple; multi-layer iterative stacking produces fractal-like visual complexity with minimal code. Exponentially decaying glow gives patterns a neon light feel
#define NUM_LAYERS 4.0 // Tunable: number of iteration layers, more = more complex
vec3 finalColor = vec3(0.0);
vec2 uv0 = uv; // Preserve original UV for global coloring
for (float i = 0.0; i < NUM_LAYERS; i++) {
uv = fract(uv * 1.5) - 0.5; // Tunable: 1.5 is the scale factor
float d = length(uv) * exp(-length(uv0));
vec3 col = palette(length(uv0) + i * 0.4 + iTime * 0.4);
d = sin(d * 8.0 + iTime) / 8.0; // Tunable: 8.0 is the ripple frequency
d = abs(d);
d = pow(0.01 / d, 1.2); // Tunable: 0.01 is glow width, 1.2 is decay exponent
finalColor += col * d;
}
Step 8: Trigonometric Interference Patterns
What: Use sin/cos to mutually perturb coordinates in iterations, generating water caustic-like interference patterns
Why: Superposition of trigonometric functions produces complex Moire-like interference patterns; a few iterations yield highly organic visual effects
#define MAX_ITER 5 // Tunable: iteration count, more = richer detail
vec2 p = mod(uv * TAU, TAU) - 250.0; // TAU period ensures tileability
vec2 i = p;
float c = 1.0;
float inten = 0.005; // Tunable: intensity coefficient
for (int n = 0; n < MAX_ITER; n++) {
float t = iTime * (1.0 - 3.5 / float(n + 1));
i = p + vec2(cos(t - i.x) + sin(t + i.y),
sin(t - i.y) + cos(t + i.x));
c += 1.0 / length(vec2(p.x / (sin(i.x + t) / inten),
p.y / (cos(i.y + t) / inten)));
}
c /= float(MAX_ITER);
c = 1.17 - pow(c, 1.4); // Tunable: 1.4 is the contrast exponent
vec3 colour = vec3(pow(abs(c), 8.0));
Step 9: Multi-Layer Depth Compositing
What: Render the same pattern at different zoom levels, using depth fade-in/out to simulate parallax
Why: Multi-scale stacking breaks the mechanical feel of a single scale, producing a pseudo-3D depth effect
#define NUM_DEPTH_LAYERS 4.0 // Tunable: number of depth layers
float m = 0.0;
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_DEPTH_LAYERS) {
float z = fract(iTime * 0.1 + i);
float size = mix(15.0, 1.0, z); // Dense far away, sparse up close
float fade = smoothstep(0.0, 0.6, z) * smoothstep(1.0, 0.8, z); // Fade at both ends
m += fade * patternLayer(uv * size, i, iTime);
}
Step 10: Post-Processing Pipeline
What: Apply gamma correction, contrast enhancement, saturation adjustment, and vignette in sequence
Why: Post-processing transforms "technically correct" output into "visually pleasing" final results
// Gamma correction
col = pow(clamp(col, 0.0, 1.0), vec3(1.0 / 2.2));
// Contrast enhancement (S-curve)
col = col * 0.6 + 0.4 * col * col * (3.0 - 2.0 * col);
// Saturation adjustment
col = mix(col, vec3(dot(col, vec3(0.33))), -0.4); // Tunable: -0.4 increases saturation, positive reduces it
// Vignette
vec2 q = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.7);
Common Variants in Detail
Variant 1: Hexagonal Grid + Truchet Arcs
Difference from base version: Replaces the square grid with a hexagonal grid coordinate system, drawing three randomly oriented arcs within each hexagonal cell; arcs form maze-like continuous paths between cells
Key modified code:
// Hexagon distance field
float hex(vec2 p) {
p = abs(p);
return max(dot(p, vec2(0.5, 0.866025)), p.x);
}
// Hexagonal grid coordinates (returns xy=cell-local coords, zw=cell ID)
const vec2 s = vec2(1.0, 1.7320508);
vec4 getHex(vec2 p) {
vec4 hC = floor(vec4(p, p - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
vec4 h = vec4(p - hC.xy * s, p - (hC.zw + 0.5) * s);
return dot(h.xy, h.xy) < dot(h.zw, h.zw)
? vec4(h.xy, hC.xy)
: vec4(h.zw, hC.zw + vec2(0.5, 1.0));
}
// Truchet three-arc: one arc for each of three directions
float r = 1.0;
vec2 q1 = p - vec2(0.0, r) / s;
vec2 q2 = rot2(6.28318 / 3.0) * p - vec2(0.0, r) / s;
vec2 q3 = rot2(6.28318 * 2.0 / 3.0) * p - vec2(0.0, r) / s;
// Take nearest arc
float d = min(min(length(q1), length(q2)), length(q3));
d = abs(d - 0.288675) - 0.1; // 0.288675 = sqrt(3)/6, arc radius
Variant 2: Water Caustic Interference Pattern
Difference from base version: Does not use domain repetition grids; instead generates full-screen interference textures through trigonometric iteration, seamlessly tileable
Key modified code:
#define TAU 6.28318530718
#define MAX_ITER 5 // Tunable: iteration count
vec2 p = mod(uv * TAU, TAU) - 250.0;
vec2 i = p;
float c = 1.0;
float inten = 0.005;
for (int n = 0; n < MAX_ITER; n++) {
float t = iTime * (1.0 - 3.5 / float(n + 1));
i = p + vec2(cos(t - i.x) + sin(t + i.y),
sin(t - i.y) + cos(t + i.x));
c += 1.0 / length(vec2(p.x / (sin(i.x + t) / inten),
p.y / (cos(i.y + t) / inten)));
}
c /= float(MAX_ITER);
c = 1.17 - pow(c, 1.4);
vec3 colour = vec3(pow(abs(c), 8.0));
colour = clamp(colour + vec3(0.0, 0.35, 0.5), 0.0, 1.0); // Aquatic color shift
Variant 3: Polar Concentric Rings + Animated Arc Segments
Difference from base version: Uses polar coordinates instead of Cartesian grids, drawing concentric ring arc segments with independent animation, suitable for radar/HUD style
Key modified code:
#define NUM_RINGS 20.0 // Tunable: ring count
#define PALETTE vec3(0.0, 1.4, 2.0) + 1.5
vec2 plr = vec2(length(p), atan(p.y, p.x));
float id = floor(plr.x * NUM_RINGS + 0.5) / NUM_RINGS;
// Each ring rotates independently
p *= rot2(id * 11.0);
p.y = abs(p.y); // Mirror symmetry
// Concentric ring SDF
float rz = 1.0 - pow(abs(sin(plr.x * 3.14159 * NUM_RINGS)) * 1.25, 2.5);
// Arc segment animation
float arc = plr.y + sin(iTime + id * 5.5) * 1.52 - 1.5;
rz *= smoothstep(0.0, 0.05, arc);
// Per-ring coloring
vec3 col = (sin(PALETTE + id * 5.0 + iTime) * 0.5 + 0.5) * rz;
Variant 4: Multi-Layer Depth Parallax Network
Difference from base version: Renders grid nodes and connections at multiple zoom levels, using depth fade-in/out to produce a pseudo-3D effect
Key modified code:
#define NUM_DEPTH_LAYERS 4.0 // Tunable: number of depth layers
// Random vertex position within each cell
vec2 GetPos(vec2 id, vec2 offs, float t) {
float n = hash21(id + offs);
return offs + vec2(sin(t + n * 6.28), cos(t + fract(n * 100.0) * 6.28)) * 0.4;
}
// Line segment SDF
float df_line(vec2 a, vec2 b, vec2 p) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
// Multi-layer compositing
float m = 0.0;
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_DEPTH_LAYERS) {
float z = fract(iTime * 0.1 + i);
float size = mix(15.0, 1.0, z);
float fade = smoothstep(0.0, 0.6, z) * smoothstep(1.0, 0.8, z);
m += fade * NetLayer(uv * size, i, iTime);
}
Variant 5: Fractal Apollian Pattern
Difference from base version: Uses iterative fold-and-invert transforms to generate infinitely detailed aperiodic fractal patterns, combined with HSV coloring
Key modified code:
float apollian(vec4 p, float s) {
float scale = 1.0;
for (int i = 0; i < 7; ++i) { // Tunable: iteration count (5~12)
p = -1.0 + 2.0 * fract(0.5 * p + 0.5); // Space folding
float r2 = dot(p, p);
float k = s / r2; // Tunable: s is scaling factor (1.0~1.5)
p *= k; // Inversion mapping
scale *= k;
}
return abs(p.y) / scale;
}
// 4D slice animation for smooth morphing
vec4 pp = vec4(p.x, p.y, 0.0, 0.0) + offset;
pp.w = 0.125 * (1.0 - tanh(length(pp.xyz)));
float d = apollian(pp / 4.0, 1.2) * 4.0;
// HSV coloring
float hue = fract(0.75 * length(p) - 0.3 * iTime) + 0.3;
float sat = 0.75 * tanh(2.0 * length(p));
vec3 col = hsv2rgb(vec3(hue, sat, 1.0));
In-Depth Performance Optimization
1. Control Iteration Count
The iteration loop is the biggest performance bottleneck. Increasing NUM_LAYERS from 4 to 8 halves performance. On mobile, keep it at 3 or fewer layers.
2. Avoid Branching
Replace if/else with branchless step()/smoothstep()/mix() alternatives:
// Bad: if(rnd > 0.5) p.y = -p.y;
// Good: p.y *= sign(rnd - 0.5); // or use mix
3. Merge Distance Field Calculations
Combine multiple shape SDFs using min()/max() and apply a single smoothstep, rather than rendering each shape separately.
4. Precompute Constants
Compute sin/cos pairs (e.g., rotation matrices) once outside the loop; write irrational numbers like 1.7320508 (sqrt(3)) as direct constants.
5. Minimize atan Calls
atan is an expensive function. If you only need periodic angular variation, consider approximating with dot.
6. LOD Strategy
Reduce iteration count at distance/when zoomed out:
int iters = int(mix(3.0, float(MAX_ITER), smoothstep(0.0, 1.0, 1.0 / scale)));
7. Use smoothstep Instead of pow
pow(x, n) is slower than smoothstep on some GPUs, and smoothstep naturally clamps to [0,1].
Complete Combination Suggestion Examples
1. + Noise Texture
Overlay Perlin/Simplex noise perturbation on distance fields to give geometric patterns an organic/eroded feel. Triangle noise (as used in "Overly Satisfying") is an efficient low-cost alternative:
d += triangleNoise(uv * 10.0) * 0.05; // Noise perturbation amount is tunable
2. + Post-Processing Cross-Hatch
Overlay cross-hatching effects on patterns to simulate hand-drawn/printmaking style (as used in "Hexagonal Maze Flow"):
float gr = dot(col, vec3(0.299, 0.587, 0.114)); // Grayscale
float hatch = (gr < 0.45) ? clamp(sin((uv.x - uv.y) * 125.6) * 2.0 + 1.5, 0.0, 1.0) : 1.0;
col *= hatch * 0.5 + 0.5;
3. + SDF Boolean Operations
Combine multiple base patterns through min (union), max (intersection), and subtraction into complex geometry:
float d = max(hexSDF, -circleSDF); // Hexagon minus circle = hexagonal ring
4. + Domain Warping
Apply sin/cos distortion to UVs before domain repetition, producing flowing/swirling effects:
uv += 0.05 * vec2(sin(uv.y * 5.0 + iTime), sin(uv.x * 3.0 + iTime));
5. + Radial Blur / Motion Blur
Average multiple samples in the polar coordinate direction on the final color, producing rotational motion blur to enhance dynamism.
6. + Pseudo-3D Lighting
Use SDF gradients as normals and add simple diffuse/specular lighting to give 2D patterns a relief/embossed appearance (as in "Apollian with a twist" shadow casting method).