Dieser Effekt wurde von der
Episode von Powerpuff Girls inspiriert. Ich wollte den Effekt der Ausbreitung von Farben in einer Schwarz-Weiß-Welt erzeugen, ihn aber
in den Koordinaten des Weltraums implementieren , um zu sehen, wie
Farbe Objekte malt und nicht wie in einem Cartoon flach auf dem Bildschirm verteilt.
Ich habe den Effekt in der neuen
Lightweight-Rendering-Pipeline der Unity-Engine erstellt, einem integrierten Beispiel für die Pipeline der Scriptable Rendering-Pipeline. Alle Konzepte gelten für andere Pipelines, aber einige integrierte Funktionen oder Matrizen können unterschiedliche Namen haben. Ich habe auch den neuen Nachbearbeitungsstapel verwendet, aber im Tutorial werde ich eine detaillierte Beschreibung seiner Einstellungen weglassen, da er in anderen Handbüchern, beispielsweise in
diesem Video, recht gut beschrieben ist.
Der Effekt der Nachbearbeitung in Graustufen
Nur als Referenz: So sieht eine Szene ohne Nachbearbeitungseffekte aus.
Für diesen Effekt habe ich das neue Unity 2018-Nachbearbeitungspaket verwendet, das vom Paketmanager heruntergeladen werden kann. Wenn Sie nicht wissen, wie man es benutzt, empfehle ich
dieses Tutorial .
Ich habe meinen eigenen Effekt geschrieben, indem ich die in C # geschriebenen Klassen PostProcessingEffectSettings und PostProcessEffectRenderer erweitert habe, deren Quellcode
hier zu sehen
ist . Tatsächlich habe ich mit diesen Effekten auf der CPU-Seite (in C # -Code) nichts besonders Interessantes gemacht, außer dass ich dem Inspektor eine Gruppe allgemeiner Eigenschaften hinzugefügt habe, sodass ich im Tutorial nicht erklären werde, wie dies zu tun ist. Ich hoffe mein Code spricht für sich.
Fahren wir mit dem Shader-Code fort und beginnen mit dem Graustufeneffekt. Im Tutorial werden die Shaderlab-Datei, die Eingabestrukturen und der Vertex-Shader nicht geändert, sodass Sie den Quellcode
hier sehen können . Stattdessen kümmern wir uns um den Fragment-Shader.
Um eine Farbe in eine Graustufe umzuwandeln,
reduzieren wir
den Wert jedes Pixels auf einen Luminanzwert , der seine
Helligkeit beschreibt. Dies kann erreicht werden, indem das Skalarprodukt des
Farbwerts der Kameratextur und des
gewichteten Vektors genommen wird , der den Beitrag jedes Farbkanals zur Gesamtfarbhelligkeit beschreibt.
Warum verwenden wir Skalarprodukte? Vergessen Sie nicht, dass Skalarprodukte wie folgt berechnet werden:
dot(a, b) = a x * b x + a y * b y + a z * b z
In diesem Fall multiplizieren wir jeden Kanal des
Farbwerts mit dem
Gewicht . Dann fügen wir diese Produkte hinzu, um sie auf einen einzigen Skalarwert zu reduzieren. Wenn die RGB-Farbe in den Kanälen R, G und B dieselben Werte hat, wird die Farbe grau.
So sieht der Shader-Code aus:
float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); float3 weight = float3(0.299, 0.587, 0.114); float luminance = dot(fullColor.rgb, weight); float3 greyscale = luminance.xxx; return float4(greyscale, 1.0);
Wenn der Basis-Shader korrekt konfiguriert ist, sollte der Nachbearbeitungseffekt den gesamten Bildschirm in Graustufen färben.
Farbwiedergabe im Weltraum
Da dies ein Nachbearbeitungseffekt ist, haben
wir im Vertex-Shader
keine Informationen zur Szenengeometrie. In der Nachbearbeitungsphase haben wir nur das
von der Kamera gerenderte Bild und den
Abstand der abgeschnittenen Koordinaten zum Abtasten. Wir möchten jedoch, dass sich der Farbeffekt über Objekte verteilt, als ob er auf der Welt stattfinden würde, und nicht nur auf einem Flachbildschirm.
Um diesen Effekt in die Geometrie der Szene zu zeichnen, benötigen wir die
Koordinaten des Weltraums jedes Pixels. Um von den
Koordinaten des Raums der abgeschnittenen Koordinaten zu den
Koordinaten des Weltraums zu gelangen , müssen wir eine
Transformation des Koordinatenraums durchführen .
Um von einem Koordinatenraum in einen anderen zu gelangen, wird normalerweise eine Matrix benötigt, die die Transformation vom Koordinatenraum A in den Raum B definiert. Um von A nach B zu gelangen, multiplizieren wir den Vektor im Koordinatenraum A mit dieser Transformationsmatrix. In unserem Fall führen wir den folgenden Übergang durch: den
Raum der abgeschnittenen Koordinaten (Clipraum) ->
Ansichtsraum (Ansichtsraum) ->
Weltraum (Weltraum) . Das heißt, wir benötigen die Clip-to-View-Space-Matrix und die View-to-World-Space-Matrix, die Unity bereitstellt.
Die
Einheitskoordinaten des abgeschnittenen Koordinatenraums haben jedoch keinen z-Wert , der die Tiefe des Pixels oder den Abstand zur Kamera bestimmt. Wir brauchen diesen Wert, um vom Raum der abgeschnittenen Koordinaten in den Artenraum zu gelangen. Beginnen wir damit!
Tiefenpufferwert abrufen
Wenn die Rendering-Pipeline aktiviert ist, zeichnet sie im
Ansichtsfenster eine Textur, in der
die z-Werte in einer Struktur
gespeichert sind ,
die als
Tiefenpuffer bezeichnet wird . Wir können diesen Puffer abtasten, um den fehlenden
z-Wert unseres Koordinatenraums von abgeschnittenen Koordinaten zu erhalten!
Stellen Sie zunächst sicher, dass der
Tiefenpuffer tatsächlich gerendert wird, indem Sie im Inspektor auf den Abschnitt „Zusätzliche Daten hinzufügen“ der Kamera klicken und aktivieren, dass das Kontrollkästchen „Tiefenstruktur erforderlich“ aktiviert ist. Stellen Sie außerdem sicher, dass die Option MSAA zulassen für die Kamera aktiviert ist. Ich weiß nicht, warum dieser Effekt überprüft werden muss, aber es ist so. Wenn der
Tiefenpuffer gezeichnet ist, sollte im
Frame-Debugger die Stufe
„Depth Prepass“ angezeigt werden .
Erstellen Sie einen _CameraDepthTexture-Sampler in der
hlsl-Datei TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture);
Schreiben wir nun die GetWorldFromViewPosition-Funktion und überprüfen damit zunächst
den Tiefenpuffer. (Später werden wir es erweitern, um eine Position in der Welt zu bekommen.)
float3 GetWorldFromViewPosition (VertexOutput i) { float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; return z.xxx; }
Zeichnen Sie im Fragment-Shader den Wert des Tiefen-Textur-Samples.
float3 depth = GetWorldFromViewPosition(i); return float4(depth, 1.0);
So sehen meine Ergebnisse aus, wenn es nur eine hügelige Ebene in der Szene gibt (ich habe alle Bäume ausgeschaltet, um das Testen der Werte des Weltraums weiter zu vereinfachen). Ihr Ergebnis sollte ähnlich aussehen. Schwarz-Weiß-Werte beschreiben den Abstand von der Geometrie zur Kamera.
Hier sind einige Schritte, die Sie ausführen können, wenn Sie auf Probleme stoßen:
- Stellen Sie sicher, dass für die Kamera die Tiefenwiedergabe aktiviert ist.
- Stellen Sie sicher, dass für die Kamera MSAA aktiviert ist.
- Versuchen Sie, die nahe und ferne Ebene der Kamera zu ändern.
- Stellen Sie sicher, dass die Objekte, die Sie im Tiefenpuffer sehen möchten, einen Shader mit einem Tiefenpass verwenden. Dies stellt sicher, dass das Objekt in den Tiefenpuffer zeichnet. Alle Standard-Shader in LWRP tun dies.
Wertschöpfung im Weltraum
Nachdem wir nun alle Informationen haben, die für den
Raum der abgeschnittenen Koordinaten erforderlich sind, wollen wir uns in den
Artenraum und dann in den
Weltraum verwandeln.
Beachten Sie, dass sich die für diese Operationen erforderlichen Transformationsmatrizen bereits in der SRP-Bibliothek befinden. Sie sind jedoch in der C # -Bibliothek der Unity-Engine enthalten, daher habe ich sie in den
Rader in der Render-Funktion des
ColorSpreadRenderer- Skripts
eingefügt :
sheet.properties.SetMatrix("unity_ViewToWorldMatrix", context.camera.cameraToWorldMatrix); sheet.properties.SetMatrix("unity_InverseProjectionMatrix", projectionMatrix.inverse);
Erweitern wir nun unsere GetWorldFromViewPosition-Funktion.
Zuerst müssen wir die Position im Ansichtsfenster ermitteln, indem wir
die Position im abgeschnittenen Koordinatenraum mit der InverseProjectionMatrix multiplizieren . Wir müssen auch etwas mehr Voodoo-Magie mit einer Bildschirmposition machen, die damit zusammenhängt, wie Unity seine Position im Raum abgeschnittener Koordinaten speichert.
Schließlich können wir
die Position im Ansichtsfenster mit ViewToWorldMatrix multiplizieren , um die Position im
Weltraum zu erhalten .
float3 GetWorldFromViewPosition (VertexOutput i) {
Lassen Sie uns überprüfen, ob die Positionen im globalen Raum korrekt sind. Zu diesem Zweck habe ich einen
Shader geschrieben , der nur die Position eines Objekts im
Weltraum zurückgibt. Dies ist eine ziemlich einfache Berechnung, die auf einem regulären Shader basiert, dessen Richtigkeit vertrauenswürdig ist. Deaktivieren Sie den Effekt der Nachbearbeitung und machen Sie einen Screenshot dieses Test-Shaders für
den Weltraum . Mein nach dem Anwenden des Shaders auf die Erdoberfläche in der Szene sieht so aus:
(Beachten Sie, dass die Werte im Weltraum viel größer als 1,0 sind. Machen Sie sich also keine Sorgen, dass diese Farben sinnvoll sind. Stellen Sie stattdessen sicher, dass die Ergebnisse für die „wahren“ und „berechneten“ Antworten gleich sind.) Kehren wir als Nächstes zum Test zurück Das Objekt ist gewöhnliches Material (und nicht das Testmaterial des Weltraums) und schaltet dann den Nachbearbeitungseffekt wieder ein. Meine Ergebnisse sehen folgendermaßen aus:
Dies ist dem Test-Shader, den ich geschrieben habe, völlig ähnlich, dh die Berechnungen des Weltraums sind höchstwahrscheinlich korrekt!
Zeichnen eines Kreises im Weltraum
Jetzt, wo wir
Positionen im Weltraum haben , können wir einen Farbkreis in der Szene zeichnen! Wir müssen den
Radius einstellen, innerhalb dessen der Effekt Farbe zeichnet. Draußen wird das Bild durch den Effekt in Graustufen gerendert. Um es einzustellen, müssen Sie die Werte für
den Effektradius (
_MaxSize ) und den Mittelpunkt des Kreises (_Center) anpassen. Ich habe diese Werte in der C #
ColorSpread- Klasse so festgelegt, dass sie im Inspektor sichtbar sind. Erweitern wir unseren Fragment-Shader, indem wir ihn zwingen,
zu überprüfen, ob sich das aktuelle Pixel innerhalb des Kreisradius befindet :
float4 Frag(VertexOutput i) : SV_Target { float3 worldPos = GetWorldFromViewPosition(i);
Schließlich können wir die Farbe basierend darauf zeichnen, ob sie sich innerhalb eines
Radius im
Weltraum befindet . So sieht der Basiseffekt aus!
Hinzufügen von Spezialeffekten
Ich werde ein paar weitere Techniken betrachten, mit denen die Farbe über den Boden verteilt wird. Die volle Wirkung ist viel mehr, aber das Tutorial ist bereits zu umfangreich geworden, sodass wir uns auf das Wichtigste beschränken werden.
Kreisvergrößerungsanimation
Wir wollen, dass sich der Effekt auf der ganzen Welt ausbreitet, das heißt, als würde er wachsen. Dazu müssen Sie den
Radius je nach Zeit ändern.
_StartTime gibt den Zeitpunkt an, zu dem der Kreis wachsen soll. In meinem Projekt habe ich ein zusätzliches Skript verwendet, mit dem Sie auf eine beliebige Stelle auf dem Bildschirm klicken können, um das Wachstum des Kreises zu starten. In diesem Fall entspricht die Startzeit der Zeit, zu der die Maus gedrückt wurde.
_GrowthSpeed legt die Geschwindigkeit fest, mit der der Kreis vergrößert wird.
Wir müssen auch die Entfernungsprüfung aktualisieren, um die aktuelle Entfernung mit dem zunehmenden
Radius des Effekts zu vergleichen , und nicht mit _MaxSize.
So sollte das Ergebnis aussehen:
Hinzufügen zum Rauschradius
Ich wollte, dass der Effekt eher einer Farbunschärfe ähnelt, nicht nur einem wachsenden Kreis.
Fügen Sie dazu dem Radius des Effekts Rauschen hinzu, damit die Verteilung ungleichmäßig ist.
Zuerst müssen wir die Textur im
Weltraum abtasten . Die UV-Koordinaten von i.screenPos befinden sich im
Bildschirmbereich. Wenn wir darauf basierend abtasten, bewegt sich die Form des Effekts mit der Kamera. Verwenden wir also die Koordinaten im
Weltraum . Ich habe den Parameter
_NoiseTexScale hinzugefügt, um die
Skalierung des Rauschtextur-Samples zu steuern, da die Koordinaten im Weltraum ziemlich groß sind.
Lassen Sie uns nun die Rauschtextur abtasten und diesen Wert zum Radius des Effekts hinzufügen. Ich habe die _NoiseSize-Skala verwendet, um die Rauschgröße besser steuern zu können.
So sehen die Ergebnisse nach einigen Optimierungen aus:
Abschließend
Sie können die Aktualisierungen der Tutorials auf meinem
Twitter verfolgen, und auf
Twitch gebe ich Codierungs-Streams aus! (Außerdem streame ich von Zeit zu Zeit Spiele. Seien Sie also nicht überrascht, wenn Sie mich in meinem Pyjama sitzen und Kingdom Hearts 3 spielen sehen.)
Danksagung: