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.

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 & 0 x1 ) != 0 ? - 1 : 1 , ( bonmSign & 0 x2 ) != 0 ? - 1 : 1 , ( bonmSign & 0 x4 ) != 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) .

Radial Blitz is my 3d mobile action game. Download it for iOS and join me on Twitter or Facebook for my continued exploration of the technology behind it.

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 & 0 x1 ) != 0 ? - 1 : 1 , ( bonmSign & 0 x2 ) != 0 ? - 1 : 1 , ( bonmSign & 0 x4 ) != 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 ; }