Packing normals

Started by
4 comments, last by patw 15 years, 5 months ago
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...
}

Advertisement
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
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.
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.
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  1Packed Value:   1  1  1  1Unpacked Value: 1  1  1Difference:     0  0  0Original Value: 0  0  0Packed Value:   0  0  0  0Unpacked Value: 0  0  0Difference:     0  0  0Original Value: 0.25  0.5  0.75Packed Value:   0.247059  0.937255  0.996078  1Unpacked Value: 0.249634  0.499756  0.749756Difference:     0.00036639  0.00024426  0.000244379Original Value: 0.333  0.666  0.999Packed Value:   0.333333  0.207843  0.309804  0.992157Unpacked Value: 0.332682  0.665852  0.998045Difference:     0.000318021  0.000147521  0.000955045Original Value: 0.123  0.123  0.123Packed Value:   0.121569  0.388235  0.92549  0.490196Unpacked Value: 0.122618  0.122618  0.12219Difference:     0.000381537  0.000381537  0.000810362Original Value: 0.1  0.2  0.3Packed Value:   0.0980392  0.52549  0.396078  0.196078Unpacked Value: 0.099658  0.199805  0.29912Difference:     0.000341967  0.000195414  0.000879765Finished 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.
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.

This topic is closed to new replies.

Advertisement