Files
skills/shader-dev/techniques/fractal-rendering.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

Fractal Rendering Skill

Use Cases

  • Rendering self-similar mathematical structures: Mandelbrot/Julia sets (2D), Mandelbulb (3D), IFS fractals (Menger/Apollonian)
  • Procedural textures or backgrounds requiring infinite detail
  • Real-time generation of complex geometric visual effects (music visualization, sci-fi scenes, abstract art)
  • Suitable for ShaderToy, demo scene, procedural content generation

Core Principles

Fractal rendering is essentially visualization of iterative systems, falling into three categories:

1. Escape-Time Algorithm

Iterate Z <- Z^2 + c, count escape steps. Distance estimation by simultaneously tracking the derivative Z':

Z  <- Z^2 + c
Z' <- 2*Z*Z' + 1
d(c) = |Z|*log|Z| / |Z'|

2. Iterated Function System (IFS / KIFS)

Fold-sort-scale-offset iteration produces self-similar structures:

p = abs(p)                          // fold
sort p.xyz descending               // sort
p = Scale * p - Offset * (Scale-1)  // scale and offset

3. Spherical Inversion Fractals

fract() space folding + spherical inversion p *= s/dot(p,p):

p = -1.0 + 2.0 * fract(0.5*p + 0.5)
k = s / dot(p, p)
p *= k; scale *= k

All 3D fractals are rendered via Sphere Tracing (Ray Marching).

Implementation Steps

Step 1: Coordinate Normalization

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

Step 2: 2D Mandelbrot Escape-Time Iteration

float distanceToMandelbrot(in vec2 c) {
    vec2 z  = vec2(0.0);
    vec2 dz = vec2(0.0);
    float m2 = 0.0;

    for (int i = 0; i < MAX_ITER; i++) {
        if (m2 > BAILOUT * BAILOUT) break;
        // Z' -> 2*Z*Z' + 1
        dz = 2.0 * vec2(z.x*dz.x - z.y*dz.y,
                         z.x*dz.y + z.y*dz.x) + vec2(1.0, 0.0);
        // Z -> Z^2 + c
        z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c;
        m2 = dot(z, z);
    }
    return 0.5 * sqrt(dot(z,z) / dot(dz,dz)) * log(dot(z,z));
}

Step 3: Mandelbulb Distance Field (Spherical Coordinate Power-N)

float mandelbulb(vec3 p) {
    vec3 z = p;
    float dr = 1.0;
    float r;

    for (int i = 0; i < FRACTAL_ITER; i++) {
        r = length(z);
        if (r > BAILOUT) break;
        float theta = atan(z.y, z.x);
        float phi   = asin(z.z / r);
        dr = pow(r, POWER - 1.0) * dr * POWER + 1.0;
        r = pow(r, POWER);
        theta *= POWER;
        phi *= POWER;
        z = r * vec3(cos(theta)*cos(phi),
                      sin(theta)*cos(phi),
                      sin(phi)) + p;
    }
    return 0.5 * log(r) * r / dr;
}

Step 4: Menger Sponge Distance Field (KIFS)

float mengerDE(vec3 z) {
    z = abs(1.0 - mod(z, 2.0));  // infinite tiling
    float d = 1000.0;

    for (int n = 0; n < IFS_ITER; n++) {
        z = abs(z);
        if (z.x < z.y) z.xy = z.yx;
        if (z.x < z.z) z.xz = z.zx;
        if (z.y < z.z) z.yz = z.zy;
        z = SCALE * z - OFFSET * (SCALE - 1.0);
        if (z.z < -0.5 * OFFSET.z * (SCALE - 1.0))
            z.z += OFFSET.z * (SCALE - 1.0);
        d = min(d, length(z) * pow(SCALE, float(-n) - 1.0));
    }
    return d - 0.001;
}

Step 5: Apollonian Distance Field (Spherical Inversion)

vec4 orb;  // orbit trap

float apollonianDE(vec3 p, float s) {
    float scale = 1.0;
    orb = vec4(1000.0);

    for (int i = 0; i < INVERSION_ITER; i++) {
        p = -1.0 + 2.0 * fract(0.5 * p + 0.5);
        float r2 = dot(p, p);
        orb = min(orb, vec4(abs(p), r2));
        float k = s / r2;
        p *= k;
        scale *= k;
    }
    return 0.25 * abs(p.y) / scale;
}

Step 6: Ray Marching

float rayMarch(vec3 ro, vec3 rd) {
    float t = 0.01;
    for (int i = 0; i < MAX_STEPS; i++) {
        float precis = PRECISION * t;
        float h = map(ro + rd * t);
        if (h < precis || t > MAX_DIST) break;
        t += h * FUDGE_FACTOR;
    }
    return (t > MAX_DIST) ? -1.0 : t;
}

Step 7: Normal Calculation

// 4-tap tetrahedral method (recommended)
vec3 calcNormal(vec3 pos, float t) {
    float precis = 0.001 * t;
    vec2 e = vec2(1.0, -1.0) * precis;
    return normalize(
        e.xyy * map(pos + e.xyy) +
        e.yyx * map(pos + e.yyx) +
        e.yxy * map(pos + e.yxy) +
        e.xxx * map(pos + e.xxx));
}

Step 8: Shading & Lighting

vec3 shade(vec3 pos, vec3 nor, vec3 rd, vec4 trap) {
    vec3 light1 = normalize(LIGHT_DIR);
    float diff = clamp(dot(light1, nor), 0.0, 1.0);
    float amb  = 0.7 + 0.3 * nor.y;
    float ao   = pow(clamp(trap.w * 2.0, 0.0, 1.0), 1.2);

    vec3 brdf = vec3(0.4) * amb * ao + vec3(1.0) * diff * ao;

    vec3 rgb = vec3(1.0);
    rgb = mix(rgb, vec3(1.0, 0.8, 0.2), clamp(6.0*trap.y, 0.0, 1.0));
    rgb = mix(rgb, vec3(1.0, 0.55, 0.0), pow(clamp(1.0-2.0*trap.z, 0.0, 1.0), 8.0));
    return rgb * brdf;
}

Step 9: Camera

void setupCamera(vec2 uv, vec3 ro, vec3 ta, float cr, out vec3 rd) {
    vec3 cw = normalize(ta - ro);
    vec3 cp = vec3(sin(cr), cos(cr), 0.0);
    vec3 cu = normalize(cross(cw, cp));
    vec3 cv = normalize(cross(cu, cw));
    rd = normalize(uv.x * cu + uv.y * cv + 2.0 * cw);
}

Complete Code Template

3D Apollonian fractal (spherical inversion type) with full ray marching pipeline, orbit trap coloring, and AO. Ready to run in ShaderToy.

// Fractal Rendering — Apollonian (Spherical Inversion) Template

#define MAX_STEPS 200
#define MAX_DIST 30.0
#define PRECISION 0.001
#define INVERSION_ITER 8    // Tunable: 5-12
#define AA 1                // Tunable: 1=no AA, 2=4xSSAA

vec4 orb;

float map(vec3 p, float s) {
    float scale = 1.0;
    orb = vec4(1000.0);

    for (int i = 0; i < INVERSION_ITER; i++) {
        p = -1.0 + 2.0 * fract(0.5 * p + 0.5);
        float r2 = dot(p, p);
        orb = min(orb, vec4(abs(p), r2));
        float k = s / r2;
        p     *= k;
        scale *= k;
    }
    return 0.25 * abs(p.y) / scale;
}

float trace(vec3 ro, vec3 rd, float s) {
    float t = 0.01;
    for (int i = 0; i < MAX_STEPS; i++) {
        float precis = PRECISION * t;
        float h = map(ro + rd * t, s);
        if (h < precis || t > MAX_DIST) break;
        t += h;
    }
    return (t > MAX_DIST) ? -1.0 : t;
}

vec3 calcNormal(vec3 pos, float t, float s) {
    float precis = PRECISION * t;
    vec2 e = vec2(1.0, -1.0) * precis;
    return normalize(
        e.xyy * map(pos + e.xyy, s) +
        e.yyx * map(pos + e.yyx, s) +
        e.yxy * map(pos + e.yxy, s) +
        e.xxx * map(pos + e.xxx, s));
}

vec3 render(vec3 ro, vec3 rd, float anim) {
    vec3 col = vec3(0.0);
    float t = trace(ro, rd, anim);

    if (t > 0.0) {
        vec4 tra = orb;
        vec3 pos = ro + t * rd;
        vec3 nor = calcNormal(pos, t, anim);

        vec3 light1 = normalize(vec3(0.577, 0.577, -0.577));
        vec3 light2 = normalize(vec3(-0.707, 0.0, 0.707));
        float key = clamp(dot(light1, nor), 0.0, 1.0);
        float bac = clamp(0.2 + 0.8 * dot(light2, nor), 0.0, 1.0);
        float amb = 0.7 + 0.3 * nor.y;
        float ao  = pow(clamp(tra.w * 2.0, 0.0, 1.0), 1.2);

        vec3 brdf = vec3(0.40) * amb * ao
                  + vec3(1.00) * key * ao
                  + vec3(0.40) * bac * ao;

        vec3 rgb = vec3(1.0);
        rgb = mix(rgb, vec3(1.0, 0.80, 0.2), clamp(6.0 * tra.y, 0.0, 1.0));
        rgb = mix(rgb, vec3(1.0, 0.55, 0.0), pow(clamp(1.0 - 2.0*tra.z, 0.0, 1.0), 8.0));

        col = rgb * brdf * exp(-0.2 * t);
    }
    return sqrt(col);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    float time = iTime * 0.25;
    float anim = 1.1 + 0.5 * smoothstep(-0.3, 0.3, cos(0.1 * iTime));

    vec3 tot = vec3(0.0);

    #if AA > 1
    for (int jj = 0; jj < AA; jj++)
    for (int ii = 0; ii < AA; ii++)
    #else
    int ii = 1, jj = 1;
    #endif
    {
        vec2 q = fragCoord.xy + vec2(float(ii), float(jj)) / float(AA);
        vec2 p = (2.0 * q - iResolution.xy) / iResolution.y;

        vec3 ro = vec3(2.8*cos(0.1 + 0.33*time),
                       0.4 + 0.3*cos(0.37*time),
                       2.8*cos(0.5 + 0.35*time));
        vec3 ta = vec3(1.9*cos(1.2 + 0.41*time),
                       0.4 + 0.1*cos(0.27*time),
                       1.9*cos(2.0 + 0.38*time));
        float roll = 0.2 * cos(0.1 * time);

        vec3 cw = normalize(ta - ro);
        vec3 cp = vec3(sin(roll), cos(roll), 0.0);
        vec3 cu = normalize(cross(cw, cp));
        vec3 cv = normalize(cross(cu, cw));
        vec3 rd = normalize(p.x*cu + p.y*cv + 2.0*cw);

        tot += render(ro, rd, anim);
    }

    tot /= float(AA * AA);
    fragColor = vec4(tot, 1.0);
}

Common Variants

1. 2D Mandelbrot (Distance Estimation Coloring)

Pure 2D, no ray marching needed. Complex iteration + distance coloring.

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 p = (2.0*fragCoord - iResolution.xy) / iResolution.y;
    float tz = 0.5 - 0.5*cos(0.225*iTime);
    float zoo = pow(0.5, 13.0*tz);
    vec2 c = vec2(-0.05, 0.6805) + p * zoo; // Tunable: zoom center point

    vec2 z = vec2(0.0), dz = vec2(0.0);
    for (int i = 0; i < 300; i++) {
        if (dot(z,z) > 1024.0) break;
        dz = 2.0*vec2(z.x*dz.x-z.y*dz.y, z.x*dz.y+z.y*dz.x) + vec2(1.0,0.0);
        z  = vec2(z.x*z.x-z.y*z.y, 2.0*z.x*z.y) + c;
    }

    float d = 0.5*sqrt(dot(z,z)/dot(dz,dz))*log(dot(z,z));
    d = clamp(pow(4.0*d/zoo, 0.2), 0.0, 1.0);
    fragColor = vec4(vec3(d), 1.0);
}

2. Mandelbulb Power-N

Spherical coordinate trigonometric functions; POWER parameter controls morphology.

#define POWER 8.0       // Tunable: 2-16
#define FRACTAL_ITER 4  // Tunable: 2-8

float mandelbulbDE(vec3 p) {
    vec3 z = p;
    float dr = 1.0, r;
    for (int i = 0; i < FRACTAL_ITER; i++) {
        r = length(z);
        if (r > 2.0) break;
        float theta = atan(z.y, z.x);
        float phi   = asin(z.z / r);
        dr = pow(r, POWER - 1.0) * dr * POWER + 1.0;
        r = pow(r, POWER);
        theta *= POWER; phi *= POWER;
        z = r * vec3(cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi)) + p;
    }
    return 0.5 * log(r) * r / dr;
}

3. Menger Sponge (KIFS)

abs() folding + conditional sorting, regular geometric fractal.

#define SCALE 3.0
#define OFFSET vec3(0.92858,0.92858,0.32858)
#define IFS_ITER 7

float mengerDE(vec3 z) {
    z = abs(1.0 - mod(z, 2.0));
    float d = 1000.0;
    for (int n = 0; n < IFS_ITER; n++) {
        z = abs(z);
        if (z.x < z.y) z.xy = z.yx;
        if (z.x < z.z) z.xz = z.zx;
        if (z.y < z.z) z.yz = z.zy;
        z = SCALE * z - OFFSET * (SCALE - 1.0);
        if (z.z < -0.5*OFFSET.z*(SCALE-1.0))
            z.z += OFFSET.z*(SCALE-1.0);
        d = min(d, length(z) * pow(SCALE, float(-n)-1.0));
    }
    return d - 0.001;
}

4. Quaternion Julia Set

Quaternion Z <- Z^2 + c (4D), with fixed c parameter; visualized by taking a 3D slice.

vec4 qsqr(vec4 a) {
    return vec4(a.x*a.x - a.y*a.y - a.z*a.z - a.w*a.w,
                2.0*a.x*a.y, 2.0*a.x*a.z, 2.0*a.x*a.w);
}

float juliaDE(vec3 p, vec4 c) {
    vec4 z = vec4(p, 0.0);
    float md2 = 1.0, mz2 = dot(z, z);
    for (int i = 0; i < 11; i++) {
        md2 *= 4.0 * mz2;
        z = qsqr(z) + c;
        mz2 = dot(z, z);
        if (mz2 > 4.0) break;
    }
    return 0.25 * sqrt(mz2 / md2) * log(mz2);
}
// Animated c: vec4 c = 0.45*cos(vec4(0.5,3.9,1.4,1.1)+time*vec4(1.2,1.7,1.3,2.5))-vec4(0.3,0,0,0);

5. Minimal IFS Field (2D, No Ray Marching)

abs(p)/dot(p,p) + offset iteration, weighted accumulation produces a density field.

float field(vec3 p) {
    float strength = 7.0 + 0.03 * log(1.e-6 + fract(sin(iTime) * 4373.11));
    float accum = 0.0, prev = 0.0, tw = 0.0;
    for (int i = 0; i < 32; ++i) {
        float mag = dot(p, p);
        p = abs(p) / mag + vec3(-0.5, -0.4, -1.5); // Tunable: offset values
        float w = exp(-float(i) / 7.0);
        accum += w * exp(-strength * pow(abs(mag - prev), 2.3));
        tw += w;
        prev = mag;
    }
    return max(0.0, 5.0 * accum / tw - 0.7);
}

Performance & Composition

Performance Tips

  • Core bottleneck: outer ray marching x inner fractal iteration (e.g., 200 x 8 = 1600 map calls per pixel)
  • Reduce MAX_STEPS to 60-100, compensate with fudge factor 0.7-0.9
  • Hit threshold precis = 0.001 * t relaxes with distance
  • Fractal iteration: break immediately when |z|^2 > bailout
  • Reducing iterations from 8 to 4-5 has minimal visual impact
  • Use 4-tap normals instead of 6-tap to save 33%
  • Use AA=1 during development, AA=2 for release (AA=3 = 9x overhead)
  • Avoid pow() inside loops; manually expand for low powers

Composition Techniques

  • Volumetric light: accumulate exp(-10.0 * h) during ray march for god rays
  • Tone Mapping: ACES + sRGB gamma for handling high-frequency detail
  • Transparent refraction: negative distance field reverse ray march + Beer's law absorption
  • Orbit Trap coloring: map trap values to HSV or emissive colors
  • Soft shadows: ray march toward light, accumulate min(k * h / t) for soft shadows

Further Reading

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