Erstellen Sie in Unity einen Farbverteilungseffekt


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) { //    float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; //      float4 result = mul(unity_InverseProjectionMatrix, float4(2*i.screenPos-1.0, z, 1.0)); float3 viewPos = result.xyz / result.w; //      float3 worldPos = mul(unity_ViewToWorldMatrix, float4(viewPos, 1.0)); return worldPos; } 

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); // ,      .  //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= _MaxSize? 0 : 1; //   float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); //   float luminance = dot(fullColor.rgb, float3(0.2126729, 0.7151522, 0.0721750)); float3 greyscale = luminance.xxx; // ,       float3 color = (1-blend)*fullColor + blend*greyscale; return float4(color, 1.0); } 

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.

 //           float timeElapsed = _Time.y - _StartTime; float effectRadius = min(timeElapsed * _GrowthSpeed, _MaxSize); //  ,      effectRadius = clamp(effectRadius, 0, _MaxSize); 

Wir müssen auch die Entfernungsprüfung aktualisieren, um die aktuelle Entfernung mit dem zunehmenden Radius des Effekts zu vergleichen , und nicht mit _MaxSize.

 // ,         //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= effectRadius? 0 : 1; //     ... 

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.

 //          float2 worldUV = worldPos.xz; worldUV *= _NoiseTexScale; 

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.

 //     float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, worldUV).r; effectRadius -= noise * _NoiseSize; 

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:

Source: https://habr.com/ru/post/de436456/


All Articles