Encoding floats to RGBA – the final?
The saga continues! In short, I need to pack a floating point number in [0..1) range into several channels of 8 bit/channel render texture. My previous approach is not ideal.
Turns out some folks have figured out an approach that finally seems to work.
Here it is for my own reference:
- gamedev.net forum post by gjaegy
- Suggestion right there on my previous blog post comments
- Repost gamerendering blog
- Repost on gamedev.net forums again.
So here’s the proper way:
inline float4 EncodeFloatRGBA( float v ) {
float4 enc = float4(1.0, 255.0, 65025.0, 160581375.0) * v;
enc = frac(enc);
enc -= enc.yzww * float4(1.0/255.0,1.0/255.0,1.0/255.0,0.0);
return enc;
}
inline float DecodeFloatRGBA( float4 rgba ) {
return dot( rgba, float4(1.0, 1/255.0, 1/65025.0, 1/160581375.0) );
}
That is, the difference from the previous approach is that the “magic” (read: hardware dependent) bias is replaced with subtracting next component’s encoded value from the previous component’s encoded value.
Maybe the problems you’re having are related to gamma correction?
The hardware is by default going to try to gamma correct the output of the fragment program, but you don’t want that if you want to keep predictable levels of precision. Also, when you sample back out of the texture the reverse gamma correction is also applied, since it’s assumed that artists designed the texture in gamma space.
There’s a good article in GPU Gems 3 about this that might help: http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html . Obviously they’re talking about lighting accuracy but maybe it’s an issue in your case too.
I don’t think hardware does gamma correction, unless I tell it to. I’m not setting sRGB states on the samplers or on the framebuffer writes.
Aras, you have an uncanny ability to be working on the same things I am working on, with similar results. You were a step ahead of me on this one :)
@Pat: well, you are a step ahead on encoding normals in two channels :P
Figuring out a good encoding for normals is next step on my list. Is the spherical encoding the one you settled on?
@Aras: Yeah I am really a big fan of spherical normals. They take more operations, but the assumption that is floating around about view-space normals not having a -z is totally false, so you need to either store spherical normals, or you need to reserve a bit (somewhere) for +/- on the Z value you reconstruct via sqrt(1 – x * x + y * y).
I did some performance testing a while ago that seemed to indicate that sampling a texture for atan2() is better than actually performing atan2(), especially on older ATI cards, but my current implementation doesn’t do this. I want to re-run the test first.
I haven’t tested this as thoroughly as you have been, but it still might help you out. Check out my post in this thread:
http://www.gamedev.net/community/forums/topic.asp?topic_id=486847
That’s great!
It’s a perfect match for storing depth in texture for our depth pass. My previous approach was based on storing integral and fractional parts of log2 of depth (multiplied by 16) and future decoding with exp2. It required only 2 channels of texture and with perfectly precision restored original depth value, but takes 7 instructions for encoding and 3 for decoding. Your new approach takes only 3 and 2 instruction respectively with the same precision (I can’t find any differences in precision test).
Thanks.
@corysama: yeah, I’ve tried your approach as well. It works when done on the CPU, but has small errors here and there when done on the GPU.
@Pat: I’ve experimented with spherical and spheremap based normal encodings. Here are my results so far: article link.
255^0 = 1
255^1 = 255
255^2 = 65025
255^3 = 16581375
Shouldn’t the last component be 16581375 instead of 160581375?
@imbusy: whoops, yeah.
[...] is worthwhile for a developer making this decision. Aras’ blog has other nice bits such as packing a float into RGBA and SSAO [...]
would it be quicker and would you lose any precision if you precalculated the divisions as constants like
const float x = 1.0/255.0;
const float y = 1/65025.0;
const float z = 1/16581375.0;
inline float4 EncodeFloatRGBA( float v ) {
float4 enc = float4(1.0, 255.0, 65025.0, 16581375.0) * v;
enc = frac(enc);
enc -= enc.yzww * float4(x,x,x,0.0);
return enc;
}
inline float DecodeFloatRGBA( float4 rgba ) {
return dot( rgba, float4(1.0, x, y,z) );
}
@stephen: this is HLSL/Cg code. And unlike C/C++ compilers, shader compilers are allowed to change divisions into multiplications by inverse (even if it’s not exactly the same under floating point). So this actually compiles into multiplications by inverses already.