I got really excited about doing palette-based sprite rendering. I’ve never done this before and it sounded like a fun challenge — not too complex, not too time-consuming, but also not something I know exactly how to do. So I decided to do it after all. Although, once I searched for some info on how people have done it, I realized that it’s all pretty straight-froward.
A palette like I imagine is accomplished with a shader. This shader is given a palette lookup and it replaces any colors it encounters with a corresponding color from the palette. But I can’t feed a shader non-standard data and so the lookup table is usually in a form of texture. This texture encodes the colors in some way across its area. Technically, in short, the way this works is that I calculate a single value in the 0-255 range from whatever color each spite pixel is, for example, using the red channel. I then use a 256 x 1 texture to look up that pixel value by location along a custom texture line and use that color instead:
Of course, this can be a 2048×1 texture or 16×1 or 256×256, all depending on how I encode values. But I am really unlikely to need some granularity. The difference between 256 colors is fairly minimal and I wouldn’t need such granularity. I would definitely not need all 256 colors, I will probably have some 5 or something. That said, I might have some colors that do not get converted — which means, they get converted into themselves.
Anyway, let me make a shader that derives from my custom-tintable RetroAA-based shader that takes a palette texture:
The actual code is just taking the input pixel, grabing the red channel value and looking up the color at the palette texture’s position. If I just apply the shader as is, I get this sort of result:
This is because my current sprite’s red channel values are pretty much all in range of the first lookup block — red. Some strips along the wall sides are actually blue because the sprite has lighter/redder parts there.
To test my shader properly, I used the red channel value from the source sprite, which I recolored to have unique red values:
For these values, it doesn’t matter what green and blue channels are — they are just for me to tell the colors apart:
And if I use a custom “palettable” texture with some similar colors (zoomed in and cropped here):
Then the end result looks something like this:
Of course, I am picking colors at random here, which I don’t want to do.
So, of course, we make a new asset:
I can add a custom color selection for my palette color translation, for example:
I can show the exact RGB colors I pick and what values they produce and the corresponding color they output. So I can use these colors to make the sprites. I can also just print-screen this, put in Photoshop and pick the colors to use for the tiles. These colors will then safely convert into their paletted versions.
I’m not going to use red channel, because that limits the colors I can use for sprites. Instead, I’ll average the three channels.
For debug, I need to see the ranges I have chosen that will go into the final palettable texture:
In fact, now I can just make it and also show in the editor:
It’s pretty obvious the colors I chose aren’t very spaced out in terms of their RGB average channel values, which isn’t saying a lot. And also it doesn’t really matter as long as each color has the right pixel. In fact, I don’t really need ranges — just the exact pixel at the exact location. But this is prettier and clearer.
If I wire the palette texture directly in-game, I get the tileset styles this way:
of course, I need multiple palettes (or what’s the point?) to translate this into using the proper material for right game world objects based on what palette a tile wants (based on its room’s palette, based on the theme’s room designation). There a long chain of “passing the palette through”.
The “data” part is where I specify the palette definition in the theme’s style entries (a unique palette for each “location”):
I also show a little preview, because I like making previews.
On the world-object side, I need functionality to actually assign the correct material to the sprite renderers, so I made a script for that that the world objects (tiles) can use:
I can’t make a new material for each sprite renderer, and I have to reuse materials for performance, which means I need a “global” place which creates and reuses materials based on the texture.
So a palette makes a lookup texture based on its color specs, then material provider uses that texture for a material, then material applicator asks for the right material for its tile’s palette’s texture, and places it on the sprite renderers.
And the result is very… striking:
Each of these rooms (blue-room; green-hall; orange-default) now has the palette correctly applied from the level’s theme. Yay!
This actually took less time and effort than I expected. I was worried this would be a troublesome feature, but in reality palette lookups are a common thing people do and it’s not that difficult with some planning. And if this all goes really badly, I can just disable palette code in the shader and it all goes back to un-paletted versions.
Now I have to make sure my sprites have the right limited default palette colors. I also imagine that I want to reuse these colors, but not re-specify them in each palette definition. So I need a base palette, which is just the current palette but only with the input values:
And the actual palettes have to specify the base palette and will use those exact input colors and can only specify output colors:
Now just to recolor all my tiles (temporarily):
This is, of course, a giant mess. I imagine that for real art I will select some nice colors as base colors. For now I care that it works for all sprites and recolors them correctly, even if actual colors make no sense:
And that’s pretty much it. I now have palettes to style individual rooms in the dungeon, while reusing the shapes/geometry. I can use a red brick wall for halls in one level and same but gray brick wall for rooms in another. And I have bright annoying colors I now have to endure. Actually, let me significantly tone them down significantly before I have a stroke while working on this further: