Remplacement des palettes dans le jeu à l'aide de shaders

Dans ce blog, je vais vous montrer ma technique préférée que j'utilise activement dans mon jeu Vagabond: remplacer les palettes.

L'échange de palette est une modification de la palette de textures. Dans l'article, nous l'implémentons à l'aide de shaders. Autrefois, c'était une technique utile, vous permettant d'ajouter de la variabilité aux ressources sans perte inutile de mémoire. Aujourd'hui, il est utilisé dans la génération procédurale pour créer de nouvelles ressources.



Préparation d'image


La première étape consiste à préparer les images pour remplacer les palettes. Dans un bitmap, chaque pixel contient une couleur, mais nous devons plutôt contenir son index de couleur dans la palette. Pour cette raison, nous séparerons la structure de l'image (zones de la même couleur) des vraies couleurs.

En fait, certains formats d'image prennent en charge cette méthode de stockage. Par exemple, le format PNG a la possibilité d'enregistrer les couleurs indexées. Malheureusement, de nombreuses bibliothèques de chargement d'images créent un tableau de couleurs, même si l'image a été enregistrée en mode indexé. Cela s'applique également à la bibliothèque SFML que j'utilise. À l'intérieur, il utilise stb_image , qui "supprime automatiquement la palette" des images, c'est-à-dire remplace les index par la couleur de palette correspondante.

Par conséquent, pour éviter ce problème, vous devez stocker l'image et la palette séparément. L'image est enregistrée en nuances de gris et le niveau de gris de chaque pixel correspond à son indice de couleur dans la palette.

Voici un exemple de ce que nous espérons recevoir:


Pour ce faire, j'utilise une petite fonction Python qui utilise la bibliothèque Pillow :

import io import numpy as np from PIL import Image def convert_to_indexed_image(image, palette_size): # Convert to an indexed image indexed_image = image.convert('RGBA').convert(mode='P', dither='NONE', colors=palette_size) # Be careful it can remove colors # Save and load the image to update the info (transparency field in particular) f = io.BytesIO() indexed_image.save(f, 'png') indexed_image = Image.open(f) # Reinterpret the indexed image as a grayscale image grayscale_image = Image.fromarray(np.asarray(indexed_image), 'L') # Create the palette palette = indexed_image.getpalette() transparency = list(indexed_image.info['transparency']) palette_colors = np.asarray([[palette[3*i:3*i+3] + [transparency[i]] \ for i in range(palette_size)]]).astype('uint8') palette_image = Image.fromarray(palette_colors, mode='RGBA') return grayscale_image, palette_image 

Tout d'abord, la fonction convertit l'image en mode palette. Elle la réinterprète ensuite comme une image en niveaux de gris. Récupère ensuite la palette. Rien de compliqué, le travail principal est effectué par la bibliothèque Pillow.

Shader


Après avoir préparé les images, nous sommes prêts à écrire un shader pour remplacer les palettes. Il existe deux stratégies pour transférer une palette vers un shader: vous pouvez utiliser une texture ou un tableau homogène. J'ai découvert qu'il est plus facile d'utiliser un tableau homogène, donc je l'ai utilisé.

Voici mon shader, je l'ai écrit en GLSL, mais je pense qu'il peut être facilement transféré dans une autre langue pour créer des shaders:

 #version 330 core in vec2 TexCoords; uniform sampler2D Texture; uniform vec4 Palette[32]; out vec4 Color; void main() { Color = Palette[int(texture(Texture, TexCoords).r * 255)]; } 

Nous utilisons simplement une texture pour lire le canal rouge du pixel actuel. Le canal rouge est une valeur à virgule flottante dans la plage de 0 à 1, nous la multiplions donc par 255 et la convertissons en int pour obtenir le niveau de gris d'origine de 0 à 255, qui est stocké dans l'image. Ensuite, nous l'utilisons pour obtenir la couleur de la palette.

L'animation au début de l'article est tirée de captures d'écran dans le jeu dans lesquelles j'utilise les palettes suivantes pour changer la couleur du corps du personnage:

Source: https://habr.com/ru/post/fr480836/


All Articles