Road Texture UV Remapping in Unity

Generating multiple road variations from a single texture

Keagan Ladds
8 min readOct 8, 2021

Introduction

I, like most engineers, like figuring out how things work by taking them apart and attempting to put them back together again. My recent dive down the rabbit hole of game development has been no exception. After getting some basic car mechanics working in Unity, I wanted something to drive on, like perhaps a road. This led me to an excellent tutorial series by Sebastian Lague about creating the geometry for a road that follows a spline. A short while later (read many hours later) I could draw a path along my terrain and generate a road that follows this path. As I looked at my beautiful road, in all its DefaultHDMaterial goodness, I thought to myself, “Hmmm wouldn’t it be nice if that road had some asphalt and maybe even some lines on it…”

I “Found” a Road Texture

While digging around the game files of a game that I had recently been playing, I came across an asphalt road texture. This was the only road texture that had any lines on it so I was properly confused as to how the various kinds of asphalt roads were being generated in the game. Closely comparing the patterns of the white and yellow lines of the texture and in-game led me to conclude that this single texture was being used to texture all of the asphalt roads in the game.

An asphalt road texture I found while digging through the game files of a game that I was recently playing.

I came across several asphalt roads in the game, such as single and double lane roads with different line configurations. It seemed to me that the texture for the lane section of asphalt road was missing and that the texture I found was just for the shoulder.

A screenshot showing an asphalt road (taken from PCGamingWiki)

It’s All There if You Know Where To Look

The solution was staring me right in the face the whole time. I was so focused on the fact that this single image could not be scaled, rotated or mirrored in any possible way to even closely resemble the double lane roads I was seeing in the game. It took me a while to realise that all of the road texture information I needed was indeed in this single texture. I saw that the shoulder of the road could be textured by the entire image, the centerline(s) could be textured by the portion of the texture containing the lines, and the asphalt for the lane could be textured by repeating the middle section of the texture a bunch of times. If my explanation doesn’t make too much sense don’t worry, my even worse diagram below will help confuse you some more.

An illustration of using the single texture to build up a section of road

This was great news! I now knew that I somehow needed to chop up the texture and then piece it back together like some sort of Frankenstein creation, but how? I first thought of just using GIMP and doing it manually, but the effort of trying to make sure that I correctly aligned the normal and mask maps with the now chopped up diffuse texture put me off. Another huge disadvantage of doing it manually would be that I would need to create a new texture for each type of road. Nope, I am too lazy for that! Also, the curiosity of figuring out how they did it in the game was just too damn high.

From my very limited knowledge of texturing I figured that the only way I could achieve this kind of texture manipulation would be to play around with the UVs. The geometry that I was generating had UVs between 0-1 along the width of the road. I thought about specifying different UVs when generating the road geometry but that approach would be unnecessarily cumbersome and would require generating different geometry for each type of road instead of just being able to apply a new material to change the road type. That was a no-go for me, so I was stuck with the 0-1 UVs along the width.

Let the UV (Re-)Mapping Fun Begin

Before creating a custom shader to perform the UV mapping, I first needed to get my head around the concept of mapping between geometry UV space and texture UV space. To come up with a plan, I pencilled out a few diagrams to try and help myself understand exactly what I was trying to achieve. Below is my attempt at getting the muddled mess from my brain into a diagram.

NOTE: when referring to geometry and texture UVs, I am referring only to the horizontal component of the UV as I don’t remap the vertical component.

Diagrams showing how I planned on mapping between geometry UV space and texture UV space.

Mapping the Shoulder

The shoulder of the road was the easiest to map. Basically, all I needed was to be able to remap the whole texture space between 0 and some given ShoulderWidth of the geometry space. The only trick here was that I wanted to be able to tile the shoulder texture by some factor ShoulderTiling. This mapping should only take place between 0 and the ShoulderWidth in geometry space. This is represented logically as follows:

if (UV.x < ShoulderWidth)
MappedUV.x = UV.x * ShoulderTiling;

Mapping the Lane

Next came the lane. I was initially stumped at how to do this because I needed to repeat only a section of the texture space. After some playing around and remembering my good ol’ friend the mathematical operator modulo, I figured it was possible to repeat in texture space with a width LaneTilingWidth, tiled by some factor LaneTiling and offset by LaneTilingOffset using fairly simple arithmetic. This mapping should only take place for the LaneWidth, offset by the ShoulderWidth in geometry space. This is represented logically as follows:

if (UV.x > ShoulderWidth && UV.x < ShoulderWidth + LaneWidth)
MappedUV.x = LaneTilingOffset + UV.x % LaneTilingWidth * LaneTiling;

Mapping the Centre

Finally came the centre, this again was easy to map because I just needed to be able to tile the texture by some factor CenterTiling and offset it by CenterOffset. This mapping should only take place for any space remaining after the shoulder and lane in geometry space. This is represented logically as follows:

if (UV.x > ShoulderWidth + LaneWidth)
MappedUV.x = CenterOffset + UV.x * CenterTiling

Half Way There

And that is it, with the 3 fairly straightforward if statements, it is possible to remap the texture to fill up a lane.

Imgflip

At this point, if you give this a try, you will notice something strange. This is only mapping half of the lane to the entire road mesh. In other words, the other side of the road and shoulder is missing. While this can easily be fixed by dividing the geometry space UV by two and adding another 3 if statements, I chose a different way which I will explain a little later.

Let’s Make a Shader

As the title promises, this article is about remapping a road texture in Unity. To implement the shader I opted to use Shader Graph with Unity’s HDRP. At first, I tried recreating the above logic using a combination of Shader Graph’s Rectangle nodes to mask off the different if sections. While this ‘worked’, I would not recommend it.

The horror that was my subgraph to implement the remapping logic.

After giving up on trying to neaten the subgraph, I threw it away and just added a Custom Function node. This handy little node lets you specify some HLSL code as either a file or a string, which can then be included in your shader graph. After throwing together some basic HLSL code, I put the following in the Custom Function as a string:

MappedUV = UV;
if (UV.x < ShoulderWidth)
{
MappedUV.x = UV.x * ShoulderTiling;
}
else if (UV.x >= ShoulderWidth && UV.x < ShoulderWidth + LaneWidth)
{
MappedUV.x = UV.x % LaneTilingWidth * LaneTiling + LaneTilingOffset;
}
else {
MappedUV.x = (UV.x + CenterOffset) * CenterTiling;
}

I then specified the inputs and outputs of the Custom Function, making sure they exactly matched the variables specified in the code. If you don’t, or you do something you shouldn’t be doing, you will get treated to a useless error message and a pink and black error texture, yay!

Mirror Mirror on the Wall, Who Is the Most Symmetrical Road of Them All

When starting with the texture mapping, I made one big assumption that I should most likely have mentioned sooner. I assumed that the roads that I would be texturing would be symmetrical, i.e. the remapped texture could be mirrored across the middle of the road so that the shoulder, lane, and centre would appear twice, as mirror images of one another.

To achieve this, I remapped the 0-1 UV range to 1-0-1 which I somehow managed to get right with the following clunky collection of nodes.

If it works it ain’t stupid… right?

Plugging the mirrored UVs into the custom remapping function yields the desired results: a road texture with a shoulder and lane on either side, separated by one or more lines. Remember to use the remapped UVs as an input to the other texture sampling nodes.

Using the mirrored UVs as an input to the custom remapping function

Results

After endlessly tweaking the various tiling, offset, and width parameters of the remapping function, it was possible to generate several road texture variants from a single input texture. Below are just some of the road variants that could be generated, without needing to regenerate the actual road geometry.

Double Lane Roads
Single Lane Roads (with and without shoulder markings)

Conclusion

That’s it folks! After way too much time spent researching, tinkering, and tweaking I managed to use UV remapping to easily generate various asphalt roads from a single texture. While my technique is not the most performant (who thought it was a good idea to put if statements in my HLSL shader code?), I found it to be very useful not only for roads but also to understand UV mapping in general.

Feel free to reach out to me if you have any questions.

LinkedIn: https://www.linkedin.com/in/keaganladds/

I will leave you with a money shot I took in Unity after layering some road cracks and playing around with the Post Processing stack.

--

--

Keagan Ladds

Software engineer @ Tesla & serial tinkerer, writing about the tech things that keep me up at night.