I wrote a shader to replace the palette of a sprite with another palette. Is this the best way? Any way to optimize?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By alfredbaudisch

I wrote a simple canvas_item shader that gets an input base palette (the original palette of the sprite) and a new palette (the replacement palette) and correctly replaces the original colors with the new colors.

With this shader I don’t need to create any sprite masks, I can use the original sprites.

Sprite + base palette:
enter image description here
enter image description here

Tile created by https://gumroad.com/mnrart.

New palette + sprite with colors replaced (shader running) - don’t mind the saturated colors, I chose them to be easier to see the shader at work:
enter image description here
enter image description here

To be able to find the replacement color, for every pixel, I search the original color with a custom function called find_color_in_base_palette, which loops the base_palette looking for the color (the function returns the x position of the color). Then I read the color from the new_palette that is in the same pixel position.

  1. I am happy with the end result (It works!), but I am not totally comfortable with using a loop to look for the base color for EVERY pixel of the sprite. Is there any better way to do it?
  2. I am not sure about the number that comes out of textureSize(base_palette, 0). In the loop I iterate from 0 up to the x textureSize: for(float x = 0.0; x <= pal_size; x += pixel_size). Since there is no way to debug values, I wonder what exact value comes from textureSize?

The shader:

shader_type canvas_item;
render_mode blend_mix;

uniform sampler2D base_palette;
uniform sampler2D new_palette;

float find_color_in_base_palette(in vec4 color, float pal_size, float pixel_size) {
	for(float x = 0.0; x <= pal_size; x += pixel_size)	 {		
		vec4 pal_col = texture(base_palette, vec2(x, 0.0));
				
		if(pal_col.rgba == color.rgba) {
			return x;
		}
	}
	
	return -1.0;
}

void fragment() {
	vec4 color = texture(TEXTURE, UV);
	ivec2 size = textureSize(base_palette, 0);
	float pos = find_color_in_base_palette(color, float(size.x), TEXTURE_PIXEL_SIZE.x);	
	
    // We found the position of the color in the base palette, so fetch a new color from the new palette
	if(pos != -1.0) {
		COLOR = texture(new_palette, vec2(pos, 0.0));
	}
    // The color is not in the base palette, so we don't know its position. Keep the base color.
    else {
		COLOR = color;
	}	
}
:bust_in_silhouette: Reply From: glenneric1

You might use a hash function to convert each input color to a single number. Use modulus to trim that down to a relatively small size, let’s say big enough to hold several times more entries than you need. Then use that new number as an index to the new palette.

Also, you could use the natural adjacent pixel similarities to begin each new search at the same position as where the last search ended. A green is mostly likely to be found just after another green, for instance.

:bust_in_silhouette: Reply From: alfredbaudisch

DO NOT USE THE SHADER THAT I POSTED. It is very inefficient for a game.

Instead, there is a very simplified way of palette swapping in Godot, as explained in this short video:

Note: my shader still has some uses if you want to change the colors of just a few sprites without changing the sprite itself and without creating masks.