Drawing Normal Maps with SpriteBatch

Drawing normal maps with XNA's SpriteBatch becomes problematic as soon as you want to rotate your sprites. This article will explain one way to rotate your normal maps without losing the performance gained through batched rendering.

Source: Download .zip
Github: NormalMapRotation

I’ve been working with Alientrap on a new game called Cryptark for the past few months. When I first started working with them, they described the game as being 2D with a lighting system that made heavy use of normal maps. Up until that point, the only work I had done with normal maps was with one of my terrain generators. So while I understood the basic concept, I didn’t realize there would be a problem with using SpriteBatch to draw rotated, normal-mapped sprites until I started to implement the lighting system.

The Problem with Rotating Normal Maps

Normal maps encode the direction of a texture’s surface into RGB colors. Every pixel in the texture is used by the lighting system to calculate how a light’s ray influences the color of that pixel. Since the color of any pixel on a normal map defines the direction of the surface, it’s easy to visualize the normal map’s colors needing to change as the normal map is rotated. For example, if a surface normal is pointing to the right, it will need to point to the left when the normal map is rotated 180 degrees.

Example of Non-Rotated and Rotated Normal Map

Pictured above is an example of normals that aren’t rotated (left), and normals that are rotated (right). Notice how the color doesn’t change on the left image, which means that the surface normal at that point isn’t changing either.

Lighting Results for Non-Rotated and Rotated Normals

Pictured above are the results of both the non rotated (left) and rotated (right) normals when used with a stationary directional light that comes in from the top right of the screen. Notice how the one on the left has the appearance of the light following it.

Different Methods for Rotating Normals

Rotating normals is easy enough to do in a shader, but there are multiple ways to get the sprite’s rotation information into the shader:

Encoding the Angle

The color parameter only supports a range of values between 0 and 1. An angle in XNA (when using radians) is generally in the range of -pi to pi. This means we’ll have to convert the angle to a range of 0 to 1 before passing it into the shader, and then convert it back once it’s in the shader.

public float EncodeAngle(float angleRads)
{
    angleRads = MathHelper.WrapAngle(angleRads);

    // Range will be [0, 2*pi]
    if (angleRads < 0)
        angleRads += MathHelper.TwoPi;

    // Convert to [0, 1]
    angleRads /= MathHelper.TwoPi;

    return angleRads;
}

Decoding the Angle

Now we need to convert the angle back to radians. Most of this shader is standard for sampling from a normal map. The only lines directly related to rotating the normal is the line float angle = color.r * TWO_PI; and the lines that define the rotation matrix.

static const float TWO_PI = 6.2831853071795864769252867665590057683943387987502116f;

sampler TextureSampler : register(s0);

float4 PixelShaderFunction(float2 texCoord : TEXCOORD0, float4 color : COLOR0) : COLOR0
{
    float angle = color.r * TWO_PI;
    float cosAngle = cos(angle);
    float sinAngle = sin(angle);
    float3x3 rotation = {
        cosAngle, -sinAngle, 0,
        sinAngle, cosAngle, 0,
        0, 0, 1
    };
    float4 normalMap = tex2D(TextureSampler, texCoord);
    float3 normal = 2 * normalMap.rgb - 1;
    float4 final = float4(1, 1, 1, normalMap.a);

    normal = mul(normal, rotation);
    final.rgb = (normal + 1) * 0.5f;
    final.rgb *= final.a;
    return final;
}

technique Technique1
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Putting It Together

That’s basically it. Now you just draw your normal maps using SpriteBatch like normal, with the encoded angle passed in through the color’s R component. There’s still the G, B, and A components left unused. In Cryptark, we’re using the G and B components to handle normal maps that need to be flipped horizontally or vertically. And then we use the A component for when we want to draw a semi-transparent normal map (which isn’t used very often).

There’s many different lighting systems out there, so how exactly you put this method to use will vary depending on your own needs. In Cryptark, we use a deferred lighting system where we render all the normal maps to a single render target, and then loop through all the lights and render their contributions to another render target.

I’ll include a very basic version of NdotL lighting in the sample source code for this article. I’ll also include a timer that shows the performance of a couple other rendering methods. If you read this and spotted any errors or have any questions, please feel free to contact me by email or on twitter. Thanks for reading!

Source: Download .zip
Github: NormalMapRotation