Files
skills/shader-dev/techniques/polar-uv-manipulation.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

13 KiB
Raw Permalink Blame History

WebGL2 Adaptation Requirements

Code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt to WebGL2:

  • Use canvas.getContext("webgl2")
  • IMPORTANT: Version directive must strictly be on the first line: When injecting shader code into HTML, ensure nothing precedes #version 300 es — no newlines, spaces, comments, or other characters. Common pitfall: accidentally adding \n when concatenating template strings, causing the version directive to appear on line 2-3
  • First line of shader: #version 300 es, add precision highp float; for fragment shaders
  • Vertex shader: attributein, varyingout
  • Fragment shader: varyingin, gl_FragColor → custom out vec4 fragColor, texture2D()texture()
  • ShaderToy's void mainImage(out vec4 fragColor, in vec2 fragCoord) must be adapted to standard void main() entry

IMPORTANT: GLSL Type Strictness Warning:

  • vec2 = float is illegal: types must match exactly, e.g., float r = length(uv) not vec2 r = length(uv)
  • Function return types must match: commonly used fbm() / noise() return float, cannot be assigned to vec2
  • If you need a vec2 type, use vec2(fbm(...), fbm(...)) or vec2(value) constructor

Polar Coordinates & UV Manipulation

Use Cases

  • Radially symmetric effects: flowers, kaleidoscopes, gears, radial patterns
  • Spiral patterns: galaxies, vortices, spiral staircases
  • Ring/tunnel effects: tube flying, torus twisting, circular UI elements
  • Polar coordinate shapes: cardioid, rose curves, stars, and other shapes defined by r(θ)
  • Vortex animations: swirls, rotational warping, card game backgrounds (e.g., Balatro)
  • Fractal/repetitive structures: recursive symmetric patterns based on angular subdivision

Core Principles

Polar coordinates convert (x, y) to (r, θ):

  • r = length(p) — distance to origin
  • θ = atan(y, x) — angle from positive x-axis, range [-π, π]

Inverse transform: x = r·cos(θ), y = r·sin(θ)

Manipulation effects:

  • Modifying θ → rotation, warping, kaleidoscope
  • Modifying r → scaling, radial ripples
  • θ += f(r) → spiral effect
Spiral Type Equation Code
Archimedean spiral r = a + bθ theta += radius
Logarithmic spiral r = ae^(bθ) theta += log(radius)
Rose curve r = cos(nθ) r - A*sin(n*theta)

Implementation Steps

Step 1: UV Normalization and Centering

// Range [-1, 1], most commonly used
vec2 uv = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);

// Range [-aspect, aspect] x [-1, 1]
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;

// Pixelated style (Balatro style)
float pixel_size = length(iResolution.xy) / PIXEL_FILTER;
vec2 uv = (floor(fragCoord * (1.0/pixel_size)) * pixel_size - 0.5*iResolution.xy) / length(iResolution.xy);

Step 2: Cartesian → Polar Coordinates

float r = length(uv);
float theta = atan(uv.y, uv.x); // [-PI, PI]

// Reusable function
vec2 toPolar(vec2 p) { return vec2(length(p), atan(p.y, p.x)); }

// Normalized angle to [0, 1]
vec2 polar = vec2(atan(uv.y, uv.x) / 6.283 + 0.5, length(uv));

Step 3: Polar Space Operations

3a. Radial Swirl

float spin_amount = 0.25;
float new_theta = theta - spin_amount * 20.0 * r;

3b. Angular Twist

float twist_angle = theta + 2.0 * iTime + sin(theta) * sin(iTime) * 3.14159;

3c. Archimedean Spiral

vec2 spiral_uv = vec2(theta_normalized, r);
spiral_uv.y -= spiral_uv.x; // Unwrap into spiral band

3d. Logarithmic Spiral

float shear = 2.0 * log(r);
float c = cos(shear), s = sin(shear);
mat2 spiral_mat = mat2(c, -s, s, c);

3e. Kaleidoscope

float rep = 12.0;          // Number of symmetry axes
float sector = TAU / rep;
float a = polar.y;
float c_idx = floor((a + sector * 0.5) / sector);
a = mod(a + sector * 0.5, sector) - sector * 0.5;
a *= mod(c_idx, 2.0) * 2.0 - 1.0; // Mirror

3f. Spiral Arm Compression

float NB_ARMS = 5.0;
float COMPR = 0.1;
float phase = NB_ARMS * (theta - shear);
theta = theta - COMPR * cos(phase);
float arm_density = 1.0 + NB_ARMS * COMPR * sin(phase);

Step 4: Polar → Cartesian Reconstruction

vec2 new_uv = vec2(r * cos(new_theta), r * sin(new_theta));

vec2 toRect(vec2 p) { return vec2(p.x * cos(p.y), p.x * sin(p.y)); }

// Balatro-style round-trip (offset to screen center)
vec2 mid = (iResolution.xy / length(iResolution.xy)) / 2.0;
vec2 warped_uv = vec2(r * cos(new_theta) + mid.x, r * sin(new_theta) + mid.y) - mid;

Step 5: Polar Coordinate Shape SDF

// Cardioid
float a = atan(p.x, p.y) / 3.141593; // atan(x,y) makes the heart face upward
float h = abs(a);
float heart_r = (13.0*h - 22.0*h*h + 10.0*h*h*h) / (6.0 - 5.0*h);
float dist = r - heart_r;

// Rose curve
float rose_dist = abs(r - A_coeff * sin(PETAL_FREQ * theta) - 0.5);

// Rendering
float shape = smoothstep(0.01, -0.01, dist);

Step 6: Coloring and Anti-Aliasing

// fwidth adaptive anti-aliasing
float aa = smoothstep(-1.0, 1.0, value / fwidth(value));

// Resolution-based anti-aliasing
float aa_size = 2.0 / iResolution.y;
float edge = smoothstep(0.5 - aa_size, 0.5 + aa_size, value);

// Radial gradient coloring
vec3 color = vec3(1.0, 0.4 * r, 0.3);
color *= 1.0 - 0.4 * r;

// Inter-spiral-band anti-aliasing
float inter_spiral_aa = 1.0 - pow(abs(2.0 * fract(spiral_uv.y) - 1.0), 10.0);

Complete Code Template

// === Polar Coordinates & UV Manipulation Complete Template ===
// Paste directly into ShaderToy to run

#define PI 3.14159265359
#define TAU 6.28318530718

// ===== Adjustable Parameters =====
#define MODE 0            // 0=swirl, 1=spiral, 2=kaleidoscope, 3=rose curve
#define SPIRAL_TYPE 0     // 0=Archimedean, 1=logarithmic (MODE=1)
#define NUM_ARMS 5.0      // Number of spiral arms (MODE=1)
#define KALEID_SEGMENTS 6.0 // Kaleidoscope segments (MODE=2)
#define PETAL_COUNT 5.0   // Number of petals (MODE=3)
#define SWIRL_STRENGTH 3.0 // Swirl intensity (MODE=0)
#define ANIM_SPEED 1.0    // Animation speed
#define COLOR_SCHEME 0    // 0=warm, 1=cool, 2=rainbow

vec2 toPolar(vec2 p) {
    return vec2(length(p), atan(p.y, p.x));
}

vec2 toRect(vec2 p) {
    return vec2(p.x * cos(p.y), p.x * sin(p.y));
}

vec2 kaleidoscope(vec2 polar, float segments) {
    float sector = TAU / segments;
    float a = polar.y;
    float c = floor((a + sector * 0.5) / sector);
    a = mod(a + sector * 0.5, sector) - sector * 0.5;
    a *= mod(c, 2.0) * 2.0 - 1.0;
    return vec2(polar.x, a);
}

vec3 getColor(float t, int scheme) {
    if (scheme == 1) return 0.5 + 0.5 * cos(TAU * (t + vec3(0.0, 0.33, 0.67)));
    if (scheme == 2) return 0.5 + 0.5 * cos(TAU * t + vec3(0.0, 2.1, 4.2));
    return vec3(1.0, 0.4 + 0.4 * cos(t * TAU), 0.3 + 0.2 * sin(t * TAU));
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);
    vec2 polar = toPolar(uv);
    float r = polar.x;
    float theta = polar.y;
    float t = iTime * ANIM_SPEED;
    vec3 col = vec3(0.0);
    float aa = 2.0 / iResolution.y;

    #if MODE == 0
    // --- Swirl mode ---
    float swirl_theta = theta - SWIRL_STRENGTH * r + t;
    vec2 warped = toRect(vec2(r, swirl_theta));
    warped *= 10.0;
    float pattern = sin(warped.x) * cos(warped.y);
    pattern += 0.5 * sin(2.0 * warped.x + t) * cos(2.0 * warped.y - t);
    float val = smoothstep(-0.1, 0.1, pattern);
    col = mix(
        getColor(r * 0.5, COLOR_SCHEME),
        getColor(r * 0.5 + 0.5, COLOR_SCHEME),
        val
    );
    col *= exp(-r * 0.5);

    #elif MODE == 1
    // --- Spiral mode ---
    #if SPIRAL_TYPE == 0
        float spiral = theta / TAU + 0.5;
        float bands = spiral + r;
        bands -= t * 0.1;
        float arm = fract(bands * NUM_ARMS);
    #else
        float shear = 2.0 * log(max(r, 0.001));
        float phase = NUM_ARMS * (theta - shear);
        float arm = 0.5 + 0.5 * cos(phase);
        arm *= 1.0 + NUM_ARMS * 0.1 * sin(phase);
    #endif
    float brightness = smoothstep(0.0, 0.4, arm) * smoothstep(1.0, 0.6, arm);
    col = getColor(theta / TAU + t * 0.05, COLOR_SCHEME) * brightness;
    col *= exp(-r * r * 0.5);
    col += 0.15 * exp(-r * r * 8.0);

    #elif MODE == 2
    // --- Kaleidoscope mode ---
    vec2 kp = kaleidoscope(polar, KALEID_SEGMENTS);
    vec2 rect = toRect(kp);
    rect *= 4.0;
    rect += vec2(t * 0.3, 0.0);
    vec2 cell_id = floor(rect + 0.5);
    vec2 cell_uv = fract(rect + 0.5) - 0.5;
    float cell_hash = fract(sin(dot(cell_id, vec2(127.1, 311.7))) * 43758.5453);
    float d = length(cell_uv);
    float truchet = abs(d - 0.35);
    if (cell_hash > 0.5) {
        truchet = min(truchet, abs(length(cell_uv - 0.5) - 0.5));
    } else {
        truchet = min(truchet, abs(length(cell_uv + 0.5) - 0.5));
    }
    col = getColor(cell_hash + r * 0.2, COLOR_SCHEME);
    col *= smoothstep(0.05, 0.0, truchet - 0.03);
    col *= smoothstep(3.0, 0.0, r);

    #elif MODE == 3
    // --- Rose curve mode ---
    float rose_r = 0.6 * cos(PETAL_COUNT * theta + t);
    float dist = abs(r - abs(rose_r));
    float ribbon_width = 0.04;
    float rose_shape = smoothstep(ribbon_width + aa, ribbon_width - aa, dist);
    float depth = 0.5 + 0.5 * cos(PETAL_COUNT * theta + t);
    col = getColor(theta / TAU, COLOR_SCHEME) * depth;
    col *= rose_shape;
    float center = smoothstep(0.08 + aa, 0.08 - aa, r);
    col += getColor(0.5, COLOR_SCHEME) * center * 0.5;
    #endif

    col = pow(col, vec3(1.0 / 2.2));
    fragColor = vec4(col, 1.0);
}

Common Variants

Variant 1: Dynamic Vortex Background (Balatro Style)

Cartesian→Polar→Cartesian round-trip + iterative domain warping

float new_angle = atan(uv.y, uv.x) + speed
    - SPIN_EASE * 20.0 * (SPIN_AMOUNT * uv_len + (1.0 - SPIN_AMOUNT));
vec2 mid = (screenSize.xy / length(screenSize.xy)) / 2.0;
uv = vec2(uv_len * cos(new_angle) + mid.x,
           uv_len * sin(new_angle) + mid.y) - mid;
uv *= 30.0;
for (int i = 0; i < 5; i++) {
    uv2 += sin(max(uv.x, uv.y)) + uv;
    uv  += 0.5 * vec2(cos(5.1123 + 0.353*uv2.y + speed*0.131),
                       sin(uv2.x - 0.113*speed));
    uv  -= cos(uv.x + uv.y) - sin(uv.x*0.711 - uv.y);
}

Variant 2: Polar Torus Twist (Ring Twister Style)

Direct rendering in polar space, angular slicing to simulate 3D torus

vec2 uvr = vec2(length(uv), atan(uv.y, uv.x) + PI);
uvr.x -= OUT_RADIUS;
float twist = uvr.y + 2.0*iTime + sin(uvr.y)*sin(iTime)*PI;
for (int i = 0; i < NUM_FACES; i++) {
    float x0 = IN_RADIUS * sin(twist + TAU * float(i) / float(NUM_FACES));
    float x1 = IN_RADIUS * sin(twist + TAU * float(i+1) / float(NUM_FACES));
    vec4 face = slice(x0, x1, uvr);
    col = mix(col, face.rgb, face.a);
}

Variant 3: Galaxy / Logarithmic Spiral (Galaxy Style)

log(r) equiangular spiral + FBM noise + spiral arm compression

float rho = length(uv);
float ang = atan(uv.y, uv.x);
float shear = 2.0 * log(rho);
mat2 R = mat2(cos(shear), -sin(shear), sin(shear), cos(shear));
float phase = NB_ARMS * (ang - shear);
ang = ang - COMPR * cos(phase) + SPEED * t;
uv = rho * vec2(cos(ang), sin(ang));
float gaz = fbm_noise(0.09 * R * uv);

Variant 4: Archimedean Spiral Band (Wave Greek Frieze Style)

Polar unwrap into spiral band, creating vortex animation within the band

vec2 U = vec2(atan(U.y, U.x)/TAU + 0.5, length(U));
U.y -= U.x;                                    // Archimedean unwrap
U.x = arc_length(ceil(U.y) + U.x) - iTime;     // Arc length parameterization
vec2 cell_uv = fract(U) - 0.5;
float vortex = dot(cell_uv,
    cos(vec2(-33.0, 0.0)
        + 0.3 * (iTime + cell_id.x)
        * max(0.0, 0.5 - length(cell_uv))));

Variant 5: Complex / Polar Duality (Jeweled Vortex Style)

Complex arithmetic replaces explicit trigonometric functions for conformal mapping

float e = n * 2.0;
float a = atan(u.y, u.x) - PI/2.0;
float r = exp(log(length(u)) / e);      // r^(1/e)
float sc = ceil(r - a/TAU);
float s = pow(sc + a/TAU, 2.0);
col += sin(cr + s/n * TAU / 2.0);
col *= cos(cr + s/n * TAU);
col *= pow(abs(sin((r - a/TAU) * PI)), abs(e) + 5.0);

Performance & Composition

Performance Tips

  • Pole safety: float r = max(length(uv), 1e-6); to avoid division by zero
  • Trigonometric optimization: When both sin/cos are needed, use a rotation matrix mat2 ROT(float a) { float c=cos(a),s=sin(a); return mat2(c,s,-s,c); }
  • Kaleidoscope is naturally optimized: All expensive computation happens in a single sector, visual complexity ×N
  • Loop control: Rose curves and other multi-loop effects work well with 4-8 loops; don't go too high
  • Pixel downsampling: floor(fragCoord / pixel_size) * pixel_size quantizes coordinates to reduce computation

Composition Tips

  • Polar + FBM: Sample noise in transformed space → organic spiral textures
  • Polar + Truchet: Lay Truchet tiles after kaleidoscope folding → geometric tunnel effects
  • Polar + SDF: r(θ) defines contour + SDF boolean operations / glow
  • Polar + Checkerboard: sign(sin(u*PI*4.0)*cos(uvr.y*16.0)) → circular checkerboard
  • Polar + Post-Processing: Gamma + vignette + contrast enhancement for improved visual quality

Further Reading

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