binary nightmare #5

Rainbow Bridge

Shaders have been the cornerstone for GPU programming for some time, so it was inevitable that we’d end up developing our own to reach the visual look and feel that we’re after. As is often the case - different engines, have different processes for shader and material development. Unity is no stranger in this regard, in order to ease some of the development we opted to use a hybrid approach. One that would enable Dan to visualize his ideas in a way he’s comfortable with in order to communicate them to myself. We opted to use Shader Forge to aid our development, opposed to me hand rolling everything - this would save time and share the workload. I could focus more directly on refining Dan’s idea’s either directly via shader forge, or through the use of Shader Lab and/or Cg.

We have some interesting requirements - our game focus’ heavily on visual customization. We have some baselines achievements that we’re very keen to solve early, specifically in regards to paint job customization, and establishing the art pipeline. We know that we’re using PBR (Physically Based Rendering, not Pabst Blue Ribbon), thankfully Unity and Shader Forge handle most of this for us, meaning that we need only concern ourselves with the texture data and how that is built up.

For the current faction, we have four material types, paint, matte paint, metallic two-tone (much like a car paint), and metal (more specifically for rusted details to give that authentic Rat-Rod look). I’m only going to cover the basics of how these shaders have been constructed, specifically the simple paint.

Game development relies heavily upon iteration and shaders certainly benefit from iterative development. As a result, there is a feedback loop that hooks back into itself as improvements are made, visual artefacts addressed, and texture usage optimized.

First steps…

Initially, Dan sat with shader forge, and pieced together a very basic shader that relied upon the texture data that he’d baked in its rawest form - this made for a lot of individual texture maps, which means a lot of texture taps, and consequently, the shaders performance was far from optimal, but it did allow for the communication of the general concept that he had in mind and give a base starting point.

The list of textures at this stage, and their purposes:

Albedo Map

Basic unlit colour information.

Specular Map

Specular reflectivity and colour.

Gloss Map

Reflectivity and glossiness of the surface.

Ambient Occlusion Map

Soft shadows (particularly in corners)

Emissive Map

Indication of light emissions from a surface

Mask Map

Paint colouration.

Normal Map

Information about surface detail.

That’s quite a lot of individual textures, as we decided early on that we’d maintain high-resolution textures for gamers who have the hardware, this is, in my opinion, a crazy use of texture space. The raw textures, at 4K, weigh in at 64 megabytes each, that’s the source TGA images without any compression. That’s 448 megabytes on disk, and we already knew that some of our ships would require more textures than this just due to their general scale. It’s also important to note that we have a fair few unique chassis planned. With this approach we’d be in the 10’s of gigabytes in source data very soon, this wouldn’t be acceptable just from a storage perspective. Even using TGA’s RLE, we’d only see sporadic savings on disk, in the region of 30% per file.

We also have runtime memory usage issues, the compressed results range between 10.7(DXT1) - 21.3 megabyte(DXT5), we were still in the realms of excessive memory consumption within the game. Whilst the options exists to lower the resolution, this all feels a bit wrong and wasteful.

Let's take a closer look at these textures

Albedo

RGB Texture

DXT 1 Compression

10.7 MB

Specular Map

RGB Texture

DXT 1 Compression

10.7 MB

Gloss Map

RGB Texture

DXT 1 Compression

10.7 MB

Ambient Occlusion Map

RGB Texture

DXT 1 Compression

10.7 MB

Emissive Map

RGB Texture

DXT 1 Compression

10.7 MB

Mask Map

RGBA Texture

DXT 5 Compression

21.3 MB

Normal Map

RGB

DXT 1 Compression

10.7 MB

That’s 85.5 MB, that may seem more reasonable than the raw source result, but it’s still actually excessive for our requirements. There are several problems here that need to be addressed, I’ve been a little cheeky and not mentioned one factor about most of these textures. The specular, gloss, ambient occlusion and emissive maps are all grey for our painted, matte and metallic two-tone materials.

So we can combine 42.8 MB of this data into a single RGBA texture, at 21.3MB in memory, and reduce our source texture size from 256 MB to 64 MB, it’s a win on all fronts, it also means that we’ll only tap the texture for information once to get four texture results, that’ll also optimize the shader speed itself, and reduce any potential texture thrashing(essentially texture swaps) that may occur.

So step one was extremely simple, reduction of memory, and improved performance by combining four textures into one single texture.

A Splash Of Colour

So we’ve established a simple method for combining a bunch of textures to reduce memory consumption. The main feature after this is directly related to allowing the user to customize the paint job to their tastes based upon pre-defined patterns. Enter the pattern mask.

We couple this with our albedo and four user-defined colours to produce our final results. It’s a simple process, take the red channel, take the first tint colour, and albedo, we selectively lerp using the red channel, and pull the albedo data from the albedo texture, or blank data. Then we simply multiply the result by the tint colour.

Pseudo Code:

Lerp(0, Albedo, mask.r) * Tint1.

We do this process for all of the channels, green, blue and alpha, along with the rest of the tint colours, we simple add the results together, this gives us our final albedo map recoloured based upon our mask.

The only thing left to do is to select all the origin albedo information that hasn’t been covered by the pattern mask, and slot that into the resulting texture.

I covered the optimizations that I made to our normal map, in last month’s blog - this optimization increases size at the expense of cleaner result, as it suffers from fewer compression artefacts.

What’s Next?

Our shaders are far from complete, but the baseline colour customization is completed. Right now we use the albedo to infer some dirty and detail, but we’re considering dirt maps, detritus maps along with exposing underlying metal, and some other trickery to show some wear and tear to our craft as they see use.

If you’re curious about these results, you can go and take a look here