Oct 26, 2010

Best Fit Normal Map Generator + Source Code

The BFN generator is finally here! I have found time for this tonight.

So, this small application allows you to generate each face of a BFN cube-map. If you want to encode the BFN only as the length of the best fit normal or transform it to a 2D texture (as suggested in Crytek's presentation), you will have a little bit of work to do but it should not be such a big deal. You can select the resolution of the cube-map as well as the bias vector at compilation time.

Some screen-shots for 256x256x6 cube-maps:

At the origin: the BFN cube-map with a bias vector=vec3(0.5,0.5,0.5).
in the +X direction: the regular normalization cube-map.
In the +X+Y direction: a BFN cubemap with a bias vector=vec3(0.5,0.5,8.0/255.0). It results in less precision for normals having negative Z value and more precision for normals having positive Z value.

The reconstruction error of the regular normalization cube-map as compared to the ground-truth normal map. (scale factor of 50)

The reconstruction error of the BFN cube-map with bias a vector=vec3(0.5,0.5,0.5).
Some reconstruction errors are still visible. (scale factor of 50)

The reconstruction error of the BFN cube-map with bias a vector=vec3(0.5,0.5,8.0/255.0).
No more error patterns are visible. (scale factor of 50)

Reconstruction error on the back faces (scale factor of 50). There is a large error when using a bias vector of (0.5,0.5,8.0/255.0). However, it is not important since only the positive Z values are important when storing normals for a light pre-pass or deferred renderer.

Error when computing a specular lob with the regular normalization cube-map. The specular exponent is 64.

No noticeable difference when changing the bias vector of the BFN.

You can download the source here!

And finally, the important references :
  • Amantide ray marching paper.
  • Crytek's presentation from SIGGRAPH 2010 as well as their 2D BFN texture (which I recommend) can be downloaded here.


  1. Thank you, a great contribution to the community :)

  2. Sorry for the late answer! :)
    Thank you very much!

  3. Thanks! Great job!
    It seems like the (at least for me) bottleneck in the program is the printing though :)
    I moved it outside the inner-most loop and got a 10x speed improvement!

  4. Ahah! Nice! :)
    And you are welcome! I am glad to help any developers who reach this blog. :)

  5. Oh, thats nice, correct bias value is important for view space normals, where negative-z values are rare and incorrect... i want to play with this parameter, but source code is not available now.. Please, can you reupload it or send via email?

  6. Thank you. I will reupload the source code soon and keep you informed.

  7. Closed, I have reuploaded the best fit normal demo with bias here: http://sebastien.hillaire.free.fr/demos/BFN.zip

  8. Thank you! Your source code was very important for me, and I want to share results of my little researh:

    1. It is very good, that it is possible to change BFN cubemap size and bias vector, first of all I play with this parameters and I can approve that cubemap 512x512 with bias float3(127.5, 127.5, 8.0/255.0) is really good, BUT:

    2. Potentially BFN cubemap with best fit scale mixed with normalization factor can be more precise than original Crytek's idea, but in this case cubemap mipmapping is real problem, so I deside to use hybrid method...

    3. Instead of using cubemap with premultiplied best fit length and normalization factor, I prefer to use best fit only stored in one-channel (R8) cubemap. The difference from Crytek's method is that not-possible to convert cubemap to 2D-texture when bias vector not equal to float3(0.5, 0.5, 0.5). So I preffer to use 1-channel cubemap with best fit only.

    4. Best fit length better to store with multiplied max(abs(N.x), abs(N.y), abs(N.z)) for better length packing and divide with this value on shader

    5. This texture can be easily mipmapped using nearest-filter, this helps to prevent texture cache polution

    -many code improvements
    -nice print in console (:
    -command line support (cube size, mipmaps, bias, format)
    -direct output to DDS file format
    -R8 and RGB8 cubemap support (R8 - best fit length only, RGB8 - best fit length and normalization factor)
    -mipmaping (up to desired level) or base level only

    SHADER code for packing/unpacking with R8-mipmapped BFN-cubemap (posted in source code of modified files):

    // NOTE: usage in shader:

    // normalized bias values, used on BFN cubemap generation pass

    #define BIAS float3(127.5 / 255.0, 127.5 / 255.0, 8.0 / 255.0)

    float3 UnpackNormal(float3 n)
    const float3 c1 = 1.0 / (1.0 - BIAS);
    const float3 c2 = 1.0 - c1;

    return normalize(n * c1 + c2);

    float3 PackNormal(float3 n)
    float3 an = abs(n);
    float bestfit = texture(us_Bfn, n).x;
    float maxabs = max(max(an.x, an.y), an.z);

    const float3 c3 = 1.0 - BIAS;
    const float3 c4 = BIAS;

    return n * (bestfit / maxabs) * c3 + c4;

    P.S. If you are interested, I can send modified source code to you, just post your email here or write me to steelratsoftware at gmail.com

    P.P.S. Thank you very much! My view-space normals are more precise now!

    P.P.P.S. Sorry for bad english (:

  9. Hi!

    1. Nice!

    2. Yes, mipmap should not be used when using the bfn cubemap.

    3. Unfortunatly, you are ,right, you cannot use the tricky transformation to the 2D texture. And you are very correct to only use the best fit length! :)

    4. Ah! Very good! I will have a look a this by myself.

    5. Yes, nearest filter will work. But you have to generate yourself each mimap level right?

    You are welcome for the source code; and your english is good! :)
    Anyway, I am going to send you an email right now.

  10. My first fail:

    It is incorrect to use nearest mipmap filter, we should compute best-fit for each mipmap individually ):

    Hmmmm... it will work well with best-fit-normalization cubemap, we can compute not all levels (up to 32x32) and use mipmaped cube with anisotropic filter... testing required (: