Vanity: Halo Reach Player Armor Image Generator

Recently, I’ve had my hands in a project called Vanity, which is a great little application geared towards helping Halo Reach players pick out their ideal armor configuration before they start buying things in-game. Once finished, it displays the rank and credits required by the game in order to obtain the selected armor configuration, and also allows you the option to save the generated image onto your computer.

All of the content and UI was created by my buddy Nate (a more detailed and interesting description of his portion of the work can be found here at his website), and I tied everything together in code. After I initially spent a few days or so messing around with a windows forms version of the application, we decided to scrap that and use WPF instead for its more advanced presentation features. Vanity is written in C#.NET and boasts custom imaging code (see below) and a caching system created from scratch. The entire application ended up weighing in at around 130 megabytes due to the sheer amount of images (currently 3,117 to be specific) required in order to display all armor piece and color combinations.

Upon release, Louis Wu over at hbo.bungie.org mentioned Vanity in their news section and was kind enough to offer us a secondary hosting source for the application which we greatly appreciate. Bungie also recognized the utility that Vanity offered and posted about it in their blog as well. Due to the large amount of publicity following its initial release, the hbo servers experienced over one terabyte of bandwidth the day after, and another terabyte throughout the next couple of weeks. So far, there have been over 22k unique visitors and 2TB of bandwidth consumed.

If you’re interested in trying out Vanity for yourself, you can find the download link here. In order to extract the Vanity.rar archive you will need to download WinRar, and in order for Vanity to run properly, you will also need to have the .NET Framework 3.5 installed on your computer. Make sure to not change the directory structure when running the application, as it must be run in the same directory as Guardian.dll and the content directory. As usual, any comments or suggestions that you may have are always welcome.

Also, in the spirit of open source and the nature of this blog in general, I’ve included a snippet of code below containing my custom DrawImage method which has been heavily optimized for use in Vanity due to the large amount of drawing performed within. This method clocks in around twice as fast as it’s GDI equivalent Graphics.DrawImage method on my machine, and also does proper alpha blending which Graphics.DrawImage seems to fall short on. The alpha blending formula I’ve came up with performs a simple linear interpolation starting from the base alpha, but the color blending formulas were a bit more involved. I know it may be hard to read due to the heavy use of pointers and integer shifts in place of floating point math, but understand that this was a necessary evil in order to squeeze the absolute most performance possible out of it.

In order to use, you will need to pass it a canvas image, the image you wish to draw on top of that, and the area that you will be drawing in. I’ve made some pretty big assumptions on these arguments for the pixel format and dimensions in particular, so it may require some minor tweaking depending on your specific needs, but it should be a good starting point if you need something similar in one of your projects.

private static void DrawImage(Bitmap canvas, Bitmap render, Rectangle area)
{
    if (canvas != null && render != null && area != null)
    {
        int canvasWidth = render.Width;
        int canvasHeight = render.Height;
        int xMin = area.X;
        int xMax = xMin + area.Width;
        int yMin = area.Y;
        int yMax = yMin + area.Height;
        BitmapData renderBits = render.LockBits(new Rectangle(0, 0, canvasWidth, canvasHeight), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        BitmapData canvasBits = canvas.LockBits(new Rectangle(0, 0, canvasWidth, canvasHeight), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);

        unsafe
        {
            for (int y = yMin; y < yMax; y++)
            {
                byte* ptrRender = (byte*)renderBits.Scan0 + y * renderBits.Stride;
                byte* ptrCanvas = (byte*)canvasBits.Scan0 + y * canvasBits.Stride;
                for (int x = xMin; x < xMax; x++)
                {
                    byte alpha = ptrRender[(x << 2) + 3];
                    if (alpha > 0)
                    {
                        // colorResult = newAlpha * newColor + (1 - newAlpha) * canvasColor
                        ptrCanvas[(x << 2)] = (byte)((alpha * (ptrRender[(x << 2)]) + (256 - alpha) * (ptrCanvas[(x << 2)])) >> 8);
                        ptrCanvas[(x << 2) + 1] = (byte)((alpha * (ptrRender[(x << 2) + 1]) + (256 - alpha) * (ptrCanvas[(x << 2) + 1])) >> 8);
                        ptrCanvas[(x << 2) + 2] = (byte)((alpha * (ptrRender[(x << 2) + 2]) + (256 - alpha) * (ptrCanvas[(x << 2) + 2])) >> 8);

                        // alphaResult = oldAlpha + (1 - oldAlpha) * newAlpha
                        ptrCanvas[(x << 2) + 3] = (byte)(((ptrCanvas[(x << 2) + 3] << 8) + (256 - ptrCanvas[(x << 2) + 3]) * alpha) >> 8);
                    }
                }
            }
        }
        render.UnlockBits(renderBits);
        canvas.UnlockBits(canvasBits);
    }
}

Leave a Reply