 Programming

Importing Blender object normal maps with permutations

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 & 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 *= sign; order *= sign; order *= sign; var rgb = C.Permute( new[]{ "R","G","B" }, bonmOrder ); debug_log (sign < 0 ? "-" : "+") + rgb + " " + (sign < 0 ? "-" : "+") + rgb + " " + (sign < 0 ? "-" : "+") + rgb; Tunnel.World.Blocks.BlenderConfig.ONM = new float4x4( order.X, order.X, order.X, 0, order.Y, order.Y, order.Y, 0, order.Z, order.Z, order.Z, 0, order.W, order.W, order.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[] items, int x ) { var t = new List(); 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; }