Substituindo paletas no jogo usando shaders

Neste blog, mostrarei minha técnica favorita que uso ativamente no meu jogo Vagabond: substituindo paletas.

A troca de paleta é uma alteração na paleta de textura. No artigo, implementamos usando shaders. Antigamente, essa era uma técnica útil, permitindo adicionar variabilidade aos recursos sem desperdício de memória desnecessário. Hoje é usado na geração de procedimentos para criar novos recursos.



Preparação da imagem


O primeiro passo é preparar as imagens para substituir as paletas. Em um bitmap, cada pixel contém uma cor, mas precisamos conter seu índice de cores na paleta. Por esse motivo, separaremos a estrutura da imagem (áreas da mesma cor) das cores reais.

De fato, alguns formatos de imagem suportam esse método de armazenamento. Por exemplo, o formato PNG tem a capacidade de salvar cores indexadas. Infelizmente, muitas bibliotecas de carregamento de imagens criam uma matriz de cores, mesmo que a imagem tenha sido salva no modo indexado. Isso também se aplica à biblioteca SFML que eu uso. No interior, ele usa stb_image , que "remove automaticamente a paleta" das imagens, ou seja, substitui índices pela cor da paleta correspondente.

Portanto, para evitar esse problema, você precisa armazenar a imagem e a paleta separadamente. A imagem é gravada em tons de cinza, e o nível de cinza de cada pixel corresponde ao seu índice de cores na paleta.

Aqui está um exemplo do que esperamos receber:


Para conseguir isso, eu uso uma pequena função Python que usa a 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 

Primeiro, a função converte a imagem no modo de paleta. Em seguida, ela a reinterpreta como uma imagem em escala de cinza. Em seguida, recupera a paleta. Nada complicado, o trabalho principal é realizado pela biblioteca Pillow.

Shader


Depois de preparar as imagens, estamos prontos para escrever um shader para substituir as paletas. Existem duas estratégias para transferir uma paleta para um sombreador: você pode usar uma textura ou uma matriz homogênea. Eu descobri que é mais fácil usar uma matriz uniforme, então eu a usei.

Aqui está o meu shader, escrevi no GLSL, mas acho que pode ser facilmente transferido para outro idioma para criar 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)]; } 

Nós apenas usamos uma textura para ler o canal vermelho do pixel atual. O canal vermelho é um valor de ponto flutuante no intervalo de 0 a 1, então o multiplicamos por 255 e o convertemos em int para obter o nível de cinza original de 0 a 255, que é armazenado na imagem. Em seguida, usamos para obter a cor da paleta.

A animação no início do artigo é tirada de capturas de tela no jogo, nas quais utilizo as paletas a seguir para alterar a cor do corpo do personagem:

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


All Articles