Files
skills/shader-dev/techniques/procedural-2d-pattern.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

11 KiB

2D Procedural Patterns

Use Cases

  • Repeating/aperiodic 2D patterns: grids, hexagons, Truchet, interference patterns, kaleidoscopes, spirals, Lissajous
  • Procedural backgrounds, UI textures, sci-fi HUD/radar
  • Fractals, water caustics, and other natural phenomena
  • Infinite detail, seamless tiling, parameter-driven visual effects

Core Principles

2D procedural patterns = domain transforms + distance fields + color mapping:

  1. Domain repetition: fract()/mod() folds the infinite plane into repeating cells
  2. Cell identification: floor() extracts integer coordinates as hash seeds, driving per-cell random variations
  3. Distance field (SDF): mathematical functions compute pixel-to-shape distance, smoothstep renders edges
  4. Color mapping: cosine palette a + b*cos(2pi(c*t+d)) or HSV
  5. Layer compositing: multi-layer loop results blended via addition/multiplication/mix

Key formulas:

// UV normalization
uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
// Domain repetition
cell_uv = fract(uv * SCALE) - 0.5;
cell_id = floor(uv * SCALE);
// Cosine palette
col = a + b * cos(6.28318 * (c * t + d));
// Hexagon SDF
hex(p) = max(dot(abs(p), vec2(0.5, 0.866025)), abs(p).x);
// 2D rotation
mat2(cos(a), -sin(a), sin(a), cos(a));

Implementation Steps

Step 1: UV Normalization

vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;

Step 2: Domain Repetition

#define SCALE 4.0
vec2 cell_uv = fract(uv * SCALE) - 0.5;
vec2 cell_id = floor(uv * SCALE);

Hexagonal grid domain repetition:

const vec2 s = vec2(1, 1.7320508);
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);
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: Per-Cell Randomization

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;

Step 4: SDF Shape Drawing

// Circle
float d = length(cell_uv) - radius;

// Hexagon
float hex_sdf(vec2 p) {
    p = abs(p);
    return max(dot(p, vec2(0.5, 0.866025)), p.x);
}

// Line segment
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
float shape = 1.0 - smoothstep(radius - 0.008, radius + 0.008, length(cell_uv));

Step 5: Polar Coordinate Rings/Arcs

vec2 polar = vec2(length(uv), atan(uv.y, uv.x));
float ring_id = floor(polar.x * NUM_RINGS + 0.5) / NUM_RINGS;
float ring = 1.0 - pow(abs(sin(polar.x * 3.14159 * NUM_RINGS)) * 1.25, 2.5);
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

vec3 palette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.263, 0.416, 0.557);
    return a + b * cos(6.28318 * (c * t + d));
}

Step 7: Iterative Stacking & Glow

#define NUM_LAYERS 4.0
vec3 finalColor = vec3(0.0);
vec2 uv0 = uv;
for (float i = 0.0; i < NUM_LAYERS; i++) {
    uv = fract(uv * 1.5) - 0.5;
    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;
    d = abs(d);
    d = pow(0.01 / d, 1.2);
    finalColor += col * d;
}

Step 8: Trigonometric Interference

#define MAX_ITER 5
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));

Step 9: Multi-Layer Depth Compositing

#define NUM_DEPTH_LAYERS 4.0
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 * patternLayer(uv * size, i, iTime);
}

Step 10: Post-Processing

col = pow(clamp(col, 0.0, 1.0), vec3(1.0 / 2.2));                          // Gamma
col = col * 0.6 + 0.4 * col * col * (3.0 - 2.0 * col);                    // Contrast S-curve
col = mix(col, vec3(dot(col, vec3(0.33))), -0.4);                          // Saturation
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); // Vignette

Complete Code Template

// ====== 2D Procedural Pattern Template ======
// Ready to run in ShaderToy

#define SCALE 3.0
#define NUM_LAYERS 4.0
#define ZOOM_FACTOR 1.5
#define GLOW_WIDTH 0.01
#define GLOW_POWER 1.2
#define WAVE_FREQ 8.0
#define ANIM_SPEED 0.4
#define RING_COUNT 10.0

vec3 palette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.263, 0.416, 0.557);
    return a + b * cos(6.28318 * (c * t + d));
}

float hash21(vec2 p) {
    return fract(sin(dot(p, vec2(141.173, 289.927))) * 43758.5453);
}

mat2 rot2(float a) {
    float c = cos(a), s = sin(a);
    return mat2(c, -s, s, c);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
    vec2 uv0 = uv;
    vec3 finalColor = vec3(0.0);

    for (float i = 0.0; i < NUM_LAYERS; i++) {
        uv = fract(uv * ZOOM_FACTOR) - 0.5;
        float d = length(uv) * exp(-length(uv0));
        vec3 col = palette(length(uv0) + i * 0.4 + iTime * ANIM_SPEED);
        d = sin(d * WAVE_FREQ + iTime) / WAVE_FREQ;
        d = abs(d);
        d = pow(GLOW_WIDTH / d, GLOW_POWER);
        finalColor += col * d;
    }

    finalColor = pow(clamp(finalColor, 0.0, 1.0), vec3(1.0 / 2.2));
    finalColor = finalColor * 0.6 + 0.4 * finalColor * finalColor * (3.0 - 2.0 * finalColor);
    vec2 q = fragCoord / iResolution.xy;
    finalColor *= 0.5 + 0.5 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.7);

    fragColor = vec4(finalColor, 1.0);
}

Common Variants

Variant 1: Hexagonal Truchet Arcs

float hex(vec2 p) {
    p = abs(p);
    return max(dot(p, vec2(0.5, 0.866025)), p.x);
}

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 triple arcs
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;
float d = min(min(length(q1), length(q2)), length(q3));
d = abs(d - 0.288675) - 0.1;

Variant 2: Water Caustic Interference

#define TAU 6.28318530718
#define MAX_ITER 5
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);

Variant 3: Polar Concentric Ring Arc Segments

#define NUM_RINGS 20.0
#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;
p *= rot2(id * 11.0);
p.y = abs(p.y);
float rz = 1.0 - pow(abs(sin(plr.x * 3.14159 * NUM_RINGS)) * 1.25, 2.5);
float arc = plr.y + sin(iTime + id * 5.5) * 1.52 - 1.5;
rz *= smoothstep(0.0, 0.05, arc);
vec3 col = (sin(PALETTE + id * 5.0 + iTime) * 0.5 + 0.5) * rz;

Variant 4: Multi-Layer Depth Parallax Network

#define NUM_DEPTH_LAYERS 4.0
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;
}
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);
}
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 Apollonian

float apollian(vec4 p, float s) {
    float scale = 1.0;
    for (int i = 0; i < 7; ++i) {
        p = -1.0 + 2.0 * fract(0.5 * p + 0.5);
        float r2 = dot(p, p);
        float k = s / r2;
        p *= k;
        scale *= k;
    }
    return abs(p.y) / scale;
}
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;
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));

Performance & Composition

Performance:

  • Iteration loops are the biggest bottleneck; NUM_LAYERS 4->8 halves performance; mobile should use 3 layers or fewer
  • Use step()/smoothstep()/mix() instead of if/else
  • Merge multiple SDFs with min()/max(), then apply a single smoothstep
  • Precompute sin/cos pairs outside loops; write irrational constants as literal values
  • atan is expensive; use dot approximation when only periodicity is needed
  • LOD: reduce iterations for distant objects int iters = int(mix(3.0, float(MAX_ITER), smoothstep(...)));
  • smoothstep is often better than pow and inherently clamps to [0,1]

Combinations:

  • + Noise: d += triangleNoise(uv * 10.0) * 0.05; for organic erosion feel
  • + Cross-hatch: grayscale thresholds + sin lines to simulate hand-drawn style
  • + SDF Boolean: min (union) / max (intersection) / subtraction for complex geometry
  • + Domain distortion: uv += 0.05 * vec2(sin(uv.y*5.+iTime), sin(uv.x*3.+iTime));
  • + Radial blur: multi-sample average along polar coordinate direction
  • + Pseudo-3D lighting: SDF gradient as normal, add diffuse/specular for embossed look

Further Reading

For complete step-by-step tutorials, mathematical derivations, and advanced usage, see reference