So verwenden Sie einen Shader, um die Farben eines Sprites dynamisch auszutauschen

In diesem Lernprogramm erstellen wir einen einfachen Farbwechsel-Shader, der Sprites schnell neu einfärben kann. Der Shader erleichtert das Hinzufügen von Abwechslung zu einem Spiel, er ermöglicht dem Spieler, seinen Charakter anzupassen, und er kann verwendet werden, um den Sprites Spezialeffekte hinzuzufügen, z. B. dass sie blinken, wenn der Charakter Schaden erleidet.

Obwohl wir hier Unity für Demo und Quellcode verwenden, funktioniert das Grundprinzip in vielen Game Engines und Programmiersprachen.

Demo

Sie können die Unity-Demo oder die WebGL-Version (25 MB +) auschecken, um das Endergebnis in Aktion zu sehen. Verwenden Sie die Farbauswahl, um den oberen Buchstaben neu zu färben. (Die anderen Zeichen verwenden alle dasselbe Sprite, wurden jedoch auf ähnliche Weise neu eingefärbt.) Klicken Sie auf Hit-Effekt Damit die Zeichen alle kurz weiß leuchten.

Die Theorie verstehen

Hier ist die Beispieltextur, mit der wir den Shader demonstrieren:

Ich habe diese Textur von http://opengameart.org/content/classic-hero heruntergeladen und etwas bearbeitet.

Es gibt viele Farben auf dieser Textur. So sieht die Palette aus:

Lassen Sie uns darüber nachdenken, wie wir diese Farben in einem Shader austauschen können.

Jede Farbe hat einen eindeutigen RGB-Wert, daher ist es verlockend, Shader-Code zu schreiben, der besagt, dass "die Texturfarbe gleich ist diese RGB-Wert, ersetzen Sie es mit Das RGB-Wert ". Dies skaliert jedoch für viele Farben nicht gut und ist eine recht kostspielige Operation. Wir möchten definitiv alle bedingten Aussagen vollständig vermeiden.

Stattdessen verwenden wir eine zusätzliche Textur, die die Ersatzfarben enthält. Nennen wir diese Textur a tauschen Sie die Textur aus.

Die große Frage ist, wie verknüpfen wir die Farbe von der Sprite-Textur mit der Farbe von der Swap-Textur? Die Antwort ist, wir werden die rote (R) -Komponente aus der RGB-Farbe verwenden, um die Swap-Textur zu indizieren. Das bedeutet, dass die Swap-Textur 256 Pixel breit sein muss, denn so viele verschiedene Werte kann die rote Komponente annehmen.

Gehen wir das alles in einem Beispiel durch. Hier sind die roten Farbwerte der Farben der Sprite-Palette:

Angenommen, wir möchten die Umriss- / Augenfarbe (schwarz) des Sprites durch die Farbe Blau ersetzen. Die Umrissfarbe ist die letzte auf der Palette - die Farbe mit dem roten Wert von 25. Wenn wir diese Farbe austauschen möchten, müssen wir in der Swap-Textur das Pixel im Index 25 auf die Farbe setzen, die die Kontur sein soll: Blau.

Die Swap-Textur, wobei die Farbe am Index 25 auf Blau gesetzt ist.

Wenn der Shader auf eine Farbe mit einem roten Wert von 25 trifft, wird sie durch die blaue Farbe aus der Swap-Textur ersetzt:

Beachten Sie, dass dies möglicherweise nicht wie erwartet funktioniert, wenn zwei oder mehr Farben in der Sprite-Textur denselben Rotwert haben! Bei dieser Methode müssen die Rotwerte der Farben in der Sprite-Textur unterschiedlich sein.

Beachten Sie auch, dass, wie Sie in der Demo sehen können, das Platzieren eines transparenten Pixels an einem beliebigen Index in der Swap-Textur dazu führt, dass die Farben, die diesem Index entsprechen, nicht ausgetauscht werden.

Den Shader implementieren

Wir werden diese Idee umsetzen, indem Sie einen vorhandenen Sprite-Shader ändern. Da das Demo-Projekt in Unity erstellt wurde, verwende ich den standardmäßigen Unity-Sprite-Shader.

Der Standard-Shader (der für dieses Lernprogramm relevant ist) enthält lediglich ein Muster der Farbe aus dem Hauptatlas der Textur und multipliziert diese Farbe mit einer Scheitelpunktfarbe, um den Farbton zu ändern. Die resultierende Farbe wird dann mit dem Alpha multipliziert, um das Sprite bei niedrigeren Opazitäten dunkler zu machen.

Als erstes müssen Sie dem Shader eine zusätzliche Textur hinzufügen:

Eigenschaften [PerRendererData] _MainTex ("Sprite Texture", 2D) = "Weiß"  _SwapTex ("Farbdaten", 2D) = "transparent"  _Color ("Tint", Color) = (1,1,1.) , 1) [MaterialToggle] PixelSnap ("Pixelfang", Float) = 0

Wie Sie sehen, haben wir hier jetzt zwei Texturen. Der erste, _MainTex, ist die Sprite-Textur; der zweite, _SwapTex, ist die Swap-Textur.

Wir müssen auch einen Sampler für die zweite Textur definieren, damit wir tatsächlich darauf zugreifen können. Wir verwenden einen 2D-Textur-Sampler, da Unity keine 1D-Sampler unterstützt:

sampler2D _MainTex; sampler2D _AlphaTex; float _AlphaSplitEnabled; sampler2D _SwapTex;

Nun können wir endlich den Fragment-Shader bearbeiten:

fixed4 SampleSpriteTexture (float2 uv) fixed4 color = tex2D (_MainTex, uv); if (_AlphaSplitEnabled) color.a = tex2D (_AlphaTex, uv) .r; Farbe zurückgeben;  fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb * = c.a; Rückkehr c; 

Hier ist der relevante Code für den Standard-Fragment-Shader. Wie du siehst, c ist die Farbe, die aus der Haupttextur entnommen wurde; es wird mit der Scheitelpunktfarbe multipliziert, um einen Farbton zu erhalten. Der Shader verdunkelt die Sprites mit geringeren Opazitäten.

Nachdem wir die Hauptfarbe abgetastet haben, lassen Sie uns auch die Swap-Farbe abtasten. Bevor wir dies tun, entfernen wir jedoch den Teil, der sie mit der Tönungsfarbe multipliziert, so dass wir mit dem echten Rotwert der Textur abtasten, nicht mit dem getönten.

fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord); fixed4 swapCol = tex2D (_SwapTex, float2 (c.r, 0));

Wie Sie sehen, entspricht der gemessene Farbindex dem Rotwert der Hauptfarbe.

Lassen Sie uns nun unsere endgültige Farbe berechnen:

fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord); fixed4 swapCol = tex2D (_SwapTex, float2 (c.r, 0)); fixed4 final = lerp (c, swapCol, swapCol.a); 

Dazu müssen wir zwischen der Hauptfarbe und der ausgelagerten Farbe interpolieren, wobei das Alpha der ausgelagerten Farbe als Schritt verwendet wird. Wenn die ausgetauschte Farbe transparent ist, entspricht die endgültige Farbe der Hauptfarbe. Wenn jedoch die getauschte Farbe vollständig undurchsichtig ist, entspricht die endgültige Farbe der getauschten Farbe.

Vergessen wir nicht, dass die Endfarbe mit dem Farbton multipliziert werden muss:

fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord); fixed4 swapCol = tex2D (_SwapTex, float2 (c.r, 0)); fixed4 final = lerp (c, swapCol, swapCol.a) * IN.color;

Nun müssen wir uns überlegen, was passieren soll, wenn wir eine Farbe auf der Haupttextur austauschen möchten, die nicht vollständig undurchsichtig ist. Wenn wir beispielsweise ein blaues, halbtransparentes Geister-Sprite haben und seine Farbe in Lila umwandeln möchten, möchten wir nicht, dass der Geist mit den getauschten Farben undurchsichtig ist. Wir möchten die ursprüngliche Transparenz beibehalten. Also lass uns das tun:

fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord); fixed4 swapCol = tex2D (_SwapTex, float2 (c.r, 0)); fixed4 final = lerp (c, swapCol, swapCol.a) * IN.color; final.a = c.a;

Die endgültige Farbtransparenz sollte der Transparenz der Haupttexturfarbe entsprechen. 

Da der ursprüngliche Shader den RGB-Wert der Farbe mit dem Alpha-Wert der Farbe multipliziert, sollten wir dies auch tun, um den Shader gleich zu halten:

fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord); fixed4 swapCol = tex2D (_SwapTex, float2 (c.r, 0)); fixed4 final = lerp (c, swapCol, swapCol.a) * IN.color; final.a = c.a; final.rgb * = c.a; Rückkehr endgültig; 

Der Shader ist jetzt fertig. Wir können eine Swap-Farbtextur erstellen, sie mit anderen Farbpixeln füllen und sehen, ob das Sprite die Farben korrekt ändert. 

Natürlich wäre diese Methode nicht sehr nützlich, wenn wir ständig Swap-Texturen von Hand erstellen müssten! Wir wollen sie prozedural generieren und modifizieren…

Einrichten einer Beispiel-Demo

Wir wissen, dass wir eine Swap-Textur benötigen, um unseren Shader nutzen zu können. Wenn mehrere Charaktere gleichzeitig verschiedene Paletten für dasselbe Sprite verwenden möchten, benötigt jedes dieser Zeichen eine eigene Auslagerungsstruktur. 

Am besten ist es, wenn Sie diese Swap-Texturen einfach dynamisch erstellen, während wir die Objekte erstellen.

Zunächst definieren wir eine Swap-Textur und ein Array, in dem wir alle getauschten Farben verfolgen:

Texture2D mColorSwapTex; Farbe [] mSpriteColors;

Als Nächstes erstellen wir eine Funktion, in der wir die Textur initialisieren. Wir verwenden das RGBA32-Format und setzen den Filtermodus auf Punkt:

public void InitColorSwapTex () Texture2D colorSwapTex = new Texture2D (256, 1, TextureFormat.RGBA32, false, false); colorSwapTex.filterMode = FilterMode.Point; 

Lassen Sie uns nun sicherstellen, dass alle Pixel der Textur transparent sind, indem Sie alle Pixel löschen und die Änderungen anwenden:

für (int i = 0; i < colorSwapTex.width; ++i) colorSwapTex.SetPixel(i, 0, new Color(0.0f, 0.0f, 0.0f, 0.0f)); colorSwapTex.Apply();

Wir müssen auch die Swap-Textur des Materials auf die neu erstellte setzen:

mSpriteRenderer.material.SetTexture ("_ SwapTex", colorSwapTex);

Zum Schluss speichern wir den Verweis auf die Textur und erstellen das Array für die Farben:

mSpriteColors = neue Farbe [colorSwapTex.width]; mColorSwapTex = colorSwapTex;

Die vollständige Funktion ist wie folgt:

public void InitColorSwapTex () Texture2D colorSwapTex = new Texture2D (256, 1, TextureFormat.RGBA32, false, false); colorSwapTex.filterMode = FilterMode.Point; für (int i = 0; i < colorSwapTex.width; ++i) colorSwapTex.SetPixel(i, 0, new Color(0.0f, 0.0f, 0.0f, 0.0f)); colorSwapTex.Apply(); mSpriteRenderer.material.SetTexture("_SwapTex", colorSwapTex); mSpriteColors = new Color[colorSwapTex.width]; mColorSwapTex = colorSwapTex; 

Beachten Sie, dass nicht für jedes Objekt eine separate Textur von 256x1px verwendet werden muss. Wir könnten eine größere Textur erzeugen, die alle Objekte abdeckt. Wenn wir 32 Zeichen benötigen, können wir eine Textur der Größe 256x32px erstellen und sicherstellen, dass jedes Zeichen nur eine bestimmte Zeile in dieser Textur verwendet. Jedes Mal, wenn wir eine Änderung an dieser größeren Textur vornehmen mussten, müssen wir jedoch mehr Daten an die GPU übergeben, was diese wahrscheinlich weniger effizient machen würde.

Es ist auch nicht notwendig, für jedes Sprite eine eigene Swap-Textur zu verwenden. Wenn der Charakter beispielsweise über eine Waffe verfügt und diese Waffe ein separates Sprite ist, kann die Swap-Textur problemlos mit dem Charakter gemeinsam genutzt werden (sofern die Sprite-Textur der Waffe keine Farben verwendet, die rote Werte haben, die mit diesen identisch sind.) des Charakters Sprite).

Es ist sehr nützlich zu wissen, was die roten Werte bestimmter Sprite-Parts sind, also erstellen wir eine enum das wird diese Daten halten:

public enum SwapIndex Outline = 25, SkinPrim = 254, SkinSec = 239, HandPrim = 235, HandSec = 204, ShirtPrim = 62, ShirtSec = 70, ShoePrim = 253, ShoeSec = 248, Hosen = 72,

Dies sind alle Farben, die vom Beispielzeichen verwendet werden.

Jetzt haben wir alle Dinge, die wir zum Erstellen einer Funktion benötigen, um die Farbe tatsächlich auszutauschen:

public void SwapColor (SwapIndex-Index, Farbe Farbe) mSpriteColors [(int) index] = color; mColorSwapTex.SetPixel ((int) index, 0, color); 

Wie Sie sehen können, ist hier nichts Besonderes. Wir setzen einfach die Farbe im Farbfeld unseres Objekts und setzen auch den Pixel der Textur auf einen geeigneten Index. 

Beachten Sie, dass wir die Änderungen nicht wirklich jedes Mal auf die Textur anwenden möchten, wenn wir diese Funktion tatsächlich aufrufen. wir würden sie lieber anwenden, wenn wir tauschen alles die Pixel, die wir wollen.

Schauen wir uns ein Beispiel für die Verwendung der Funktion an:

 SwapColor (SwapIndex.SkinPrim, ColorFromInt (0x784a00)); SwapColor (SwapIndex.SkinSec, ColorFromInt (0x4c2d00)); SwapColor (SwapIndex.ShirtPrim, ColorFromInt (0xc4ce00)); SwapColor (SwapIndex.ShirtSec, ColorFromInt (0x784a00)); SwapColor (SwapIndex.Pants, ColorFromInt (0x594f00)); mColorSwapTex.Apply ();

Wie Sie sehen, ist es ziemlich einfach zu verstehen, was diese Funktionsaufrufe tun, wenn Sie sie lesen: In diesem Fall ändern sie beide Hautfarben, beide Hemdfarben und die Hosenfarbe.

Hinzufügen eines Hit-Effekts zur Demo

Als Nächstes sehen wir, wie wir den Shader verwenden können, um einen Treffer-Effekt für unser Sprite zu erzeugen. Bei diesem Effekt werden alle Farben des Sprites in Weiß getauscht, für einen kurzen Zeitraum auf diese Weise beibehalten und anschließend die ursprüngliche Farbe wiederhergestellt. Der Gesamteffekt ist, dass das Sprite weiß blinkt.

Zunächst erstellen wir eine Funktion, die alle Farben vertauscht, aber die Farben des Arrays des Objekts nicht wirklich überschreibt. Wir werden diese Farben brauchen, wenn wir den Hit-Effekt doch abschalten wollen.

public void SwapAllSpritesColorsTemporarily (Color color) für (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, color); mColorSwapTex.Apply(); 

Wir könnten nur durch die Enumerationen iterieren, aber wenn Sie die gesamte Textur durchlaufen, wird sichergestellt, dass die Farbe ausgetauscht wird, auch wenn eine bestimmte Farbe nicht in der definiert ist SwapIndex.

Nun, da die Farben vertauscht sind, müssen wir einige Zeit warten und zu den vorherigen Farben zurückkehren. 

Zuerst erstellen wir eine Funktion, die die Farben zurücksetzt:

public void ResetAllSpritesColors () für (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, mSpriteColors[i]); mColorSwapTex.Apply(); 

Nun definieren wir den Timer und eine Konstante:

Float mHitEffectTimer = 0.0f; const float cHitEffectTime = 0.1f;

Lassen Sie uns eine Funktion erstellen, die den Treffereffekt startet:

public void StartHitEffect () mHitEffectTimer = cHitEffectTime; SwapAllSpritesColorsTemporarily (Color.white); 

Und in der Update-Funktion wollen wir überprüfen, wie viel Zeit der Timer noch hat, jeden Tick verringern und nach Ablauf der Zeit einen Reset anfordern:

public void Update () if (mHitEffectTimer> 0.0f) mHitEffectTimer - = Time.deltaTime; if (mHitEffectTimer <= 0.0f) ResetAllSpritesColors();  

Das ist es - jetzt, wann StartHitEffect aufgerufen wird, blinkt das Sprite für einen Moment weiß und kehrt dann zu seinen vorherigen Farben zurück.

Zusammenfassung

Dies ist das Ende des Tutorials! Ich hoffe, Sie finden die Methode akzeptabel und den Shader nützlich. Es ist ein wirklich einfaches, aber es funktioniert gut für Pixel-Art-Sprites, die nicht viele Farben verwenden. 

Die Methode müsste ein wenig geändert werden, wenn wir ganze Farbgruppen auf einmal austauschen möchten, was definitiv einen komplizierteren und teureren Shader erfordert. In meinem eigenen Spiel verwende ich jedoch nur wenige Farben, daher passt diese Technik perfekt.