Sign in to follow this  

packing a float into a A8R8G8B8 texture (shader)

Recommended Posts

Hi guys, I would like to pack a float in the range [0.0..1.0] into an integer texture (A8R8G8B8 format), and also converting the texture value back into a float. I don't want to use a 32F texture. Is that possible? Does anyone have an efficient way of packing/unpacking such a value? I am using HLSL, but it seems there is no existing function for that. Thanks a lot, Greg

Share this post

Link to post
Share on other sites
Any sampling of the packed texture may result in you getting back an incorrect value from what you originally put in (depending on what filtering you specify).

If you pack data into the format you specified, and try to build a float value from it in a shader, I doubt it would work.

4 short ints = 4 bytes
1 float = 4 bytes

I understand your logic, but current shading languages (at least GLSL) dont let you manipulate bits (bit shifting) like C/C++ does.

Why don't you want to use floating point textures? They were implemented for a reason (access to floating point values in a texture).

Share this post

Link to post
Share on other sites
It's perfectly reasonable to want to encode a float into an A8R8G8B8 texture. A lot of older hardware doesn't support floating point textures, and moderately old hardware is really slow when using them.

Lets take an example - say I want to encode 98742/216236 with 24 bits of precision into X8R8G8B8. Random numbers I came up with by mashing on the keyboard.

Well, 98742/216236 = 0.42889250633567028616881555337687 according to windows calculator.

First, lets pretend we only want 8 bits of precision. How do we do it? Well, we only have 256 possible values for our number to be. If we just returned this in our pixel shader, it would get truncated to something. The question is, what does it get truncated to?

Remember we only have 256 possible values. Lets scale it to a range of 0-255 then. To do this, we just multiply by 255.

0.42889250633567028616881555337687*255 = 109.3675891155959229730479661111
(again, windows calculator)

So we have 109.####.... All we keep is the 109 though. But we actually output normalized values to our pixel shader, so we divide this by 255 again, and get
109/255 = 0.42745098039215686274509803921569

Our error right now is 0.001441525943513423423717514161

So now lets go on to 16 bit precision. We stored 109/255, but we lost all those decimal places after the 109. Let's try to store some of those too!

The previous number was 109.3675891155959229730479661111
and we lost .3675891155959229730479661111

Using the same method,
.3675891155959229730479661111*255 = 93.7352244769603581272313583305
floor(93.7352244769603581272313583305) = 93

So now we're basically storing 109 and 93. One more time for good measure!

0.7352244769603581272313583305*255 = 187.4822416248913224439963742775
floor(187.4822416248913224439963742775) = 187

Now we have bytes 109, 93, 187. Each can be stored in 8 bits. To decode we just backtrack:

109/255^1 + 93/255^2 + 187/255^3 = 0.42889247725233884403434576444957

Our original number was 0.42889250633567028616881555337687

The difference is 0.000000029083331442134469788927
Much better than 0.001441525943513423423717514161!

I hope I didn't confuse you. Hopefully you can see how this would translate into shader code - I could post some as I use it in multiple projects, but right now I should stop slacking off at work =P.

Share this post

Link to post
Share on other sites
In ASM language (easily translated with equivalent instructions in GLSL or HLSL):

To encode from float to RGBA:

PARAM packFactors = { 1.0, 256.0, 65536.0, 16777216.0 };
MUL val, val, packFactors;
FRC val, val;

To decode from RGBA to float:

PARAM extract = { 1.0, 0.00390625, 0.0000152587890625, 0.000000059604644775390625 };
DP4 val, val, extract;


Share this post

Link to post
Share on other sites
Original post by Unfadable
How can you store a fractional value in a component of A8R8G8B8? Do you mean frc * 256?

Even though you're stuffing it into A8R8G8B8, it still expects normalized values [0, 1] as output from the pixel shader.

Share this post

Link to post
Share on other sites

Even though you're stuffing it into A8R8G8B8, it still expects normalized values [0, 1] as output from the pixel shader.

Fair enough, I'm a little dense sometimes.

Lets say my number is 5.43

val = 5.43 * packFactors = { 5.43, 1390.08, 355860.48, 91100282.88}

frc(val) = {0.43, 0.08, 0.48, 0.88};

Did I do this correctly? When I do the extraction part, I'm not getting my original number back.

Share this post

Link to post
Share on other sites
The FRC instruction will return on each component its fractional part. So if your input vector is (1.29, 9.571, 0.23, 55.68) it will return (0.29, 0.571, 0.23, 0.68).

I didn't mention it because I thought it was obvious, but this piece of code expects the inputs to be in the [0-1] range. If the numbers you want to encode/decode are bigger than 1, you need to divide by a constant (the maximum range) when encoding, and multiply by this same constant when decoding.

It is also true that filtering will mess up the results.


Share this post

Link to post
Share on other sites
Here's an example. Let's say your input number of 0.431211921. You want to encode that into an RGBA (8 bits integer per channel) pixel.

First you multiply the number by the encoding factors:
(0.431211921, 0.431211921, 0.431211921, 0.431211921) *
(1.0, 256.0, 65536.0, 16777216.0) =
(0.431211921, 110.3902518, 28259.90445, 7234535.54)

Next, you apply the FRC instruction, you get:
(0.431211921, 0.3902518, 0.90445, 0.54).

At this point, those values get stored in your integer RGBA pixel, so they get rounded to 8 bits:
(110/256, 99/256, 231/256, 138/256).

So if you output to the color buffer the value of the pixel, you'll end up with color (110, 99, 231, 138).

Now, for decoding, you get your original pixel value and perform a DP4 with the decoding coefficients:
(110/256, 99/256, 231/256, 138/256) ^
(1.0, 0.00390625, 0.0000152587890625, 0.000000059604644775390625) =
0.4296875 + 0.00151062 + 0.000013767 + 0.000000031 =

... and the original value was 0.431211921

Note that I used my calculator to do the calculations, and it didn't have enough digits for the lower values, so the actual precision should be even better than that.


Share this post

Link to post
Share on other sites
for your information, I am using the code given in that CodeSampler thread:

encode (fDist is the normalized distance):

const float4 bitSh = float4( 256*256*256, 256*256, 256, 1);
const float4 bitMsk = float4( 0, 1.0/256.0, 1.0/256.0, 1.0/256.0);

float4 comp;
comp = fDist * bitSh;
comp = frac(comp);
comp -= comp.xxyz * bitMsk;
return comp;

decode (vec is the rgba encoded value):

const float4 bitShifts = float4(1.0/(256.0*256.0*256.0), 1.0/(256.0*256.0), 1.0/256.0, 1);
return dot(vec.xyzw , bitShifts);

The code given by Ysaneya introduces some artefacts, certainly cause by the missing "comp -= comp.xxyz * bitMsk;" (which is actually the difference between Ysaneya's code and the one given in the CodeSampler thread).

Right now the result is very good, it allows me to store the scene camera depth texture using a ARGB8 rendertarget, which allows using the same Z-buffer as the main surface (and thus writing the Z buffer during my camera-depth texture generation, allowing early Z-cull during the main scene rendering pass)!

Well done guys, thanks a lot !

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