Имитация подстановок палитры с помощью шейдеров OpenGL (в LibGDX)

Я пытаюсь использовать LibGDX для создания маленькой игры в стиле ретро, ​​и я бы хотел, чтобы игроки выбирали цвета нескольких символов, поэтому я подумал о загрузке индексированных изображений png и затем программном обновлении палитр... Как я ошибался ^^ U

Кажется, что цветные паллеты - это что-то в прошлом, и также кажется, что лучший вариант для достижения аналогичного результата - использование шейдеров.

Вот изображение, объясняющее, что я пытаюсь сейчас:

What I'm trying to do

Я намерен использовать 2 изображения. Один из них pixel_guy.png - это png-изображение с 6 цветами (эти цвета являются его оригинальной палитрой). Другое изображение colortable.png было бы размером 6x6 пикселей, которое содержит 6 палитр по 6 цветов каждая (каждая строка представляет собой другую палитру). Цвета из первой строки пикселей colortable.png будут соответствовать цветам, используемым в pixel_guy.png, это будет первая/оригинальная палитра, а в других строках будут палитры с 2 по 6. То, что я пытаюсь достичь, - это использовать цветную первую палитру, чтобы индексировать пиксельные цвета, а затем изменить палитру, отправив число (от 2 до 6) в шейдер.

После некоторого исследования я нашел сообщение в gamedev stackexchange, и, видимо, это то, что я искал, поэтому я попытался его протестировать.

Я создал шейдеры Vertex и Fragment Shaders и загрузил мои текстуры (ту, чью палитру я хотел поменять, и та, которая содержала несколько палитр для этого), но выход представляет собой неожиданное белое изображение.

Мой вершинный шейдер:

attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;

uniform mat4 u_projTrans;

varying vec4 v_color;
varying vec2 v_texCoords;

void main() {
    v_color = a_color;
    v_texCoords = a_texCoord0;
    gl_Position = u_projTrans * a_position;
}

Мой фрагмент шейдера:

    // Fragment shader
// Thanks to Zack The Human https://gamedev.stackexchange.com/info/43294/creating-a-retro-style-palette-swapping-effect-in-opengl/

uniform sampler2D texture; // Texture to which we'll apply our shader? (should its name be u_texture?)
uniform sampler2D colorTable; // Color table with 6x6 pixels (6 palettes of 6 colors each)
uniform float paletteIndex; // Index that tells the shader which palette to use (passed here from Java)

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

Код, который я использовал, чтобы привязать текстуру и передать номер палитры в шейдер:

package com.test.shaderstest;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;

public class ShadersTestMain extends ApplicationAdapter {
    SpriteBatch batch;

    Texture imgPixelGuy;
    Texture colorTable;

    private ShaderProgram shader;
    private String shaderVertIndexPalette, shaderFragIndexPalette;

    @Override
    public void create () {
        batch = new SpriteBatch();

        imgPixelGuy = new Texture("pixel_guy.png"); // Texture to which we'll apply our shader
        colorTable =  new Texture("colortable.png"); // Color table with 6x6 pixels (6 palettes of 6 colors each)

        shaderVertIndexPalette = Gdx.files.internal("shaders/indexpalette.vert").readString();
        shaderFragIndexPalette = Gdx.files.internal("shaders/indexpalette.frag").readString();

        ShaderProgram.pedantic = false; // important since we aren't using some uniforms and attributes that SpriteBatch expects

        shader = new ShaderProgram(shaderVertIndexPalette, shaderFragIndexPalette);
        if(!shader.isCompiled()) {
            System.out.println("Problem compiling shader :(");
        }
        else{
            batch.setShader(shader);
            System.out.println("Shader applied :)");
        }

        shader.begin();
        shader.setUniformi("colorTable", 1); // Set an uniform called "colorTable" with index 1
        shader.setUniformf("paletteIndex", 2.0f); // Set a float uniform called "paletteIndex" with a value 2.0f, to select the 2nd palette 
        shader.end();

        colorTable.bind(1); // We bind the texture colorTable to the uniform with index 1 called "colorTable"
    }

    @Override
    public void render () {
        Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        batch.begin();
        batch.draw(imgPixelGuy, 0, 0); // Draw the image with the shader applied
        batch.end();
    }
}

Я не знаю, правильно ли это передать значение поплавка в единицу фрагмента. Не уверен ни о том, как фрагмент кода, который я пытался использовать, работает.

Изменить: Я попробовал изменения, предложенные TenFour04, и они работали безупречно:)

Вот предварительно обработанный спрайт

Pre-processed sprite

Я обновил репозиторий с изменениями (Java-код здесь, Fragment Shader здесь), в случае, если кому-то это интересно, его можно скачать здесь: https://bitbucket.org/hcito/libgdxshadertest

Изменить 2: Я только что добавил в репозиторий последнюю оптимизацию, которую консультировал TenFour04 (передать индекс палитры каждому спрайту в R-канале, вызывающем метод sprite.setColor()), и снова он работал отлично:)

Ответ 1

Я заметил несколько проблем.

1) В вашем шейдере фрагмента не должно vec2 index = vec2(color.r + paletteIndex, 0); быть vec2 index = vec2(color.r, paletteIndex);, поэтому текстура спрайта сообщает вам, какая строка и paletteIndex сообщает вам, какой столбец таблицы цветов посмотреть?

2) Индекс палитры используется в качестве координаты текстуры, поэтому он должен быть числом от 0 до 1. Установите его так:

int currentPalette = 2; //A number from 0 to (colorTable.getHeight() - 1)
float paletteIndex = (currentPalette + 0.5f) / colorTable.getHeight();
//The +0.5 is so you are sampling from the center of each texel in the texture

3) Вызов shader.end происходит внутри batch.end, поэтому вам нужно установить форму на каждом кадре, а не в create. Вероятно, хорошая идея также привязать вашу вторичную текстуру к каждому кадру, если вы еще немного мультитекстурируете позже.

@Override
public void render () {
    Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

    colorTable.bind(1);

    //Must return active texture unit to default of 0 before batch.end 
    //because SpriteBatch does not automatically do this. It will bind the
    //sprite texture to whatever the current active unit is, and assumes
    //the active unit is 0
    Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0); 

    batch.begin(); //shader.begin is called internally by this line
    shader.setUniformi("colorTable", 1);
    shader.setUniformf("paletteIndex", paletteIndex);
    batch.draw(imgPixelGuy, 0, 0);
    batch.end(); //shader.end is called internally by this line

}

4) Графический процессор может сделать это для вас в любом случае, но поскольку вы не используете цвета вершин, вы можете удалить две строки с v_color из вашего вершинного шейдера.

5) Вероятно, вы также хотите, чтобы прозрачные пиксели оставались прозрачными. Поэтому я бы изменил последнюю строку вашего шейдера фрагмента, чтобы сохранить альфа:

gl_FragColor = vec4(indexedColor.rgb, color.a); 

6) Вероятно, это причина, по которой вы все белили. В вашем шейдере фрагмента вы должны использовать v_texCoords вместо gl_TexCoord[0].xy, что-то из настольного OpenGL и не используется в OpenGL ES. Итак, ваш шейдер фрагмента должен быть:

uniform sampler2D texture; // Texture to which we'll apply our shader? (should its name be u_texture?)
uniform sampler2D colorTable; // Color table with 6x6 pixels (6 palettes of 6 colors each)
uniform float paletteIndex; // Index that tells the shader which palette to use (passed here from Java)
varying vec2 v_texCoords;

void main()
{
    vec4 color = texture2D(texture, v_texCoords);
    vec2 index = vec2(color.r, paletteIndex);
    vec4 indexedColor = texture2D(colorTable, index);
    gl_FragColor = vec4(indexedColor.rgb, color.a); // This way we'll preserve alpha      
}

7) Исходный спрайт должен быть предварительно обработан для сопоставления с столбцами таблицы поиска палитры. Ваш шейдер вытягивает координату из красного канала текстуры. Поэтому вам нужно сделать замену цвета каждого цвета пикселей в исходном изображении. Вы можете сделать это вручную заранее. Вот пример:

Тон кожи - это индекс 2 из шести столбцов (0-5). Так же, как мы вычислили paletteIndex, мы нормализуем его до центра пикселя: skinToneValue = (2+0.5) / 6 = 0.417. 6 для шести столбцов. Затем в вашей программе рисования вам нужно соответствующее значение красного цвета.

0.417 * 255 = 106, который равен 6A в шестнадцатеричном виде, поэтому вы хотите цвет # 6A0000. Замените все пиксели кожи этим цветом. И так далее для других оттенков.


Edit:

Еще одна оптимизация заключается в том, что вы, вероятно, захотите объединить все ваши спрайты. Теперь вы должны сгруппировать все ваши спрайты для каждой палитры отдельно и вызвать batch.end для каждого из них. Вы можете избежать этого, поместив paletteIndex в цвет вершины каждого спрайта, так как мы все равно не использовали вершинный цвет.

Таким образом, вы можете установить его на любой из четырех цветных каналов спрайта, скажем, на канал R. Если вы используете класс Sprite, вы можете вызвать sprite.setColor(paletteIndex, 0,0,0); В противном случае вызовите batch.setColor(paletteIndex,0,0,0); перед вызовом batch.draw для каждого из спрайтов.

Вершинный шейдер должен объявить varying float paletteIndex; и установить его следующим образом:

paletteIndex = a_color.r;

Фрагмент-шейдер должен будет объявить varying float paletteIndex; вместо uniform float paletteIndex;.

И, конечно, вы больше не должны называть shader.setUniformf("paletteIndex", paletteIndex);.