Index Palette Shader

Index Palette Shader
A few months ago I made and released a game jam game that featured a cool shader based off of Dan Fessler's HD Index Painting tutorial. The idea is to take a normal image and render it with an extremely limited palette. Essentially it's like a game boy shader, and with the help of a dither map texture it actually creates the illusion that there are more colors than there actually are.


The game jam version of the shader was rather clunky, and required the game to manually render a dither texture over the entire screen. Yesterday I spent some time figuring out how to apply the dither directly in the shader itself to make it more general purposed. Here's the entire shader:
#version 130
uniform sampler2D texture; // The main input texture (the screen.)
uniform sampler2D palette; // The palette texture.
uniform float shift; // The shift amount on the palette texture.
uniform float offset; // The offset for the random noise generation.
uniform float screenScale; // The current scale of the screen. (1x, 2x, etc)
uniform vec2 screenSize; // The size of the core game screen (320 x 240)
uniform float noiseAlpha; // The amount of alpha the noise should have.

// A weird way to generate a random number with a vec2 seed.
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);

void main() {
// The size of the game window.
vec2 screenSizeScaled = screenScale * screenSize;
// The pixel coordinate being operated on.
vec2 pixpos = gl_TexCoord[0].xy;

// Get dither pixel
vec2 overlayCoord = floor(gl_FragCoord.xy / screenScale);
// Get 1 or 0 based on the pixel location.
float overlayPixelColor = mod(overlayCoord.x + overlayCoord.y, 2);
// Dither is black and white every other pixel.
vec4 overlayPixel = vec4(overlayPixelColor, overlayPixelColor, overlayPixelColor, 1);

// Scale the frag position to match the screen scale
vec2 scaledpos = floor(pixpos * screenSizeScaled);
// Adjust the position based on the scale of the screen.
scaledpos -= mod(scaledpos, screenScale);
// Convert back to 0 - 1 coordinate space.
scaledpos /= screenSizeScaled;

// Get base color.
vec4 pixcol = texture2D(texture, pixpos);

// Mix dither texture.
pixcol = mix(pixcol, overlayPixel, 0.1);

// Determine the brightness of the pixel in a dumb way.
float gray = (pixcol.r + pixcol.g + pixcol.b) / 3;

// Round it to the nearest 0.25.
gray = round(gray / 0.25) * 0.25;

// Add some noise.
gray += (rand(scaledpos + offset) * 2 - 1) * noiseAlpha;

// Map the palette to the pixel based on the brightness and shift.
pixcol = texture2D(palette, vec2(gray, shift));

// Multiply through the gl_Color for final output.
gl_FragColor = pixcol * gl_Color;

The shader does require a 2d texture for the palette itself. For this game the palette I used was this:


The "shift" uniform in the shader determines the Y coordinate to sample the palette on. A shift of 0 will sample the top most Y, and a shift of 1 will sample the bottom most. You can use this to dynamically change the palette during the game.

The shader needs to have the "screenScale" uniform set to the current scale of the screen. This will make sure that the pixel size is corrected for the dither and the noise. The shader also needs to know the "screenSize" in order for the noise pixels to be the correct size. You can also just set the "noiseAlpha" to 0 if you don't want any of that stuff to show up.

To make the noise change every update the "offset" value should be set to a random float 0 - 1 every update.

Here's what the game looks like without the shader:


And turning on the shader:


new comment!

Post your comment!