分析一下 EncodeFloatRGBA() & DecodeFloatRGBA()
的实现
翻翻 Unity Built in Shaders 是一个涨姿势的活动。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// from UnityCG.cginc inline float4 EncodeFloatRGBA( float v ) { float4 kEncodeMul = float4(1.0, 256.0, 65536.0, 16777216.0); float kEncodeBit = 1.0/256.0; float4 enc = kEncodeMul * v; enc = frac (enc); enc -= enc.yzww * kEncodeBit; return enc; } inline float DecodeFloatRGBA( float4 enc ) { float4 kDecodeDot = float4(1.0, 1/256.0, 1/65536.0, 1/16777216.0); return dot( enc, kDecodeDot ); } |
其中,EncodeFloatRGBA
函数将1个32bit的float数据(取值范围0~1之间)转换为float4类型。因此 EncodeFloatRGBA
相当于把一个32bit的float拆成了4份。 DecodeFloatRGBA
函数则将float4还原为float。
十进制浮点数与二进制浮点数
十进制
每位代表10的幂,因此有:
其中,每一位的范围为 0~9, 例如
二进制
每位代表2的幂,因此有:
其中,每一位的范围为 0~1, 例如
Float32 的表示规格
符号位s: 代表此Float的正负,1为负,0为正。
指数位e: 代表此Float的指数范围,8bit可表示的数值范围为0~255,由于指数有正负符号,因此国际标准IEEE 754规定,指数需要减去127,由此将0~255映射到了-127~128的范围内。
底数位m: 代表此Float的底数,也就是Float内真正存储的数值,由于科学记数法的底数形式,得出Float小数点前的第一位总是为1(在二进制中),因此省去最高位的1不存储,例如1.00101、1.11010等等会被保存为00101、11010,因此23-bit的底数实际可以表示24-bit位的数据。
1234.5678 as an example
将十进制数 1234.5678 用 float32 来表示:
将十进制浮点数1234.5678 转换为二进制浮点数为10011010010.1001000101011
用科学计数法表示,挪动小数点得到最高位的1的右侧,并记录挪动位数
10011010010.1001000101011 = 1.00110100101001000101011 * 210
由此,可以计算出符号位s,指数位e,底数m
符号位s = 0,(正数s=0)
指数位e = 10001001,(挪动了小数点10次,10 + 127 = 137,再转换为二进制)
底数位m = 00110100101001000101011,(省略最前面的1)
最终计算出的32-bit Float 为
0100 0100 1001 1010 0101 0010 0010 1011
EncodeFloatRGBA 分析
EncodeFloatRGBA函数输入参数为float v, 返回参数为float4 enc,其中的内部数据变化可以图解如下:
下面以 float v = 0.123456789 为例,追踪这个函数内部的数据变化
首先将v = 0.123456789转换为二进制,则v = 0.000111111001101011011101010
根据代码
1 2 |
float4 kEncodeMul = float4(1.0, 256.0, 65536.0, 16777216.0); float4 enc = kEncodeMul * v; |
这个点乘的意义,从二进制的角度来看是移位,256 = 28 , 65536 = 216 , 16777216 = 224
从十进制的角度来理解,12.34 * 101 = 123.4,就是将小数点右移一位,指数代表移动位数
对二进制同样,1101.1011 * 22 = 110110.11,小数点右移两位,指数为负则左移小数点
因此,对v* float4(20, 28, 216, 224),意味着将v的小数点分别右移0、8、16、24位
结果为:
1 2 3 4 |
enc.r = 0 . 000111111001101011011101010 enc.g = 000011111 . 1001101011011101010 enc.b = 00001111110011010 . 11011101010 enc.a = 0000111111001101011011101 . 010 |
下一步代码
enc = frac (enc);
frac()
函数代表舍去整数部分,只保留分数
1 2 3 4 |
enc.r = 0. 000111111001101011011101010 enc.g = 0. 1001101011011101010 enc.b = 0. 11011101010 enc.a = 0. 010 |
下一步代码
1 2 3 4 |
float kEncodeBit = 1.0/256.0; // (为了方便解释,在这里增加一个临时变量) float4 tmp = enc.yzww * kEncodeBit; enc -= tmp; |
与前面同理,1/256 = 2-8, 意味着将二进制数的小数点左移8位
1 2 3 4 |
tmp.r = 0. 000000001001101011011101010 // (将enc.g 的小数点左移8位得到) tmp.g = 0. 0000000011011101010 // (将enc.b 的小数点左移8位得到) tmp.b = 0. 00000000010 // (将enc.a 的小数点左移8位得到) tmp.a = 0. 00000000010 // (将enc.a 的小数点左移8位得到) |
再用 enc - tmp
使得各分量后相同位数被消除(保留前8位)
最后一位的 enc.a 因为也减去了 tmp.w 因此会产生一些误差,tmp.w非常小可以忽略不计
最后,对比v和enc的值
可以看出,得到了与最初图示相同的结果。注意enc.a
虽然有误差,但 0.00111111110 与 0.010 差值很小,可以忽略不计。
DecodeFloatRGBA 分析
DecodeFloatRGBA函数与EncodeFloatRGBA作用相反,它的输入参数为float4 enc, 返回参数为float v,其中的内部数据变化依旧可以图解如下:
根据代码,
kDecodeDot = float4(1.0, 1/256.0, 1/65536.0, 1/16777216.0)
= float4(20, 2-8 , 2-16 , 2-24)
因此将它与 enc
点乘的结果,就是将enc的rgba分量的小数点分别左移相应的位数再求和
举例:
这样就还原了 float v
的数值