In deze zelfstudie maken we een eenvoudige kleuroverbruggende arcering die sprites tijdens het ganse leven opnieuw kan kleuren. De shader maakt het veel eenvoudiger om variatie aan een spel toe te voegen, stelt de speler in staat om zijn karakter aan te passen en kan worden gebruikt om speciale effecten aan de sprites toe te voegen, zoals ze laten flitsen wanneer het personage schade oploopt.
Hoewel we Unity hier voor de demo en broncode gebruiken, werkt het basisprincipe in veel game engines en programmeertalen.
Je kunt de Unity-demo of de WebGL-versie (25MB +) bekijken om het uiteindelijke resultaat in actie te zien. Gebruik de kleurkiezers om het bovenste teken opnieuw in te kleuren. (De andere tekens gebruiken allemaal dezelfde sprite, maar zijn ook opnieuw gekleurd.) Klik op Hit Effect om de tekens allemaal even wit te laten knipperen.
Dit is de voorbeeldtextuur die we gaan gebruiken om de arcering te demonstreren:
Ik heb deze textuur gedownload van http://opengameart.org/content/classic-hero en deze enigszins bewerkt.Er zijn nogal wat kleuren op deze textuur. Dit is hoe het palet eruit ziet:
Laten we nu eens nadenken over hoe we deze kleuren in een arcering kunnen vervangen.
Elke kleur heeft een unieke RGB-waarde, dus het is verleidelijk om een shadercode te schrijven die zegt: "als de textuurkleur gelijk is aan deze RGB-waarde, vervang deze door dat RGB-waarde ". Dit is echter niet goed voor veel kleuren, en het is een vrij dure bewerking. We willen absoluut alle voorwaardelijke uitspraken volledig vermijden, in feite.
In plaats daarvan gebruiken we een extra textuur, die de vervangende kleuren bevat. Laten we deze textuur a noemen ruil textuur.
De grote vraag is, hoe koppelen we de kleur van de spritetextuur aan de kleur van de ruiltextuur? Het antwoord is dat we de rode (R) component uit de RGB-kleur gebruiken om de swap-structuur te indexeren. Dit betekent dat de swap-structuur 256 pixels breed moet zijn, omdat dat het aantal verschillende waarden is dat de rode component kan hebben.
Laten we dit in een voorbeeld bespreken. Hier zijn de rode kleurwaarden van de kleuren van het spritepalet:
Laten we zeggen dat we de omtrek / oogkleur (zwart) op de sprite willen vervangen door de kleur blauw. De omtrekkleur is de laatste kleur in het palet, de kleur met een rode waarde 25
. Als we deze kleur willen verwisselen, moeten we in de swap-structuur de pixel op index 25 instellen op de kleur die we willen dat de omtrek is: blauw.
Als de arcering nu een kleur met een rode waarde van 25 tegenkomt, wordt deze vervangen door de blauwe kleur uit de ruilstructuur:
Merk op dat dit niet werkt zoals verwacht als twee of meer kleuren op de spritetextuur dezelfde rode waarde delen! Bij gebruik van deze methode is het belangrijk om de rode waarden van de kleuren in de sprite-structuur anders te houden.
Merk ook op dat, zoals u in de demo kunt zien, het plaatsen van een transparante pixel op een willekeurige index in de ruiltextuur zal resulteren in geen kleuromwisseling voor de kleuren die overeenkomen met die index.
We zullen dit idee implementeren door een bestaande sprite-arcering aan te passen. Aangezien het demoproject is gemaakt in Unity, zal ik de standaard Unity-sprite-shader gebruiken.
Alle standaard shader doet (die relevant is voor deze tutorial) is een voorbeeld van de kleur uit de hoofdstructuuratlas en vermenigvuldig die kleur met een vertexkleur om de tint te wijzigen. De resulterende kleur wordt vervolgens vermenigvuldigd met de alfa, om de sprite donkerder te maken bij lagere opaciteit.
Het eerste wat we moeten doen is een extra textuur aan de arcering toevoegen:
Eigenschappen [PerRendererData] _MainTex ("Sprite Texture", 2D) = "wit" _SwapTex ("Kleurgegevens", 2D) = "transparant" _Color ("Tint", Kleur) = (1,1,1 , 1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
Zoals je kunt zien, hebben we hier nu twee texturen. De eerste, _MainTex
, is de sprite textuur; de tweede, _SwapTex
, is de ruiltextuur.
We moeten ook een sampler definiëren voor de tweede structuur, zodat we er daadwerkelijk toegang toe hebben. We gebruiken een 2D texture sampler, omdat Unity geen 1D samplers ondersteunt:
sampler2D _MainTex; sampler2D _AlphaTex; zweven _AlphaSplitEnabled; sampler2D _SwapTex;
Nu kunnen we de fragmentshader eindelijk bewerken:
fixed4 SampleSpriteTexture (float2 uv) fixed4 color = tex2D (_MainTex, uv); if (_AlphaSplitEnabled) color.a = tex2D (_AlphaTex, uv) .r; terugkeer kleur; fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb * = c.a; terugkeer c;
Hier is de relevante code voor de standaard fragmentshader. Zoals je kunt zien, c
is de kleur die wordt bemonsterd uit de hoofdstructuur; het wordt vermenigvuldigd met de vertex-kleur om het een tint te geven. De arcering verduistert ook de sprites met lagere opaciteit.
Nadat we de hoofdkleur hebben bemonsterd, kunnen we ook de wisselkleur samplen, maar voordat we dit doen, verwijderen we het gedeelte dat het vermenigvuldigt met de tintkleur, zodat we een steekproef nemen met de echte rode waarde van de textuur, niet de getinte kleur.
fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord); fixed4 swapCol = tex2D (_SwapTex, float2 (c.r, 0));
Zoals u kunt zien, is de bemonsterde kleurenindex gelijk aan de rode waarde van de hoofdkleur.
Laten we nu onze uiteindelijke kleur berekenen:
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);
Om dit te doen, moeten we interpoleren tussen de hoofdkleur en de verwisselde kleur met de alpha van de verwisselde kleur als de stap. Op deze manier, als de verwisselde kleur transparant is, is de uiteindelijke kleur gelijk aan de hoofdkleur; maar als de verwisselde kleur volledig ondoorzichtig is, dan is de uiteindelijke kleur gelijk aan de verwisselde kleur.
Laten we niet vergeten dat de uiteindelijke kleur moet worden vermenigvuldigd met de tint:
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;
Nu moeten we overwegen wat er moet gebeuren als we een kleur willen verwisselen op de hoofdstructuur die niet volledig ondoorzichtig is. Als we bijvoorbeeld een blauwe, semi-transparante ghost-sprite hebben en de kleur ervan paars willen verwisselen, willen we niet dat de geest met de verwisselde kleuren dekkend is, we willen de oorspronkelijke transparantie behouden. Dus laten we dat doen:
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;
De uiteindelijke kleurtransparantie moet gelijk zijn aan de transparantie van de hoofdtextuurkleur.
Omdat de oorspronkelijke arcering de RGB-waarde van de kleur met de alpha van de kleur vermenigvuldigde, zouden we dat ook moeten doen, om de shader hetzelfde te houden:
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; terugkeer definitief;
De arcering is nu voltooid; we kunnen een verwisselde kleurstructuur maken, opvullen met verschillende kleurpixels en kijken of de sprite de kleuren correct verandert.
Natuurlijk zou deze methode niet erg handig zijn als we de hele tijd texturen met de hand moesten maken! We willen ze procedureel genereren en wijzigen ...
We weten dat we een swaptextuur nodig hebben om gebruik te kunnen maken van onze shader. Verder, als we meerdere karakters tegelijkertijd verschillende paletten voor dezelfde sprite willen laten gebruiken, zal elk van deze karakters zijn eigen ruiltextuur nodig hebben.
Het is dan het beste als we deze swap-texturen gewoon dynamisch creëren, terwijl we de objecten maken.
Laten we eerst een ruiltextuur en een array definiëren waarin we alle verwisselde kleuren bij houden:
Texture2D mColorSwapTex; Kleur [] mSpriteColors;
Laten we vervolgens een functie maken waarin we de textuur initialiseren. We zullen het RGBA32-formaat gebruiken en de filtermodus instellen op Punt
:
public void InitColorSwapTex () Texture2D colorSwapTex = new Texture2D (256, 1, TextureFormat.RGBA32, false, false); colorSwapTex.filterMode = FilterMode.Point;
Laten we er nu voor zorgen dat alle pixels van de textuur transparant zijn, door alle pixels te wissen en de wijzigingen toe te passen:
voor (int i = 0; i < colorSwapTex.width; ++i) colorSwapTex.SetPixel(i, 0, new Color(0.0f, 0.0f, 0.0f, 0.0f)); colorSwapTex.Apply();
We moeten ook de ruiltextuur van het materiaal instellen op de nieuw gecreëerde:
mSpriteRenderer.material.SetTexture ("_ SwapTex", colorSwapTex);
Ten slotte bewaren we de verwijzing naar de textuur en maken we de array voor de kleuren:
mSpriteColors = nieuwe kleur [colorSwapTex.width]; mColorSwapTex = colorSwapTex;
De volledige functie is als volgt:
public void InitColorSwapTex () Texture2D colorSwapTex = new Texture2D (256, 1, TextureFormat.RGBA32, false, false); colorSwapTex.filterMode = FilterMode.Point; voor (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 op dat het niet nodig is voor elk object om een afzonderlijke 256x1px-textuur te gebruiken; we kunnen een grotere textuur maken die alle objecten bedekt. Als we 32 tekens nodig hebben, kunnen we een structuur van 256x32 px maken en ervoor zorgen dat elk teken alleen een specifieke rij in die structuur gebruikt. Telkens wanneer we echter een wijziging in deze grotere structuur moesten aanbrengen, moesten we meer gegevens doorgeven aan de GPU, waardoor deze waarschijnlijk minder efficiënt zou zijn.
Het is ook niet nodig om voor elke sprite een afzonderlijke verwisselingstextuur te gebruiken. Als het personage bijvoorbeeld een wapen heeft dat is uitgerust en dat wapen een afzonderlijke sprite is, kan het de ruilstructuur gemakkelijk delen met het personage (zolang de sprite-structuur van het wapen geen kleuren gebruikt die dezelfde rode waarden hebben als die van de karaktersprite).
Het is erg handig om te weten wat de rode waarden van bepaalde sprite-onderdelen zijn, dus laten we een maken enum
die deze gegevens bevat:
openbare enum SwapIndex Outline = 25, SkinPrim = 254, SkinSec = 239, HandPrim = 235, HandSec = 204, ShirtPrim = 62, ShirtSec = 70, ShoePrim = 253, ShoeSec = 248, Broek = 72,
Dit zijn alle kleuren die door het voorbeeldpersonage worden gebruikt.
Nu hebben we alle dingen die we nodig hebben om een functie te creëren om de kleur daadwerkelijk te wisselen:
openbare void SwapColor (SwapIndex-index, kleurkleur) mSpriteColors [(int) index] = kleur; mColorSwapTex.SetPixel ((int) index, 0, kleur);
Zoals je kunt zien, is hier niets speciaals; we stellen gewoon de kleur in de kleurenset van ons object in en stellen ook de pixel van de textuur in op een geschikte index.
Merk op dat we niet echt de veranderingen in de textuur willen toepassen elke keer dat we deze functie daadwerkelijk aanroepen; we passen ze liever toe zodra we ruilen allemaal de pixels die we willen.
Laten we een voorbeeld van het gebruik van de functie bekijken:
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 ();
Zoals u kunt zien, is het vrij gemakkelijk te begrijpen wat deze functie-aanroepen doen door ze gewoon te lezen: in dit geval veranderen ze zowel de huidskleuren, zowel de overhemdkleuren als de broekkleur..
Laten we vervolgens eens kijken hoe we de shader kunnen gebruiken om een hiteffect voor onze sprite te maken. Dit effect zal alle kleuren van de sprite in wit veranderen, het zo houden voor een korte tijd en dan teruggaan naar de originele kleur. Het algehele effect is dat de sprite wit knippert.
Allereerst, laten we een functie maken die alle kleuren omwisselt, maar de kleuren van de array van het object niet daadwerkelijk overschrijft. We zullen deze kleuren nodig hebben als we tenslotte het slageffect willen uitschakelen.
public void SwapAllSpritesColors Temporary (Color color) for (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, color); mColorSwapTex.Apply();
We zouden alleen door de enums heen kunnen herhalen, maar itereren door de hele textuur zorgt ervoor dat de kleur wordt verwisseld, zelfs als een bepaalde kleur niet is gedefinieerd in de SwapIndex
.
Nu de kleuren zijn verwisseld, moeten we even wachten en terugkeren naar de vorige kleuren.
Laten we eerst een functie maken waarmee de kleuren worden gereset:
public void ResetAllSpritesColors () for (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, mSpriteColors[i]); mColorSwapTex.Apply();
Laten we nu de timer en een constante definiëren:
float mHitEffectTimer = 0.0f; const float cHitEffectTime = 0.1f;
Laten we een functie maken die het hiteffect start:
public void StartHitEffect () mHitEffectTimer = cHitEffectTime; SwapAllSpritesColorsTemporarily (Color.white);
En in de updatefunctie, laten we controleren hoeveel tijd er nog over is op de timer, elk vinkje verlagen en een reset vragen wanneer de tijd om is:
public void Update () if (mHitEffectTimer> 0.0f) mHitEffectTimer - = Time.deltaTime; if (mHitEffectTimer <= 0.0f) ResetAllSpritesColors();
Dat is het - nu, wanneer StartHitEffect
wordt genoemd, de sprite knippert even wit en gaat dan terug naar de vorige kleuren.
Dit markeert het einde van de tutorial! Ik hoop dat je de methode acceptabel vindt en de shader nuttig. Het is een heel eenvoudige, maar het werkt prima voor sprites uit pixelillustraties die niet veel kleuren gebruiken.
De methode zou een beetje moeten worden gewijzigd als we hele groepen kleuren in één keer zouden willen verwisselen, wat zeker een meer gecompliceerde en dure arcering zou vereisen. In mijn eigen spel gebruik ik echter heel weinig kleuren, dus deze techniek past perfect.