The X5-Framework is a Unity-based Framework to create online conventions, made at Super Crowd. For the framework, I wrote the general-purpose shader and art-pipeline.
Palette/Color LUT Shader
The X5-Framework has sprites. A lot of sprites! And our art and level departments have even more desire to reuse these sprites in all various occasions. The shader I’ve written takes a sprite in a certain color scheme and uses a palette texture to map these to the desired colors. This allows us to reuse all sprites and recolor them according to the setting of a level or booth.
The scheme uses the R and B channels to point to the texture coordinate of the palette. (R)ed is the brightness or palette Y-axis, (G)reen is the outline overlay and (B)lue is the palette color index or palette X-axis. The reason I chose R and B instead of R and G is that the DXT1/5 compressions give the green channel an extra bit (DXT1/5 basically encodes colors in RGB565). Our art style relies on flat-colored areas and bold outlines and since the outlines draw more visual attention (also as part of the silhouette), I wanted them to be extra crisp.
This approach has some errors though. Artists need to be careful arranging their palette in a way that areas that are close to each other in the sprite are also close in the palette, or the interpolated color coordinate will result in a color that is not intended. This happens a lot, but is mostly barely noticeable. But for when it does happen, there is a version that does a manual bilinear interpolation and applies the palette to each of the texel individually.
Note that the shader also supports implicit sorting for isometric view. I’ve covered this in a twitter thread a while ago.
Sprite to LUT Sprite
When we built the MAG online and first introduced the palette shader, our artists where drawing the sprites using a special color palette. This was teadious for them, so we decided to build a tool that would convert a normal-colored sprite into the palette-ready version. I first considered writing a custom tool in Rust for this, but noticed that it would be much smarter to do it within Unity and Burst, due to usuability (stay within the editor) and platform independence (some of our artist are on Mac).
For each sprite, the tool takes the color version and the outlines as two separate files, because it is sometimes hard to distinguish between the (mostly) black outline and something that is intended to be black. The tool will try to map each non-transparent pixel to a color in the palette. The coordinate of the closest color inside the palette will be used and written into the new palette-ready sprite.
Sometimes, especially within gradients between two colors, a pixel happens to be closer to an entirely different color and will ruin the entire image. To filter these out, in a second pass, each pixel will receive the color of the most common pixel in its neighbourhood (3x3). This helps a lot, but in retrospective, this pass should have been done twice, or the assets should have been created without any antialiasing at all, but some of them had already been produced in this way.
Alpha without Alpha Channel
This was inspired by looking at the RenderDoc capture of “Ori and the Blind Forest”. All of their textures have no alpha channel, but instead use a chroma-key (think green-screen, example here). This approach basically halves the amount of VRAM (and disk space) for each texture, since you can use DXT1/BC1 texture compression (0.5 bytes per pixel) instead of DXT5/BC5 (1 byte per pixel) while not cutting down hard on quality.
I didn’t have the time to build an auto chroma-key replacement. My dirty approach was to use black (0,0,0 RGB) as the key “chroma” and use a custom Unity texture imported to store every color information within 128-255 (but only for R and G, since the blue channel will get an extra quality dip by brightness weighting in the texture compressor). Every color below 127 would be considered transparent within the shader. This even slightly improved quality compared to our previous DTX5/BC5 + Crunch compression approach.
Methode | Build Size (Standalone) | Sprite atlas build time* | GPU-Mem.** per atlas page |
---|---|---|---|
DXT5 + Crunch Q100 | 33,5 MB | 4:43 Min. | 5.3 MB |
DXT1 + Implicit Alpha | 33,3 MB (-1%) | 1:56 Min. (-60%) | 2.7 MB (-50%) |
*3891 Sprites, Unity 2020.2.3f
** While Crunch compression reduces the disk space for a texture, it can’t reduce its size on the GPU.