Sign in to follow this  
mackron

Packing normals

Recommended Posts

mackron    122
Gday, I'm trying to convert and store a normal into an R11G11B10 render target via a GLSL fragment shader. However, my graphics doesn't support that format (GeForce 6200), so I've been trying to sort of emulate it with a regular RGBA8 format. Basically I want the x component of the normal to use the first 11 bits, the y component to use the next 11 bits and the z component to use the last 10 bits. So, would anyone have any ideas as to how to implement this given the two GLSL declarations below? Thanks.
vec4 pack_normal(in vec3 normal)
{
    // Do something...
}

vec3 unpack_normal(in vec4 packedNormal)
{
    // Do something...
}

Share this post


Link to post
Share on other sites
Ashaman73    13715
Ok, here we go. Just a conecpt, optimize at will :)

//11bit = 2048
//10bit = 1024
//8 bit = 256
vec4 pack_normal(in vec3 normal)
{
float x_hi = floor(normal.x*2048/256)*256;
float x_lo = floor(normal.x*2048)-x_hi;
x_hi = x_hi / 256;
float z_hi = floor(normal.x*1024/256)*256;
float z_lo = floor(normal.x*1024)-z_hi;
z_hi = z_hi / 256;

return vec4(x_hi,x_lo,z_hi,z_lo)/256;
}

vec3 unpack_normal(in vec4 packedNormal)
{
float x = (packedNormal.x*2048+packedNormal.y*256)/256;
float z = (packedNormal.z*2048+packedNormal.w*256)/256;
float y = 1 - sqrt(x*x+z*z);

return vec3(x,y,z);
}

--
Ashaman

Share this post


Link to post
Share on other sites
MJP    19786
An alternative you can use if your GPU supports R16G16F is to store the normal as a spherical coordinate with rho = 1. You can also use a lookup texture for the conversion back to cartesian, if you're math-bound.

Share this post


Link to post
Share on other sites
patw    223
I echo what MJP has said. I have found that 16 bits of integer (or float) is sufficient for storing normals in spherical format. Actually, just 8-bits for each component is decent. I have been storing normals in a r8g8b8a8 target, with Red and Green storing theta and phi, and Blue/Alpha storing 16-bits of depth.

Depending on what space you are storing normals in (I'm storing world space, I'm lazy) you should be able to get even better precision.

Again, echoing what MJP said, using a lookup texture can help significantly (depending on video card). Some cards will not bat an eye at an atan2/sincos call, and some cards will choke. Some of the ATI chipset families implement their shader units as 5 ALUs, with 4 of the ALUs able to only perform (bad terminology here) 'simple' operations. Mul/Div/Add/Sub but no pow, sin/cos/etc...I think the term is 'transcendental' but I'm not positive.

In any case, another vote for spherical storage.

Share this post


Link to post
Share on other sites
mackron    122
Thanks a lot guys.

I've never actually heard of the spherical coordinate system before, so I'll check it out.

I tried out you code this morning Ashaman but it didn't seem to work correctly. My test app just gave completely wrong results. However, I did manage to create my own implementation. I'm not sure how good or correct it is, though. Below is the entire source code to my little test app with my implementation. It uses GLM for the vector stuff in case anyone is wondering. It assumes that each component of the input normal is scaled and biased in the [0, 1] range.


#include <iostream>
#include <conio.h>
#include <glm/glm.h>
using namespace glm;

vec4 pack_normal(vec3 normal)
{
// Multiplying the vector by vec3(2048.0, 2048.0, 1024.0) results in values of 1
// not being represented correctly. Changing to these values seems to work alright.
normal *= vec3(2047.0f, 2047.0f, 1023.0f);

// The high part of the x axis is the first 8 bits. This is stored in the entire 8
// bits of the x component of the return value. The low part is the last 3 bits and
// is stored in the first 3 bits of the y component of the return value.
float x_hi = floor(normal.x * (1.0f / 8.0f));
float x_lo = floor(normal.x - x_hi * 8.0f) * 32.0f;

// The high part of the y axis is the first 5 bits. This is stored in the lower 5 bits
// of the y component of the return value. The low part is the last 6 bits and is stored
// in the first 6 bits of the z component of the return value.
float y_hi = floor(normal.y * (1.0f / 64.0f));
float y_lo = floor(normal.y - y_hi * 64.0f) * 4.0f;

// The high part of the z axis is the first 2 bits. This is stored in the lower 2 bits
// of the z component of the return value. The low part is the last 8 bits and is
// stored in the entire alpah component of the return value.
float z_hi = floor(normal.z * (1.0f / 256.0f));
float z_lo = floor(normal.z - z_hi * 256.0f);

return vec4(x_hi, x_lo + y_hi, y_lo + z_hi, z_lo) / 255.0f;
}

vec3 unpack_normal(vec4 packedNormal)
{
packedNormal *= 255.0f;

float x_hi = packedNormal.x * 8.0f;
float x_lo = floor(packedNormal.y * (1.0f / 32.0f));

float y_hi = (packedNormal.y - x_lo * 32.0f) * 64.0f;
float y_lo = floor(packedNormal.z * (1.0f / 4.0f));

float z_hi = (packedNormal.z - y_lo * 4.0f) * 256.0f;
float z_lo = packedNormal.a;

vec3 scale = vec3(1.0f / 2047.0f, 1.0f / 2047.0f, 1.0f / 1023.0f);
return vec3(x_hi + x_lo, y_hi + y_lo, z_hi + z_lo) * scale;
}


void DoTest(vec3 normal)
{
vec4 packed = pack_normal(normal);
vec3 unpacked = unpack_normal(packed);
vec3 difference = normal - unpacked;

std::cout << "Original Value: " <<
normal.x << " " << normal.y << " " << normal.z << std::endl;

std::cout << "Packed Value: " <<
packed.x << " " << packed.y << " " << packed.z << " " << packed.a << std::endl;

std::cout << "Unpacked Value: " <<
unpacked.x << " " << unpacked.y << " " << unpacked.z << std::endl;

std::cout << "Difference: " <<
difference.x << " " << difference.y << " " << difference.z << std::endl << std::endl;
}

int main()
{
// Do some tests.
DoTest(vec3(1.0f, 1.0f, 1.0f));
DoTest(vec3(0.0f, 0.0f, 0.0f));
DoTest(vec3(0.25f, 0.5f, 0.75f));
DoTest(vec3(0.333f, 0.666f, 0.999f));
DoTest(vec3(0.123f, 0.123f, 0.123f));
DoTest(vec3(0.1f, 0.2f, 0.3f));

std::cout << "Finished testing..." << std::endl;
_getch();
return 0;
}






The results of those tests are as follows:


Original Value: 1 1 1
Packed Value: 1 1 1 1
Unpacked Value: 1 1 1
Difference: 0 0 0

Original Value: 0 0 0
Packed Value: 0 0 0 0
Unpacked Value: 0 0 0
Difference: 0 0 0

Original Value: 0.25 0.5 0.75
Packed Value: 0.247059 0.937255 0.996078 1
Unpacked Value: 0.249634 0.499756 0.749756
Difference: 0.00036639 0.00024426 0.000244379

Original Value: 0.333 0.666 0.999
Packed Value: 0.333333 0.207843 0.309804 0.992157
Unpacked Value: 0.332682 0.665852 0.998045
Difference: 0.000318021 0.000147521 0.000955045

Original Value: 0.123 0.123 0.123
Packed Value: 0.121569 0.388235 0.92549 0.490196
Unpacked Value: 0.122618 0.122618 0.12219
Difference: 0.000381537 0.000381537 0.000810362

Original Value: 0.1 0.2 0.3
Packed Value: 0.0980392 0.52549 0.396078 0.196078
Unpacked Value: 0.099658 0.199805 0.29912
Difference: 0.000341967 0.000195414 0.000879765

Finished testing...


So do those test results look alright? Is that the kind of precision I should expect? Also, is the actual algorithm I use to pack/unpack alright?

Thanks a lot.

Share this post


Link to post
Share on other sites
patw    223
This may be useful, may not be: http://flickr.com/photos/killerbunny/sets/72157606936662291/

That's a Flickr set of the same scene using 4 different g-buffer normal storage methods. Ignore the white dots, those are pinholes in the scene. Levels of grey indicate the amount of error between the real world space normal, and the one obtained from the g-buffer. The areas which have error are different between spherical and Cartesian. The results of the 16-bit targets are pretty much identical; no error.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this