Slik bruker du en skygge til å bytte dynamisk en Sprite-farger

I denne opplæringen lager vi en enkel fargebyter som kan hente sprites på fluen. Shader gjør det mye enklere å legge til variasjon i et spill, gjør at spilleren kan tilpasse sin karakter, og kan brukes til å legge til spesialeffekter for sprites, for eksempel å få dem til å blinke når tegnet tar skade.

Selv om vi bruker Unity for demo og kildekoden her, vil det grunnleggende prinsippet fungere i mange spillmotorer og programmeringsspråk.

Demo

Du kan sjekke Unity demo, eller WebGL-versjonen (25MB +), for å se det endelige resultatet i aksjon. Bruk fargeplukkerne til å tilbakekalle toppkarakteren. (De andre tegnene bruker alle samme sprite, men har blitt likt på samme måte.) Klikk Hit Effect for å få tegnene til å blinke hvitt kort.

Forstå teorien

Her er eksemplet tekstur vi skal bruke for å demonstrere shader:

Jeg lastet ned denne teksten fra http://opengameart.org/content/classic-hero, og redigerte den litt.

Det er ganske mange farger på denne tekstur. Slik ser paletten ut:

La oss nå tenke på hvordan vi kan bytte disse fargene i en skygge.

Hver farge har en unik RGB-verdi knyttet til den, så det er fristende å skrive skyggekode som sier "hvis teksturfargen er lik dette RGB-verdi, erstatt den med at RGB-verdi. "Dette skaler imidlertid ikke bra for mange farger, og er ganske dyrt. Vi vil definitivt unngå å bruke betingede utsagn helt, faktisk.

I stedet vil vi bruke en ekstra tekstur, som vil inneholde utskiftningsfarger. La oss kalle denne teksten a bytte tekstur.

Det store spørsmålet er, hvordan kobler vi fargene fra sprite tekstur til fargen fra swap tekstur? Svaret er at vi skal bruke den røde komponenten (R) fra RGB-fargene for å indeksere bytte tekstur. Dette betyr at bytte tekstur må være 256 piksler bred, fordi det er hvor mange forskjellige verdier den røde komponenten kan ta.

La oss gå over alt dette i et eksempel. Her er de røde fargevennene til spritepalettens farger:

La oss si at vi ønsker å erstatte konturen / øyefarge (svart) på sprite med fargen blå. Skissefargen er den siste på paletten - den med en rød verdi på 25. Hvis vi ønsker å bytte denne fargen, må vi i pekestrukturen sette piksel på indeks 25 til fargen vi vil ha på konturen: blå.

Bytte tekstur, med fargen på indeks 25 satt til blå.

Nå, når skyggen møter en farge med en rød verdi på 25, erstatter den den med den blå fargen fra bytte tekstur:

Vær oppmerksom på at dette kanskje ikke fungerer som forventet hvis to eller flere farger på sprite-tekstur har samme røde verdi! Når du bruker denne metoden, er det viktig å holde de røde verdiene av fargene i spritteksturen forskjellig.

Vær også oppmerksom på at, som du ser i demoen, vil det ikke bli farget bytte for fargene som svarer til den indeksen som setter en gjennomsiktig piksel på en indeks i bytte tekstur..

Implementere Shader

Vi implementerer denne ideen ved å endre en eksisterende sprite shader. Siden demo-prosjektet er laget i Unity, bruker jeg standard Unity sprite shader.

All standard shader gjør (som er relevant for denne opplæringen) er å prøve fargen fra hovedteksturatlasen og formere den fargen med en toppunktfarge for å endre fargen. Den resulterende fargen blir deretter multiplisert med alfa, for å gjøre sprite mørkere ved lavere opasiteter.

Det første vi må gjøre er å legge til en ekstra tekstur til shader:

Egenskaper [PerRendererData] _MainTex ("Sprite Texture", 2D) = "hvit"  _SwapTex ("Color Data", 2D) = "transparent"  _Color ("Tint", Color) = , 1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0

Som du kan se, har vi to teksturer her nå. Den første, _MainTex, er sprite tekstur; den andre, _SwapTex, er bytte tekstur.

Vi må også definere en sampler for den andre teksten, slik at vi faktisk kan få tilgang til den. Vi bruker en 2D tekstur sampler, siden Unity ikke støtter 1D samplers:

sampler2D _MainTex; sampler2D _AlphaTex; flyte _AlphaSplitEnabled; sampler2D _SwapTex;

Nå kan vi endelig redigere fragment shader:

fixed4 SampleSpriteTexture (float2 uv) fixed4 color = tex2D (_MainTex, uv); hvis (_AlphaSplitEnabled) color.a = tex2D (_AlphaTex, uv) .r; returnere farge;  fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb * = c.a; returnere c; 

Her er den aktuelle koden for standard fragment shader. Som du kan se, c er fargen samplet fra hovedteksturen; det blir multiplisert med toppunktet farge for å gi det en fargetone. Skyggeren mørker også sprites med lavere opasiteter.

Etter prøvetaking av hovedfargen, la oss også prøve ut byttefargen, men før vi gjør det, la vi fjerne delen som multipliserer den med fargen fargen, slik at vi sampler med teksturens virkelige røde verdi, ikke den tonede.

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

Som du kan se er den samplede fargeindeksen lik den røde verdien av hovedfargen.

La oss nå beregne vår endelige farge:

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

For å gjøre dette må vi interpolere mellom hovedfargen og den bytte farge ved å bruke alfaen av den bytte fargen som trinnet. På denne måten, hvis den bytte fargen er gjennomsiktig, vil den endelige fargen være lik hovedfargen; men hvis den bytte fargen er helt ugjennomsiktig, vil den endelige fargen være lik den bytte fargen.

La oss ikke glemme at den endelige fargen må multipliseres med fargen:

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;

Nå må vi vurdere hva som skal skje hvis vi ønsker å bytte en farge på hovedtekstur som ikke er helt ugjennomsiktig. For eksempel, hvis vi har en blå, halv gjennomsiktig spøkelsesprite, og ønsker å bytte fargen til lilla, vil vi ikke at spøkelset med byttefarger skal være ugjennomsiktig, vi vil bevare den opprinnelige gjennomsiktigheten. Så la oss gjøre 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.color; final.a = c.a;

Den endelige fargegennomsynligheten skal være lik gjennomsiktigheten i hovedteksturfargen. 

Til slutt, siden den opprinnelige skyggeren multipliserte fargens RGB-verdi med fargenes alfa, bør vi også gjøre det for å holde shader det samme:

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; returnere endelig; 

Shader er fullført nå; Vi kan lage en byttefargestruktur, fylle den med forskjellige fargepiksler, og se om sprite endrer farger på riktig måte. 

Selvfølgelig ville denne metoden ikke være veldig nyttig hvis vi måtte lage bytte teksturer for hånd hele tiden! Vi vil generere og endre dem prosedyrisk ...

Sette opp en eksempeldemono

Vi vet at vi trenger en bytte tekstur for å kunne bruke vår shader. Videre, hvis vi vil la flere tegn bruke forskjellige paletter for samme sprite samtidig, vil hver av disse tegnene ha sin egen byttestruktur. 

Det vil da være best, hvis vi bare lager disse byttestrukturene dynamisk, slik vi lager objekter.

Først av, la oss definere en byttestruktur og en matrise der vi skal holde styr på alle byttefarger:

Texture2D mColorSwapTex; Farge [] mSpriteColors;

Deretter la vi lage en funksjon der vi skal initialisere tekstur. Vi bruker RGBA32-format og setter filtermodus til Punkt:

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

La oss nå sørge for at alle teksturens piksler er gjennomsiktige, ved å rydde alle piksler og bruke endringene:

for (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å også sette materialets byttetekst til den nyopprettede en:

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

Til slutt lagrer vi referansen til tekstur og lager matrise for fargene:

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

Den komplette funksjonen er som følger:

offentlig tomrom InitColorSwapTex () Texture2D colorSwapTex = ny Texture2D (256, 1, TextureFormat.RGBA32, false, false); colorSwapTex.filterMode = FilterMode.Point; for (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; 

Merk at det ikke er nødvendig for hvert objekt å bruke en separat 256x1px tekstur; Vi kunne lage en større tekstur som dekker alle gjenstandene. Hvis vi trenger 32 tegn, kan vi lage en tekstur med størrelsen 256x32px, og sørg for at hvert tegn bruker bare en bestemt rad i teksten. Men hver gang vi trengte å gjøre en endring i denne større tekstur, måtte vi sende mer data til GPU, noe som sannsynligvis ville gjøre dette mindre effektivt.

Det er heller ikke nødvendig å bruke en separat byttestruktur for hver sprite. For eksempel, hvis tegnet har et våpen utstyrt, og at våpenet er et eget sprite, så kan det enkelt dele bytte tekstur med tegnet (så lenge våpenets sprite tekstur ikke bruker farger som har røde verdier som er identiske med de av tegnspriten).

Det er veldig nyttig å vite hva de røde verdiene til bestemte sprite-deler er, så la oss lage en enum som vil holde disse dataene:

offentlig tekst SwapIndex Outline = 25, SkinPrim = 254, SkinSec = 239, HandPrim = 235, HandSec = 204, ShirtPrim = 62, ShirtSec = 70, ShoePrim = 253, ShoeSec = 248, Bukser = 72,

Dette er alle fargene som brukes av eksempelkarakteren.

Nå har vi alle de tingene vi trenger for å skape en funksjon for å faktisk bytte fargen:

Offentlig ugyldig SwapColor (SwapIndex-indeks, Fargefarge) mSpriteColors [(int) index] = farge; mColorSwapTex.SetPixel ((int) indeks, 0, farge); 

Som du kan se, er det ingenting fancy her; Vi har bare satt fargen i objektets fargearray og også satt teksturens piksel på en passende indeks. 

Legg merke til at vi ikke egentlig vil bruke endringene i tekstur hver gang vi faktisk kaller denne funksjonen; Vi vil helst bruke dem når vi bytter alle pikslene vi vil.

La oss se på et eksempel bruk av funksjonen:

 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 ser, er det ganske enkelt å forstå hva disse funksjonene samtaler gjør, bare ved å lese dem: i dette tilfellet endrer de begge hudfarger, både skjortefarger og bukserfargen.

Legge til en hit-effekt på demonstrasjonen

La oss se hvordan vi kan bruke shader til å skape en hit-effekt for vår sprite. Denne effekten bytter alle spritens farger til hvitt, holder den på den måten i en kort periode, og deretter går du tilbake til den opprinnelige fargen. Den samlede effekten vil være at spritet blinker hvitt.

Først av alt, la oss lage en funksjon som bytter alle fargene, men overskriver egentlig ikke fargene fra objektets array. Vi trenger disse fargene når vi vil tømme slår effekten, tross alt.

Offentlig tomt SwapAllSpritesColorsTemporary (Fargefarge) for (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, color); mColorSwapTex.Apply(); 

Vi kan deterere bare gjennom enums, men iterating gjennom hele tekstur vil sikre at fargen byttes selv om en bestemt farge ikke er definert i SwapIndex.

Nå som fargene byttes, må vi vente litt tid og gå tilbake til tidligere farger. 

La oss først lage en funksjon som vil nullstille fargene:

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

La oss nå definere timeren og en konstant:

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

La oss lage en funksjon som vil starte hit-effekten:

offentlig tomgang StartHitEffect () mHitEffectTimer = cHitEffectTime; SwapAllSpritesColorsTemporarily (Color.white); 

Og i oppdateringsfunksjonen, la oss sjekke hvor mye tid som er igjen på timeren, redusere det hvert kryss, og ring for tilbakestilling når tiden er oppe:

offentlig ugyldig oppdatering () hvis (mHitEffectTimer> 0.0f) mHitEffectTimer - = Time.deltaTime; hvis (mHitEffectTimer <= 0.0f) ResetAllSpritesColors();  

Det er det-nå, når StartHitEffect kalles, sprite vil blinke hvitt et øyeblikk og deretter gå tilbake til sine tidligere farger.

Sammendrag

Dette markerer slutten av opplæringen! Jeg håper du finner metoden akseptabel og shader nyttig. Det er en veldig enkel, men det fungerer fint for pixel art sprites som ikke bruker mange farger. 

Metoden ville måtte endres litt hvis vi ønsket å bytte hele grupper av farger samtidig, noe som definitivt ville kreve en mer komplisert og kostbar shader. I mitt eget spill bruker jeg imidlertid ganske få farger, slik at denne teknikken passer perfekt.