13

I'm trying to use LibGDX to make a retro-style little game, and I'd like to let the players to choose the colors of several characters, so I thought about loading png indexed images and then updating palettes programatically... How wrong I was ^^U

It seems that color pallettes are something of the past, and also seems that the best option to achieve a similar result is by using Shaders.

Here is an image explaining what I'm trying right now:

What I'm trying to do

My intention is to use 2 images. One of them, pixel_guy.png is a png image with only 6 colors (those colors are its original palette). The other image, colortable.png, would be a 6x6 pixel png that contains 6 palettes of 6 colors each (each row is a different palette). The colors from the first row of pixels of colortable.png would match the colors used in pixel_guy.png, that would be the first/original palette, and the other rows would be palettes 2 to 6. What I try to achieve is to use colortable's first palette to index pixelguy colors and then change the palette by sending a number (from 2 to 6) to the shader.

After doing some research, I found a post in gamedev stackexchange, and apparently it was what I was looking for, so I tried to test it.

I created the Vertex and Fragment Shaders and loaded my textures (the one whose palette I wanted to swap, and the one that contained several palettes for that), but the output is an unexpected white image.

My Vertex Shader:

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

My Fragment Shader:

    // Fragment shader
// Thanks to Zack The Human https://gamedev.stackexchange.com/questions/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;      
}

The code I used to make the texture binding and pass the palette number to the shader:

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

I Don't know if that's the correct way to pass a float value to an uniform of the fragment. Not sure either about how the code snippet I tried to use works.

Edit: I tried the changes suggested by TenFour04 and they worked flawlessly :)

Here is the pre-processed sprite

Pre-processed sprite

I have updated the repository with the changes, (Java code here, Fragment Shader here), in case somebody is interested it can be downloaded here: https://bitbucket.org/hcito/libgdxshadertest

Edit 2: I've just added to the repository the last optimization that TenFour04 adviced (to pass the palette index to each sprite within the R channel calling sprite.setColor() method), and again it worked perfect :)

Community
  • 1
  • 1
hcito
  • 153
  • 1
  • 5
  • No idea :/ I know my English is not very good, and that I am a bit clumsy and all, but I'm really trying to do my best here ^^U Thank you very much for helping me, Tenfour :) (By the way, I need 5 points more in reputation to upvote your answer ^^U) – hcito Oct 01 '14 at 18:34

1 Answers1

10

I noticed a few issues.

1) In your fragment shader, shouldn't vec2 index = vec2(color.r + paletteIndex, 0); be vec2 index = vec2(color.r, paletteIndex);, so the sprite texture tells you which row and the paletteIndex tells you which column of the color table to look at?

2) The palette index is being used as a texture coordinate, so it needs to be a number between 0 and 1. Set it like this:

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) A call to shader.end happens inside batch.end so you need to set your uniforms on every frame, not in create. Probably a good idea to also bind your secondary texture each frame as well, in case you do any multitexturing later.

@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's 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) The GPU might do this for you anyway, but since you aren't using vertex colors, you can remove the two lines involving v_color from your vertex shader.

5) You probably also want transparent pixels to stay transparent. So I would change the last line of your fragment shader to preserve alpha:

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

6) This one is probably the reason you were getting all white. In your fragment shader, you should be using v_texCoords instead of gl_TexCoord[0].xy, which is something from desktop OpenGL and not used in OpenGL ES. So your fragment shader should be:

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) Your source sprite needs to be pre-processed to map to the columns of your palette lookup table. Your shader is pulling a coordinate from the red channel of the texture. Therefore you need to do a color replacement of each pixel color in your source image. You can do this by hand ahead of time. Here's an example:

Skin tone is index 2 out of the six columns (0-5). So just like we calculated the paletteIndex, we normalize it to the center of pixel: skinToneValue = (2+0.5) / 6 = 0.417. The 6 is for the six columns. Then in your drawing program, you need the appropriate value of red.

0.417 * 255 = 106, which is 6A in hex, so you want color #6A0000. Replace all the skin pixels with this color. And so on for the other hues.


Edit:

One more optimization is that you probably want to be able to batch all your sprites together. The way it is now, you will have to group all your sprites for each palette separately and call batch.end for each of them. You can avoid this by putting paletteIndex into the vertex color of each sprite, since we weren't using vertex color anyway.

So you could set it to any of the four color channels of the sprite, let's say the R channel. If using the Sprite class, you can call sprite.setColor(paletteIndex, 0,0,0); Otherwise call batch.setColor(paletteIndex,0,0,0); before calling batch.draw for each of the sprites.

The vertex shader will need to declare varying float paletteIndex; and set it like this:

paletteIndex = a_color.r;

The fragment shader will need to declare varying float paletteIndex; instead of uniform float paletteIndex;.

And of course you no longer should call shader.setUniformf("paletteIndex", paletteIndex);.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thank you very much for answering :) I tried the changes you told me but it still shows a white rectangle. Right now I'm out of time but I'll be able to test it more thoroughly tomorrow. Anyway, if you want you can have a look at the repository (I may have screwed something when making the changes) – hcito Oct 01 '14 at 18:24
  • One mistake I made is that the glActiveTexture call needs to be before `batch.end`. I missed seeing the funky way you are getting uvs. Also, I see from your bitbucket that your source sprite has not been encoded. I'll update answer. – Tenfour04 Oct 01 '14 at 19:09
  • Also just noticed I was using getWidth instead of getHeight for calculating the palette number, which is wrong because your palettes are rows, not columns. – Tenfour04 Oct 01 '14 at 19:19
  • One more thing. Since UV coordinates are from the bottom up, palette 0 is the bottom row, and palette 5 is the top row. Or if you prefer top to bottom, you can subtract `paletteIndex` from 1 to flip it. – Tenfour04 Oct 01 '14 at 19:25
  • It worked like a charm, thank you very much! :D I added the changes to the [test repository](https://bitbucket.org/hcito/libgdxshadertest), just in case anybody else is interested in palette swapping ;) – hcito Oct 02 '14 at 20:40
  • Note the additional optimization I just added at the bottom, so you don't have to sort your sprites by palette index. You can combine all of them into one batched draw call. – Tenfour04 Oct 06 '14 at 14:43
  • I cannot thank you enough! It's certainly a great optimization! :D I'll try this as soon as I can, and then will update the repository :) Thank you very much! :D – hcito Oct 06 '14 at 21:49
  • I've been trying but I'm still not able to make last optimization work ^^U (the changes I made are updated in the [repository](https://bitbucket.org/hcito/libgdxshadertest/)) I used 2 sprites, but no matter which paletteindex value I assigned to them, they both always showed the last palette. Could this be related to how the value of the R channel is stored/interpreted? I'll continue trying tomorrow, but I'm not pretty sure about how to proceed :S is there a way of checking values in varyings from shaders? Something like a log or similar? – hcito Oct 07 '14 at 23:52
  • I looked at your code. I'm not aware of the method `spriteBatch.draw(SpriteBatch, int, int)`. Are you using an older version of libgdx? Usually Sprite objects are drawn the other way around: `sprite.draw(SpriteBatch)`. So maybe you're using an old version of libgdx that doesn't apply sprite colors when they are drawn that way? I don't know of any way to debug values within a shader. – Tenfour04 Oct 08 '14 at 01:21
  • Wait...I found the issue. In your vertex shader, you need to change `v_color = a_color;` to `paletteIndex = a_color.r;`. You can remove the line that declares `varying vec4 v_color;`. – Tenfour04 Oct 08 '14 at 01:27
  • I corrected those two things and now it works perfect :D It seems that `spriteBatch.draw(SpriteBatch, int, int)` didn't work as I thought it would. Thank you very much for the great and continuous help! :) – hcito Oct 08 '14 at 18:10
  • @hcito the code in ur test repositry works fine for me but i want to shade a sprite if car having 4 colors and transparent background in it how can i make colortable can u help me out as i have some issues regarding this. the link of image i want to shade is https://www.dropbox.com/s/yrwnrbv05k6ztsa/img_car.png?dl=0 – rana_sadam May 05 '15 at 08:22
  • Hi, sorry for the late response. I can't see your file, could you upload again? I'll try to help if I can. – hcito May 15 '15 at 23:54