Cum se utilizează un shader pentru a schimba dinamic culorile Sprite

În acest tutorial, vom crea un shader simplu de schimbare a culorilor, care poate recolora sprites în zbor. Shader-ul face mult mai ușor adăugarea unei varietăți într-un joc, permite jucătorului să-și personalizeze caracterul și poate fi folosit pentru a adăuga efecte speciale spritelor, cum ar fi făcându-le să clipească atunci când personajul ia daune.

Deși folosim Unity pentru demonstrația și codul sursă aici, principiul de bază va funcționa în multe motoare de joc și limbi de programare.

Demo

Puteți verifica demonstrația Unity sau versiunea WebGL (25MB +), pentru a vedea rezultatul final în acțiune. Utilizați pickerii de culoare pentru a recolora caracterul de sus. (Celelalte personaje folosesc același sprite, dar au fost recodobite în mod similar.) Faceți clic pe Hit Effect pentru a face caracterele să fie bliț alb scurt.

Înțelegerea teoriei

Iată textura de exemplu pe care o vom folosi pentru a demonstra shader-ul:

Am descarcat aceasta textura de la http://opengameart.org/content/classic-hero si l-am editat usor.

Există destul de multe culori pe această textura. Iată ce arată paleta:

Acum, să ne gândim cum putem schimba aceste culori într-un shader.

Fiecare culoare are o valoare RGB unică asociată cu aceasta, deci este tentant să scriem codul shader care spune "dacă culoarea texturii este egală cu acest Valoarea RGB, înlocuiți-o cu acea Valoarea RGB. "Cu toate acestea, acest lucru nu scade bine pentru multe culori și este o operație destul de costisitoare..

În schimb, vom folosi o textură suplimentară, care va conține culorile de înlocuire. Să numim textura a swap textura.

Întrebarea cea mare este cum să legăm culoarea de textura sprite de culoarea din textură swap? Răspunsul este că vom folosi componenta roșie (R) din culoarea RGB pentru a indexa textura swap. Aceasta înseamnă că textura de swap va trebui să aibă o lățime de 256 de pixeli, pentru că așa sunt multe valori diferite pe care le poate lua componenta roșie.

Să trecem peste toate astea într-un exemplu. Iată valorile culorii roșii ale culorilor paletei sprite:

Să presupunem că vrem să înlocuim culoarea ochilor (negru) pe sprite cu culoarea albastră. Culoarea conturului este ultima pe paletă - cea cu o valoare roșie de 25. Dacă vrem să schimbăm această culoare, atunci în textura de swap trebuie să setăm pixelul la indexul 25 la culoarea pe care ne dorim ca conturul să fie: albastru.

Textura swap, cu culoarea la indexul 25 setată pe albastru.

Acum, atunci când shader-ul întâlnește o culoare cu o valoare roșie de 25, acesta îl va înlocui cu culoarea albastră din textură swap:

Rețineți că este posibil ca aceasta să nu funcționeze așa cum era de așteptat dacă două sau mai multe culori din textura sprite au aceeași valoare roșie! Atunci când utilizați această metodă, este important să păstrați valorile roșii ale culorilor în textură sprite diferite.

De asemenea, rețineți că, după cum puteți vedea în demonstrație, plasarea unui pixel transparent la orice index din textura swap nu va duce la schimbarea culorilor pentru culorile corespunzătoare acelui index.

Implementarea Shader-ului

Vom implementa această idee modificând un shader existent. Deoarece proiectul demo este realizat în Unitate, voi folosi șablonul Unity sprite implicit.

Toate shader-ul implicit (care este relevant pentru acest tutorial) este eșantionarea culorii din atlasul texturii principale și multiplicarea acelei culori cu o culoare de vârf pentru a schimba nuanța. Culoarea rezultată este apoi înmulțită cu alfa, pentru a face sprite mai întunecat la opacities mai mici.

Primul lucru pe care trebuie să-l facem este să adăugăm o textura suplimentară shader-ului:

Proprietăți [PerRendererData] _MainTex ("Sprite Texture", 2D) = "alb"  _SwapTex ("Date de culoare", 2D) = "transparent"  _Color (" , 1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0

După cum puteți vedea, acum avem două texturi. Primul, _MainTex, este textura sprite; al doilea, _SwapTex, este textura swap.

De asemenea, trebuie să definim un sampler pentru cea de-a doua textură, astfel încât să putem accesa de fapt. Vom folosi un sampler de textură 2D, deoarece Unity nu acceptă samplere 1D:

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

Acum putem edita în final fragmentul de fragmentare:

fix4 SampleSpriteTexture (float2 uv) fix4 culoare = tex2D (_MainTex, uv); dacă (_AlphaSplitEnabled) color.a = tex2D (_AlphaTex, uv) .r; culoare retur;  fix4 frag (v2f IN): SV_Target fix4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb * = c.a; return c; 

Iată codul relevant pentru șablonul de fragmentare implicit. După cum puteți vedea, c este culoarea eșantionată din textură principală; este înmulțită cu culoarea vertexului pentru a da o nuanță. De asemenea, shaderul întunecă spritele cu opacities inferior.

După prelevarea probei culorii principale, să examinăm și culoarea swap-dar, înainte de a face acest lucru, să eliminăm partea care o multiplică cu culoarea nuanței, astfel încât să luăm eșantionare folosind valoarea reală a texturii, nu cea colorată.

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

După cum puteți vedea, indicele de culoare eșantionat este egal cu valoarea roșie a culorii principale.

Acum, să calculați culoarea noastră finală:

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

Pentru a face acest lucru, trebuie să interpolam între culoarea principală și culoarea schimbată utilizând alfa culorii schimbate ca pas. În acest fel, dacă culoarea schimbată este transparentă, culoarea finală va fi egală cu culoarea principală; dar dacă culoarea schimbată este complet opacă, atunci culoarea finală va fi egală cu culoarea schimbată.

Să nu uităm că culoarea finală trebuie să fie înmulțită cu nuanța:

fix4 fragment (v2f IN): SV_Target fix4 c = SampleSpriteTexture (IN.texcoord); fix4 swapCol = tex2D (_SwapTex, float2 (c.r, 0)); fix4 final = lerp (c, swapCol, swapCol.a) * culoarea IN;

Acum trebuie să luăm în considerare ceea ce ar trebui să se întâmple dacă vrem să schimbăm o culoare pe textura principală care nu este pe deplin opacă. De exemplu, dacă avem o sprite albastru, semi-transparent și dorim să ne schimbăm culoarea în violet, nu vrem ca fantoma cu culorile schimbate să fie opacă, dorim să păstrăm transparența originală. Asa ca sa facem asta:

fix4 fragment (v2f IN): SV_Target fix4 c = SampleSpriteTexture (IN.texcoord); fix4 swapCol = tex2D (_SwapTex, float2 (c.r, 0)); fix4 final = lerp (c, swapCol, swapCol.a) * culoarea IN; final.a = c.a;

Transparența finală a culorii trebuie să fie egală cu transparența culorii principale a texturii. 

În cele din urmă, deoarece shader-ul original a înmulțit valoarea RGB a culorii cu alfa-ul culorii, ar trebui să facem acest lucru și pentru a păstra shader-ul același:

fix4 fragment (v2f IN): SV_Target fix4 c = SampleSpriteTexture (IN.texcoord); fix4 swapCol = tex2D (_SwapTex, float2 (c.r, 0)); fix4 final = lerp (c, swapCol, swapCol.a) * culoarea IN; final.a = c.a; final.rgb * = c.a; retur final; 

Shader-ul este complet acum; putem crea o textura de culori swap, o putem umple cu pixeli de culoare diferiti si vom vedea daca sprite modifica corect culorile. 

Desigur, această metodă nu ar fi foarte utilă dacă ar fi trebuit să creăm texturi swap de mână tot timpul! Vom dori să le generăm și să le modificăm procedural ...

Configurarea unui exemplu de demo

Știm că avem nevoie de o textura de tip swap pentru a putea folosi șunca noastră. În plus, dacă vrem să lăsăm mai mulți caractere să utilizeze palete diferite pentru același sprite în același timp, fiecare dintre aceste caractere va avea nevoie de propriile sale texturi swap. 

Cel mai bine va fi atunci, dacă vom crea dinamic aceste texturi swap, pe măsură ce creăm obiectele.

În primul rând, să definim o textura swap și o matrice în care să ținem evidența tuturor culorilor schimbate:

Texture2D mColorSwapTex; Culoare [] mSpriteColors;

Apoi, să creăm o funcție în care vom inițializa textura. Vom folosi formatul RGBA32 și vom seta modul de filtrare la Punct:

void public InitColorSwapTex () Texture2D colorSwapTex = textură Text nou2D (256, 1, TextureFormat.RGBA32, false, fals); colorSwapTex.filterMode = FilterMode.Point; 

Acum să ne asigurăm că toți pixelii texturii sunt transparenți, prin eliminarea tuturor pixelilor și aplicarea modificărilor:

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

De asemenea, trebuie să setăm textura de swap a materialului nou creat:

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

În cele din urmă, salvăm referința la textura și creăm matricea pentru culori:

mSpriteColors = culoare nouă [colorSwapTex.width]; mColorSwapTex = colorSwapTex;

Funcția completă este după cum urmează:

void public InitColorSwapTex () Texture2D colorSwapTex = textură Text nou2D (256, 1, TextureFormat.RGBA32, false, fals); colorSwapTex.filterMode = FilterMode.Point; pentru (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; 

Rețineți că nu este necesar ca fiecare obiect să utilizeze o textura separată de 256x1px; am putea face o textura mai mare care sa acopere toate obiectele. Dacă avem nevoie de 32 de caractere, am putea face o textură de dimensiune 256x32px și asigurați-vă că fiecare caracter utilizează numai un rând specific în acea textura. Cu toate acestea, de fiecare dată când trebuia să facem o schimbare la această textura mai mare, ar fi trebuit să transmitem mai multe date GPU-ului, ceea ce ar face probabil acest lucru mai puțin eficient.

De asemenea, nu este necesar să utilizați o textura de swap separată pentru fiecare sprite. De exemplu, dacă personajul are o armă echipată și arma este un sprite separat, atunci poate împărtăși cu ușurință textura swap cu caracterul (atâta timp cât textura sprite a armei nu folosește culori care au valori roșii identice cu cele a personajului sprite).

Este foarte util să știți care sunt valorile roșii ale părților particulare de sprite, așa că să creați un enum care va deține aceste date:

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

Acestea sunt toate culorile folosite de caracterul de exemplu.

Acum avem toate lucrurile de care avem nevoie pentru a crea o funcție pentru a schimba culoarea:

public void SwapColor (Indice SwapIndex, Culoare culoare) mSpriteColors [(int) index] = culoare; mColorSwapTex.SetPixel ((int) index, 0, culoare); 

După cum puteți vedea, nu este nimic fantezie aici; am setat doar culoarea în matricea de culori a obiectului și, de asemenea, setăm pixelul texturii la un indice adecvat. 

Rețineți că nu dorim cu adevărat să aplicăm modificările texturii de fiecare dată când numim această funcție; preferăm să le aplicăm odată ce schimbăm toate pixelii pe care vrem.

Să examinăm o utilizare a funcției:

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

După cum puteți vedea, este destul de ușor de înțeles ce fac aceste apeluri de funcții doar din citirea acestora: în acest caz, schimbă atât culorile pielii, cât și culorile cămășii și culoarea pantalonilor.

Adăugarea unui efect Hit la Demo

Să vedem în continuare cum putem folosi shaderul pentru a crea un efect de lovire pentru sprite. Acest efect va schimba toate culorile spritei spre alb, păstrați-l în acest fel pentru o scurtă perioadă de timp și apoi reveniți la culoarea originală. Efectul general va fi acela că sprite va lumina alb.

Mai intai, sa cream o functie care schimba toate culorile, dar nu suprascrie culorile din matricea obiectului. Vom avea nevoie de aceste culori atunci când vom dori să oprim efectul de lovire, la urma urmei.

public void SwapAllSpritesColoriTimporat (culoarea culorii) pentru (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, color); mColorSwapTex.Apply(); 

Am putea itera numai prin enumuri, dar iterând prin întreaga textură se va asigura că culoarea este schimbată chiar dacă o anumită culoare nu este definită în SwapIndex.

Acum, când culorile sunt schimbate, trebuie să așteptăm un timp și să ne întoarcem la culorile anterioare. 

Mai întâi, să creăm o funcție care va reseta culorile:

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

Acum, să definim cronometrul și o constantă:

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

Să creăm o funcție care va începe să lovească efectul:

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

În funcția de actualizare, să verificăm cât timp a mai rămas pe cronometru, să-l micșorați la fiecare bifați și să solicitați o resetare după ce timpul a crescut:

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

Asta este - acum, când StartHitEffect se numește, sprite va clipi alb pentru un moment și apoi va reveni la culorile anterioare.

rezumat

Aceasta marchează sfârșitul tutorialului! Sper că găsiți metoda acceptabilă și utilitatea umbrei. Este foarte simplu, dar funcționează foarte bine pentru sprite de artă pixel care nu utilizează multe culori. 

Metoda ar trebui să fie schimbată puțin dacă vrem să schimbăm simultan grupe de culori, ceea ce ar necesita cu siguranță un shader mai complicat și mai scump. În jocul propriu, totuși, folosesc foarte puține culori, astfel încât această tehnică se potrivește perfect.