Hur man använder en Shader för att byta dynamiskt en Sprite-färger

I den här handledningen skapar vi en enkel färgbytesskuggare som kan lita på sprites i flygningen. Shader gör det mycket lättare att lägga till variation i ett spel, gör det möjligt för spelaren att anpassa sin karaktär och kan användas för att lägga till specialeffekter för spritesna, till exempel att de blinkar när karaktären skadar.

Även om vi använder Unity för demo- och källkoden här, kommer grundprincipen att fungera i många spelmotorer och programmeringsspråk.

demo

Du kan kolla in Unity demo eller WebGL-versionen (25MB +) för att se det slutliga resultatet i åtgärd. Använd färgplockarna för att återfärda toppteckenet. (De andra karaktärerna använder alla samma sprite, men har också lignats på samma sätt.) Klicka på Hit Effect för att få tecknen blinka vitt kort.

Förstå teorin

Här är exemplet textur som vi ska använda för att visa shader:

Jag hämtade denna textur från http://opengameart.org/content/classic-hero och redigerade den något.

Det finns en hel del färger på denna textur. Så här ser paletten ut:

Nu, låt oss tänka på hur vi kan byta dessa färger inuti en skuggare.

Varje färg har ett unikt RGB-värde som är associerat med det, så det är frestande att skriva skuggkod som säger "om texturfärgen är lika med detta RGB-värde, ersätt det med den där RGB-värde ". Det går dock inte bra för många färger, och det är ganska dyrt. Vi vill definitivt undvika några villkorliga uttalanden helt och hållet.

I stället kommer vi att använda en extra textur, som kommer att innehålla ersättningsfärgerna. Låt oss kalla denna textur a byt textur.

Den stora frågan är hur vi länkar färgen från sprittexturen till färgen från swaptexturen? Svaret är att vi använder den röda (R) -komponenten från RGB-färgen för att indexera swaptexturen. Det betyder att byte texturen måste vara 256 pixlar bred, eftersom det är så många olika värden den röda komponenten kan ta.

Låt oss gå över allt detta i ett exempel. Här är de röda färgvärdena för sprite palettens färger:

Låt oss säga att vi vill byta ut skiss / ögonfärg (svart) på spritet med färgblå. Konturfärgen är den sista på paletten - den med ett rött värde på 25. Om vi ​​vill byta den här färgen måste vi ställa in pixeln i index 25 till den färg vi vill ha i konturen: blå.

Bytet textur, med färgen vid index 25 inställd till blå.

Nu när shader möter en färg med ett rött värde på 25, kommer det att ersätta det med den blå färgen från swaptexturen:

Observera att det kanske inte fungerar som förväntat om två eller flera färger på sprittexturen delar samma röda värde! När du använder den här metoden är det viktigt att du håller de röda värdena för färgerna i sprittexturen annorlunda.

Observera också att, som du kan se i demo, innebär det att du inte sätter in en genomskinlig pixel i något index i swaptexturen utan att byta färg för de färger som motsvarar det indexet.

Genomföra Shader

Vi genomför denna idé genom att ändra en befintlig sprite shader. Eftersom demoprojektet är gjord i Unity använder jag standard Unity sprite shader.

All standard shader gör (som är relevant för denna handledning) är ett exempel på färgen från huvudtexturatlasen och multiplicera den färgen med en vertexfärg för att ändra färgtonen. Den resulterande färgen multipliceras sedan med alfa, för att göra sprite mörkare vid lägre opacitet.

Det första vi behöver göra är att lägga till en extra textur till shader:

Egenskaper [PerRendererData] _MainTex ("Sprite Texture", 2D) = "vit"  _SwapTex ("Färgdata", 2D) = "transparent"  _Color ("Tint", Färg) = , 1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0

Som du kan se har vi två texturer här nu. Den första, _MainTex, är sprite-strukturen; den andra, _SwapTex, är swaptexturen.

Vi måste också definiera en sampler för den andra strukturen, så vi kan faktiskt få tillgång till det. Vi använder en 2D texturprovtagare, eftersom Unity inte stöder 1D-provtagare:

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

Nu kan vi äntligen redigera fragmentskärmen:

fixed4 SampleSpriteTexture (float2 uv) fixed4 color = tex2D (_MainTex, uv); om (_AlphaSplitEnabled) color.a = tex2D (_AlphaTex, uv) .r; returnera färg;  fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb * = c.a; returnera c; 

Här är den relevanta koden för standard fragment shader. Som du kan se, c är färgen samplad från huvudtexturen; det multipliceras med vertexfärgen för att ge den en nyans. Skärmen mörkar också spritesna med lägre opacitet.

Efter provtagning av huvudfärgen, låt oss prova bytesfärgen också - men innan vi gör det, låt oss ta bort den del som multiplicerar den med nyansfärgen, så att vi samplar med texturens verkliga röda värde, inte den tonade.

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

Som du kan se är det samplade färgindex lika med det röda värdet för huvudfärgen.

Låt oss nu beräkna vår sista färg:

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); 

För att göra detta måste vi interpolera mellan huvudfärgen och den byta färgen med hjälp av alfabetet av den byta färgen som steget. På så sätt, om den byta färgen är transparent, kommer den slutliga färgen att vara lika med huvudfärgen; men om den byta färgen är helt ogenomskinlig kommer den slutliga färgen att vara lika med den byta färgen.

Låt oss inte glömma att den slutliga färgen måste multipliceras med nyans:

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.färg;

Nu måste vi överväga vad som ska hända om vi vill byta färg på huvudtexturen som inte är helt ogenomskinlig. Om vi ​​till exempel har en blå, halvtransparent spöksprite och vill byta färg till lila, vill vi inte att spöket med de byta färgerna är opaka, vi vill behålla den ursprungliga transparensen. Så låt oss göra det:

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.färg; final.a = c.a;

Den slutliga färggennemsynen ska vara lika med insynen i huvudtexturfärgen. 

Slutligen, eftersom den ursprungliga skuggningen multiplicerade färgens RGB-värde med färgens alfa, borde vi också göra det för att hålla shader samma:

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.färg; final.a = c.a; final.rgb * = c.a; återvända slutlig; 

Skärmen är klar nu; vi kan skapa en swap färgstruktur, fylla den med olika färgpixlar och se om sprite ändrar färgerna korrekt. 

Självklart skulle den här metoden inte vara väldigt användbar om vi var tvungna att skapa bytesstrukturer för hand hela tiden! Vi vill generera och modifiera dem procedurellt ...

Ställa in en exempeldemo

Vi vet att vi behöver en bytesstruktur för att kunna utnyttja vår skuggning. Om vi ​​vill låta flera tecken använda olika paletter för samma sprite samtidigt, kommer alla dessa tecken att behöva en egen bytstruktur. 

Då blir det bäst att vi enkelt skapar dessa bytstrukturer dynamiskt, som vi skapar objekten.

Först av, låt oss definiera en swaptextur och en matris där vi ska hålla koll på alla bytte färger:

Texture2D mColorSwapTex; Färg [] mSpriteColors;

Låt oss sedan skapa en funktion där vi initialiserar texturen. Vi använder RGBA32-format och ställer in filterläget till Punkt:

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

Låt oss nu se till att alla texturens pixlar är transparenta genom att rensa alla pixlar och tillämpa ändringarna:

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();

Vi måste också ställa in materialets byte textur till den nyskapade

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

Slutligen sparar vi referensen till strukturen och skapar matrisen för färgerna:

mSpriteColors = new Color [colorSwapTex.width]; mColorSwapTex = colorSwapTex;

Den fullständiga funktionen är enligt följande:

public void InitColorSwapTex () Texture2D colorSwapTex = ny 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; 

Observera att det inte är nödvändigt för varje objekt att använda en separat 256x1px textur; vi kunde göra en större textur som täcker alla föremål. Om vi ​​behöver 32 tecken kan vi skapa en textur av storlek 256x32px, och se till att varje tecken endast använder en specifik rad i den texturen. Men varje gång vi behövde förändra denna större textur, skulle vi behöva vidarebefordra mer data till GPU, vilket skulle göra det mindre effektivt.

Det är inte heller nödvändigt att använda en separat bytestruktur för varje sprite. Till exempel, om karaktären har ett vapen utrustat och det vapnet är ett separat sprite, så kan det enkelt dela bytestexturen med tecknet (så länge vapenets spritstruktur inte använder färger som har röda värden som är identiska med dem av teckenspriten).

Det är mycket användbart att veta vad de röda värdena för vissa sprite-delar är, så låt oss skapa en enum som kommer att hålla dessa uppgifter:

Public Enum SwapIndex Outline = 25, SkinPrim = 254, SkinSec = 239, HandPrim = 235, HandSec = 204, ShirtPrim = 62, ShirtSec = 70, ShoePrim = 253, ShoeSec = 248, Byxor = 72,

Dessa är alla färger som används av exemplet tecken.

Nu har vi alla saker vi behöver för att skapa en funktion för att faktiskt byta färg:

public void SwapColor (SwapIndex index, Färgfärg) mSpriteColors [(int) index] = färg; mColorSwapTex.SetPixel ((int) index, 0, färg); 

Som du kan se finns det inget som är fint här; Vi har bara satt färgen i vårt objekts färgmatris och ställer också in texturens pixel i ett lämpligt index. 

Observera att vi inte vill tillämpa ändringarna på strukturen varje gång vi faktiskt kallar den här funktionen. Vi skulle hellre ansöka dem när vi bytte ut Allt pixlarna vi vill.

Låt oss titta på ett exempel på användningen av funktionen:

 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 ();

Som du kan se är det ganska lätt att förstå vad dessa funktionssamtal gör just från att läsa dem: i detta fall ändrar de både hudfärger, både skjortfärger och byxorna.

Lägga till en hit-effekt på demo

Låt oss se hur vi kan använda skuggaren för att skapa en träffeffekt för vår sprite. Denna effekt kommer att byta alla spritens färger till vitt, håll det så under en kort tid och gå sedan tillbaka till originalfärgen. Den övergripande effekten blir att spritet blinkar vitt.

Låt oss först skapa en funktion som byter alla färger, men överstiger inte färgerna från objektets array. Vi kommer att behöva dessa färger när vi kommer att vilja stänga av effekten, trots allt.

public void SwapAllSpritesColorsTemporary (Färgfärg) for (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, color); mColorSwapTex.Apply(); 

Vi kan bara iterera genom enumerna, men iterering genom hela konsistensen kommer att se till att färgen byts ut även om en viss färg inte är definierad i SwapIndex.

Nu när färgerna byts ut måste vi vänta en stund och återvända tillbaka till tidigare färger. 

Låt oss först skapa en funktion som återställer färgerna:

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

Låt oss nu definiera timern och en konstant:

flyta mHitEffectTimer = 0,0f; const float cHitEffectTime = 0.1f;

Låt oss skapa en funktion som startar träffeffekten:

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

Och i uppdateringsfunktionen, låt oss kolla hur mycket tid som är kvar på timern, minska varje kryss och ring för återställning när tiden är klar:

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

Det är det nu när StartHitEffect kallas, sprite blinkar vitt ett ögonblick och sedan går tillbaka till dess tidigare färger.

Sammanfattning

Detta markerar slutet på handledningen! Jag hoppas att du hittar den metod som är acceptabel och skuggan är användbar. Det är en väldigt enkel, men det fungerar bara bra för pixel art sprites som inte använder många färger. 

Metoden skulle behöva ändras lite om vi ville byta hela grupper av färger på en gång, vilket definitivt skulle kräva en mer komplicerad och dyr skuggning. I mitt eget spel använder jag dock väldigt få färger, så den här tekniken passar perfekt.