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

374 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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: `attribute``in`, `varying``out`
- Fragment shader: `varying``in`, `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
```glsl
// 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
```glsl
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**
```glsl
float spin_amount = 0.25;
float new_theta = theta - spin_amount * 20.0 * r;
```
**3b. Angular Twist**
```glsl
float twist_angle = theta + 2.0 * iTime + sin(theta) * sin(iTime) * 3.14159;
```
**3c. Archimedean Spiral**
```glsl
vec2 spiral_uv = vec2(theta_normalized, r);
spiral_uv.y -= spiral_uv.x; // Unwrap into spiral band
```
**3d. Logarithmic Spiral**
```glsl
float shear = 2.0 * log(r);
float c = cos(shear), s = sin(shear);
mat2 spiral_mat = mat2(c, -s, s, c);
```
**3e. Kaleidoscope**
```glsl
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**
```glsl
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
```glsl
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
```glsl
// 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
```glsl
// 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
```glsl
// === 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
```glsl
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
```glsl
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
```glsl
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
```glsl
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
```glsl
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](../reference/polar-uv-manipulation.md)