Palette Swap Shader - Issues Using R-Channel as an Index

Hi all,

So I’m trying to get palette swapping working in my engine, and I’m having trouble with the shader.

My approach is this:

  1. I process a PNG image into both a 1-Dimensional palette image with each unique color, as well as an “indexed” version of the image where the R-Channel of the image corresponds to index of that color.

Example:
The palette:
nova_defaultpalette

The indexed image:
nova_indexed

The expected result:
nova

  1. In my shader, I pass in both the “SpriteTexture” (The “indexed” image referenced above), and the “PaletteTexture” (The palette image referencd above), and at each pixel of the Sprite Texture, I get the value of the R channel from this pixel, and use it as an index on the Palette Texture to get the color at that location. Or I guess I should say theoretically, because it’s not working.

What I end up getting looks like this:

Kind of just looks like the mapping is just off by a bit, but what’s strange is that most of the colors in that image on the right don’t actually exist in the original palette. I also should note that I have manually confirmed that the mapping is correct by creating a Texture2D and remapping the color data manually in code.

I’ve tried using the PaletteTexture object as a Texture1D and just passing in “color.r”. I’ve tried it as a Texture2D and pass in either “float2(color.r, 0)” or “int2(color.r, 0)” but to no avail.

Below is the entirety of the shader code:

#if OPENGL
	#define SV_POSITION POSITION
	#define VS_SHADERMODEL vs_3_0
	#define PS_SHADERMODEL ps_3_0
#else
	#define VS_SHADERMODEL vs_4_0_level_9_1
	#define PS_SHADERMODEL ps_4_0_level_9_1
#endif


bool isActive;


Texture2D SpriteTexture;
sampler2D SpriteTextureSampler = sampler_state
{
	Texture = <SpriteTexture>;
};



Texture1D PaletteTexture;
sampler1D PaletteTextureSampler = sampler_state
{
	Texture = <PaletteTexture>;
};

int PaletteTextureWidth;


struct VertexShaderOutput
{
	float4 Position : SV_POSITION;
	float4 Color : COLOR0;
	float2 TextureCoordinates : TEXCOORD0;
};

float4 MainPS(VertexShaderOutput input) : COLOR
{
	float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates);
	if (isActive && color.a > 0)
	{	
		color = tex1D(PaletteTextureSampler, color.r);
	}
	return color;
}

technique SpriteDrawing
{
	pass P0
	{
		PixelShader = compile PS_SHADERMODEL MainPS();
	}
};

What I find particularly bizarre is that if I make changes to this which are syntactically different but functionally identical.

E.g., If I the index to a variable and passing that variable in to the tex1D constructor vs. putting the expression itself in the constructor), I get totally different behavior.

		int index = color.r;
		color = tex1D(PaletteTextureSampler, index);

^ Once again, the color on the right is not in the original palette.

Thanks for any and all help.

EDIT: Thought I’d add a small snippet of the C# code as well:

            paletteEffect.IsActive().SetValue(activePalette != null);
            paletteEffect.Parameters["SpriteTexture"].SetValue(novaFaceIndexed.OutputTexture);
            paletteEffect.Parameters["PaletteTexture"].SetValue(activePalette);
            paletteEffect.CurrentTechnique.Passes[0].Apply();
            spriteBatch.DrawImage2D(novaFaceIndexed);

Does the problem also happen when adding

Filter = Point;

to the texture samplers?

Thanks for the suggestion!

I gave that a try and this was the result: It looks different than previous attempts which is promising at least. Once again though, a couple of the shades of blue on the right aren’t in the original palette which is baffling to me.

float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates);
color = tex1D(PaletteTextureSampler, color.r);

and

int index = color.r;
color = tex1D(PaletteTextureSampler, index);

are ofc not identical, in second case you truncate float to int, hence in your case 0. Post all your assets. It seems like either gradient or source is wrong.

I think another potential problem could be that the palette should be 256 pixels long. If you have a shorter (e.g. 16 color) palette as texture, when you read the pixels a shorter LUT will “compress” the colors (0-15 will be 0, 16-31 will be 1). That’s probably why your image looks two-color and those two colors look like the first colors of the LUT.

3 Likes

Got it working with both of your suggestions @KakCAT - THANK you!

I had no idea the palette needed to be 256 pixels wide - I’m quite new to shaders :slight_smile:

I can now palette and repalette accordingly:

For anyone coming across this post, what I did to fix this was:

  1. Update my palette generator to always generate a 256 pixel-wide image. I still put the colors in the same relative indexes, so in this case there are just an extra 222 empty pixels on the right side of the image.
  2. Change my sampler_state declarations to include “Filter = Point”

My final shader code:
(you can ignore that “isActive” parameter, I use that with my engine to simplify toggling effects on a per-object basis)

#if OPENGL
	#define SV_POSITION POSITION
	#define VS_SHADERMODEL vs_3_0
	#define PS_SHADERMODEL ps_3_0
#else
	#define VS_SHADERMODEL vs_4_0_level_9_1
	#define PS_SHADERMODEL ps_4_0_level_9_1
#endif


bool isActive;



Texture2D SpriteTexture;
sampler2D SpriteTextureSampler = sampler_state
{
	Texture = <SpriteTexture>;
	Filter = Point;
};



Texture1D PaletteTexture;
sampler1D PaletteTextureSampler = sampler_state
{
	Texture = <PaletteTexture>;
	Filter = Point;
};


struct VertexShaderOutput
{
	float4 Position : SV_POSITION;
	float4 Color : COLOR0;
	float2 TextureCoordinates : TEXCOORD0;
};

float4 MainPS(VertexShaderOutput input) : COLOR
{
	float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates);
	if (isActive && color.a > 0)
	{	
		color = tex1D(PaletteTextureSampler, color.r);
	}
	return color;
}

technique SpriteDrawing
{
	pass P0
	{
		PixelShader = compile PS_SHADERMODEL MainPS();
	}
};
2 Likes

I’m glad I could be of help. :slight_smile:

Actually it’s not that you need a 256 color texture. Is that in the way you have the image texture, a 256 color texture is needed to correctly.

In example, you could keep the 16 or 32 palette texture and “rearrange” the indices of the texture (if it’s 16 color, instead of using 0,1,2 as indices in the main texture, use 0,16,32,… instead)

However 256x1 texture is cheap and probably not worth the hassle.

I coded a shader that is kind of similar to yours. Someone on this board gave me an interesting idea that I will pass on to you. Have you thought about just putting the colors in an array and passing it to your shader?