Tags

, , ,

We used a lot of object normal maps in RadialBlitz.
Figuring out how Blender encoded these was part of the challenge. It didn’t seem to be documented anywhere, nor was I able to get help in the forums. Finding the right values involved some intuition and experimentation.

For the impatient, the solution is RBG * float3(-2,2,-2) + float3(1,-1,1), at least for our world space. This article shows how I got to that.

This formula is used for lighting in my game Radial Blitz for iPad or iPhone. Radial Blitz – A high paced 3d reaction game.

Visuals

I first tested the maps on one of our standard target models. Trying a few obvious settings I quickly found something that looked correct.

radial_glow_blur

As we created more models though I noticed something was wrong. The lighting seemed to go in the wrong direction as they rotated. My first test model rendered reasonably even with the wrong values — the danger of using a spherical target, that doesn’t rotate enough, as a test.

I added more models to the debug menu so I could easily view them. There’s no acutal model viewer, just a way to force certain enemy groups into existence. That’s all I needed really. Without too much effort I could cycle through the models and see how the lighting works.

Intuition

We know that the object normals have to be encoding a normal in 3-space, they generally have normalized values, and they are encoded in 8-bit channels. This provides some limits on what conversions are possible.

The 3-space encoding means the RGB channels must be encoding XYZ values. I wasn’t certain what channels mapped to what dimensions. It could be YZX, or XZY. There are 6 possible combinations.

To cover the full range of possible normals, a value between -1 and 1 is needed. For graphics this is almost always encoded linearly in the 8-bit texture space. GL itself converts from the 8-bit value into a floating point one, but only in the range of 0…1. I needed to multipliy by 2 and subtract 1 to get to the -1…1 range — v * 2 -1 is a sacred graphics mantra.

Except there’s one problem. I didn’t know the orientation of these vectors compared to our game world system. Does R map to +X or -X? Two optons for each channel gives us 8 possibilities for the signs.

Permutations

That’s 6 channel orderings and 8 sign combinations, for a total of 48 different possibilities. No wonder my one-at-a-time trial approach wasn’t finding the right one. I’d need a systematic way of checking each of these.

All of these operations are linear combinations of the input, allowing the entire mapping to be encoded in a single matrix calculation. This made it easy to plugin a variable calculation in my lighting model instead of a hard-coded one.

1
2
3
4
float3 ONMRaw: req( ObjectNormalMap as Texture2D ) 
    sample( ObjectNormalMap, TexCoord, SamplerState.LinearWrap ).XYZ;
float3 ONM: req( ObjectNormalMap as Texture2D ) 
    Vector.Normalize( Vector.Transform( float4(ONMRaw,1), BlenderConfig.ONM ).XYZ );

I hooked up four keystrokes in my game. Left / Right adjusts the permutation of the signs, and Up / Down adjusts the permutation of the channels. Using keystrokes let me quickly explore the different options.

The sign permutations were a nice multiple of 2, which allowed a simple conversion from the permutation value to the channel signs. The bonm prefix means “Blender object normal map”, which would have been a bit much for a variable name.

1
2
3
var sign = float3( (bonmSign & 0x1) != 0 ? -1 : 1,
    (bonmSign & 0x2) != 0 ? -1 : 1,
    (bonmSign & 0x4) != 0 ? -1 : 1 );

The channel ordering required a tad bit more work. I found a nice implementation of lexicographic ordering at MathBlog.dk, resulting in this code to enumerate the possible channel combinations:

1
var order = C.Permute( new[]{ float4(2,0,0,-1), float4(0,2,0,-1), float4(0,0,2,-1) }, bonmOrder );

Those float4 values are the v * 2 - 1 formula, one for each channel.

The full code for the matrix creation function is at the bottom of this article.

Cycling to the solution

I could now bring up various models in the game and use the arrow keys to cycle between the possible combinations. As I was using just the normal game visuals, not a special viewer, there were always a couple of combinations that worked for each model. With a bit of pen and paper work it didn’t take long before I found the one that worked for all models: RBG * float3(-2,2,-2) + float3(1,-1,1).

Source Code

This is the code for matrix permutation function. It was called by the keystroke events, for example the left key would call AdjustBONM( -1, 0 ).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int bonmSign = 0, bonmOrder = 0;
void AdjustBONM( int asign, int aorder )
{
    bonmSign = (bonmSign + asign) % 8;
    bonmOrder = (bonmOrder + aorder) % 6;

    var sign = float3( (bonmSign & 0x1) != 0 ? -1 : 1,
        (bonmSign & 0x2) != 0 ? -1 : 1,
        (bonmSign & 0x4) != 0 ? -1 : 1 );

    var order = C.Permute( new[]{ float4(2,0,0,-1), float4(0,2,0,-1), float4(0,0,2,-1) }, bonmOrder );
    order[0] *= sign[0];
    order[1] *= sign[1];
    order[2] *= sign[2];

    var rgb = C.Permute( new[]{ "R","G","B" }, bonmOrder );
    debug_log
        (sign[0] < 0 ? "-" : "+") + rgb[0] + " " +
        (sign[1] < 0 ? "-" : "+") + rgb[1] + " " +
        (sign[2] < 0 ? "-" : "+") + rgb[2];

    Tunnel.World.Blocks.BlenderConfig.ONM = new float4x4(
        order[0].X, order[1].X, order[2].X, 0,
        order[0].Y, order[1].Y, order[2].Y, 0,
        order[0].Z, order[1].Z, order[2].Z, 0,
        order[0].W, order[1].W, order[2].W, 1,
        );
}

In case you’re looking for permutation code in Uno/C# here it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static public T[] Permute<T>( T[] items, int x ) {
    var t = new List<T>();
    for(int i=0; i < items.Length; ++i)
        t.Add(items[i]);

    int N = items.Length;
    var o = new T[N];

    int remain = x;

    for( int i=0; i < N; ++i ) {
        var f = Factorial(N - i - 1);
        int j = remain / f;
        remain = remain % f;
        o[i] = t[j];
        t.RemoveAt(j);
    }

    return o;
}

static public int Factorial( int i ) {
    if (i < 1)
        return 1;

    int p = 1;
    while( i > 1 ) {
        p *= i;
        i--;
    }
    return p;
}
Advertisements