-
Notifications
You must be signed in to change notification settings - Fork 0
FractalEquation DesignGuide
Companion pages: Technical Index · CalculatorGen Authoring · CalculatorGen Architecture · User-facing CalcGen Guide · User Bulb 3D Guide (user-facing) · Resources & Bibliography
The 2-D CalcGen pipeline is the right backbone for escape-time work in the complex plane, but the User Bulb engine extends the same idea to ℝ³ via two non-equivalent algebras: triplex (the Mandelbulb's spherical-coordinate trick) and quaternion (a four-dimensional skew field where the classical Julia/Mandelbrot recurrence is well-defined).
Quaternions are numbers of the form
with the Hamilton multiplication rules
Multiplication is associative but not commutative. Conjugation is
For two quaternions p and q, the product expands explicitly as
A 3-D slice through the 4-D quaternion Julia/Mandelbrot set fixes one of the four components (the
shell uses d = 0 by default) and varies the other three across screen pixels and the ray-marcher.
The natural lift of z² + c to ℍ is
where q² = q*q per the multiplication table above. The User Bulb body equivalent in DSL form:
let q2 = quat_mul(q, q);
return q2 + c;
In C# (Roslyn body) form:
var q2 = new Quat(
q.A * q.A - q.B * q.B - q.C * q.C - q.D * q.D,
2 * q.A * q.B,
2 * q.A * q.C,
2 * q.A * q.D);
return q2 + c;(The cross-terms cancel for q * q because p = q — many terms simplify pairwise.)
Quaternion fractals are 4-D objects sliced into 3-D. The standard distance estimator follows the same template as the 2-D Mandelbrot case (Quilez):
…where q' is the running derivative tracked alongside the iteration. For q_{n+1} = q_n^2 + c,
the chain rule gives
(again, non-commutative multiplication — order matters; the constant on the right is the derivative
of +c). The User Bulb engine carries q' as a quaternion derivative alongside q in the DE mode
combo's analytic setting.
| Property | Triplex (v^p via spherical coords) |
Quaternion (q² + c) |
|---|---|---|
| Algebra closed? | No (singular at the poles) | Yes — proper skew field |
| Symmetry | Power-p rotational |
Translation under q → q + ℝk
|
| Famous instance | Mandelbulb (p = 8) | Norton's quaternion Julia |
| Smoothness of boundary | Cusps at z-axis | Smooth except at slice singularities |
| DE robustness | Heuristic | Analytic |
| Speed | Faster — fewer multiplies | Slower — 16-term multiply |
Both are first-class authoring modes in the User Bulb editor; the Algebra combo flips between them.
Note
Quaternion mode is not limited to q² + c. The Quat type exposes a full analytic transcendental
library — Pow (integer self-multiply and fractional exp(exp·log q) branches), Exp/Log/Sqrt/Inverse,
and the complete trig, hyperbolic, and inverse families evaluated on the quaternion's principal axis — so
Quat.Sin(z² ) + c or Quat.Pow(z, 2.5) + c are valid maps. Every op obeys an escape contract: undefined
inputs return a non-finite quaternion (which the DE loop escapes as a pixel) rather than throwing, because the
hot loop has no try/catch. The same surface is reachable from the Sandbox DSL via the q* functions, and
the Sandbox path is what renders quaternion sets on the GPU. See
UserBulb-Guide §4 and
§19.5 for the full API and worked examples.
A practical introduction to designing fractal equations for the CalcGen DSL. By the end of this guide you should be able to:
- Read and explain any equation expressed in the CalcGen DSL.
- Predict the rough character of its output (filaments, lobes, spirals, symmetric vs asymmetric, smooth vs feathered).
- Modify a given equation to push the result in a chosen direction (more symmetry, more chaos, deeper detail, smoother boundary).
- Construct new equations from scratch for any CalcGen-supported fractal
family, not just the classical Mandelbrot
z² + c.
The CalcGen DSL grammar reference is in CalcGen-UserGuide.md. This guide
focuses on the why — what the equations mean, how to think about them,
how to change them.
Every fractal the CalcGen engine renders is an escape-time fractal. The recipe is identical for every equation in this guide:
- For each pixel on the screen, take the complex coordinate
c(the pixel's position on the complex plane). - Start with
z = 0(or sometimes another seed — see §10). - Apply the user equation repeatedly:
z_{n+1} = f(z_n, c). - After each step, ask: has
|z|exceeded the bailout radius? The engine uses|z|² ≥ 1024(or≥ 4for the classic Mandelbrot contract). - Either:
-
Escaped: the iteration count
nat which it escaped tells us how fast the orbit blew up — color the pixel based on that count (with a smooth-iter correction for anti-aliased gradients). -
Bounded: after
maxItersteps the orbit still hasn't escaped. Color the pixel as "in the set" — the deep, usually-black or dark-themed colour.
-
Escaped: the iteration count
The fractal you see is the boundary between escaped pixels and the bounded set. That boundary is the structure the iteration produces.
The single function f(z, c) is the entire creative space. Change f
and you change the fractal. The DSL is the language in which you write
f.
The mapping f rearranges the complex plane on every step. Points near
the boundary of the bounded set are arbitrarily close to points whose
orbits diverge. As you zoom in, the boundary keeps revealing finer
structure because the iteration keeps separating "stays bounded" from
"escapes" pixels at every length scale. That's the source of the
fractal dimension.
External reading on the mathematical foundations:
- Wikipedia: Mandelbrot set
- Wikipedia: Escape-time fractal
- Inigo Quilez — Smooth iteration count
- John Milnor, Dynamics in One Complex Variable (book) — the
canonical treatment of
z² + cand its generalisations.
Take the simplest equation, the classical Mandelbrot:
z*z + c
Read this as z_{n+1} = z_n² + c. Two pieces:
| Piece | Role |
|---|---|
z*z |
The feedback — operates on the previous iterate. |
+ c |
The forcing — the constant for this pixel that breaks |
| the orbit out of the trivial fixed point at 0. |
Almost every escape-time fractal in this guide has the same skeleton:
some-function-of(z, n, prev) + something-involving(c)
The function of z decides how fast magnitudes grow. The function of
c decides where the boundary sits. Mix in n (iteration index) or
prev (previous iterate z_{n-1}) to break the pure z-feedback and
get richer dynamics.
The engine doesn't just colour by the raw escape count — that would produce visible iso-iter bands. It computes a smooth correction:
smooth_n = n + 1 − log₂(log₂(|z|))
This gives a continuous escape value across the boundary, producing the gradient-smoothed look. You don't need to write this — it happens automatically in the renderer.
z*z + c
The most-iterated equation in mathematics. Cardioid + bulb structure. Symmetric across the real axis. All other quadratic Julia sets are slices of this fractal's parameter space.
z*z*z + c (degree 3 — 2-fold rotational symmetry)
z*z*z*z + c (degree 4 — 3-fold)
z^5 + c (degree 5 — 4-fold)
z^d + c (general — (d−1)-fold symmetry)
Each increment of the exponent adds another symmetry lobe. The body becomes more star-shaped; the boundary acquires more arms.
External: Wikipedia: Multibrot.
z*z + 0.5*z + c
Adds a linear pull-back. Distorts the cardioid; can rotate the whole
silhouette. The coefficient on z shifts the location of the principal
attractor away from 0. Negative coefficients pull the silhouette in
the opposite direction.
z*z*z - 0.5*z + c
A cubic plus a linear suppression term. Produces twisted figure-8
silhouettes. Mixed coefficients on different powers of z are how to
break the rigid d-fold symmetry of pure z^d + c shapes.
0.7*z*z + c
Shrinks the feedback. The cardioid shrinks; the boundary becomes
smoother because the forcing +c dominates more steps. Try
coefficients between 0.1 and 2.0 to see the boundary breathe.
These break the holomorphic (complex-differentiable) chain by folding real or imaginary components. The result is dramatically different silhouettes that look more like landscapes than the round Mandelbrot.
conj(z)*conj(z) + c
conj(z) is (re(z), −im(z)). Squaring the conjugate produces 3-fold
boundary symmetry (instead of the Mandelbrot's 2-fold). The cardioid
becomes a deltoid; bulbs are arranged threefold.
External: Wikipedia: Tricorn.
fold(z)*fold(z) + c
fold(z) is (|re|, |im|) — each component absolute-valued. The
silhouette looks like a sinking ship surrounded by waves at high
contrast. Discover the "antenna" feature by zooming in near the lower
edge.
External: Wikipedia: Burning Ship.
conj(fold(z))*conj(fold(z)) + c
Combines both anti-holomorphic ops. fold(z) = (|re|, |im|), then
conj negates the imaginary part: (|re|, −|im|). Squaring gives
(|re|² − |im|², −2|re||im|) — same magnitude envelope as Burning
Ship but with the imaginary component flipped. Produces a vertically-
mirrored ship silhouette with the Tricorn's deltoid hint in the body.
Note: fold(conj(z)) is not equivalent — conj negates im, then
fold re-positives it, so the conjugate cancels and you get the plain
Burning Ship. The order conj(fold(z)) matters.
Anti-holomorphic operators (conj, fold) disable the distance
estimate. The engine still renders the escape-time silhouette
correctly; you just lose the surface-normal shading that's available
on the holomorphic equations. Iso-iter banding is more visible. Use
the smooth-iter colour gradient to compensate.
Division enables Newton-shaped, Cassini-oval, and rational-pole maps.
z*z + c/(z + 2)
Quadratic feedback plus a rational forcing term. The denominator
(z + 2) introduces a pole at z = −2. Pixels whose orbit grazes that
neighbourhood get the forcing amplified. Produces a Mandelbrot-like
silhouette with the left side reshaped by the amplified c term.
Note: this is not simply (z² + c)/(z + a) — that family is
degenerate because z₀=0 ⇒ z₁=c/a and f(c/a) = c/a (instant fixed
point, no fractal). Keep the polynomial feedback separate from the
rational term to avoid the algebraic cancellation.
z*z + c/(1 + z*z)
The forcing is now a rational function of z. Suppresses the forcing
inside the unit disk; amplifies it outside. The silhouette becomes
more concentrated near the origin and develops a thin shell of
boundary at larger radii.
z*z + 1/(z*z + c)
Standard quadratic feedback plus a reciprocal whose poles sit at
z = ±√(−c) — they move with c, so every pixel sees a different
singular structure. Orbits that pass near a pole get violently
amplified. Produces a primary Mandelbrot lobe plus secondary rational
structure where the pole pair approaches the real axis.
Note: avoid bare 1/z constructions like (1/z + c)². Because the
engine seeds z₀ = 0, 1/0 immediately produces Inf, the orbit
becomes NaN on the next step, and NaN never satisfies the bailout
test — every pixel registers as "in set" (solid colour). Always guard
the reciprocal with a denominator that's non-zero at z=0, e.g.
1/(z*z + c) or 1/(z + 2).
Division disables perturbation and BLA. Deep zooms past ~1e15 become slow (the DD/QD HpDirect path engages but without SA acceleration). Distance estimate stays available via the quotient rule. Avoid division if you intend to zoom past ~1e10 interactively.
The CalcGen DSL exposes the holomorphic continuations of the standard transcendentals. They produce periodic structures in the imaginary axis (sin / cos) or exponential asymmetry (exp / log).
exp(z) + c
The exponential map: exp(a+bi) = e^a · (cos b + i sin b). The
silhouette is dramatically asymmetric in the real axis: right half
(positive Re(z)) escapes in 1–2 iterations because |exp(z)| is
e^Re(z). Left half oscillates. Boundary is fractally feathered.
External: Devaney's papers on exponential dynamics — search "Devaney exponential dynamics".
sin(z) + c
sin(a+bi) = sin(a)·cosh(b) + i cos(a)·sinh(b). The cosh factor blows
up in the imaginary axis. Silhouette has narrow horizontal bands of
bounded behaviour separated by escape strips at multiples of π.
log(1 + z) + c
log has a branch cut along the negative real axis. The 1 + offset
shifts the cut so it doesn't pass through z₀=0 — without that shift,
log(0) = −∞ immediately on the first iteration and every pixel
registers as escaped/in-set uniformly (no fractal). With the offset,
the silhouette develops a curved boundary that flattens toward the
asymptote.
Avoid bare log(z) + c: with seed z₀=0 the first step yields
-Inf + 0i, the orbit becomes NaN, and the renderer produces a
single solid colour. Always guard log arguments to be non-zero at
the seed.
sin(z) + cos(c)
Constant c enters through cos, periodic in Re(c). Result is a
quasi-periodic mosaic of bounded regions repeating with period 2π
horizontally and Cassini-like vertically.
0.5*sin(z*z) + c
Squares z inside the sin, then halves the magnitude. Produces a
dampened oscillation around the classical Mandelbrot silhouette —
fine ripples ride on top of the cardioid boundary.
tan(z) + c
tan(z) = sin(z)/cos(z). Poles at cos(z) = 0 (every odd multiple
of π/2 on the real axis). The silhouette has periodic vertical lines
of singularities; bounded regions appear as ovals between them.
sinh(z) + c
cosh(z) + c
tanh(z) + c
sinh / cosh are the hyperbolic analogues — they grow exponentially
in BOTH directions of the real axis (unlike exp which grows only to
the right). tanh saturates to ±1 and produces nearly-flat bounded
regions with sharp transitions.
sqrt(z) + c
Desugars to exp(0.5*log(z)). The principal branch of √z. Bounded
set is concentrated near the origin with two "tail" structures along
the imaginary axis (where the branch cut is approached from above and
below).
sin(pi*z) + c — sin with period 2 on the real axis
exp(z) + e — exp shifted by Euler's constant
z*z + pi*c — quadratic with π-scaled forcing
pi and e parse as their numeric values (Math.PI, Math.E)
respectively, exactly like writing 3.14159....
pow(z, 3) + c — cubic Multibrot, general-power form
pow(z, -2) + c — inverse ("Donut") map, finite at the z=0 seed
pow(z, 2.5) + c — fractional Multibrot
z*pow(z, -3) + c*pow(c, -2) — "Movie Reel" mixed inverse powers
pow is distinct from the ^ operator. ^ takes a non-negative integer
exponent ≤ 64 and stays a polynomial (fully deep-zoomable). pow(base, exp)
accepts any exponent — negative, fractional, or complex — and evaluates it
as the principal complex power, matching the SandboxExpression runtime and
System.Numerics.Complex.Pow. It is zero-guarded: pow(0, 0) = 1 and
pow(0, k) = 0, so a negative-power map is finite at the z = 0 Mandelbrot
seed. (The naïve 1/z^k form is NaN there and blanks the image — always use
pow for negative powers.) Being transcendental (exp·log under the hood), it
disables perturbation / BLA / SA and the distance estimate.
atan(z) + c — bounded inverse-tangent map
asin(z) + c — bounded; all-inside is the correct render
z*z + asin(c) + c — escaping driver + inverse-trig, with normals
asinh(z*z) + c — chain rule exercised in the dz/dc derivative
asin acos atan asinh acosh atanh are the holomorphic inverse functions
(asin/acos/atan via System.Numerics.Complex; asinh/acosh/atanh via the
log/sqrt continuations, since the BCL Complex lacks them). They are the one
non-polynomial family that keeps the analytic distance estimate: the
differentiator carries their closed-form dz/dc rules —
so surface normals and exterior distance estimate work for them (on the shallow
direct path — like the other transcendentals they disable deep-zoom
perturbation). The √ radicand is lowered through an internal Sqrt node via
the full complex Complex.Sqrt, so a negative real radicand still yields the
correct imaginary derivative.
Bounded maps. Pure
asin,acos,atanhorbits stay small and may never exceed the bailout — a solid "all inside the set" render is correct, not a failure. Drive them with an escaping term (az*z) for classic banding.
z*z + fract(z) + c — domain-warped / tiled Mandelbrot
z*z + 0.1*floor(z*4) + c — quantised feedback ("pixelated" bands)
z*z + 0.2*sign(re(z)) + c — sign-driven asymmetry
Each applies its real function to the real and imaginary parts independently
(f(a+bi) = f(a) + f(b)·i), like fold. On AVX2 the rounding functions use the
native vector intrinsics (Avx.Floor/Ceiling/RoundToNearestInteger =
round-half-to-even = Math.Round/RoundToZero = Math.Truncate); sign
scalarises per lane. They are piecewise-constant (kinked), so they disable
perturbation / BLA / SA and the distance estimate.
Transcendentals disable perturbation, BLA, and SA. The deep-zoom path
falls back to DD/QD HpDirect with scalar-precision transcendental
calls inside the QD chain (precision degrades to ~16 decimal digits
inside each sin/exp etc. call).
Distance estimate (surface normals) availability across the non-polynomial families:
| Family | Perturbation / BLA / SA | Distance estimate |
|---|---|---|
sin cos tan sinh cosh tanh exp log sqrt |
off | on (holomorphic chain rule) |
asin acos atan asinh acosh atanh |
off | on (analytic rules, §6.11) |
pow(base, exp) |
off | off |
floor round ceil trunc fract sign |
off | off |
conj fold re im abs arg atan2 min max mod clamp |
off | off (non-holomorphic) |
The Phoenix family extends z_{n+1} = f(z_n, c) to
z_{n+1} = f(z_n, z_{n-1}, c). The previous iterate z_{n-1} is a
new feedback term. CalcGen exposes this as prev.
z*z + c + 0.56667*prev
The classical Shigehiro Ushiki construction. The 0.56667 coefficient
on prev is the standard parameter. Try values between 0.3 and 0.8
to see the silhouette deform.
External: Wikipedia: Phoenix fractal.
z*z + c - 0.4*prev
Negative prev coefficient. The silhouette becomes more concentrated;
arms shrink back into the body.
z*z*z + c + 0.5*prev
Phoenix coupling on a degree-3 polynomial. Combines the 3-fold Multibrot symmetry with the prev-step feedback.
z*z + 0.3*z*prev + c
Phoenix coupling via a product term. The previous iterate now scales
the linear z contribution rather than entering additively.
prev disables distance estimate and perturbation. Tracking
dprev/dc properly requires a second derivative state vector (a
future engine extension). For now, prev equations render at scalar
or AVX2 escape-time only.
Most fractal equations are autonomous: the step f(z, c) doesn't
depend on the iteration index. The CalcGen DSL exposes n (or its
alias iter) so you can make f depend on the step number explicitly.
This breaks the classical theory but produces interesting visual
results — the rule itself drifts over time.
z*z + c + 0.001*n
Adds a tiny constant push proportional to the iteration count. The silhouette gradually shifts off-axis as orbits accumulate the drift.
sin(z*n) + exp(c + z) + z
The argument to sin is multiplied by n. As n grows the "frequency"
of sin increases linearly, so neighbouring pixels with slightly
different Im(z) land in increasingly different phases. Produces fine
filament structure that intensifies near the boundary. The exp(c + z)
adds an exponential right-side asymmetry; replace with cosh(c + z)
to mirror it.
z*z + c/(1 + 0.01*n)
Forcing weakens as iteration progresses. Late-iter pixels rely on pure feedback. Boundary fattens because forcing can't push out late orbits.
if mod(n, 2) > 0.5 then z*z + c else z*z - c
Alternates between adding and subtracting c based on n's parity.
Treats odd and even iterations differently. Result is a more textured
silhouette with high-frequency boundary detail.
n disables perturbation, BLA, SA, AND distance estimate. The
classical theory assumes the step function is autonomous; iter-dependent
equations break those assumptions. Renders at scalar / AVX2 escape-time
only. No deep zoom optimisations.
CalcGen's if cond then a else b lets you splice two different step
functions together at a boundary defined by a real-valued comparison
on the current iterate.
if abs(z) > 1 then z*z + c else z*z*z + c
abs(z) here is |z|² (the DSL's squared-magnitude convention).
Quadratic step when the orbit is inside |z|² ≤ 1; cubic step
outside. The silhouette is the union of the classical Mandelbrot
(inside the unit disk) and a Multibrot-cubic shell (outside).
if re(z) > 0 then z*z + c else conj(z)*conj(z) + c
Holomorphic Mandelbrot on the right half-plane; Tricorn-like on the left. Produces a literal left-right mirror with different boundary characters on each side.
if im(z) > 0 then z*z + c else z*z*z + c
Mandelbrot above the real axis, Multibrot-cubic below. Sharp asymmetric silhouette.
if arg(z) > 0 then z*z + c else z*z - c
arg(z) is the polar angle. Upper half-plane (arg > 0) adds c;
lower half subtracts. Produces a Mandelbrot silhouette with a folded
boundary along the real axis.
if abs(z) > 100 then z + c else z*z + c
Switches to a linear (slow-escape) step once the orbit gets far from
the origin. Effectively raises the bailout radius without raising it
literally — orbits beyond |z|² = 100 linger one step longer before
being declared escaped.
if disables perturbation, BLA, SA (the δ-Taylor expansion has no
closed form across the branch boundary). Distance estimate stays
available inside each branch — there's a discontinuity along the
locus where the condition flips, which the engine doesn't currently
detect as a feature.
arg(z) returns the polar angle of z in (−π, π]. It's a real
scalar lifted back to complex as (arg, 0). Lets you encode
phase-dependent dynamics.
z*z + 0.1*arg(z) + c
Adds a small angle-proportional drift. Produces spiral arms in the boundary structure.
z*z + c*exp(arg(z))
The forcing magnitude is scaled by the orbit's current phase. Pixels whose orbits spin produce different forcing than pixels whose orbits stay near the real axis. Strong asymmetric spiral structure.
z*z + 0.05*atan2(z, c) + c
atan2(y, x) is the two-argument arctangent: atan2(y, x) = arg(x + iy). Treats z as the imag part and c as the real part of
a phasor. Produces a slowly-rotating phase term that varies smoothly
across the parameter plane.
arg and atan2 are non-holomorphic. Disable distance estimate,
perturbation, BLA, SA. On AVX2 the binary atan2 per-lane scalarises
(no SIMD intrinsic for binary atan2) — costs ~4× scalar atan2 per
body but keeps the surrounding pipeline 4-wide.
External: Wikipedia: atan2.
These act on the real parts of their operands only. Produce piecewise- linear envelopes, periodic wraps, and clamping behaviour.
min(z*z, max(z, -1.0)) + c
Clamps the feedback to the range [max(z, −1), z²]. Produces a
silhouette that follows the Mandelbrot at high magnitudes but flattens
near the origin.
z*z + mod(z, 1.0) + c
Wraps Re(z) to the interval [0, 1) and adds the residual to the
feedback. Produces a quasi-periodic horizontal texture in the
silhouette.
max(z*z, sqr(z)) + c
Tautological in pure z (both branches equal) but a useful template when you want to combine two different step laws and take the larger magnitude winner.
z*z + mod(z*z + c, 2.0)
Adds a horizontal saw-tooth wave to the feedback. Boundary develops periodic ledges.
min / max / mod are non-holomorphic. Disable distance estimate,
perturbation, BLA, SA. On AVX2 min / max use Vector256.Min /
Vector256.Max intrinsics (stay vectorised); mod falls back to
per-lane scalar %.
The DSL's i is the imaginary unit literal (0, 1). Lets you inject
complex coefficients without the awkward re/im decomposition.
z*z + c + i
Adds the imaginary unit as a literal constant translation. Equivalent
to running the classical Mandelbrot in a parameter plane shifted down
by i — the silhouette is identical to the standard cardioid,
translated upward by one unit in the imaginary direction.
Note: avoid the tempting i*z + c as a "rotated Mandelbrot". It's a
linear Möbius map with z₀=0 producing the period-4 cycle
0 → c → (1+i)c → ic → 0 → ..., bounded by √2·|c| for every c.
It never escapes anywhere on the visible plane, so the result is a
uniform "in set" colour. You need at least a quadratic in z to get
a fractal silhouette — see 12.2.
i*z*z + c
The quadratic feedback is rotated by 90° each step. Produces a silhouette with twisted lobes rotated relative to the classical Mandelbrot.
0.5*z*z + 0.3*i*z + c
A quadratic with both real and imaginary coefficients on different
powers of z. The full parameter space of degree-2 polynomials.
i is a holomorphic constant. Distance estimate, perturbation, BLA,
SA all stay enabled. The differentiator returns 0 for i, but the
chain rule still produces correct values via Mul (e.g.
d(i·z)/dz = i).
Most "my equation renders as one solid colour" reports trace to the seed. The
engine starts every orbit at z₀ = 0, so the very first step evaluates
f(0, c). If that produces Inf / NaN, or a trivially bounded cycle, the
whole plane collapses to a single colour and there is no fractal to see. Run
through this table before blaming the renderer.
| Construct | What happens at z₀ = 0
|
Guard / fix |
|---|---|---|
1/z, (1/z + c)^2
|
1/0 = Inf → NaN next step; NaN never bails → everything "in set" |
Guard the denominator so it is non-zero at 0: 1/(z*z + c), 1/(z + 2)
|
log(z) + c |
log(0) = −∞ → NaN; uniform solid |
Offset the argument off the branch point: log(1 + z) + c
|
sqrt(z) at a pole chain |
Fine at 0 (sqrt(0)=0) but watch downstream /
|
Keep any following division guarded as above |
i*z + c (linear in z) |
Period-4 cycle 0 → c → (1+i)c → ic → 0 — bounded for every c, so nothing escapes |
Add a genuine quadratic: i*z*z + c
|
(z^2 + c)/(z + a) |
z₁ = c/a, then instant fixed point f(c/a)=c/a
|
Keep polynomial feedback separate from the rational term: z*z + c/(z + a)
|
conj(fold(z)) vs fold(conj(z))
|
Not equal — conj negates im, fold re-positives it, so fold(conj(z)) cancels back to plain Burning Ship |
Pick the order deliberately; conj(fold(z)) is the mirrored hybrid |
Tip
Quick test for a suspected seed problem: temporarily prepend a tiny non-zero
shift, e.g. change f(z,c) to reference z + 0.0001 in the offending term. If
the solid colour breaks up, the culprit was a singularity at the seed and the
real fix is one of the guards above.
The DE (surface-normal / "3-D relief" shading) needs the equation to stay
holomorphic so the chain rule that tracks dz/dc has a closed form. Use this
as a helper when you want the relief look:
| Keeps DE on (holomorphic) | Turns DE off (non-holomorphic) |
|---|---|
+ - *, integer power ^, sqr, division /
|
conj, fold
|
sin cos tan sinh cosh tanh exp log sqrt |
arg, atan2
|
constants i, pi, e
|
min, max, mod
|
prev, iter / n
|
Division and transcendentals keep DE but still cost you perturbation / BLA / SA
(see §14); only pure polynomial-in-z equations
keep everything. So the recipe for "custom fractal with relief shading and
deep zoom" is: stay polynomial.
Practical recipes for changing a working equation to push the result in a chosen direction.
| Have | Try |
|---|---|
Asymmetric exp
|
Replace exp(x) with cosh(x) — |
| grows in both Re directions equally. | |
| One-sided branch | Use fold(...) on the relevant operand |
| to mirror the half-plane. | |
| Single-lobe silhouette | Increase the polynomial degree: |
z*z + c → z*z*z + c (more lobes). |
| Have | Try |
|---|---|
| Smooth boundary | Multiply transcendental argument by n: |
sin(z) + c → sin(z*n) + c. |
|
| Boundary too smooth | Mix two laws via if: |
if abs(z) > 1 then z^3 + c else z^2 + c |
|
| Want feathering | Mix exp + sin: exp(z) + sin(c) + z. |
| Have | Try |
|---|---|
| Magnitude grows too fast | Scale down the feedback: z*z + c → |
0.5*z*z + c. |
|
| Right side escapes too fast |
exp(c+z) → cosh((c+z)/2). |
| Hyperbolic too steep | Divide argument: cosh(z) + c → |
cosh(z/4) + c. |
| Have | Try |
|---|---|
| Boundary too soft | Higher polynomial degree. |
| Magnitudes bounded too long | Add a c multiplication step: z*z + c
|
→ z*z*c + c. |
|
| Want explosive boundary | Use exp(z) somewhere in the feedback. |
| Have | Try |
|---|---|
| Pure radial silhouette | Inject i: z*z + c → i*z*z + c. |
| Want spiral arms | Add arg(z)-driven term: |
z*z + 0.1*arg(z) + c. |
|
| Want time-rotating dynamics | Multiply by i^n (effectively, use n
|
inside a phase): sin(z*n) + c. |
Use n (or iter). Examples:
-
z*z + c + 0.001*n— slow linear drift. -
z*z + c/(1 + 0.01*n)— decaying forcing. -
sin(z*n) + c— frequency increases each step. -
if mod(n, 2) > 0.5 then a else b— alternating step laws.
Accept that you lose distance estimate, perturbation, BLA, SA when
you use n. Use scalar / AVX2 escape-time only.
Stick to polynomial in z + c. Avoid conj, fold, div,
prev, iter, transcendentals, if. The classical Mandelbrot and
all its Multibrot/Phoenix/coupling variants stay deep-zoomable through
perturbation, BLA, SA, and DD/QD HpDirect.
| Family | Deep zoom |
|---|---|
z^d + c (any d) |
✓ all the way to QD precision |
Polynomial in z + c
|
✓ full perturbation + generic SA |
Phoenix (prev) |
✗ scalar only past ~1e12 |
| Anti-holomorphic | ✗ scalar only past ~1e12 |
| Transcendental | ✗ HpDirect DD/QD; no perturbation |
Conditional (if) |
✗ scalar/AVX2 only past ~1e12 |
iter / prev
|
✗ scalar/AVX2 escape-time only |
The CalcGen engine emits five execution paths for every equation:
-
Scalar — plain
doubleper pixel. Always available. - AVX2 / AVX-512 SIMD — vector per 4 / 8 pixels. Almost always available.
- Perturbation — δ-Taylor expansion against a reference orbit. Required for deep zoom past ~1e12.
- BLA / SA — skip iterations via series approximation. Required for fast deep zoom.
- DD/QD HpDirect — DoubleDouble / QuadDouble arithmetic per pixel. Fallback for deep zoom when perturbation is off.
Operators that disable specific paths:
| Operator | Disables |
|---|---|
conj, fold
|
Perturbation, BLA, SA, DE |
/ (div) |
Perturbation, BLA, SA |
sin/cos/tan/ |
Perturbation, BLA, SA |
sinh/cosh/tanh/ |
(DE still works — holomorphic) |
exp/log/sqrt
|
|
asin/acos/atan/ |
Perturbation, BLA, SA |
asinh/acosh/atanh
|
(DE still works — analytic rules, §6.11) |
pow(base,exp) |
Perturbation, BLA, SA, DE |
floor/round/ceil/ |
Perturbation, BLA, SA, DE |
trunc/fract/sign
|
|
re,im,abs,clamp
|
Perturbation, BLA, SA, DE |
arg,atan2
|
Perturbation, BLA, SA, DE |
min,max,mod
|
Perturbation, BLA, SA, DE |
if |
Perturbation, BLA, SA |
prev |
Perturbation, BLA, SA, DE |
iter / n
|
Perturbation, BLA, SA, DE |
i |
(none — holomorphic constant) |
pi, e
|
(none — real constants) |
When everything is gated off, the renderer still produces correct escape-time output via the scalar / AVX2 path. You only lose the acceleration features and surface-normal shading.
- Paul Bourke's fractals page — long archive of equation families with visual examples.
- FractalForums.org — active community, many novel equation discoveries.
- Wikipedia: List of fractals by Hausdorff dimension
- Wikipedia: Newton fractal
- Wikipedia: Buddhabrot
- John Milnor, Dynamics in One Complex Variable (book) — the
authoritative treatment of
z² + cdynamics. - Robert Devaney, An Introduction to Chaotic Dynamical Systems (book) — covers the exponential family and broader chaos theory.
- Inigo Quilez articles — practical derivations of distance estimates, smooth iteration, surface normals.
- Kalles Fraktaler — the reference C++ deep-zoom Mandelbrot renderer; pioneered many of the perturbation/BLA/SA techniques this engine uses.
- Ultra Fractal — commercial fractal renderer with a full scripting language for custom equations.
- Pauldelbrot 2014 — "Glitch detection in perturbation Mandelbrot", the original glitch-detection criterion. Search for the FractalForums thread.
- Botsch 2013 — series approximation for
z² + c. The basis for CalcGen's SA emitter. - Zhuoran 2021 — Bilinear approximation, the basis for CalcGen's BLA emitter.
| Term | Meaning |
|---|---|
| Autonomous map | Step function f(z, c) independent of iteration index. |
| Bailout |
|z|² threshold at which the engine declares escape. |
| Cardioid | The heart-shaped main body of the Mandelbrot set. |
| DE | Distance Estimate — surface-normal shading; gives the |
| "3D relief" look. | |
| Holomorphic | Complex-differentiable in the Wirtinger sense; preserves |
| the chain rule that DE needs. | |
| Julia set | Slice of a fractal at fixed c and varying z₀. (The |
engine uses fixed z₀ = 0 and varying c — the |
|
| Mandelbrot parameter space.) | |
| Non-autonomous | Step function depends on iteration index n explicitly. |
| Perturbation | δ-expansion technique that lets the engine compute one |
| reference orbit at high precision and propagate it to | |
per-pixel offsets in plain double. |
|
| SA | Series Approximation — skip many iterations at once via |
| a Taylor expansion of the polynomial. | |
| BLA | Bilinear Approximation — a faster variant of SA that |
| handles arbitrary polynomial steps. | |
| Smooth iter | Continuous correction to the integer escape count, used |
| for gradient-smoothed colouring. | |
| Wirtinger | Pair of partial derivatives ∂/∂z, ∂/∂z̄ used to |
| reason about complex-differentiability. |
- CalcGen-UserGuide.md — the precise DSL grammar reference, supported operators, gating rules.
- Avalonia-UserGuide.md — UI walkthrough including the User Equation editor.
- UserBulb-Guide.md — the 3D-analogue equation designer (raymarched escape-time fractals using vec3/quat instead of complex arithmetic).