MotoGP: custom paint jobs

Originally posted to Shawn Hargreaves Blog on MSDN, Monday, March 2, 2009

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:

image

Our design had the following features:

We had strict efficiency requirements:

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

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

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

Blog index   -   Back to my homepage