Reemplazar paletas en el juego usando sombreadores

En este blog te mostraré mi técnica favorita que utilizo activamente en mi juego Vagabond: reemplazar paletas.

El intercambio de paletas es un cambio en la paleta de texturas. En el artículo, lo implementamos usando sombreadores. En los viejos tiempos, esta era una técnica útil, que le permitía agregar variabilidad a los recursos sin desperdicio innecesario de memoria. Hoy se utiliza en la generación de procedimientos para crear nuevos recursos.



Preparación de imagen


El primer paso es preparar las imágenes para reemplazar las paletas. En un mapa de bits, cada píxel contiene un color, pero debemos contener su índice de color en la paleta. Debido a esto, separaremos la estructura de la imagen (áreas del mismo color) de los colores reales.

De hecho, algunos formatos de imagen admiten este método de almacenamiento. Por ejemplo, el formato PNG tiene la capacidad de guardar colores indexados. Desafortunadamente, muchas bibliotecas de carga de imágenes crean una variedad de colores, incluso si la imagen se guardó en modo indexado. Esto también se aplica a la biblioteca SFML que uso. En el interior, utiliza stb_image , que automáticamente "elimina la paleta" de imágenes, es decir reemplaza los índices con el color de la paleta correspondiente.

Por lo tanto, para evitar este problema, debe almacenar la imagen y la paleta por separado. La imagen se graba en tonos de gris, y el nivel de gris de cada píxel corresponde a su índice de color en la paleta.

Aquí hay un ejemplo de lo que esperamos recibir:


Para lograr esto, uso una pequeña función de Python que usa la biblioteca 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 

Primero, la función convierte la imagen al modo de paleta. Luego lo reinterpreta como una imagen en escala de grises. Luego recupera la paleta. Nada complicado, el trabajo principal lo realiza la biblioteca Pillow.

Shader


Una vez preparadas las imágenes, estamos listos para escribir un sombreador para reemplazar las paletas. Hay dos estrategias para transferir una paleta a un sombreador: puede usar una textura o una matriz homogénea. Descubrí que es más fácil usar una matriz homogénea, así que lo usé.

Aquí está mi sombreador, lo escribí en GLSL, pero creo que se puede transferir fácilmente a otro idioma para crear sombreadores:

 #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)]; } 

Solo usamos una textura para leer el canal rojo del píxel actual. El canal rojo es un valor de coma flotante en el rango de 0 a 1, por lo que lo multiplicamos por 255 y lo convertimos en int para obtener el nivel de gris original de 0 a 255, que se almacena en la imagen. Luego lo usamos para obtener el color de la paleta.

La animación al comienzo del artículo está tomada de capturas de pantalla del juego en las que uso las siguientes paletas para cambiar el color del cuerpo del personaje:

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


All Articles