16 KiB
Die Mandelbrot-Menge: Von Null auf Visualisierung
1. Was sind komplexe Zahlen?
1.1 Die imaginäre Einheit
Normale Zahlen (reelle Zahlen) kennen wir alle: 1, 2, 3.5, -7, π usw. Diese liegen alle auf einem eindimensionalen Zahlenstrahl.
Komplexe Zahlen erweitern dieses Konzept in die zweite Dimension. Der Trick: Wir erfinden eine neue Zahl namens i (die imaginäre Einheit) mit der Eigenschaft:
i² = -1
Das ist zunächst verwirrend, weil keine normale Zahl diese Eigenschaft hat (sowohl 2² = 4 als auch (-2)² = 4). Aber mathematisch können wir mit i genauso rechnen wie mit normalen Zahlen.
1.2 Aufbau komplexer Zahlen
Eine komplexe Zahl besteht aus zwei Teilen:
z = a + bi
- a = Realteil (eine normale Zahl)
- b = Imaginärteil (auch eine normale Zahl)
- i = die imaginäre Einheit
Beispiele:
3 + 4i→ Realteil: 3, Imaginärteil: 42 - 5i→ Realteil: 2, Imaginärteil: -57 + 0i = 7→ eine rein reelle Zahl0 + 3i = 3i→ eine rein imaginäre Zahl
1.3 Visualisierung: Die komplexe Ebene
Statt einem Zahlenstrahl verwenden wir eine Ebene:
- x-Achse (horizontal): Realteil
- y-Achse (vertikal): Imaginärteil
Die Zahl 3 + 4i liegt also am Punkt (3, 4) in dieser Ebene.
Imaginärteil (y)
↑
5 |
4 | • (3+4i)
3 |
2 |
1 |
0 |-------|-------|→ Realteil (x)
-1 | 2 4
-2 |
1.4 Rechnen mit komplexen Zahlen
Addition
Einfach: Addiere Real- und Imaginärteile getrennt.
(a + bi) + (c + di) = (a + c) + (b + d)i
Beispiel:
(3 + 4i) + (1 + 2i) = (3+1) + (4+2)i = 4 + 6i
Multiplikation
Hier wird's interessanter. Wir multiplizieren wie bei Klammern, aber beachten dass i² = -1:
(a + bi) × (c + di) = ac + adi + bci + bdi²
= ac + adi + bci + bd(-1)
= (ac - bd) + (ad + bc)i
Beispiel:
(3 + 4i) × (1 + 2i) = 3×1 + 3×2i + 4i×1 + 4i×2i
= 3 + 6i + 4i + 8i²
= 3 + 10i + 8×(-1)
= 3 + 10i - 8
= -5 + 10i
Spezialfall: Quadrieren
Besonders wichtig für die Mandelbrot-Menge ist das Quadrieren:
(a + bi)² = (a + bi) × (a + bi)
= a² + abi + abi + (bi)²
= a² + 2abi + b²i²
= a² + 2abi - b²
= (a² - b²) + 2abi
Merke dir diese Formel:
(a + bi)² = (a² - b²) + (2ab)i
Beispiel:
(3 + 4i)² = (3² - 4²) + (2×3×4)i
= (9 - 16) + 24i
= -7 + 24i
1.5 Betrag (Abstand vom Ursprung)
Der Betrag einer komplexen Zahl ist ihr Abstand vom Ursprung (0, 0). Mit dem Satz des Pythagoras:
|a + bi| = √(a² + b²)
Beispiel:
|3 + 4i| = √(3² + 4²) = √(9 + 16) = √25 = 5
In der Ebene visualisiert ist das die Länge des Pfeils vom Ursprung zum Punkt (3, 4).
2. Die Mandelbrot-Iteration
2.1 Die Grundidee
Die Mandelbrot-Menge ist eine Menge von komplexen Zahlen c, die eine bestimmte Eigenschaft haben.
Für jede komplexe Zahl c führen wir folgende Iteration durch:
z₀ = 0
z₁ = z₀² + c = 0² + c = c
z₂ = z₁² + c
z₃ = z₂² + c
z₄ = z₃² + c
...
Die Frage: Bleibt die Folge z₀, z₁, z₂, z₃, ... beschränkt (in der Nähe des Ursprungs), oder explodiert sie gegen unendlich?
Definition: Die Zahl c gehört zur Mandelbrot-Menge, wenn die Folge beschränkt bleibt (nicht gegen unendlich geht).
2.2 Beispiel 1: c = 0
z₀ = 0
z₁ = 0² + 0 = 0
z₂ = 0² + 0 = 0
z₃ = 0² + 0 = 0
...
Die Folge bleibt bei 0. c = 0 gehört zur Mandelbrot-Menge.
2.3 Beispiel 2: c = 1
z₀ = 0
z₁ = 0² + 1 = 1
z₂ = 1² + 1 = 2
z₃ = 2² + 1 = 5
z₄ = 5² + 1 = 26
z₅ = 26² + 1 = 677
...
Die Folge explodiert! c = 1 gehört NICHT zur Mandelbrot-Menge.
2.4 Beispiel 3: c = -1
z₀ = 0
z₁ = 0² + (-1) = -1
z₂ = (-1)² + (-1) = 1 - 1 = 0
z₃ = 0² + (-1) = -1
z₄ = (-1)² + (-1) = 0
...
Die Folge springt zwischen -1 und 0 hin und her. Sie bleibt beschränkt. c = -1 gehört zur Mandelbrot-Menge.
2.5 Beispiel 4: c = i (eine komplexe Zahl!)
z₀ = 0
z₁ = 0² + i = i
z₂ = i² + i = -1 + i
z₃ = (-1 + i)² + i
Berechnen wir z₃ mit unserer Quadrierformel (a=-1, b=1):
(-1 + i)² = ((-1)² - 1²) + (2×(-1)×1)i
= (1 - 1) + (-2)i
= -2i
z₃ = -2i + i = -i
Weiter:
z₄ = (-i)² + i = (0 + (-1)i)² + i
= (0² - (-1)²) + (2×0×(-1))i + i
= -1 + 0i + i
= -1 + i
Moment! z₄ = z₂. Die Folge wiederholt sich jetzt:
z₂ = -1 + i
z₃ = -i
z₄ = -1 + i
z₅ = -i
...
Die Folge bleibt beschränkt (pendelt zwischen zwei Werten). c = i gehört zur Mandelbrot-Menge.
3. Der Algorithmus zur Visualisierung
3.1 Warum nicht unendlich lange testen?
Wir können nicht unendlich viele Iterationen durchführen. Daher verwenden wir zwei Abbruchkriterien:
Kriterium 1: Divergenz erkannt Mathematisch bewiesen: Wenn der Betrag von z jemals größer als 2 wird (|z| > 2), dann wird die Folge definitiv gegen unendlich gehen.
Da |z| = √(real² + imag²) ist, können wir stattdessen prüfen:
real² + imag² > 4
(Das spart die Wurzelberechnung, weil 2² = 4)
Kriterium 2: Maximale Iterationen Wir setzen eine Obergrenze, z.B. 100 oder 1000 Iterationen. Wenn wir diese erreichen ohne Divergenz, nehmen wir an, dass c zur Menge gehört.
3.2 Der Pseudocode
Für jede komplexe Zahl c = (c_real, c_imag):
z_real = 0
z_imag = 0
iteration = 0
max_iterations = 1000
solange (iteration < max_iterations):
// Berechne z² + c
z_real_neu = z_real² - z_imag² + c_real
z_imag_neu = 2 × z_real × z_imag + c_imag
z_real = z_real_neu
z_imag = z_imag_neu
// Divergenz-Check
wenn (z_real² + z_imag² > 4):
break // Divergenz erkannt!
iteration = iteration + 1
// Ergebnis: iteration
// - Wenn iteration = max_iterations: c ist in der Menge (schwarz färben)
// - Sonst: c ist außerhalb (Farbe basierend auf iteration)
3.3 Wichtige Details zur Implementierung
1. Reihenfolge der Berechnung
// FALSCH:
z_real = z_real * z_real - z_imag * z_imag + c_real;
z_imag = 2 * z_real * z_imag + c_imag; // Nutzt bereits das neue z_real!
// RICHTIG:
double z_real_neu = z_real * z_real - z_imag * z_imag + c_real;
double z_imag_neu = 2 * z_real * z_imag + c_imag;
z_real = z_real_neu;
z_imag = z_imag_neu;
2. Optimierung der Divergenz-Prüfung
// Berechne die Quadrate nur einmal:
double z_real_squared = z_real * z_real;
double z_imag_squared = z_imag * z_imag;
double z_real_neu = z_real_squared - z_imag_squared + c_real;
double z_imag_neu = 2 * z_real * z_imag + c_imag;
// Divergenz-Check
if (z_real_squared + z_imag_squared > 4.0) {
break;
}
4. Von Pixeln zur komplexen Ebene
4.1 Das Problem
Dein Bildschirm hat Pixel mit Koordinaten:
- x: 0 bis fenster_breite (z.B. 0 bis 800)
- y: 0 bis fenster_hoehe (z.B. 0 bis 600)
Die interessante Region der Mandelbrot-Menge liegt bei:
- Real-Achse: ungefähr -2.5 bis +1.0
- Imaginär-Achse: ungefähr -1.0 bis +1.0
Wir müssen jeden Pixel auf einen Punkt in der komplexen Ebene abbilden.
4.2 Die Mapping-Formel
Schritt 1: Normalisiere Pixelkoordinaten auf [0, 1]
x_normiert = x / fenster_breite // Wert zwischen 0 und 1
y_normiert = y / fenster_hoehe // Wert zwischen 0 und 1
Schritt 2: Skaliere auf den gewünschten Bereich
c_real = min_real + x_normiert × (max_real - min_real)
c_imag = min_imag + y_normiert × (max_imag - min_imag)
Beispiel mit konkreten Zahlen
Angenommen:
- Fenster: 800×600 Pixel
- Bereich: real [-2.5, 1.0], imaginär [-1.0, 1.0]
Für Pixel (400, 300) (die Fenstermitte):
x_normiert = 400 / 800 = 0.5
y_normiert = 300 / 600 = 0.5
c_real = -2.5 + 0.5 × (1.0 - (-2.5))
= -2.5 + 0.5 × 3.5
= -2.5 + 1.75
= -0.75
c_imag = -1.0 + 0.5 × (1.0 - (-1.0))
= -1.0 + 0.5 × 2.0
= -1.0 + 1.0
= 0.0
Die Fenstermitte entspricht also der komplexen Zahl c = -0.75 + 0i.
4.3 Koordinatensysteme beachten
Problem: In den meisten Grafiksystemen ist y=0 oben und y wächst nach unten. In der Mathematik wächst die imaginäre Achse nach oben.
Lösung 1: Y-Koordinate umdrehen
y_normiert = 1.0 - (y / fenster_hoehe)
Lösung 2: min_imag und max_imag vertauschen
c_imag = max_imag - y_normiert × (max_imag - min_imag)
Beide führen zum gleichen Ergebnis: Die Mandelbrot-Menge wird richtig herum angezeigt.
4.4 Seitenverhältnis (Aspect Ratio)
Wenn dein Fenster nicht quadratisch ist, musst du aufpassen, dass Kreise nicht zu Ellipsen werden.
Option 1: Passe den Bereich an das Fensterverhältnis an
aspect_ratio = fenster_breite / fenster_hoehe
real_span = max_real - min_real
imag_span = max_imag - min_imag
// Zentriere und passe an
center_real = (min_real + max_real) / 2
center_imag = (min_imag + max_imag) / 2
if (aspect_ratio > 1.0) {
// Fenster ist breiter als hoch → erweitere Real-Achse
half_span = imag_span * aspect_ratio / 2
min_real = center_real - half_span
max_real = center_real + half_span
} else {
// Fenster ist höher als breit → erweitere Imaginär-Achse
half_span = real_span / aspect_ratio / 2
min_imag = center_imag - half_span
max_imag = center_imag + half_span
}
Option 2: Rendere in ein quadratisches Bild und zeige es zentriert im Fenster
5. Färbung und Visualisierung
5.1 Basis-Färbung
Das Einfachste:
- In der Menge (max_iterations erreicht): Schwarz
- Außerhalb: Färbung basierend auf der Iterationszahl
if (iteration == max_iterations) {
farbe = SCHWARZ
} else {
// iteration geht von 0 bis max_iterations-1
helligkeitswert = iteration / max_iterations
farbe = helligkeitswert // z.B. 0 = schwarz, 1 = weiß
}
5.2 Farb-Paletten
Statt Graustufen kannst du Farbverläufe verwenden:
Linear mapping:
farbindex = (iteration × 256) / max_iterations
farbe = farbpalette[farbindex]
Beliebte Paletten:
- Regenbogen (HSV mit variierendem Hue)
- Blau-Violett-Verlauf
- Feuer (Schwarz → Rot → Orange → Gelb → Weiß)
5.3 Smooth Coloring (kontinuierliche Färbung)
Problem: Mit der einfachen Methode sieht man harte "Bänder" zwischen den Iterationsstufen.
Lösung: Nutze den finalen Betragswert für eine feinere Färbung:
wenn divergiert:
// Wie stark über der Grenze sind wir?
betrag_quadrat = z_real² + z_imag²
// Smooth iteration count
smooth_iteration = iteration + 1 - log(log(betrag_quadrat) / log(2)) / log(2)
// Nutze smooth_iteration für Färbung
Das ergibt sanfte Farbverläufe ohne Bänder. (Dies ist mathematisch etwas fortgeschritten, aber das Ergebnis sieht viel besser aus.)
6. Zoom und Navigation
6.1 Zoom implementieren
Beim Zoomen verkleinerst du den sichtbaren Bereich der komplexen Ebene.
Zoom um einen Faktor:
zoom_factor = 2.0 // 2x näher heran
center_real = (min_real + max_real) / 2
center_imag = (min_imag + max_imag) / 2
span_real = (max_real - min_real) / zoom_factor
span_imag = (max_imag - min_imag) / zoom_factor
min_real = center_real - span_real / 2
max_real = center_real + span_real / 2
min_imag = center_imag - span_imag / 2
max_imag = center_imag + span_imag / 2
6.2 Zoom auf Mausposition
Wenn der User auf Pixel (mouse_x, mouse_y) klickt:
// Berechne die komplexe Zahl an der Mausposition
mouse_real = min_real + (mouse_x / fenster_breite) × (max_real - min_real)
mouse_imag = min_imag + (mouse_y / fenster_hoehe) × (max_imag - min_imag)
// Zentriere auf diese Position und zoome
span_real = (max_real - min_real) / zoom_factor
span_imag = (max_imag - min_imag) / zoom_factor
min_real = mouse_real - span_real / 2
max_real = mouse_real + span_real / 2
min_imag = mouse_imag - span_imag / 2
max_imag = mouse_imag + span_imag / 2
6.3 Interaktive Navigation
Pan (Verschieben):
verschiebung_real = delta_x / fenster_breite × (max_real - min_real)
verschiebung_imag = delta_y / fenster_hoehe × (max_imag - min_imag)
min_real -= verschiebung_real
max_real -= verschiebung_real
min_imag -= verschiebung_imag
max_imag -= verschiebung_imag
7. Performance-Optimierungen
7.1 Frühe Abbrüche
Periodizitätsprüfung: Speichere z in regelmäßigen Abständen. Wenn z sich wiederholt, bist du in einem Zyklus → in der Menge.
Hauptkardioiden-Test: Der große "Körper" der Mandelbrot-Menge (eine Kardioidform) kann analytisch getestet werden:
// Teste ob c in der Hauptkardioide liegt
q = (c_real - 0.25)² + c_imag²
if (q × (q + (c_real - 0.25)) < 0.25 × c_imag²) {
// In der Hauptkardioide → direkt als "in der Menge" werten
return max_iterations
}
// Teste ob c im Periode-2-Bulb liegt
if ((c_real + 1)² + c_imag² < 0.0625) {
// Im Bulb → in der Menge
return max_iterations
}
7.2 Symmetrie nutzen
Die Mandelbrot-Menge ist symmetrisch zur reellen Achse:
- Wenn (a + bi) in der Menge ist, dann auch (a - bi)
Du kannst nur die obere Hälfte berechnen und spiegeln.
7.3 Multi-Threading
Jeder Pixel kann unabhängig berechnet werden → perfekt für Parallelisierung!
Teile das Bild in Streifen oder Kacheln auf und berechne diese in verschiedenen Threads.
8. Interessante Koordinaten zum Erkunden
Klassische Ansicht (Gesamtüberblick)
Real: [-2.5, 1.0]
Imag: [-1.25, 1.25]
Seepferdchen-Tal
Real: [-0.75, -0.735]
Imag: [0.1, 0.115]
max_iterations: 500+
Spiralen
Real: [-0.7, -0.65]
Imag: [0.4, 0.45]
max_iterations: 1000+
Mini-Mandelbrot
Real: [-0.16, -0.14]
Imag: [1.025, 1.045]
max_iterations: 2000+
Tipp: Je tiefer du zoomst, desto höher sollte max_iterations sein, um Details zu sehen!
9. Häufige Fehler und deren Lösung
9.1 "Ich sehe nur Schwarz oder nur Weiß"
Ursachen:
- max_iterations zu niedrig oder zu hoch
- Koordinatenbereich außerhalb der interessanten Region
- Mapping-Formel falsch
Lösung: Beginne mit der klassischen Ansicht und max_iterations = 100.
9.2 "Das Bild ist verzerrt"
Ursache: Seitenverhältnis nicht korrekt berücksichtigt.
Lösung: Siehe Abschnitt 4.4 zur Aspect Ratio.
9.3 "Die Iteration zählt falsch"
Ursache: Du verwendest das neue z_real bei der Berechnung von z_imag_neu.
Lösung: Speichere beide neue Werte in temporären Variablen.
9.4 "Beim Zoomen wird das Bild unscharf"
Ursache: Bei tiefen Zoom-Stufen reicht die Präzision von double nicht mehr aus.
Lösung:
- Nutze höhere Präzision (z.B.
long double) - Für extreme Zooms: arbitrary-precision Bibliotheken (GMP, MPFR)
9.5 "Die Berechnung ist sehr langsam"
Lösungen:
- Reduziere max_iterations für Vorschau-Rendering
- Implementiere Multi-Threading
- Nutze Periodizitätsprüfung und analytische Tests
- Rendere in niedrigerer Auflösung und skaliere hoch
10. Zusammenfassung: Die komplette Visualisierungspipeline
FÜR JEDEN PIXEL (x, y) IM FENSTER:
1. MAPPING: Konvertiere Pixel zu komplexer Zahl
c_real = min_real + (x / breite) × (max_real - min_real)
c_imag = min_imag + (y / höhe) × (max_imag - min_imag)
2. ITERATION: Teste ob c in der Menge liegt
z_real = 0
z_imag = 0
iteration = 0
SOLANGE iteration < max_iterations:
// Berechne z² + c
temp_real = z_real² - z_imag² + c_real
temp_imag = 2 × z_real × z_imag + c_imag
z_real = temp_real
z_imag = temp_imag
// Divergenz?
WENN z_real² + z_imag² > 4:
BREAK
iteration++
3. FÄRBUNG: Basierend auf Iterationszahl
WENN iteration == max_iterations:
farbe = SCHWARZ (in der Menge)
SONST:
farbe = berechne_farbe(iteration)
4. ZEICHNE: Setze Pixel (x, y) auf farbe
Viel Erfolg!
Die Mandelbrot-Menge ist ein faszinierendes mathematisches Objekt. Beim Hineinzoomen entdeckst du immer neue Strukturen – Spiralen, Seepferdchen-Täler, Mini-Kopien der gesamten Menge.
Ein paar abschließende Tipps:
- Beginne mit niedrigen max_iterations (50-100) für schnelle Tests
- Implementiere Zoom und Pan für interaktive Exploration
- Experimentiere mit verschiedenen Farbpaletten
- Speichere interessante Koordinaten, die du findest
Die Selbstähnlichkeit der Mandelbrot-Menge ist unendlich – du kannst beliebig tief zoomen und findest immer neue Details (bis zur Grenze der Gleitkomma-Präzision).