Création d'un effet de palette de style rétro dans OpenGL

37

Je travaille sur un jeu ressemblant à Megaman où je dois changer la couleur de certains pixels au moment de l'exécution. Pour référence : dans Megaman, lorsque vous changez d'arme sélectionnée, la palette du personnage principal change pour refléter l'arme sélectionnée. Toutes les couleurs du sprite ne changent pas, seulement certaines changent .

Ce type d’effet était courant et assez facile à faire sur la NDA puisque le programmeur avait accès à la palette et au mappage logique entre les pixels et les index de la palette. Sur un matériel moderne, cependant, cela pose un peu plus de problèmes car le concept de palettes n’est pas le même.

Toutes mes textures sont en 32 bits et n'utilisent pas de palettes.

Je sais qu'il existe deux manières d'obtenir l'effet souhaité, mais je suis curieux de savoir s'il existe de meilleurs moyens d'obtenir cet effet facilement. Les deux options que je connais sont:

  1. Utilisez un shader et écrivez du GLSL pour effectuer le comportement "permutation de palette".
  2. Si les shaders ne sont pas disponibles (par exemple, car la carte graphique ne les prend pas en charge), il est alors possible de cloner les textures "originales" et de générer différentes versions avec les modifications de couleur pré-appliquées.

Idéalement, j'aimerais utiliser un shader, car il semble simple et nécessite peu de travail supplémentaire par rapport à la méthode de duplication de texture. Je crains que la duplication de textures juste pour changer une couleur en elles gaspille de la VRAM - ne devrais-je pas m'en inquiéter?

Edit : J'ai fini par utiliser la technique de la réponse acceptée et voici mon shader pour référence.

uniform sampler2D texture;
uniform sampler2D colorTable;
uniform float paletteIndex;

void main()
{
        vec2 pos = gl_TexCoord[0].xy;
        vec4 color = texture2D(texture, pos);
        vec2 index = vec2(color.r + paletteIndex, 0);
        vec4 indexedColor = texture2D(colorTable, index);
        gl_FragColor = indexedColor;      
}

Les deux textures sont 32 bits, une texture est utilisée comme table de correspondance contenant plusieurs palettes de la même taille (dans mon cas, 6 couleurs). J'utilise le canal rouge du pixel source comme index de la table des couleurs. Cela a fonctionné comme un charme pour réaliser la permutation de palette à la Megaman!

Zack l'humain
la source

Réponses:

41

Je ne m'inquiéterais pas de perdre de la VRAM pour quelques textures de caractères.

Pour moi, utiliser votre option 2. (avec différentes textures ou différents décalages UV si cela convient) est la voie à suivre: plus flexible, basé sur les données, moins d'impact sur le code, moins de bugs, moins de soucis.


Ceci mis à part, si vous commencez à accumuler des tonnes de caractères avec des tonnes d’animations de sprites en mémoire, vous pourriez peut-être commencer à utiliser ce qui est recommandé par OpenGL , les palettes de bricolage:

Textures palettisées

La prise en charge de l' extension EXT_paletted_texture a été abandonnée par les principaux fournisseurs de GL. Si vous avez vraiment besoin de textures palettisées sur du nouveau matériel, vous pouvez utiliser des shaders pour obtenir cet effet.

Exemple de shader:

//Fragment shader
#version 110
uniform sampler2D ColorTable;     //256 x 1 pixels
uniform sampler2D MyIndexTexture;
varying vec2 TexCoord0;

void main()
{
  //What color do we want to index?
  vec4 myindex = texture2D(MyIndexTexture, TexCoord0);
  //Do a dependency texture read
  vec4 texel = texture2D(ColorTable, myindex.xy);
  gl_FragColor = texel;   //Output the color
}

Il s’agit simplement d’échantillonnage ColorTable(une palette dans RGBA8), d’ MyIndexTextureune texture carrée de 8 bits en couleurs indexées. Reproduit simplement le fonctionnement des palettes de style rétro.


L'exemple cité ci-dessus en utilise deux sampler2D, où il pourrait en utiliser un sampler1D+ un sampler2D. Je suppose que c'est pour des raisons de compatibilité ( pas de textures unidimensionnelles dans OpenGL ES ) ... Mais tant pis, pour le bureau OpenGL, ceci peut être simplifié à:

uniform sampler1D Palette;             // A palette of 256 colors
uniform sampler2D IndexedColorTexture; // A texture using indexed color
varying vec2 TexCoord0;                // UVs

void main()
{
    // Pick up a color index
    vec4 index = texture2D(IndexedColorTexture, TexCoord0);
    // Retrieve the actual color from the palette
    vec4 texel = texture1D(Palette, index.x);
    gl_FragColor = texel;   //Output the color
}

Paletteest une texture unidimensionnelle de couleurs "réelles" (par exemple GL_RGBA8), et IndexedColorTextureest une texture bidimensionnelle de couleurs indexées (généralement GL_R8, ce qui vous donne 256 index). Pour les créer, il existe plusieurs outils de création et formats de fichier image prenant en charge les images palettisées. Il devrait être possible de trouver celui qui correspond à vos besoins.

Laurent Couvidou
la source
2
Exactement la réponse que j'aurais donnée, mais plus détaillée. Serait +2 si je pouvais.
Ilmari Karonen
J'ai du mal à obtenir que cela fonctionne pour moi, principalement parce que je ne comprends pas comment l'index dans la couleur est déterminé - pourriez-vous éventuellement en dire un peu plus?
Zack The Human
Vous devriez probablement lire sur les couleurs indexées . Votre texture devient un tableau 2D d'indices, ces index correspondant aux couleurs d'une palette (généralement une texture unidimensionnelle). Je vais modifier ma réponse pour simplifier l’exemple donné par le site Web OpenGL.
Laurent Couvidou
Je suis désolé, je n'étais pas clair dans mon commentaire original. Je connais bien les couleurs indexées et leur fonctionnement, mais je ne vois pas comment elles fonctionnent dans le contexte d'un shader ou d'une texture 32 bits (car elles ne sont pas indexées, n'est-ce pas?).
Zack The Human
Le problème, c’est que vous avez ici une palette 32 bits contenant 256 couleurs et une texture d’ indices 8 bits (allant de 0 à 255). Voir mon montage;)
Laurent Couvidou
10

Je peux penser à cela de deux manières.

Voie n ° 1: GL_MODULATE

Vous pouvez faire le sprite en nuances de gris et GL_MODULATEla texture quand il est dessiné avec une seule couleur unie.

Par exemple,

entrez la description de l'image ici

entrez la description de l'image ici

Cela ne vous permet pas beaucoup de souplesse, cependant, vous ne pouvez aller que dans les tons plus clairs et plus foncés de la même teinte.

Voie n ° 2: ifénoncer (mauvais pour la performance)

Vous pouvez également colorer vos textures avec des valeurs clés impliquant R, G et B. Ainsi, par exemple, la première couleur est (1,0,0), la deuxième couleur est (0,1,0), la troisième la couleur est (0,0,1).

Vous déclarez un couple d'uniformes comme

uniform float4 color1 ;
uniform float4 color2 ;
uniform float4 color3 ;

Ensuite, dans le shader, la couleur finale du pixel est similaire à

float4 pixelColor = texel.r*color1 + texel.g*color2 + texel.b*color3 ;

color1, color2, color3, Sont uniformes afin qu'ils puissent être réglés avant l'exécution de shaders ( en fonction de ce que Megaman « costume » est), le Shader ne fonctionne tout simplement le reste.

Vous pouvez également utiliser des ifinstructions si vous avez besoin de plus de couleurs, mais le contrôle d'égalité doit fonctionner correctement (les textures sont généralement R8G8B8comprises entre 0 et 255 dans le fichier de texture, mais dans le shader, vous avez des flotteurs rgba).

Voie n ° 3: Stockez simplement de manière redondante les différentes images colorées dans la carte des images-objets

entrez la description de l'image ici

C'est peut-être le moyen le plus simple, si vous n'essayez pas follement de conserver de la mémoire

bobobobo
la source
2
N ° 1 peut être soigné, mais les n ° 2 et n ° 3 sont des conseils horriblement mauvais. Le GPU est capable d'indexer à partir de cartes (dont une palette est fondamentalement) et le fait mieux que ces deux suggestions.
Skrylar
6

J'utiliserais des shaders. Si vous ne souhaitez pas les utiliser (ou ne pouvez pas les utiliser), choisissez une texture (ou une partie d'une texture) par couleur et n'utilisez que du blanc net. Cela vous permettra de teinter la texture (par exemple à travers glColor()) sans que vous ayez à vous soucier de créer des textures supplémentaires pour les couleurs. Vous pouvez même échanger des couleurs à la volée sans travail de texture supplémentaire.

Mario
la source
C'est une très bonne idée. J'y avais pensé il y a quelque temps, mais cela ne m'est pas venu à l'esprit lorsque j'ai posté cette question. Cela présente une complexité supplémentaire, car toute image-objet utilisant ce type de texture composite nécessitera une logique de dessin plus complexe. Merci pour cette suggestion géniale!
Zack The Human
Oui, vous aurez également besoin de x passes par sprite, ce qui peut ajouter beaucoup de complexité. Étant donné que le matériel actuel prend généralement en charge au moins les pixel shaders de base (même les GPU des netbooks), je les utiliserais plutôt. Cela peut être intéressant si vous souhaitez simplement colorer des éléments spécifiques, par exemple des barres de santé ou des icônes.
Mario
3

"Toutes les couleurs du sprite ne changent pas, seulement certaines changent."

Les vieux jeux faisaient des tours de palette parce que c'était tout ce qu'ils avaient. De nos jours, vous pouvez utiliser plusieurs textures et les combiner de différentes manières.

Vous pouvez créer une texture de masque en niveaux de gris séparée que vous utiliserez pour déterminer les pixels qui changeront de couleur. Les zones blanches dans la texture reçoivent la modification complète de la couleur et les zones noires restent inchangées.

En langue de shader:

vec3 baseColor = texture (BaseSampler, att_TexCoord) .rgb;
quantité de flottement = texture (MaskSampler, att_TexCoord) .r;
vec3 color = mix (baseColor, baseColor * ModulationColor, quantité);

Vous pouvez également utiliser la multi-texturation à fonction fixe avec différents modes de combinateur de texture.

Il y a évidemment beaucoup de façons différentes de s'y prendre. vous pouvez placer des masques de différentes couleurs dans chacun des canaux RVB ou moduler les couleurs en décalant la teinte dans un espace colorimétrique HSV.

Une autre option, peut-être plus simple, consiste à dessiner simplement l'image-objet en utilisant une texture distincte pour chaque composant. Une texture pour les cheveux, une pour l'armure, une pour les bottes, etc. Chaque couleur peut être teinte séparément.

ccxvii
la source
2

Premièrement, on parle généralement de cyclisme (cycle de palette), ce qui pourrait donc aider votre Google-fu.

Cela dépendra exactement des effets que vous voulez produire et si vous utilisez du contenu 2D statique ou des éléments 3D dynamiques (par exemple, voulez-vous simplement gérer les effets de sélection / textures, ou restituer une scène 3D entière puis un échange de palette)? Aussi, si vous vous limitez à un nombre de couleurs ou utilisez la couleur.

Une approche plus complète, utilisant des shaders, consisterait à produire des images indexées par couleur avec une texture de palette. Faire une texture mais au lieu d'avoir des couleurs, avoir une couleur qui représente un index à rechercher puis une autre texture de couleurs qui est la plaque. Par exemple, le rouge pourrait être la coordonnée t des textures, et le vert pourrait être la coordonnée s. Utilisez un shader pour faire la recherche. Et animer / modifier les couleurs de cette texture de palette. Ce serait assez proche de l'effet rétro mais ne fonctionnerait que pour du contenu 2D statique tel que des sprites ou des textures. Si vous réalisez de véritables graphismes rétro, vous pourrez peut-être produire des sprites de couleur indexés réels et écrire quelque chose pour les importer et les palettes.

Vous voudrez également déterminer si vous allez utiliser une palette «globale». Comme si un ancien matériel fonctionnait, moins de mémoire pour les palettes car vous n’avez besoin que d’une seule, mais plus difficile à produire, car vous devez synchroniser la palette sur toutes les images) ou attribuer à chaque image sa propre palette. Cela vous donnerait plus de flexibilité mais utiliserait plus de mémoire. Bien sûr, vous pouvez faire les deux, mais vous ne pouvez utiliser que des palettes pour les éléments pour lesquels vous effectuez un cycle de couleurs. Déterminez également dans quelle mesure vous ne voudriez pas limiter vos couleurs, les anciens jeux en utilisant 256 couleurs par exemple. Si vous utilisez une texture, vous obtiendrez le nombre de pixels de sorte qu'un 256 * 256 vous donnera

Si vous voulez simplement un effet factice bon marché, tel que de l’eau qui coule, vous pouvez créer une seconde texture contenant un masque en niveaux de gris masquant la zone que vous souhaitez modifier, puis utilisez les textures mastiquées pour modifier la teinte / saturation / luminosité de la valeur dans le fond. texture de masque en niveaux de gris. Vous pouvez être encore plus paresseux et ne changer que la teinte / luminosité / saturation de tout ce qui se trouve dans une gamme de couleurs.

Si vous en aviez besoin en 3D, vous pourriez essayer de générer l'image indexée à la volée, mais le processus serait lent, car vous auriez à regarder la palette, vous seriez probablement limité dans la gamme de couleurs.

Sinon, vous pouvez simplement le simuler dans le shader, si l’échange de couleur ==, changez-le. Cela serait lent et ne fonctionnerait qu'avec un nombre limité de couleurs permutantes, mais il ne faut pas insister sur le matériel actuel, en particulier si vous ne vous occupez que de graphismes 2D et que vous pouvez toujours marquer les sprites dont les couleurs sont échangées et utiliser un shader différent.

Sinon, faites-leur vraiment beaucoup, faites un tas de textures animées pour chaque état.

Voici une excellente démonstration du cyclisme sur palette réalisée en HTML5 .

David C. Bishop
la source
0

Je pense que les ifs sont assez rapides pour que l'espace colorimétrique [0 ... 1] soit divisé en deux:

if (color.r > 0.5) {
   color.r = (color.r-0.5) * uniform_r + uniform_base_r;
} else {
  color.r *=2.0;
} // this could be vectorised for all colors

De cette façon, les valeurs de couleur comprises entre 0 et 0,5 sont réservées aux valeurs de couleur normales; et 0.5 - 1.0 sont réservés aux valeurs "modulées", où 0.5..1.0 est mappé sur toute plage sélectionnée par l'utilisateur.

Seule la résolution de couleur est tronquée de 256 valeurs par pixel à 128 valeurs.

On peut probablement utiliser des fonctions intégrées telles que max (color-0.5f, 0.0f) pour supprimer complètement les ifs (en les échangeant en multiplications par zéro ou un ...).

Aki Suihkonen
la source