Day 2: Fractals — Homework
Checkpoints
These verify you've understood the core material.
C1. Julia Mouse. Modify the Mandelbrot shader to render a Julia set controlled by the mouse. Fix c to come from iMouse.xy (mapped to z from the pixel position. Drag around and watch the Julia set change—connected structures shatter into dust as you cross outside the Mandelbrot set.
C2. Higher Powers. The iteration
vec2 ccube(vec2 z) {
return cmul(cmul(z, z), z);
}
For arbitrary powers, use polar form:
vec2 cpow(vec2 z, float n) {
float r = length(z);
float theta = atan(z.y, z.x);
return pow(r, n) * vec2(cos(n * theta), sin(n * theta));
}
Try
C3. Animated Zoom. Animate a zoom into the Mandelbrot set. To zoom smoothly, the magnification factor
Modify normalize_coord to incorporate zoom:
vec2 normalize_coord(vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = uv - vec2(0.5, 0.5);
uv.x *= iResolution.x / iResolution.y;
float zoom = pow(2.0, iTime); // doubles every second
return uv * 2.5 / zoom;
}
Then zoom into a specific point by adding an offset:
vec2 center = vec2(-0.745, 0.186); // seahorse valley
vec2 c = center + normalize_coord(fragCoord);
Try different zoom rates (change the base from 2.0) and different center points. What happens to the boundary as you zoom past
C4. Apollonian Animation. Animate the Apollonian gasket by letting the iteration count grow with time:
int max_iter = int(mod(iTime * 5.0, 50.0)) + 1;
Watch each iteration reveal a new layer of circles.
Explorations
These extend the day's topics in interesting directions.
E1. Julia Animation. Animate c along the boundary of the main cardioid:
vec2 cardioid(float t) {
vec2 eit = vec2(cos(t), sin(t));
return cmul(eit, (vec2(2.0, 0.0) - eit)) / 4.0;
}
vec2 c = cardioid(iTime * 0.5);
Every point on the cardioid gives a Julia set with a parabolic fixed point—right at the edge of connectivity. What do you observe about the Julia sets as
E2. Other Escape-Time Fractals. The Mandelbrot iteration
Burning Ship:
— take absolute values before squaring. Center around𝑧 𝑛 + 1 = ( | R e ( 𝑧 𝑛 ) | + 𝑖 | I m ( 𝑧 𝑛 ) | ) 2 + 𝑐 to see the main structure. Then try zooming into the small blob in the lower-left (around( − 0 . 5 , − 0 . 5 ) at 20× zoom) to find a miniature "burning ship."( − 1 . 8 , − 0 . 0 5 ) Tricorn:
where𝑧 𝑛 + 1 = ¯ 𝑧 2 𝑛 + 𝑐 is the complex conjugate. Also called the "Mandelbar."¯ 𝑧 = 𝑥 − 𝑖 𝑦 Celtic:
— absolute value of real part after squaring.𝑧 𝑛 + 1 = ( | R e ( 𝑧 2 𝑛 ) | + 𝑖 I m ( 𝑧 2 𝑛 ) ) + 𝑐
The escape condition
E3. Sierpinski Carpet. Render the Sierpinski carpet using the same iterate-and-test approach as the Apollonian gasket.
The carpet lives in the unit square
Scale coordinates by 3 to see which of 9 sub-cells you're in
If you're in the center cell, stop (this region is removed)
Otherwise, use
fractto zoom into that cell and repeat
Color the carpet black and removed regions white (or vice versa). The key insight: floor(p * 3.0) gives the cell indices (0, 1, or 2 in each dimension), and fract(p * 3.0) gives position within that cell, ready for the next iteration.
E4. Sierpinski Triangle. Apply the same subdivision technique to the Sierpinski triangle.
We use a right triangle with vertices at
Bottom-left sub-triangle:
𝑥 + 𝑦 < 0 . 5 Bottom-right sub-triangle:
𝑥 > 0 . 5 Top sub-triangle:
𝑦 > 0 . 5 Middle (removed): everything else
Each sub-triangle maps back to the full triangle by scaling by 2 and translating. Work out the three transformations and implement the shader.
Extension: The right triangle looks tilted. Apply an affine transformation at the end to map it to an equilateral triangle centered at the origin. The transformation is a shear followed by a scale.
E5. Apollonian Domain Coloring. Modify the Apollonian gasket to track which circle you inverted through most recently. After iteration, color by this: red for c1, green for c2, blue for c3, yellow for outer.
What structure do you see? Where are the boundaries between colored regions? How does this relate to the limit set?
Challenges
These require ideas beyond the lecture.
H1. Split-Screen Julia Explorer. Build a side-by-side display with the Mandelbrot set on the left and a Julia set on the right. The mouse position in the left half selects the parameter
Requirements:
Left half shows Mandelbrot, right half shows Julia—each properly centered and aspect-corrected within its half
Mouse position determines
, but only when clicking/dragging on the left half𝑐 A small colored dot marks the current
value on the Mandelbrot side𝑐 Both halves should use the same coloring scheme
The coordinate setup is the tricky part: each half of the screen needs its own independent mapping to
Watch the Julia set transform as you drag across the Mandelbrot boundary—connected sets shatter into dust.
H2. Newton Fractal. Newton's method finds roots of
This converges to one of the three cube roots of unity:
You'll need complex division:
vec2 cdiv(vec2 z, vec2 w) {
float denom = dot(w, w);
return vec2(z.x*w.x + z.y*w.y, z.y*w.x - z.x*w.y) / denom;
}
How do you determine which root the orbit converged to? The basin boundaries form a fractal—every point on the boundary has all three basins arbitrarily close by.
H3. Escape Radius Proofs. Prove the facts that justify the escape-time algorithm.
For the Mandelbrot set:
If
, then| 𝑐 | > 2 .𝑐 ∉ M If
for some| 𝑧 𝑛 | > 2 (with𝑛 ), the orbit escapes to infinity.| 𝑐 | ≤ 2
For Julia sets: If
Hint: The key inequality is
H4. Period Detection. Some orbits in the Mandelbrot set are periodic—they settle into cycles. A period-1 orbit converges to a fixed point; period-2 alternates between two values (
To detect periodicity, track the last few iterates and check if the current value matches one of them. You'll need separate variables for
Implement a shader that colors the Mandelbrot set by period: one color for period-1, another for period-2, another for period-3. Points that escape get colored by iteration count as usual; points that don't converge to a detected period stay black.
What structure do you find?
H5. Orbit Traps. The escape-time algorithm colors points by when the orbit escapes. An orbit trap colors points by where the orbit goes—specifically, how close it passes to a geometric "trap" shape.
During iteration, track the minimum distance from
Good traps to try:
Point trap:
length(z)— distance to originCircle trap:
abs(length(z) - 1.0)— distance to unit circleCross trap:
min(abs(z.x), abs(z.y))— distance to axes
Experiment with coloring: palette(trap_dist * 5.0) or glow effects like exp(-trap_dist * 20.0).
The results reveal structure invisible in standard escape-time coloring.
Projects
Project 1: Grid of Julia Sets
Create a grid where each cell shows the Julia set for a different parameter
// Grid setup
float L = 0.5; // cell size
// Grid formula
vec2 cell_id = floor(p / L + 0.5); // which cell (round to nearest)
vec2 cell_center = cell_id * L; // center of this cell
vec2 local = p - cell_center; // offset from center, in [-L/2, L/2]
Map cell_center to a parameter local scaled to
Color the Julia sets dark on a light background (inverted from the usual convention). When you zoom out, the grid reveals the Mandelbrot set: cells with
Project 2: Deep Zoom with Distance Estimation
The escape-time algorithm breaks down at high magnification: boundaries become blocky staircases of iteration bands. Increasing max_iter helps but never fully solves it. Distance estimation produces smooth, resolution-independent boundaries by computing the actual distance to the Mandelbrot set.
Part A: The Problem
Implement an animated zoom (C3) targeting the seahorse valley at
Part B: Distance to an Implicit Curve
For any implicit curve
This is the first-order Taylor approximation:
For the Mandelbrot set, we need the gradient of the escape-time function with respect to
This is just the chain rule. Track
When the orbit escapes, the distance to the Mandelbrot boundary is:
The
To read more, see
Distance Estimation Method for Fractals, Lindsay Robert Wilson
Distance to Fractals, Inigo Quilez
Part C: Implementation
Track z and dz as two vec2s. Important: update dz before z in the loop, since the derivative formula uses the current z. Use a large escape radius (256) for numerical stability in the log formula:
vec2 z = vec2(0.0, 0.0);
vec2 dz = vec2(0.0, 0.0);
for (int i = 0; i < 256; i++) {
if (length(z) > 256.0) break;
// Update dz BEFORE z (uses current z)
dz = 2.0 * cmul(z, dz) + vec2(1.0, 0.0);
z = cmul(z, z) + c;
}
float r = length(z);
float dr = length(dz);
float d = 0.5 * r * log(r) / dr;
Build a complete shader that renders the Mandelbrot set using distance estimation with a glow effect: exp(-d * scale). Start with the standard view (no zoom) and adjust scale until you see a bright glow near the boundary that fades to dark farther away. Protect against division by zero when dr is very small.
Part D: Adding Zoom
Add animated zoom to your Part C shader using the exponential zoom from C3. The challenge: as you zoom in, the glow becomes invisibly thin (the distance shrinks in world coordinates).
The fix: multiply d by the zoom factor before computing the glow. This keeps the visual thickness constant:
float zoom = pow(2.0, iTime);
// ... compute d as before ...
float scaled_d = d * zoom;
float glow = exp(-scaled_d * 200.0);
Target the seahorse valley at
Part E: Smooth Coloring for the Exterior
Distance estimation handles the boundary, but the exterior still shows discrete iteration bands. A fractional escape time removes them.
When the orbit escapes at iteration
// After the loop, if escaped:
float log_zn = log(length(z));
float nu = log(log_zn / log(2.0)) / log(2.0);
float smooth_iter = float(i) + 1.0 - nu;
Use smooth_iter instead of i for coloring. The bands vanish.
Part F: Putting It Together
Build a polished deep zoom combining:
Distance estimation for crisp boundaries at any magnification
Smooth coloring for the exterior
Animated exponential zoom (C3)
A visually interesting target (minibrots, spirals, dendrites)
Compare your result to pure escape-time rendering at the same magnification. The difference is dramatic.
Extension: Dual Complex Numbers
Instead of tracking z and dz separately, pack them into a vec4 where .xy is the value and .zw is the derivative. Define arithmetic so derivatives propagate automatically:
vec4 dcSqr(vec4 a) {
return vec4(
a.x*a.x - a.y*a.y, // z²: real
2.0*a.x*a.y, // z²: imag
2.0*(a.x*a.z - a.y*a.w), // (z²)': real
2.0*(a.x*a.w + a.y*a.z) // (z²)': imag
);
}
vec4 dcAdd(vec4 a, vec4 b) {
return a + b;
}
Then the iteration is just z = dcAdd(dcSqr(z), c) where c = vec4(c.xy, 1.0, 0.0).
Project 3: Orbit Visualization
Instead of coloring by iteration count, visualize the actual orbit. Let the mouse control the starting point
Compute iterates
𝑧 0 , 𝑧 1 , … , 𝑧 𝑁 Draw a small circle at each iterate
Connect consecutive iterates with line segments (use the segment SDF from Day 1)
Color by iteration index—early iterates blue, later iterates red
Start with the Mandelbrot/Julia iteration: for a fixed
Once it works for complex dynamics, try the same visualization for the Apollonian gasket or Sierpinski carpet. Watch the point bounce between inversions or zoom into sub-cells. The iteration logic differs but the visualization technique is identical.