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[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; } |