New Panel Texturing

I’ve been working on few new additional features to the Subtransit texturing subsystem. The game uses a slightly unconventional approach to texturing which permits significantly higher detail density than simply using unique textures per object. I will talk about the texturing subsystem more in the following posts, but this post should give a general hint of how it’s structured.

So while driving the train, one of the most important parts about the experience is the drivers controls and the drivers panel. In the 81-717 train, the panel is a big console located right in front of the driver, containing most of the primary train controls. It is made up of several panels called blocks (numbered from 1 to 7):

In Subtransit, each of the train wagons is unique to an extent (defined by its unique wagon number, as well as minor differences in the cabin and in the interior). This means that the scuff marks, scratches and other damage, re-painting traces and paint colors and so on are all varied and different in every single wagon.

While it wasn’t originally planned that the train panels would have a complicated rendering procedure, the need for easy adjustment of panel colors, preference for having unique cabins and so on was all justification for blending the final panel material in runtime. Here is a test render of the panel after all the improvements I’ve added to the base shader. This does not reflect how it will look like in the final game, it is merely a test mockup!


The final texture is mix of ten different layers with different masks (some of them are even masked twice or combined together in various ways):

  1. Base texture of the panel which defines the top-most layer of paint
  2. Detail layer which adds additional micro-normals detail to the panel paint (not used on the screenshot above)
  3. Primary wear layer which adds areas where paint is completely peeled off, revealing the bottom-most layer (usually it is the oldest paint or simply the rust of the panel itself).
  4. Secondary wear layer, usually left after one of the previous repaints where base paint has peeled off, but has not revealed the primary layer
  5. Letter emboss layer which adds embossing of the letters printed on the panel
  6. Letter inking layer which adds the actual letters of printed text, masked to simulate paint peeling
  7. Primary wear layer embossing, which simulates the thickness of secondary & base layers of paint
  8. Secondary wear layer embossing, which simulates thickness of the base layer of paint
  9. Dirt layer, which simply simulates dirt accumulation around crevices and other parts of the panel (also not used on the screenshot above)

The base texture of the paneling is simply the texture of paint applied to the panel mesh. I will go in greater detail about this phase in one of the next posts.

Primary wear layer

The primary wear layer uses an existing feature in our texturing subsystem that permits adding edge wear and other types of damage, masked by a curvature map. One of the new features I’ve added is the ability to replace curvature map by a ‘boosting’ map. A curvature map would mask out edge wear based on local curvature (only positive curvature is used for edge wear). The boost map extends this to also permit applying arbitrary wear patterns onto the surface:

mask1 In other words, the masking values (opacity of the edge wear layer) are calculated as following for the two different cases:

mask_curvature = ((input - 0.5)*2.0) * wear_texture
mask_boost = clamp(input*2.0, 0, 1) * wear_texture + clamp((input-0.5)*2.0, 0, 1)

Afterwards, the masking value is put through a threshold filter (if required) to create sharpness in transition from edge wear to no edge wear. The boosting mask for this panel looks like the picture on the right. So for a boosting mask like this gray areas are equal to 0.5 and indicate zones where primary wear may be applied. White areas are equal to +1.0 boost and indicate zones where wear will definitely be applied).

For the boosting mask above, this is the final rendering result with just the primary wear layer (the alternate image shows the computed wear mask):

The lowest layer of material is normally a single solid color with defined roughness and metallicity, but an additional option exists to permit specifying a single albedo map, tinting this color as well as specifying modulation of roughness. So the lowermost layer does not contribute normals (the normals of this layer are absolutely flat) and does not contribute complex variation in roughness (the variation is sampled from R channel of the albedo map).

Secondary wear layer

mask2 Secondary wear layer is quite similar, but it samples the scuffs/damages texture independently from the primary layer. Secondary wear layer also permits specifying a more complicated material. It supports the albedo map, the variation map & the normal map, as well as variation in roughness but a constant metallicity value. The mask used for secondary wear layer is always a boost map (never a curvature map).

Secondary wear layer is the new addition that I have made today. It’s simply an optional shading option that can be enabled in any material (though currently only train panels make use of it).

Now, with the secondary wear layer the panel starts to look like more intricately worn:

Letter embossing and inking

Much of this section will be explained in the later posts. Both the letters rendering and the embossing are based on distance fields – giving the letters significantly higher sharpness than they would have otherwise at the same initial texture size.

Layer embossing

This is the most fun part of this post. It is something I’ve developed today specifically for texturing the panels. In short, these layers add screen-space scalar-derived normal maps. In other words, they give normals to the paint layers, creating an indication of thickness.

For these normals, the layer masks from examples above are used as bump maps – so a sharp change in brightness creates a slope. Paradoxically, this feature derives a normal map from only a single pixel of the ‘bump map’: it makes use of the hardware features for calculating screen-space derivatives (DDX, DDY). The initial slope due to the masks is calculated in screen space. From the texture coordinates tangent space X and Y axes are derived and the slope is converted from screen space to tangent space.

So the algorithm computes the slope visible during rendering, based on pixels of the masks that reach the screen – then it converts that slope (which is specified in screen coordinates) into a slope that’s relative to the surface of the panel, giving a consistent normal map from all angles.

The final effect is quite subtle, but since this is a screens-space effect it scales really well with distance – even when you are looking at panel from a relative far away, the pixels which make up modified normals on the paint layers edges are still visible – although the effect is quite subtle at the moment.

I think that feeding layer masks before they have passed through a threshold filter will give significantly better results. Right now the ‘step’ in the normals generated is just 1 pixel (and the mechanism for finding local slope works in 2×2 pixel blocks). The quality should be severely improved and the effect made much less subtle if this step was increased to about 2-3 pixels.

As a summary to this post… all of the texturing above is highly dynamic (the seed which defines distribution of the scuffs and damage is set based on wagon number) and does not significantly depend on VRAM available! In case that user requests lower texture resolution, most of it is seen as a degradation of how finely defined the curves and shapes are on the panel. In fact, the secondary mask texture is quite low resolution at the moment (equivalent to about 512×512 pixels for the entire panel!) – it is a good indication of how a low-res masking texture affects the output quality.

Here is a good illustration of the significant effect the embossed layer normals add to the paint, but also an illustration of the artifacts caused by screen-space normals (specifically, the 2×2 blocks within which the slope is calculated are seen – due to hardware limitations, the slope is effectively constant within a 2×2 block, giving the lower resolution look):