Dans ce didacticiel, nous allons créer un matériau d’échange de couleurs simple qui peut recolorer les images-objets à la volée. Le shader facilite beaucoup l'ajout de variété à un jeu, permet au joueur de personnaliser son personnage et peut être utilisé pour ajouter des effets spéciaux aux sprites, tels que les faire clignoter lorsque le personnage subit des dégâts..
Bien que nous utilisions Unity pour la démo et le code source ici, le principe de base fonctionnera dans de nombreux moteurs de jeu et langages de programmation..
Vous pouvez consulter la démonstration Unity ou la version WebGL (25 Mo +) pour voir le résultat final en action. Utilisez les sélecteurs de couleurs pour recolorer le personnage du haut. (Les autres personnages utilisent tous le même sprite, mais ont été recolorés de la même manière.) Cliquez sur Effet de frappe faire briller brièvement tous les caractères en blanc.
Voici l'exemple de texture que nous allons utiliser pour illustrer le shader:
J'ai téléchargé cette texture de http://opengameart.org/content/classic-hero, et l'ai légèrement modifiée.Il y a pas mal de couleurs sur cette texture. Voici à quoi ressemble la palette:
Maintenant, réfléchissons à la façon dont nous pourrions échanger ces couleurs dans un shader.
Chaque couleur est associée à une valeur RVB unique. Il est donc tentant d'écrire du code de shader indiquant "si la couleur de la texture est égale à ce Valeur RVB, remplacez-la par cette Valeur RVB ". Cependant, cela ne convient pas à beaucoup de couleurs et représente une opération assez coûteuse. Nous voudrions bien éviter complètement les instructions conditionnelles, en fait.
Au lieu de cela, nous utiliserons une texture supplémentaire, qui contiendra les couleurs de remplacement. Appelons cette texture un échanger la texture.
La grande question est de savoir comment lier la couleur de la texture de l'image-objet à la couleur de la texture d'échange. La réponse est que nous allons utiliser le composant rouge (R) de la couleur RVB pour indexer la texture d'échange. Cela signifie que la texture d'échange doit avoir une largeur de 256 pixels, car c'est le nombre de valeurs différentes que le composant rouge peut prendre..
Reprenons tout cela dans un exemple. Voici les valeurs de couleur rouge des couleurs de la palette des images-objets:
Disons que nous voulons remplacer la couleur de contour / yeux (noir) sur l'image-objet par la couleur bleue. La couleur de contour est la dernière sur la palette - celle avec une valeur rouge de 25
. Si nous voulons échanger cette couleur, nous devons définir dans la texture d'échange le pixel à l'indice 25 de la couleur souhaitée pour le contour: bleu.
Désormais, lorsque le shader rencontre une couleur avec une valeur rouge de 25, il la remplace par la couleur bleue de la texture d'échange:
Notez que cela peut ne pas fonctionner comme prévu si deux couleurs ou plus sur la texture de l'image-objet partagent la même valeur de rouge! Lorsque vous utilisez cette méthode, il est important de conserver les valeurs de rouge des couleurs de la texture de l’image-objet différentes.
Notez également que, comme vous pouvez le constater dans la démo, placer un pixel transparent à n’importe quel index de la texture d’échange ne permet pas d’échanger les couleurs contre les couleurs correspondant à cet index..
Nous allons implémenter cette idée en modifiant un shader de sprite existant. Comme le projet de démonstration est créé dans Unity, je vais utiliser le shader sprite Unity par défaut..
Tout ce que fait le shader par défaut (ce qui est pertinent pour ce didacticiel) consiste à échantillonner la couleur de l’atlas de texture principal et à la multiplier par une couleur de sommet pour changer la teinte. La couleur résultante est ensuite multipliée par l'alpha pour assombrir le sprite à des opacités plus faibles..
La première chose à faire est d'ajouter une texture supplémentaire au shader:
Propriétés [PerRendererData] _MainTex ("Texture de sprite", 2D) = "blanc" _SwapTex ("Données de couleur", 2D) = "transparent" _Color ("Teinte", Couleur) = (1,1,1 , 1) [MaterialToggle] PixelSnap ("Pixel Snap", Float) = 0
Comme vous pouvez le voir, nous avons deux textures ici maintenant. Le premier, _MainTex
, est la texture du sprite; le deuxième, _SwapTex
, est la texture d'échange.
Nous devons également définir un échantillonneur pour la deuxième texture afin de pouvoir y accéder. Nous utiliserons un échantillonneur de texture 2D, car Unity ne prend pas en charge les échantillonneurs 1D:
sampler2D _MainTex; sampler2D _AlphaTex; float _AlphaSplitEnabled; sampler2D _SwapTex;
Nous pouvons maintenant éditer le fragment shader:
fixed4 SampleSpriteTexture (float2 uv) fixed4 color = tex2D (_MainTex, uv); if (_AlphaSplitEnabled) color.a = tex2D (_AlphaTex, uv) .r; retourner la couleur; fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb * = c.a; retourner c;
Voici le code approprié pour le fragment shader par défaut. Comme vous pouvez le voir, c
est la couleur échantillonnée à partir de la texture principale; il est multiplié par la couleur du sommet pour lui donner une teinte. En outre, le shader assombrit les sprites avec des opacités plus faibles.
Après avoir échantillonné la couleur principale, échantillonnons également la couleur de permutation - mais avant cela, supprimons la partie qui la multiplie par la couleur de teinte, de sorte que nous échantillonnons en utilisant la valeur rouge véritable de la texture, pas sa teinte..
fixed4 frag (v2f IN): SV_Target fixed4 c = SampleSpriteTexture (IN.texcoord); fixed4 swapCol = tex2D (_SwapTex, float2 (c.r, 0));
Comme vous pouvez le constater, l’indice de couleur échantillonné est égal à la valeur rouge de la couleur principale..
Calculons maintenant notre couleur finale:
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);
Pour ce faire, nous devons interpoler entre la couleur principale et la couleur permutée en utilisant l'alpha de la couleur permutée comme étape. Ainsi, si la couleur permutée est transparente, la couleur finale sera égale à la couleur principale. mais si la couleur échangée est totalement opaque, la couleur finale sera égale à la couleur échangée.
N'oublions pas que la couleur finale doit être multipliée par la teinte:
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;
Nous devons maintenant envisager ce qui devrait arriver si nous voulons échanger une couleur sur la texture principale qui n'est pas totalement opaque. Par exemple, si nous avons un sprite fantôme bleu semi-transparent et que nous souhaitons remplacer sa couleur par le violet, nous ne voulons pas que le fantôme opaque avec les couleurs permutées soit conservé, nous souhaitons conserver la transparence d'origine. Alors faisons ça:
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;
La transparence de la couleur finale doit être égale à la transparence de la couleur de la texture principale.
Enfin, puisque le shader d'origine multipliait la valeur RVB de la couleur par son alpha, nous devrions le faire aussi, afin de garder le shader identique:
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; retour final;
Le shader est terminé maintenant. nous pouvons créer une texture de couleur d'échange, la remplir de pixels de couleur différente et voir si l'image-objet change de couleur correctement.
Bien sûr, cette méthode ne serait pas très utile si nous devions créer des textures de swap à la main tout le temps! Nous voudrons les générer et les modifier de manière procédurale…
Nous savons que nous avons besoin d’une texture d’échange pour pouvoir utiliser notre shader. De plus, si nous voulons laisser plusieurs personnages utiliser différentes palettes pour le même sprite en même temps, chacun de ces caractères aura besoin de sa propre texture d'échange..
Ce sera donc mieux si nous créons simplement ces textures de swap de manière dynamique, comme nous créons les objets..
Tout d'abord, définissons une texture d'échange et un tableau dans lequel nous garderons trace de toutes les couleurs échangées:
Texture2D mColorSwapTex; Color [] mSpriteColors;
Ensuite, créons une fonction dans laquelle nous initialiserons la texture. Nous utiliserons le format RGBA32 et définirons le mode de filtrage sur Point
:
VoC public InitColorSwapTex () Texture2D colorSwapTex = new Texture2D (256, 1, TextureFormat.RGBA32, false, false); colorSwapTex.filterMode = FilterMode.Point;
Assurons maintenant que tous les pixels de la texture sont transparents, en effaçant tous les pixels et en appliquant les modifications:
pour (int i = 0; i < colorSwapTex.width; ++i) colorSwapTex.SetPixel(i, 0, new Color(0.0f, 0.0f, 0.0f, 0.0f)); colorSwapTex.Apply();
Nous devons également définir la texture d'échange du matériau sur celle nouvellement créée:
mSpriteRenderer.material.SetTexture ("_ SwapTex", colorSwapTex);
Enfin, nous enregistrons la référence à la texture et créons le tableau pour les couleurs:
mSpriteColors = new Color [colorSwapTex.width]; mColorSwapTex = colorSwapTex;
La fonction complète est la suivante:
VoC public InitColorSwapTex () Texture2D colorSwapTex = new Texture2D (256, 1, TextureFormat.RGBA32, false, false); colorSwapTex.filterMode = FilterMode.Point; pour (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;
Notez qu'il n'est pas nécessaire que chaque objet utilise une texture distincte 256x1px; nous pourrions faire une texture plus grande qui couvre tous les objets. Si nous avons besoin de 32 caractères, nous pouvons créer une texture de taille 256x32px et nous assurer que chaque caractère utilise uniquement une ligne spécifique dans cette texture. Cependant, chaque fois que nous devions modifier cette texture plus grande, nous devions transmettre plus de données au GPU, ce qui rendrait probablement cette solution moins efficace..
Il n'est également pas nécessaire d'utiliser une texture d'échange distincte pour chaque image-objet. Par exemple, si le personnage a une arme équipée et que cette arme est un sprite séparé, il peut facilement partager la texture de permutation avec le personnage (tant que la texture du sprite de l'arme n'utilise pas de couleurs dont les valeurs de rouge sont identiques à celles du personnage sprite).
Il est très utile de connaître les valeurs en rouge de certaines parties d’un sprite. Nous allons donc créer un enum
qui contiendra ces données:
public enum SwapIndex Outline = 25, SkinPrim = 254, SkinSec = 239, HandPrim = 235, HandSec = 204, ShirtPrim = 62, ShirtSec = 70, ShoePrim = 253, ShoeSec = 248, Pantalon = 72,
Ce sont toutes les couleurs utilisées par le personnage de l'exemple.
Nous avons maintenant tout ce dont nous avons besoin pour créer une fonction permettant d’échanger la couleur:
public void SwapColor (index SwapIndex, couleur de couleur) mSpriteColors [(int) index] = color; mColorSwapTex.SetPixel ((int) index, 0, color);
Comme vous pouvez le constater, rien d’extraordinaire ici; nous venons de définir la couleur dans le tableau de couleurs de notre objet et également de définir le pixel de la texture à un index approprié.
Notez que nous ne voulons pas vraiment appliquer les modifications à la texture à chaque fois que nous appelons cette fonction. nous préférons les appliquer une fois que nous échangeons tout les pixels que nous voulons.
Regardons un exemple d'utilisation de la fonction:
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 ();
Comme vous pouvez le constater, il est assez facile de comprendre ce que ces appels de fonction font en les lisant: dans ce cas, ils changent les deux couleurs de peau, les couleurs de la chemise et la couleur du pantalon.
Voyons maintenant comment nous pouvons utiliser le shader pour créer un effet de frappe pour notre sprite. Cet effet permutera toutes les couleurs de l'image-objet en blanc, restera ainsi pendant une brève période, puis reviendra à la couleur d'origine. L’effet général sera que l’image-objet clignote en blanc.
Tout d'abord, créons une fonction qui permute toutes les couleurs, mais ne remplace pas réellement les couleurs du tableau de l'objet. Nous aurons besoin de ces couleurs pour désactiver l’effet de frappe, après tout.
public void SwapAllSpritesColorsTemporarily (Couleur couleur) pour (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, color); mColorSwapTex.Apply();
Nous pourrions effectuer une itération uniquement à travers les énumérations, mais une itération dans l'ensemble de la texture garantira que la couleur est échangée, même si une couleur particulière n'est pas définie dans les propriétés. SwapIndex
.
Maintenant que les couleurs sont permutées, nous devons attendre un peu et revenir aux couleurs précédentes..
Commençons par créer une fonction qui réinitialisera les couleurs:
public void ResetAllSpritesColors () pour (int i = 0; i < mColorSwapTex.width; ++i) mColorSwapTex.SetPixel(i, 0, mSpriteColors[i]); mColorSwapTex.Apply();
Définissons maintenant le timer et une constante:
float mHitEffectTimer = 0.0f; const float cHitEffectTime = 0.1f;
Créons une fonction qui lancera l'effet de frappe:
vide public StartHitEffect () mHitEffectTimer = cHitEffectTime; SwapAllSpritesColorsTemporairement (Color.white);
Et dans la fonction de mise à jour, vérifions combien de temps il reste sur le chronomètre, diminuez-le d'un cran et appelez une réinitialisation une fois le délai écoulé:
void public Update () if (mHitEffectTimer> 0.0f) mHitEffectTimer - = Time.deltaTime; si (mHitEffectTimer <= 0.0f) ResetAllSpritesColors();
Ça y est, quand StartHitEffect
est appelé, le sprite clignotera en blanc pendant un moment, puis reviendra à ses couleurs précédentes.
Ceci marque la fin du tutoriel! J'espère que vous trouverez la méthode acceptable et le shader utile. C'est très simple, mais cela fonctionne très bien pour les sprites pixel art qui n'utilisent pas beaucoup de couleurs.
La méthode devrait être légèrement modifiée si nous voulions échanger des groupes entiers de couleurs à la fois, ce qui exigerait certainement un matériau plus complexe et plus coûteux. Dans mon propre jeu, cependant, j'utilise très peu de couleurs, cette technique convient donc parfaitement.