Erstellen Sie einen Neon-Vektor-Shooter in XNA Bloom und Schwarze Löcher

In dieser Serie von Tutorials zeige ich Ihnen, wie Sie einen Neon-Twin-Shooter wie Geometry Wars in XNA erstellen. Das Ziel dieser Tutorials ist es nicht, Ihnen eine exakte Nachbildung von Geometry Wars zu geben, sondern die notwendigen Elemente durchzugehen, mit denen Sie eine qualitativ hochwertige Variante erstellen können.


Überblick

In der bisherigen Serie haben wir das grundlegende Gameplay für unseren Neon-Twin-Stick-Shooter Shape Blaster eingerichtet. In diesem Lernprogramm erstellen wir den charakteristischen Neon-Look, indem Sie einen Bloom-Nachbearbeitungsfilter hinzufügen.

Warnung: Laut!

Einfache Effekte wie diese oder Partikeleffekte können ein Spiel wesentlich attraktiver machen, ohne dass Änderungen am Gameplay erforderlich sind. Die effektive Verwendung visueller Effekte ist in jedem Spiel ein wichtiger Aspekt. Nachdem Sie den Bloom-Filter hinzugefügt haben, fügen wir dem Spiel auch schwarze Löcher hinzu.


Bloom-Nachbearbeitungseffekt

Bloom beschreibt den Effekt, den Sie sehen, wenn Sie ein Objekt mit einem hellen Licht hinter sich sehen und das Licht über das Objekt zu bluten scheint. In Shape Blaster bewirkt der Bloom-Effekt, dass die hellen Linien der Schiffe und Partikel wie helle, leuchtende Neonlichter aussehen.

Sonnenlicht blüht durch die Bäume

Um die Blüte in unserem Spiel anzuwenden, müssen wir unsere Szene auf ein Renderziel rendern und dann unseren Blütefilter auf dieses Renderziel anwenden.

Bloom arbeitet in drei Schritten:

  1. Extrahieren Sie die hellen Teile des Bildes.
  2. Verwischen Sie die hellen Teile.
  3. Rekombinieren Sie das unscharfe Bild mit dem Originalbild, während Sie einige Einstellungen für Helligkeit und Sättigung vornehmen.

Jeder dieser Schritte erfordert a Shader - im Grunde ein kurzes Programm, das auf Ihrer Grafikkarte läuft. Shader in XNA werden in einer speziellen Sprache geschrieben, die als High-Level Shader Language (HLSL) bezeichnet wird. Die folgenden Beispielbilder zeigen das Ergebnis jedes Schritts.

Anfangsbild Die hellen Bereiche, die aus dem Bild extrahiert wurden Die hellen Bereiche nach dem Weichzeichnen Das Endergebnis nach Rekombination mit dem Originalbild

Hinzufügen von Bloom zu Shape Blaster

Für unseren Bloom-Filter verwenden wir das XNA Bloom Postprocess Sample.

Die Integration der Blütenprobe in unser Projekt ist einfach. Suchen Sie zunächst die beiden Codedateien aus dem Beispiel, BloomComponent.cs und BloomSettings.cs, und füge sie dem hinzu ShapeBlaster Projekt. Auch hinzufügen BloomCombine.fx, BloomExtract.fx, und GaussianBlur.fx zum Inhalts-Pipeline-Projekt.

Im GameRoot, füge hinzu ein mit Aussage für die BloomPostprocess Namensraum und fügen Sie ein BloomComponent Mitgliedsvariable.

 BloomComponent Blüte;

In dem GameRoot Konstruktor, fügen Sie die folgenden Zeilen hinzu.

 bloom = new BloomComponent (this); Components.Add (Blüte); bloom.Settings = neue BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1);

Endlich ganz am Anfang GameRoot.Draw (), fügen Sie die folgende Zeile hinzu.

 bloom.BeginDraw ();

Das ist es. Wenn Sie das Spiel jetzt ausführen, sollten Sie die Blüte sehen.

Wenn du anrufst bloom.BeginDraw (), Er leitet nachfolgende Draw-Aufrufe an ein Renderziel weiter, auf das die Blüte angewendet wird. Wenn du anrufst base.Draw () am Ende von GameRoot.Draw () Methode, die BloomComponent's Zeichnen() Methode wird aufgerufen. Hier wird die Blüte angewendet und die Szene wird in den hinteren Puffer gezogen. Daher muss zwischen den Aufrufen an alles gezeichnet werden, für das die Blüte angewendet werden muss bloom.BeginDraw () und base.Draw ().

Spitze: Wenn Sie etwas ohne Blüte zeichnen möchten (z. B. die Benutzeroberfläche), zeichnen Sie es nach dem der Anruf an base.Draw ().

Sie können die Blüteneinstellungen nach Ihren Wünschen anpassen. Ich habe folgende Werte gewählt:

  • 0,25 für die Blütenschwelle. Dies bedeutet, dass Teile des Bildes, die weniger als ein Viertel der vollen Helligkeit haben, nicht zur Blüte beitragen.
  • 4 für die Unschärfemenge. Für die mathematisch Neigten ist dies die Standardabweichung der Gaußschen Unschärfe. Größere Werte verwischen die Lichtblüte stärker. Beachten Sie jedoch, dass der Unschärfe-Shader unabhängig von der Unschärfemenge auf eine feste Anzahl von Samples eingestellt ist. Wenn Sie diesen Wert zu hoch einstellen, erstreckt sich die Unschärfe über den Radius hinaus, aus dem der Shader abtastet, und Artefakte werden angezeigt. Idealerweise sollte dieser Wert nicht mehr als ein Drittel Ihres Abtastradius betragen, um sicherzustellen, dass der Fehler vernachlässigbar ist.
  • 2 für die Intensität der Blüte, die bestimmt, wie stark die Blüte das Endergebnis beeinflusst.
  • 1 für die Basisintensität, die bestimmt, wie stark das Originalbild das Endergebnis beeinflusst.
  • 1,5 für die Blütensättigung. Dies hat zur Folge, dass das Leuchten um helle Objekte stärker gesättigte Farben aufweist als die Objekte selbst. Ein hoher Wert wurde gewählt, um das Aussehen von Neonlichtern zu simulieren. Wenn Sie in die Mitte eines hellen Neonlichts schauen, sieht es fast weiß aus, während das Glühen um es stärker gefärbt ist.
  • 1 für die Basensättigung. Dieser Wert beeinflusst die Sättigung des Basisbildes.
Ohne blüte Mit blüte

Blüte unter der Haube

Der Bloom-Filter ist im implementiert BloomComponent Klasse. Die Bloom-Komponente beginnt mit dem Erstellen und Laden der erforderlichen Ressourcen in ihrer LoadContent () Methode. Hier werden die drei erforderlichen Shader geladen und drei Renderziele erstellt.

Das erste Renderziel, sceneRenderTarget, dient zum Halten der Szene, auf die die Blüte angewendet wird. Die anderen zwei, renderTarget1 und renderTarget2, werden verwendet, um die Zwischenergebnisse zwischen jedem Rendering-Durchgang temporär zu speichern. Diese Renderziele haben die Hälfte der Auflösung des Spiels, um die Leistungskosten zu senken. Die endgültige Qualität der Blüte wird dadurch nicht beeinträchtigt, da die Blütenbilder sowieso verwischt werden.

Bloom erfordert vier Render-Durchgänge, wie in diesem Diagramm gezeigt:

In XNA die Bewirken Klasse kapselt einen Shader. Sie schreiben den Code für den Shader in einer separaten Datei, die Sie der Inhaltspipeline hinzufügen. Dies sind die Dateien mit der .fx Erweiterung, die wir zuvor hinzugefügt haben. Sie laden den Shader in einen Bewirken Objekt durch Aufrufen der Inhalt.Laden() Methode in LoadContent (). Die einfachste Möglichkeit, einen Shader in einem 2D-Spiel zu verwenden, besteht darin, den Bewirken Objekt als Parameter an SpriteBatch.Begin ().

Es gibt verschiedene Arten von Shadern, aber für den Bloom-Filter werden wir nur verwenden Pixel-Shader (manchmal angerufen Fragment-Shader). Ein Pixel-Shader ist ein kleines Programm, das für jedes gezeichnete Pixel einmal ausgeführt wird und die Farbe des Pixels festlegt. Wir gehen auf jeden der verwendeten Shader ein.

Das BloomExtract Shader

Das BloomExtract Shader ist der einfachste der drei Shader. Seine Aufgabe ist es, die Bereiche des Bildes zu extrahieren, die heller als ein bestimmter Schwellenwert sind, und dann die Farbwerte neu zu skalieren, um den gesamten Farbbereich zu verwenden. Alle Werte unter dem Schwellenwert werden schwarz.

Der vollständige Shader-Code wird unten angezeigt.

 sampler TextureSampler: register (s0); Float BloomThreshold; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Die Originalbildfarbe nachschlagen. float4 c = tex2D (TextureSampler, texCoord); // Passen Sie es so an, dass nur die Werte heller sind als der angegebene Schwellenwert. Rücklaufsättigung ((c - BloomThreshold) / (1 - BloomThreshold));  technique BloomExtract pass Pass1 PixelShader = kompilieren ps_2_0 PixelShaderFunction (); 

Machen Sie sich keine Sorgen, wenn Sie mit HLSL nicht vertraut sind. Lassen Sie uns untersuchen, wie das funktioniert.

 sampler TextureSampler: register (s0);

Dieser erste Teil deklariert einen aufgerufenen Textur-Sampler TextureSampler. SpriteBatch bindet eine Textur an diesen Sampler, wenn er mit diesem Shader gezeichnet wird. Die Angabe des Registers, an das gebunden werden soll, ist optional. Wir verwenden den Sampler, um Pixel aus der gebundenen Textur nachzuschlagen.

 Float BloomThreshold;

BloomThreshold ist ein Parameter, den wir über unseren C # -Code einstellen können.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): FARBE0 

Dies ist unsere Deklaration der Pixel-Shader-Funktion, die Texturkoordinaten als Eingabe verwendet und eine Farbe zurückgibt. Die Farbe wird als zurückgegeben float4. Dies ist eine Sammlung von vier Schwimmern, ähnlich einem Vector4 in XNA. Sie speichern die Rot-, Grün-, Blau- und Alphakomponenten der Farbe als Werte zwischen Null und Eins.

TEXCOORD0 und COLOR0 werden genannt Semantik, und sie zeigen dem Compiler an, wie texCoord Parameter und der Rückgabewert werden verwendet. Für jeden Pixelausgang, texCoord enthält die Koordinaten des entsprechenden Punktes in der Eingabetextur mit (0, 0) die obere linke Ecke und (1, 1) unten rechts sein.

 // Die ursprüngliche Bildfarbe nachschlagen. float4 c = tex2D (TextureSampler, texCoord); // Passen Sie es so an, dass nur die Werte heller sind als der angegebene Schwellenwert. Rücklaufsättigung ((c - BloomThreshold) / (1 - BloomThreshold));

Hier wird die eigentliche Arbeit geleistet. Es holt die Pixelfarbe aus der Textur und zieht sie ab BloomThreshold von jeder Farbkomponente und skaliert sie dann wieder, so dass der Maximalwert eins ist. Das sättigen() Die Funktion klemmt dann die Farbkomponenten zwischen null und eins.

Sie können das bemerken c und BloomThreshold sind nicht der gleiche Typ wie c ist ein float4 und BloomThreshold ist ein schweben. Mit HLSL können Sie Operationen mit diesen verschiedenen Typen ausführen, indem Sie im Wesentlichen die schweben in ein float4 mit allen komponenten gleich. (c - BloomThreshold) wird effektiv:

 c - float4 (BloomThreshold, BloomThreshold, BloomThreshold, BloomThreshold)

Der Rest des Shaders erstellt einfach eine Technik, die die für das Shader-Modell 2.0 kompilierte Pixel-Shader-Funktion verwendet.

Das GaussianBlur Shader

Eine Gaußsche Unschärfe verwischt ein Bild mit einer Gaußschen Funktion. Für jedes Pixel im Ausgabebild summieren wir die Pixel im Eingabebild, gewichtet nach ihrer Entfernung vom Zielpixel. Nahegelegene Pixel tragen wesentlich zur endgültigen Farbe bei, während entfernte Pixel nur wenig beitragen.

Da entfernte Pixel vernachlässigbare Beiträge leisten und die Suche nach Texturen teuer ist, werden nur Pixel innerhalb eines kurzen Radius abgetastet, anstatt die gesamte Textur abzutasten. Dieser Shader tastet Punkte innerhalb von 14 Pixeln des aktuellen Pixels ab.

Bei einer naiven Implementierung werden möglicherweise alle Punkte in einem Quadrat um das aktuelle Pixel herum abgetastet. Dies kann jedoch kostspielig sein. In unserem Beispiel müssten wir Punkte innerhalb eines 29x29-Quadrats (14 Punkte auf beiden Seiten des mittleren Pixels plus das mittlere Pixel) abtasten. Das sind insgesamt 841 Samples für jedes Pixel in unserem Bild. Zum Glück gibt es eine schnellere Methode. Es stellt sich heraus, dass eine 2D-Gaußsche Unschärfe gleichwertig ist, wenn Sie das Bild zuerst horizontal und dann wieder vertikal unscharf machen. Jede dieser eindimensionalen Unschärfen erfordert nur 29 Abtastwerte, wodurch sich die Gesamtmenge auf 58 Abtastwerte pro Pixel reduziert.

Ein weiterer Trick wird verwendet, um die Effizienz der Unschärfe weiter zu erhöhen. Wenn Sie der GPU sagen, dass sie zwischen zwei Pixeln abtasten soll, wird eine Überblendung der beiden Pixel ohne zusätzliche Leistungskosten zurückgegeben. Da unsere Unschärfe Pixel trotzdem miteinander vermischt, können wir jeweils zwei Pixel abtasten. Dadurch wird die Anzahl der benötigten Proben fast halbiert.

Nachfolgend sind die relevanten Teile der GaussianBlur Shader.

 sampler TextureSampler: register (s0); #define SAMPLE_COUNT 15 float2 SampleOffsets [SAMPLE_COUNT]; Float SampleWeights [SAMPLE_COUNT]; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 float4 c = 0; // Kombiniere eine Anzahl gewichteter Bildfilter-Taps. für (int i = 0; i < SAMPLE_COUNT; i++)  c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i];  return c; 

Der Shader ist eigentlich ganz einfach. es nimmt nur ein Array von Offsets und ein entsprechendes Array von Gewichtungen und berechnet die gewichtete Summe. Die gesamte komplexe Mathematik befindet sich tatsächlich im C # -Code, der die Versatz- und Gewichts-Arrays auffüllt. Dies geschieht im SetBlurEffectParameters () und ComputeGaussian () Methoden der BloomComponent Klasse. Beim Durchführen des horizontalen Weichzeichens, SampleOffsets wird nur mit horizontalen Offsets gefüllt (die y-Komponenten sind alle Null), und natürlich gilt das Gegenteil für den vertikalen Durchlauf.

Das BloomCombine Shader

Das BloomCombine Shader macht ein paar Dinge gleichzeitig. Es kombiniert die Blütentextur mit der Originaltextur und passt auch die Intensität und Sättigung jeder Textur an.

Der Shader beginnt mit der Deklaration von zwei Textur-Samplern und vier Float-Parametern.

 Sampler BloomSampler: register (s0); sampler BaseSampler: register (s1); Float BloomIntensity; Float BaseIntensity; Float BloomSaturation; Float BaseSaturation;

Eine Sache zu beachten ist das SpriteBatch bindet automatisch die Textur, die Sie beim Aufruf übergeben SpriteBatch.Draw () zum ersten Sampler, aber es bindet nichts automatisch an den zweiten Sampler. Der zweite Sampler wird manuell in eingestellt BloomComponent.Draw () mit der folgenden Zeile.

 GraphicsDevice.Textures [1] = sceneRenderTarget;

Als nächstes haben wir eine Hilfsfunktion, die die Sättigung einer Farbe einstellt.

 float4 AdjustSaturation (Float4-Farbe, Float-Sättigung) // Die Konstanten 0.3, 0.59 und 0.11 werden gewählt, weil das menschliche Auge // empfindlicher auf grünes Licht und weniger auf blau reagiert. Floatgrau = Punkt (Farbe, Float3 (0,3, 0,59, 0,11)); Rücklauf (Grau, Farbe, Sättigung); 

Diese Funktion nimmt eine Farbe und einen Sättigungswert an und gibt eine neue Farbe zurück. Übergeben einer Sättigung von 1 lässt die Farbe unverändert. Vorbeigehen 0 gibt Grau zurück, und Werte größer als Eins geben eine Farbe mit erhöhter Sättigung zurück. Das Übergeben von negativen Werten liegt außerhalb des vorgesehenen Verwendungszwecks, invertiert jedoch die Farbe, wenn Sie dies tun.

Die Funktion funktioniert, indem zuerst die Leuchtkraft der Farbe ermittelt wird, indem eine gewichtete Summe auf der Grundlage der Empfindlichkeit unserer Augen für rotes, grünes und blaues Licht ermittelt wird. Sie interpoliert dann linear zwischen dem Grau und der Originalfarbe um die angegebene Sättigung. Diese Funktion wird von der Pixel-Shader-Funktion aufgerufen.

 float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Blüte und Originalfarben des Basisbildes nachschlagen. float4 bloom = tex2D (BloomSampler, texCoord); float4 base = tex2D (BaseSampler, texCoord); // Farbsättigung und Intensität einstellen. Bloom = AdjustSaturation (Bloom, BloomSaturation) * BloomIntensity; base = AdjustSaturation (base, BaseSaturation) * BaseIntensity; // Verdunkeln Sie das Basisbild in Bereichen, in denen viel Blüte herrscht, // um zu verhindern, dass Dinge übermäßig ausgebrannt aussehen. Base * = (1 - gesättigt (Blüte)); // Kombiniere die beiden Bilder. Rückkehr Basis + Blüte; 

Auch dieser Shader ist ziemlich einfach. Wenn Sie sich fragen, warum das Grundbild in Bereichen mit heller Blüte dunkler gemacht werden muss, denken Sie daran, dass das Hinzufügen von zwei Farben die Helligkeit erhöht, und alle Farbkomponenten, die einen Wert größer als eins (volle Helligkeit) ergeben, werden auf eine Farbe begrenzt . Da das Bloom-Bild dem Basisbild ähnlich ist, würde ein Großteil des Bildes, das eine Helligkeit von über 50% hat, voll ausgeschöpft. Durch das Verdunkeln des Basisbildes werden alle Farben in den Farbbereich zurückgeführt, den wir richtig anzeigen können.


Schwarze Löcher

Einer der interessantesten Gegner in Geometry Wars ist das Schwarze Loch. Sehen wir uns an, wie wir in Shape Blaster etwas ähnliches machen können. Wir werden jetzt die grundlegenden Funktionen erstellen und den Feind im nächsten Tutorial erneut besuchen, um Partikeleffekte und Partikelinteraktionen hinzuzufügen.

Ein schwarzes Loch mit umkreisenden Partikeln

Grundlegende Funktionalität

Die schwarzen Löcher ziehen das Schiff des Spielers, Feinde in der Nähe und (nach dem nächsten Lernprogramm) Partikel an, stoßen aber Kugeln ab.

Es gibt viele mögliche Funktionen, die wir zum Anziehen oder Abstoßen verwenden können. Am einfachsten ist es, eine konstante Kraft anzuwenden, damit das Schwarze Loch unabhängig von der Entfernung des Objekts mit der gleichen Stärke zieht. Eine andere Möglichkeit besteht darin, die Kraft in einem maximalen Abstand linear von Null auf die volle Stärke von Objekten direkt über dem Schwarzen Loch zu erhöhen.

Wenn wir die Schwerkraft realistischer modellieren möchten, können wir das umgekehrte Quadrat der Entfernung verwenden, d. H. Die Schwerkraft ist proportional zu \ (1 / Entfernung ^ 2 \). Wir werden tatsächlich jede dieser drei Funktionen verwenden, um verschiedene Objekte zu behandeln. Die Kugeln werden mit einer konstanten Kraft abgestoßen, die Feinde und das Schiff des Spielers werden mit einer linearen Kraft angezogen und die Partikel verwenden eine umgekehrte quadratische Funktion.

Wir werden eine neue Klasse für Schwarze Löcher machen. Beginnen wir mit der Grundfunktionalität.

 class BlackHole: Entity private static Zufall rand = new Random (); private int-Trefferpunkte = 10; public BlackHole (Position Vector2) image = Art.BlackHole; Position = Position; Radius = Bild.Weite / 2f;  public void WasShot () hitpoints--; if (Trefferpunkte <= 0) IsExpired = true;  public void Kill()  hitpoints = 0; WasShot();  public override void Draw(SpriteBatch spriteBatch)  // make the size of the black hole pulsate float scale = 1 + 0.1f * (float)Math.Sin(10 * GameRoot.GameTime.TotalGameTime.TotalSeconds); spriteBatch.Draw(image, Position, null, color, Orientation, Size / 2f, scale, 0, 0);  

Die schwarzen Löcher brauchen zehn Schüsse, um zu töten. Wir passen die Skalierung des Sprites leicht an, damit es pulsiert. Wenn Sie beschließen, dass das Zerstören von Schwarzen Löchern auch Punkte gewähren sollte, müssen Sie ähnliche Anpassungen an dem vornehmen Schwarzes Loch Klasse wie wir es mit der feindlichen Klasse taten.

Als nächstes werden wir die schwarzen Löcher dazu bringen, eine Kraft auf andere Entitäten anzuwenden. Wir brauchen eine kleine Hilfsmethode von unserer EntityManager.

 public static IEnumerable GetNearbyEntities (Vector2-Position, Float-Radius) return entity.Where (x => Vector2.DistanceSquared (position, x.Position) < radius * radius); 

Diese Methode könnte durch Verwendung eines komplizierteren räumlichen Partitionierungsschemas effizienter gestaltet werden, aber für die Anzahl der Entitäten, die wir haben werden, ist dies in Ordnung. Jetzt können wir die schwarzen Löcher dazu bringen, Kraft in ihre zu bringen Aktualisieren() Methode.

 public override void Update () var entity = EntityManager.GetNearbyEntities (Position, 250); foreach (Entität in Entitäten) if (Entität ist Feind &&! (Entität als Feind) .IsActive) continue; // Aufzählungszeichen werden von schwarzen Löchern abgestoßen und alles andere wird angezogen, wenn (Entity is Bullet) entity.Velocity + = (entity.Position - Position) .ScaleTo (0.3f); else var dPos = Position - entity.Position; var length = dPos.Length (); entity.Velocity + = dPos.ScaleTo (MathHelper.Lerp (2, 0, Länge / 250f)); 

Schwarze Löcher betreffen nur Objekte innerhalb eines ausgewählten Radius (250 Pixel). Auf Kugeln innerhalb dieses Radius wird eine konstante abstoßende Kraft ausgeübt, während auf alle anderen eine lineare Anziehungskraft ausgeübt wird.

Wir müssen das Kollisionshandling für Schwarze Löcher ergänzen EntityManager. Füge hinzu ein Liste <> für Schwarze Löcher wie bei den anderen Entitäten, und fügen Sie den folgenden Code hinzu EntityManager.HandleCollisions ().

 // behandelt Kollisionen mit schwarzen Löchern für (int i = 0; i < blackHoles.Count; i++)  for (int j = 0; j < enemies.Count; j++) if (enemies[j].IsActive && IsColliding(blackHoles[i], enemies[j])) enemies[j].WasShot(); for (int j = 0; j < bullets.Count; j++)  if (IsColliding(blackHoles[i], bullets[j]))  bullets[j].IsExpired = true; blackHoles[i].WasShot();   if (IsColliding(PlayerShip.Instance, blackHoles[i]))  KillPlayer(); break;  

Schließlich öffnen Sie die EnemySpawner Klasse und lassen Sie einige schwarze Löcher schaffen. Ich beschränkte die maximale Anzahl von schwarzen Löchern auf zwei und gab eine Chance von 1 zu 600, dass in jedem Bild ein schwarzes Loch auftauchte.

 if (EntityManager.BlackHoleCount < 2 && rand.Next((int)inverseBlackHoleChance) == 0) EntityManager.Add(new BlackHole(GetSpawnPosition()));

Fazit

Wir haben Bloom mit verschiedenen Shadern hinzugefügt und Schwarze Löcher mit verschiedenen Kraftformeln. Shape Blaster fängt an, ziemlich gut auszusehen. Im nächsten Teil fügen wir einige verrückte über den oberen Partikeleffekten hinzu.