With GBuffer approach, it is often required to pack values together. One useful case is to be able to pack an integer value (often matching an enum like material ID) and a float value (inside 0…1 range).
For example in Unity High Definition Render Pipeline we have pack inside the GBuffer:
UnityのHDPRにおけるGBuffer内部では以下のようなパッキングを行っている。
DiffusionProfile (16 values) and Subsurface Mask (Float 0…1)
Material Features (8 values) and Coat Mask (Float 0…1)
Quadrant for tangent frame (8 values) and metallic (Float 0…1)
拡散反射タイプ(16個)とサブサーフェスマスク(浮動小数点数0…1)
マテリアル効果(8個)とコートマスク(浮動小数点数0…1)
接線空間における象限(16個)とメタリック値(浮動小数点数0…1)
During development we have change several times the number of bit required for our packing and it quickly come to us that we were needed to have general packing functions to encode arbitrary values. This is the topic of this short blog post.
Let’s say that we want to encode a Mask on 1 bit with a Scalar in range 0…1 with 8 bit of precision inside a shader. Mean in practice we pack both values in a component of a RGBA 32bit render target. Remember that the float value in the shader is convert at the end of the pipeline to corresponding render target format, in our case the float value will be multiply by 255 to fit into the 8bit precision of the component. We will have 7 bits to encode the float, this could be perform with a simple remapping:
Decoding should be the reverse of the operation above. First we need to retrieve the Mask value
デコードは、上記の式と反対を行う必要がある。まず最初に、マスク値から取得する。
1
Mask = int((255.0 / 128.0) * Val)
Note that here we use the int cast to remove all the Scalar value part.
For example if we have Scalar of 0 and Mask with 1, Val is suppose to be 128.0 / 255.0.
Mean the above code give us 1
Now let’s consider a RGBA1010102 render target with a Mask on 4 bit. The process is exactly the same.
First remap value to cover 6 bit for the float value and 4 bit for the Mask value
Important addition.
Due to rounding and floating point calculation on GPU it may appear that Mask reconstruction is shifted by one value.
This can be fixed by adding the smallest epsilon allowed by the render target format. i.e
// Code // extract integer part // + rcp(precisionMinusOne) to deal with precision issue i = int((val / t2) + rcp(precisionMinusOne)); // Now that we have i, solve formula in PackFloatInt for f //f = (val - t2 * float(i)) / t1 => convert in mads form f = saturate((-t2 * float(i) + val) / t1); // Saturate in case of precision issue }
// Define various variants for ease of use and code read floatPackFloatInt8bit(float f, uint i, uint numBitI) { return PackFloatInt(f, i, numBitI, 8); }
voidUnpackFloatInt8bit(float val, uint numBitI, out float f, out uint i) { UnpackFloatInt(val, numBitI, 8, f, i); }
floatPackFloatInt10bit(float f, uint i, uint numBitI) { return PackFloatInt(f, i, numBitI, 10); }
voidUnpackFloatInt10bit(float val, uint numBitI, out float f, out uint i) { UnpackFloatInt(val, numBitI, 10, f, i); }
floatPackFloatInt16bit(float f, uint i, uint numBitI) { return PackFloatInt(f, i, numBitI, 16); }
voidUnpackFloatInt16bit(float val, uint numBitI, out float f, out uint i) { UnpackFloatInt(val, numBitI, 16, f, i); }