Hi!
I need to pack four floats into three floats in such a way, that I loose as little precision of the first three original floats as possible and I’m ready to sacrifice the precision of the fourth original float. The packing and unpacking is meant to be done inside a shader, preferably an HLSL custom function inside a graph.
These original four floats are R G B A channels of a texture, where R G B store regular colour information of an image and A stores a special mask for this image.
My idea is that I split and distribute the data of the original A channel evenly between three new floats. For example, if a pixel of the original A channel is represented by 0.345656… number, then I want to take its first three digits 3, 4 and 5 and pass 3 through the new first float, 4 through the new second float and 5 through the new third float. Then reassemble them back to 0.345. This will cut it down significantly, but I’m okay with that.
I’ve tried it multiple ways but didn’t succeed. Any help would be really appreciated. Thanks!
P.S.
I’ve posted a similar question in the shader graph thread, but I’ve realised that it’s more to do with HLSL in general and I need to rephrase it, based on the research I’ve done since then. So I hope it’s okay posting it again here.
Just to maybe avoid some unnecessary work… What is it you’re expecting to gain from doing this? Modern GPUs (in a generalized description) are a 128bit wide vector processor (float4), so while you may be saving a little VRAM space by combining that value and shortening the transfer time of the texture from RAM to VRAM (Which is only going to happen when the asset is loaded into scene), you’re not gaining much else, and in fact will be losing some pixel-draw performance from having to do math to unpack the value.
Normally packing is done to store more than 4 values into the vector to avoid say having to pass a float4 and a float2, wasting 2 channels of pipeline potential, or in situations where bandwidth has become the bottleneck and not processing time.
Hi! Well, I’m not planning to gain anything with it from the performance or memory-management point of view. Long story short, I have a planar camera that writes to a render texture what it sees. It also can generate a special mask based on the depth of the scene and some other parameters. And I have access only to R G B channels of the rendertexture it writes to. The camera is provided by the third-party asset and the HDRP I’m using reserves the Alpha channel of a render texture for, well, alpha information. I don’t have enough knowledge and just don’t really want to hack into the asset or HDRP architecture itself to make them write to all four RGBA channels. So I want to squeeze both what camera sees and the mask it generates into a single RGB rendertexture.
Think explicitly where you want to use your bits. I would go for 7-bits per channel, and leave 3 for your alpha mask. You could multiple each color channel by half, and then sneak a single bit for the alpha into each one.
Something like this:
float3 Pack(float4 unpacked)
{
float3 packed = float3.zero;
// Convert alpha to a 3-bit value (0 to 7)
uint alpha = ((uint)(unpacked.w * 7.0f + 0.5f));
// Half the color value
// Use the lower or upper end of the range to encode a single alpha bit
// i.e. Values over 0.5 mean the alpha bit is 1, otherwise it is 0
packed.x = (unpacked.x * 0.5f) + 0.5f * (alpha & 0x01);
packed.y = (unpacked.y * 0.5f) + 0.5f * ((alpha >> 1) & 0x01);
packed.z = (unpacked.z * 0.5f) + 0.5f * ((alpha >> 2) & 0x01);
return packed;
}
float4 Unpack(float3 packed)
{
float4 unpacked = float4.zero;
// Use a step function on 0.5 to decode the alpha bits
// when packed in over 0.5 it will be 1 otherwise 0
float3 alphaBits = math.step(0.5f, packed);
// Use the alpha bits to reconstruct the colors
// Anything over 0.5 is reduced by 0.5
unpacked.xyz = packed - (alphaBits * new float3(0.5f));
// Double everything to restore the full 0..1 range
unpacked.xyz *= 2f;
// Use a dot product to restore each bit
unpacked.w = math.dot(alphaBits, new float3(1f, 2f, 4f));
unpacked.w /= 7f;
return unpacked;
}
Even dropping a single bit from your colors will result in a noticeable quality loss, but maybe you can use some sort of dithering scheme to make artefacts less visible. Your alpha mask is going to only have 8 values so I don’t know if that is going to be enough. You could change the encoding scheme to steal another couple bits for it from the color channels, but that will start to really be noticeable. It’s a compromise no matter what you do.
The other issue is the interpolation. Any kind of non-standard texture encoding will not be interpolated correctly so you will need to use point sampling to load the adjacent pixels, decode your values, and then do the interpolation manually. It’s not difficult math; you snap your UV texture coordinates to the nearest 4 pixels, and calculate the fractional components to do the interpolations.
My gut feeling is that you aren’t going to be very happy with the results, and it may be better to dig into the asset you are using to try to add another render target just for the results of the special alpha mask.
In any case, I will attach the script I wrote to test the encoding function above (it uses the new Unity mathematics library and its very shader-like syntax) if you want to experiment with it. It converts the intermediate value to a Color32 to simulate being written to a texture. That is why some of your packing/unpacking functions work in a single shader, but not in practice. In a single shader, you will typically have a full 32-bit value for each component so you have a lot of extra precision to use, but once you write it to a texture each channel only gets 8 bits.
Thanks! I really appreciate that you’ve explained it in such detail and attached an example. It helps a lot
My current problem is that some kind of colour correction seems to be applied by the camera when it renders to a rendertexture, therefore values shift unpredictably and math just doesn’t work out as it should in all cases.
If your render texture is created as sRGB (the default) then it might be doing an automatic linear to sRGB conversion on write which will mess with any encoding scheme you are doing. If you can create the texture as “linear”, it should bypass that conversion step.