MotoGP: custom paint jobs

MotoGP included a feature that let players customize their bikes, choosing their own color scheme and adding up to eight vector layers (rectangles, circles, or text) to build up a custom logo. Here is the editor in action:


Our design had the following features:

  • The artists created a number of  different paint patterns (flames, stripes, etc), marking each one with areas of primary, secondary, and detail color.
  • The user chooses one of these paint patterns, then configures the three colors.
  • Artists wanted to include areas such as the engine that were not affected by color replacement.
  • Artists wanted to include lighter and darker variations of the three customizable colors, so they could draw ambient occlusion directly into the bike textures.
  • Artists wanted to antialias the borders between the primary, secondary, and detail colors, and have this antialiasing preserved by the color replacement process.
  • If the user added vector layers, these would be drawn onto the side of the bike, on top of the base pattern.

We had strict efficiency requirements:

  • It had to be easy for the artists to create the source patterns. Their schedule was already too full for me to ask them to work with a complicated input format.
  • Because we had already maxed out the pixel shader used to draw the bikes, we could not do customization work in the shader (unlike this XNA Framework sample). The customization had to happen ahead of time (while loading the level) and it had to output a regular texture that could be fed into our existing shader.
  • Because we were already tight on memory, this generated texture had to be DXT compressed.
  • It would take too much bandwidth to send an entire bike texture over the network, so for multiplayer games we just sent a smaller description, and had each client machine independently recreate the texture.
  • Network clients might have to generate as many as 16 custom bike textures, so this process had to be fast enough to not significantly hurt our load times.
  • To keep the logo editor feeling responsive while the user was making changes, we had to be able to regenerate a single custom texture within 1/60 of a second.

I came up with the following plan to implement the color replacement:

  • Artists create pattern textures using red to mark the primary color, green for the secondary color, and blue for the detail color.
  • Anything drawn in monochrome (black, grey, or white) will not be replaced, so things like engine blocks must be textured entirely in monochrome.
  • Artists can include shading by using lighter or darker versions of red, green, and blue.
  • Artists can antialias between these three colors, or between colored and monochrome areas.

At runtime, creating a custom texture involved the following steps:

  • Apply color replacement to every pixel in the texture (on the CPU: this was too complex for a 1.1 pixel shader).
  • Write the results into a rendertarget.
  • Use the GPU to draw vector layers over the top.
  • Read the results back to the CPU.
  • Compress into DXT1 format.

The color replacement algorithm worked in HSV colorspace. It is probably best described in pseudocode:

    Color ApplyColorReplacement(Color sourceColor)
        // Separate the hue from the saturation and brightness
        HSV sourceHSV = ConvertRGBtoHSV(sourceColor);
        HSV saturatedHSV = new HSV(sourceHSV.Hue, 1, 1);
        Color saturatedColor = ConvertHSVtoRGB(saturatedHSV);
        // Apply color replacement to a fully saturated version of the color
        Color newColor = saturatedColor.R * customBike.PrimaryColor +
                         saturatedColor.G * customBike.SecondaryColor +
                         saturatedColor.B * customBike.DetailColor;
        // Apply a brightness adjustment, so the artists can include light/dark shading
        newColor *= sourceHSV.Brightness;
        // Apply a saturation adjustment, so monochrome areas will be left unchanged
        return Color.Lerp(sourceHSV.Brightness, newColor , sourceHSV.Saturation);

It worked! We had customizable colors. Any shading or antialiasing provided by the artists was correctly preserved.

But this implementation was too slow to meet our requirements. The color replacement function (especially the conversion between RGB and HSV) was slow, and compressing the results back into DXT format was even slower.

Hang on… who says we have to do dynamic compression at all?

DXT1 compression works by dividing the image into 4x4 blocks. Each block stores two colors in 5.6.5 format, plus a 2 bit interpolation value per texel: a total of 8 bytes.

What if we store the source patterns already compressed as DXT1, and apply our color replacement algorithm directly to the two colors within each block? This way we can change the colors without bothering to decompress and then recompress the image. It also cuts down the number of times we have to run the replacement function: now we only process two colors per 4x4 block, as opposed to sixteen.

To speed things up even further, I used the MotoGP equivalent of a custom content processor to change the source data into a custom variant of DXT1, which I guess you could call DXT-HSV. After compressing into DXT1, I preconverted the block header colors, changing them from 16 bit RGB to a 24 bit HSV format. This expanded each block from 8 to 10 bytes, and sped up the color replacement function because it no longer had to bother converting the source color from RGB to HSV.

Dang… what about the custom vector layers? We can’t draw those directly onto a DXT texture.

My final implementation still used the GPU to render the vector logo, and had to DXT compress the resulting rendertarget data. This was still pretty slow. But the logos only covered a small portion of the bike texture, so we could be smart about this recompression and only bother doing it for blocks that actually were intersected by a logo graphic.

In the end it took about 40 milliseconds to handle the most complex custom design. In a network session with 16 custom bikes, that was half a second extra load time. Good enough.

But not good enough for the editor! 40 milliseconds is two and half frames: too slow for editing the logo to feel smooth and responsive.

I fixed that by having the editor skip the DXT compression step. Unlike during gameplay, this screen was not low on memory, so compression was not important. The editor used the same HSV optimized color replacement process, then drew vector layers into a rendertarget, but instead of reading back and DXT compressing the results, it left the data in a 32 bit uncompressed rendertarget, which was textured directly onto the bike. Result: smooth logo editing at 60 frames per second.

This feature was a lot of work, but it was tremendously rewarding to see what players did with it after we released the game, and to marvel at the weird and whacky designs they came up with. Here are some of my favorite player creations:

image imageimage image

Comments (0)

Skip to main content