Criando um efeito de troca de paleta de estilo retro no OpenGL

37

Estou trabalhando em um jogo tipo Megaman , onde preciso alterar a cor de certos pixels em tempo de execução. Para referência : no Megaman, quando você muda sua arma selecionada, a paleta do personagem principal muda para refletir a arma selecionada. Nem todas as cores do sprite mudam, apenas algumas mudam .

Esse tipo de efeito era comum e bastante fácil de executar no NES, pois o programador tinha acesso à paleta e ao mapeamento lógico entre pixels e índices da paleta. No hardware moderno, porém, isso é um pouco mais desafiador, porque o conceito de paletas não é o mesmo.

Todas as minhas texturas são de 32 bits e não usam paletas.

Existem duas maneiras de obter o efeito desejado, mas estou curioso para saber se existem maneiras melhores de obter esse efeito facilmente. As duas opções que conheço são:

  1. Use um sombreador e escreva um GLSL para executar o comportamento de "troca de paleta".
  2. Se shaders não estiverem disponíveis (por exemplo, porque a placa gráfica não os suporta), é possível clonar as texturas "originais" e gerar versões diferentes com as alterações de cores pré-aplicadas.

Idealmente, eu gostaria de usar um sombreador, pois parece simples e requer pouco trabalho adicional em oposição ao método de textura duplicada. Eu me preocupo que duplicar texturas apenas para mudar uma cor esteja desperdiçando VRAM - não devo me preocupar com isso?

Edit : Acabei usando a técnica da resposta aceita e aqui está o meu shader para referência.

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;      
}

Ambas as texturas são de 32 bits, uma textura é usada como tabela de pesquisa, contendo várias paletas do mesmo tamanho (no meu caso, 6 cores). Eu uso o canal vermelho do pixel de origem como um índice para a tabela de cores. Isso funcionou como um encanto para alcançar a troca de paletas tipo Megaman!

Zack The Human
fonte

Respostas:

41

Eu não me preocuparia em desperdiçar VRAM para algumas texturas de caracteres.

Para mim, usar sua opção 2. (com diferentes texturas ou diferentes desvios de UV, se for o caso) é o caminho a seguir: mais flexível, orientado a dados, menos impacto no código, menos bugs, menos preocupações.


Isso posto de lado, se você começar a acumular toneladas de caracteres com toneladas de animações de sprite na memória, talvez possa começar a usar o que é recomendado pelo OpenGL , paletas faça você mesmo:

Texturas paletizadas

O suporte para a extensão EXT_paletted_texture foi descartado pelos principais fornecedores da GL. Se você realmente precisar de texturas paletizadas em um novo hardware, poderá usar shaders para obter esse efeito.

Exemplo de sombreador:

//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
}

Isso é simplesmente amostragem em ColorTable(uma paleta em RGBA8), usando MyIndexTexture(uma textura quadrada de 8 bits em cores indexadas). Apenas reproduz a maneira como as paletas de estilo retrô funcionam.


O exemplo acima citado usa dois sampler2D, onde ele pode realmente usar um sampler1D+ um sampler2D. Eu suspeito que isso seja por motivos de compatibilidade ( sem texturas unidimensionais no OpenGL ES ) ... Mas não importa, para o OpenGL de desktop, isso pode ser simplificado para:

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
}

Paletteé uma textura unidimensional de cores "reais" (por exemplo GL_RGBA8) e IndexedColorTextureé uma textura bidimensional de cores indexadas (normalmente GL_R8, que fornece 256 índices). Para criá-las, existem várias ferramentas de autoria e formatos de arquivo de imagem por aí que suportam imagens em paleta; deve ser possível encontrar aquele que se adapta às suas necessidades.

Laurent Couvidou
fonte
2
Exatamente a resposta que eu daria, apenas mais detalhada. Seria +2 se eu pudesse.
Ilmari Karonen
Estou tendo alguns problemas para que isso funcione para mim, principalmente porque não entendo como o índice da cor é determinado - você poderia expandir um pouco mais isso?
Zack The Human
Você provavelmente deve ler as cores indexadas . Sua textura se torna uma matriz 2D de índices, esses índices correspondentes às cores em uma paleta (geralmente uma textura unidimensional). Vou editar minha resposta para simplificar o exemplo dado pelo site OpenGL.
21430 Laurent Couvidou
Sinto muito, eu não estava claro no meu comentário original. Eu estou familiarizado com as cores indexadas e como elas funcionam, mas não tenho certeza de como elas funcionam no contexto de um sombreador ou textura de 32 bits (já que elas não são indexadas - certo?).
Zack The Human
Bem, o importante é que aqui você tem uma paleta de 32 bits contendo 256 cores e uma textura de índices de 8 bits (de 0 a 255). Veja minha edição;)
Laurent Couvidou 8/12/12
10

Existem duas maneiras de pensar em fazer isso.

Maneira # 1: GL_MODULATE

Você pode fazer o sprite em tons de cinza e GL_MODULATEa textura quando desenhada com uma única cor sólida.

Por exemplo,

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Isso não permite muita flexibilidade, no entanto, basicamente você pode ficar mais claro e mais escuro da mesma tonalidade.

Caminho # 2: ifdeclaração (ruim para o desempenho)

Outra coisa que você pode fazer é colorir suas texturas com alguns valores-chave envolvendo R, G e B. Portanto, por exemplo, a primeira cor é (1,0,0), a segunda cor é (0,1,0), a terceira a cor é (0,0,1).

Você declara alguns uniformes como

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

Em seguida, no sombreador, a cor final do pixel é algo como

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

color1, color2, color3, São uniformes para que eles possam ser definida antes que os sombreador é executado (dependendo do que megaman "terno" está em), então o shader simplesmente faz o resto.

Você também pode usar ifinstruções se precisar de mais cores, mas teria que fazer com que as verificações de igualdade funcionassem corretamente (as texturas geralmente estão R8G8B8entre 0 e 255 cada no arquivo de textura, mas no shader, você tem rgba floats)

Caminho 3: Armazene de forma redundante as diferentes imagens coloridas no mapa do sprite

insira a descrição da imagem aqui

Esta pode ser a maneira mais fácil, se você não estiver tentando economizar insanamente a memória

bobobobo
fonte
2
O número 1 pode ser arrumado, mas os números 2 e 3 são conselhos terrivelmente ruins. A GPU é capaz de indexar a partir de mapas (dos quais a paleta é basicamente) e é melhor do que as duas sugestões.
Skrylar
6

Eu usaria shaders. Se você não quiser usá-los (ou não puder), eu usaria uma textura (ou parte de uma textura) por cor e usaria apenas branco limpo. Isso permitirá que você tinja a textura (por exemplo, através glColor()) sem precisar se preocupar em criar texturas adicionais para cores. Você pode até trocar cores em execução sem precisar trabalhar com textura adicional.

Mario
fonte
Esta é uma boa ideia. Na verdade, eu já pensava nisso há algum tempo, mas não me ocorreu quando publiquei esta pergunta. Existe alguma complexidade extra com isso, pois qualquer sprite que use esse tipo de textura composta precisará de uma lógica de desenho mais complexa. Obrigado por esta sugestão incrível!
Zack The Human
Sim, você também precisará de x passes por sprite, o que pode adicionar bastante complexidade. Considerando o hardware de hoje geralmente suportando pelo menos pixel shaders básicos (mesmo as GPUs de netbooks), eu os usaria. Pode ser legal se você quiser pintar coisas específicas, como barras de saúde ou ícones.
Mario
3

"Nem todas as cores do sprite mudam, apenas algumas mudam."

Jogos antigos faziam truques de paleta, porque era tudo o que tinham. Hoje em dia, você pode usar várias texturas e combiná-las de várias maneiras.

Você pode criar uma textura de máscara em escala de cinza separada usada para decidir quais pixels mudarão de cor. As áreas brancas da textura recebem a modificação completa das cores e as áreas pretas permanecem inalteradas.

Na linguagem shader:

vec3 baseColor = textura (BaseSampler, att_TexCoord) .rgb;
quantidade flutuante = textura (MaskSampler, att_TexCoord) .r;
vec3 color = mix (baseColor, baseColor * ModulationColor, quantidade);

Ou você pode usar multi-texturização de função fixa com vários modos combinadores de textura.

Obviamente, existem muitas maneiras diferentes de você fazer isso; você pode colocar máscaras para cores diferentes em cada um dos canais RGB ou modular as cores mudando de tonalidade em um espaço de cores HSV.

Outra opção, talvez mais simples, é simplesmente desenhar o sprite usando uma textura separada para cada componente. Uma textura para os cabelos, uma para a armadura, uma para as botas, etc. Cada uma pode ser tingida separadamente.

ccxvii
fonte
2

Em primeiro lugar, geralmente é chamado de ciclismo (ciclismo de paletes), para que possa ajudar seu Google-fu.

Isso dependerá exatamente de quais efeitos você deseja produzir e se estiver lidando com conteúdo 2D estático ou material 3D dinâmico (ou seja, você só deseja lidar com rachaduras / texturas, ou renderizar uma cena 3D inteira, troque de palete). Além disso, se você estiver se limitando a várias cores ou usando cores completas.

Uma abordagem mais completa, usando shaders, seria realmente produzir imagens indexadas em cores com uma textura de palete. Faça uma textura, mas em vez de ter cores, tenha uma cor que represente um índice a ser pesquisado e, em seguida, tenha outra textura de cores que é a placa. Por exemplo, vermelho pode ser a textura t coordenada e verde pode ser a coordenada s. Use um shader para fazer a pesquisa. E Animar / Modificar as cores dessa textura de palete. Isso seria bem próximo do efeito retrô, mas funcionaria apenas para conteúdo 2D estático, como sprites ou texturas. Se você estiver criando gráficos retrô genuínos, poderá produzir sprites de cores indexados reais e escrever algo para importá-los e os paletes.

Você também vai querer descobrir se vai usar um palete 'global'. Assim como o hardware antigo funcionaria, menos memória para paletes, pois você só precisa de um, mas é mais difícil para produzir sua arte, pois precisa sincronizar o palete em todas as imagens) ou dar a cada imagem seu próprio palete. Isso daria mais flexibilidade, mas consumiria mais memória. É claro que você pode fazer as duas coisas e também usar paletes apenas para as coisas que você é ciclista em cores. Também calcule até que ponto você não deve limitar suas cores; os jogos antigos usariam 256 cores, por exemplo. Se você estiver usando uma textura, obterá o número de pixels, de modo que 256 * 256 lhe dará

Se você deseja apenas um efeito falso barato, como água corrente, você pode criar uma segunda textura que contenha uma máscara em escala de cinza da área que deseja modificar, depois use as texturas mastradas para modificar a tonalidade / saturação / brilho pela quantidade na textura de máscara em escala de cinza. Você pode ser ainda mais preguiçoso e apenas alterar a tonalidade / brilho / saturação de qualquer coisa em uma faixa de cores.

Se você precisasse dele em 3D completo, poderia tentar gerar a imagem indexada em tempo real, mas seria lento, pois você teria que procurar no palete, provavelmente também estaria limitado na variedade de cores.

Caso contrário, você pode simplesmente falsificá-lo no sombreador, se color == swapping color, troque-o. Isso seria lento e funcionaria apenas com um número limitado de cores de troca, mas não deve estressar nenhum hardware atual, especialmente se você apenas lida com gráficos 2D e sempre pode marcar quais sprites têm troca de cores e usam um sombreador diferente.

Caso contrário, realmente falsifique-os, faça um monte de texturas animadas para cada estado.

Aqui está uma ótima demonstração do ciclo de paletes feito em HTML5 .

David C. Bishop
fonte
0

Acredito que os ifs sejam rápidos o suficiente para que o espaço de cores [0 ... 1] seja dividido em dois:

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

Dessa maneira, valores de cores entre 0 e 0,5 são reservados para valores de cores normais; e 0,5 - 1,0 são reservados para valores "modulados", em que 0,5..1,0 é mapeado para qualquer intervalo selecionado pelo usuário.

Somente a resolução de cores é truncada de 256 valores por pixel para 128 valores.

Provavelmente, pode-se usar funções internas como max (color-0.5f, 0.0f) para remover os ifs completamente (trocando-os por multiplicações por zero ou um ...).

Aki Suihkonen
fonte