-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnotes.typ
More file actions
181 lines (142 loc) · 6.07 KB
/
notes.typ
File metadata and controls
181 lines (142 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#set document(title: "Mandelbrot Set")
#set quote(block: true)
#title()
= Preface
The following are notes on my attempt on rendering the Mandelbrot set with high precision.
= The Mandelbrot set
The Mandelbrot set's definition can be written in multiple ways.
#quote(
attribution: [#link(
"https://mathr.co.uk/mandelbrot/perturbation.pdf",
"Perturbation techniques applied to the Mandelbrot set",
)],
)[
Consider iterations of a quadratic polynomial $F$ over complex numbers:
$
F(z, c) = z^2 + c
F^(n+1) (z, c) = F(F^n (z, c), c)
$
The Mandelbrot set $M$ is the set of $c in CC$ for which the iterates of $z = 0$ remain bounded:
$
M = {c in CC colon F^n (0, c) arrow.not infinity "as" n arrow infinity}
$
]
= Visualizing the Mandelbrot set
Despite its simple formula, visualizing the Mandelbrot set requires more techniques than simple brute-forcing, the further we zoom into the fractal.
== Naive implementation
A naive attempt of visualizing the Mandelbrot set can look like this:
```glsl
vec2 mult(vec2 v1, vec2 v2) {
return vec2(
v1.x * v2.x - v1.y * v2.y, // Real part
v1.x * v2.y + v1.y * v2.x // Imaginary part
);
}
float compute_iterations(vec2 c) {
const float B = 256.0f;
float n = 0.0f;
vec2 z = vec2(0.0f);
for (int i = 0; i < max_iter; i++) {
z = mult(z, z) + c;
if (dot(z, z) > B * B) break;
n += 1.0f;
}
// Smooth iteration count
float sn = n - log2(log2(dot(z,z))) + 4.0;
return sn;
}
```
where `sn` (smooth iteration count) can be used to assign a specific color to the corresponding value of $c$.
However, if we try to zoom in too far the limitation on floating point precision is quite apparent.
#figure(
image("pictures/float_limitation.png", width: 40%),
caption: [Unclear render of a fractal due to imprecision of floating point numbers.],
)
== Perturbation techniques
The following explanation is adapted from #link("https://mathr.co.uk/blog/2021-05-14_deep_zoom_theory_and_practice.html#a2021-05-14_deep_zoom_theory_and_practice_3", "Deep zoom theory and practice") by Claude Heiland-Allen.
To mitigate this limitation, we must use higher precision floating point types for the calculation process. However, calculating the appropriate iteration count for each high precision floating point types is most likely too slow.
To speed up this process, we introduce perturbation techniques.
The core idea is quite simple. Since $z^2 + c$ is analytic, nearby points remain nearby under iteration.
Let upper case variables ($Z, C$) be the unperturbed reference (in high precision) and lower case variables ($z, c$) be the perturbed per pixel delta (in low precision).
Start with the iteration formula:
$
Z arrow Z^2 + C
$
Perturb the variables:
$
(Z+z) arrow (Z+z)^2 + (C + c) = (Z^2 + C) + (2Z z + z^2 + c) \
$
Then
$
z arrow 2Z z + z^2 + c.
$
We can call the sequence of values the reference point ($Z$) takes under $z^2 + c$ a _reference orbit_ and the sequences of values other perturbed value ($z$) takes a _delta orbit_.
The idea is:
+ Choose a reference point (in high precision).
+ Calculate the reference orbit
- The orbit calculation is done in high precision, but the resulting value can be stored as low precision `float`.
+ For other points, use the reference orbit to calculate their iteration counts.
We can calculate reference orbits like following:
```cpp
void ReferenceOrbit::calculate_reference(const std::complex<double> ¢er) {
// Remove previous orbit values
orbit.clear();
std::complex<long double> z{0};
for (int i{0}; i < conf::kMaxIteration; ++i) {
orbit.push_back(static_cast<std::complex<float>>(z));
z = z * z + static_cast<std::complex<long double>>(center);
if (std::norm(z) > conf::kFractalExitBoundary)
break;
}
}
```
Of course, `long doule` may not be enough for a higher zoom level.
With the reference orbit in hand, for any other point, we can calculate their iteration count as follows:
```glsl
float compute_iterations_ref(vec2 dc) {
const float B = 256.0f;
vec2 dz = vec2(0.0f);
int i = 0;
for (i = 0; i < max_iter && i < orbit.length() - 1; i++) {
dz = 2.0f * mult(orbit[i], dz) + mult(dz, dz) + dc;
if (dot(orbit[i + 1] + dz, orbit[i + 1] + dz) > B * B) break;
}
if (i == max_iter || i + 1 == orbit.length())
return 0.0f;
float sn = float(i) - log2(log2(dot(orbit[i] + dz, orbit[i] + dz))) + 4.0;
return sn;
}
```
Unfortunately, this strategy fails when the reference point chosen has a low iteration count. Naturally since this is an approximation, it starts to fail when the delta orbit stays too far away from the reference orbit or when a bunch of delta orbit obtains the same iteration count.
A bunch of delta orbit can have the same iteration count when the reference orbit exits (finishes) too quickly:
#figure(
image("pictures/expected_perturbation.png", width: 40%),
caption: [Perturbation technique applied with "good" reference point.],
)
#figure(
image("pictures/color_clump_perturbation.png", width: 40%),
caption: [Color clumps originating from many delta orbit with the same iteration count.],
)
Of course, if we face such situation, we can choose a new (hopefully better) reference orbit and recalculate the delta orbit.
#link("https://fractalforums.org/index.php?topic=4360.0", [Zhuoran from fractal forums]) suggested the following approach, which actually avoids the need to choose a new reference orbit:
```glsl
float compute_iterations_ref(vec2 dc) {
const float B = 256.0f;
vec2 dz = vec2(0.0f);
int iter;
int reference_iter = 0;
int max_reference_iter = orbit.length();
for (iter = 0; iter < max_iter; iter++) {
dz = 2.0f * mult(orbit[reference_iter], dz) + mult(dz, dz) + dc;
reference_iter++;
vec2 z = orbit[reference_iter] + dz;
if (dot(z, z) > B * B) break;
if (dot(z, z) < dot(dz, dz) || reference_iter == max_reference_iter) {
dz = z;
reference_iter = 0;
}
}
float sn = float(iter) - log2(log2(dot(orbit[iter] + dz, orbit[iter] + dz))) + 4.0;
return sn;
}
```