GBuffer helper – Packing integer and float together

この記事は、こちらの記事を日本語に翻訳したものです(翻訳の許可は取っています)。

Version : 1.0 – Living blog

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).

GBufferを利用するアプローチにおいて頻繁に様々な値をパックする必要がある。特によく使われるのは、整数値(マテリアルIDのような列挙型)と浮動小数値(0〜1の間)をパックするようなケースだ。

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:

さて、我々は1bitのマスクと0…1のスカラー値を8bit精度にエンコードしたいと思っている。これは、双方の値をRGBA32レンダーターゲットの1要素へパックすることである。忘れないで欲しいのは、シェーダ内の浮動小数点数はパイプラインの最後で対応するレンダーターゲットのフォーマットに変換される。今回の場合、浮動小数点数は255を掛けられて8bit精度の要素になる。我々は7bitを浮動小数点数のエンコードに使う。これはシンプルな変換式で実行できる。

1
(127.0 * Scalar)  / 255.0

multiply by 127 (or (1 << 7) – 1) which is 01111111 in binaries leave 1 bit available for the Mask.

二進数で0b01111111である127(もしくは(1<<7)-1)を掛けることで、1bitをマスクのために使えるようになる。

Then we divide by 255.0

そして、255で割る。

Then we need to add the bit for the mask itself at the 8th position mean value of 128 (or (1 << 8) – 1)

そして、我々はマスクのために、8番目のbitである128(もしくは(1<<8)-1)を足す。

1
(128.0 * Mask) / 255.0

So encoding is

そしてエンコード式は

1
Val = (127.0 / 255.0) * Scalar + (128.0 / 255.0) * Mask

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

ここでは、スカラー値部分をそぎ落とすためにintキャストを使う。
例えば、スカラー値0とマスク値1を持つ場合、Val128.0/255.0になるはずだ。
この場合、上記の式は1を返すことになる。

if Scalar is 1, Val is suppose to be 1.0, mean Mask = int(1.9921875) = 1. All good.

もし、スカラー値が1の場合、Valは1.0になるはずだ。これは、Mask = int(1.9921875) = 1となるので、バッチリだ。

We then retrieve the value of Scalar

次にスカラー値を取り出す。

1
Scalar = (Val - (128.0 / 255.0) * float(Mask)) / (127.0 / 255.0)

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

今、RGBA1010102のレンダーターゲットで、4bitマスクを持つ場合を考えてみよう。手順は先ほどと同じ。
まず、浮動小数点数を6bit、マスク値を4bitに再マップする。

1
Val = (63.0 / 1023.0) * Scalar + (64.0 / 1023.0) * Mask

For example if Mask is 2 (i.e 128.0 / 1023.0) and Scalar is 1.0 we get 0010 1111 11 as binaries representation.
4 bit for Mask then 6 bit for Scalar.

例えば、マスク値が2(すなわち128.0/1023.0)とスカラー値が1.0の時、0b0010_1111_11が得られる。
4bitがマスク値、6bitがスカラー値に使われている。

For decoding we first retrieve the Mask then the Scalar

デコードは、まずマスク値、そしてスカラー値の順に取り出す。

1
2
Mask = int((1023.0 / 64.0) * Val)
Scalar = (Val - (64.0 / 1023.0) * float(Mask)) / (63.0 / 1023.0)

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

重要なことがある。
GPU中で浮動小数点演算や丸めが行われる間に、マスクの再構成は一つの値によってずらされるように見えるかもしれない。(訳注:?)
これは、レンダーターゲットに許容される最小値を足すことで回避できる。すなわち、

1
Mask = int((1023.0 / 64.0) * Val + 1.0 / 1023.0)

We can easily generalize this process for any unsigned render target format and any Mask size. Here are the functions to do the work.

我々は簡単にこれらの手順を、様々な符号なしレンダーターゲットフォーマット、及び様々なマスクサイズに対して一般化できる。
ここに動く関数がある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
float PackFloatInt(float f, uint i, uint numBitI, uint numBitTarget)
{
// Constant optimize by compiler
float precision = float(1 << numBitTarget);
float maxi = float(1 << numBitI);
float precisionMinusOne = precision - 1.0;
float t1 = ((precision / maxi) - 1.0) / precisionMinusOne;
float t2 = (precision / maxi) / precisionMinusOne;

// Code
return t1 * f + t2 * float(i);
}

void UnpackFloatInt(float val, uint numBitI, uint numBitTarget, out float f, out uint i)
{
// Constant optimize by compiler
float precision = float(1 << numBitTarget);
float maxi = float(1 << numBitI);
float precisionMinusOne = precision - 1.0;
float t1 = ((precision / maxi) - 1.0) / precisionMinusOne;
float t2 = (precision / maxi) / precisionMinusOne;

// 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
float PackFloatInt8bit(float f, uint i, uint numBitI)
{
return PackFloatInt(f, i, numBitI, 8);
}

void UnpackFloatInt8bit(float val, uint numBitI, out float f, out uint i)
{
UnpackFloatInt(val, numBitI, 8, f, i);
}

float PackFloatInt10bit(float f, uint i, uint numBitI)
{
return PackFloatInt(f, i, numBitI, 10);
}

void UnpackFloatInt10bit(float val, uint numBitI, out float f, out uint i)
{
UnpackFloatInt(val, numBitI, 10, f, i);
}

float PackFloatInt16bit(float f, uint i, uint numBitI)
{
return PackFloatInt(f, i, numBitI, 16);
}

void UnpackFloatInt16bit(float val, uint numBitI, out float f, out uint i)
{
UnpackFloatInt(val, numBitI, 16, f, i);
}

And example usage:

それと使い方。

1
2
3
4
5
6
7
8
9
10
11
// Encode
outSSSBuffer0.a = PackFloatInt8bit(sssData.subsurfaceMask, sssData.diffusionProfile, 4);
// Decode
UnpackFloatInt8bit(inSSSBuffer0.a, 4, sssData.subsurfaceMask, sssData.diffusionProfile);

// Encode
outGBuffer2.a = PackFloatInt8bit(coatMask, materialFeatureId, 3);
// Decode
float coatMask;
uint materialFeatureId;
UnpackFloatInt8bit(inGBuffer2.a, 3, coatMask, materialFeatureId);