float 编码为 RGBA8

简述:简单记录一下 如何将 一个 (0-1)float 的值转换到RGBA,和如何从 RGBA 解码出一个(0-1)的float值,这个主要用在需要存储单个高精度的数到纹理时,如果只存放在单个通道那么其只能是八位存储,精度肯定不足,而如果使用32位存储,就相当于把四个通道拼接起来,这样精度就可大幅提升,注意这张纹理不可以压缩,压缩完解出来的数就无法正常还原了

Unity 中的函数

函数来自于 unity2021.3.11f1
UnityCG.cginc中的原函数代码如下:

    // Encoding/decoding [0..1) floats into 8 bit/channel RGBA. Note that 1.0 will not be encoded properly.
    inline float4 EncodeFloatRGBA( float v )
    {
        float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 16581375.0);
        float kEncodeBit = 1.0/255.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/255.0, 1/65025.0, 1/16581375.0);
        return dot( enc, kDecodeDot );
    }

关于 float

float 是一个32位浮点数
double 是64位浮点数

何为位

我们都知道计算机用的是 010101...... 的二进制,我们拿 8 位无符号整数来举例子

  • 255 二进制表示就是 11111111
  • 254 二进制表示就是 11111110
  • 10 二进制表示就是 00001010

可以看到,我们可以用 8 个 0 和 1 来存储 255 以内的整数。
以上我们用的是无符号,当使用有符号时,那么就会从32位中拿出一位来表示正负数,一般取最左边的一位
同理 32 位整数 就是用 32 个 0 和 1 存储的整数,32 位浮点数更为复杂,他还需要处理小数点,

单位的表示与进制

一个位我们记做 bit
一个字节有8位 我们记做 Byte 存储的最小单元,即:计算机最小也要存 8 位
所以我们也可以说一个 float值 占 4 个 Byte
再往下就是我们熟悉的存储单位进制了
1KB = 1024 Byte
1MB = 1024 KB
1GB = 1024 MB
1TB = 1024 GB

float 的二进制存储方式

参考自浮点数的表示方法

在计算机中一个任意二进制数 N 可以写成: N=2^e.M
其中

  • M 称为浮点数的尾数,是一个纯小数
  • e 是比例因子的指数,称为浮点数的指数,是一个整数
  • 比例因子的基数2对二进记数制的机器是一个常数
IEEE754标准,32位浮点数标准格式
  • S 是浮点数的符号位,占1位,安排在最高位,S=0表示正数,S=1表示负数
  • M 是尾数,放在低位部分,占用23位,小数点位置放在尾数域最左(最高)有效位的右边
  • E 是阶码,占用8位,阶符采用隐含方式,,即采用移码方法来表示正负指数,移码方法对两个指数大小的比较和对阶操作都比较方便,因为阶码域值大者其指数值也大。采用这种方式时,将浮点数的指数真值e变成阶码E时,应将指数e加上一个固定的偏移值127(01111111),即 E=e+127
    在IEEE754标准中,一个规格化的32位浮点数x的真值表示为
    x = (-1) ^s X(1.M)X 2^(E-127)
    e = E - 127

乘除法与位移的关系

本人不是专业搞数学的,可能描述不严谨,但是整体的原理意思是这样的
一个非 0 数乘以 则就是向左移 n 位,除以就是向右移动 n 位
以 int32 的 1 为例子:
1 的二进制表示为 00000000 00000000 00000000 00000001 为了书写方便,前面的 24 个 0 我们接下来就不写了,但是要知道他是存在的
可以写为 二进制为 00000001<<1 = 00000010
可以写为 二进制为 00000001<<3 = 00001000
我们换个数 11 其二进制(同样没写前面的24个0) 00001011
可以写为 二进制为 0000 1011<<3 = 0101 1000
可以写为 二进制为 0101 100 >>3 = 0000 1011

Unity 中的编码与解码函数

    inline float4 EncodeFloatRGBA( float v )
    {
        float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 16581375.0);
        float kEncodeBit = 1.0/255.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/255.0, 1/65025.0, 1/16581375.0);
        return dot( enc, kDecodeDot );
    }

适用于0-1的范围内的值的编码
我们通过观察一个 float 的编码过程来看其计算原理

编码过程

  • EncodeFloatRGBA( 0.523)

  • float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 160581375.0); // 声明了一个常数我们来分析一下

     //给定的数如下
     255 的二进制表示 为 1111 1111
     65025 的二进制表示为 1111 1110 0000 0001
     16581375 的二进制表示为 1111 1101 0000 0010 1111 1111
     //从二进制找不到规律,但是发现他们和255 有关系
     65025 = 255 X 255 =  255 ^2 
     16581375 = 65025 X 255 =255^3
     //单个通道的颜色值范围就是 0 - 255
     //所以得到的推理结果为 将对应的 (0-1)的数放到 255 倍 255^2倍 255^3倍
    
  • float kEncodeBit = 1.0/255.0; //后续会有除以255.0操作,这里提前做一个除法,在后续中使用乘法乘以 kEncodeBit 相当于除以了 255,一个计算优化

  • float4 enc = kEncodeMul * v; //我们把值带入看一下计算过程

        //将具体值代入
        float4 enc =  float4(1.0, 255.0, 65025.0, 160581375.0) * 0.523;
        enc =float4(0.523 , 133.365 , 34008.075 , 8672059.125);
    
  • enc = frac (enc); //取小数部分,这一步和下一步可能不好理解

      enc=frac(float4(0.523 , 133.365 , 34008.075 , 8672059.125));
      enc=float4(0.523 , 0.365 , 0.075 ,0.125)
      //R 通道没有变化,我们从G通道开始看
      //133.365 是将 0.523放大了255倍,做 frac 之后我们相当于移除掉了数据中超过1的部分,保存小数部分 ,整数部分应该存在R通道,当然目前R通道保存的还是元数据
      //34008.075 是将 0.523放大了255^2 倍,也可以理解为是在 G 通道截取小数之后,再放大 255 倍,然后再移除超过1的部分,然后将剩余小数部分存在 B 通道,
      //同理 A 通道 是在 B通道截取之后取将剩余小数放大255倍,然后做移除和截取
      //到此我们会发现,R通道的保存的小数是最全的,G 通道放大 255 之后损失了部分大于一的部分,而损失的这一部分是可以通过 R 通道的值乘以 255 取整得到的,同理,B 通道损失的整数部分可以通过 G 通道乘以 255 得到,以此类推
      //但是目前这个四个通道无法通过简单的缩小相加得到原来的值,因为 R 的小数部分和G是有重合的,G 的小数部分 和 B 是有重合的,所以有了下一步
    
  • enc -= enc.yzww * kEncodeBit; // 这一步是为了移除每个通道和其后面的通道的重合部分

       //为了统一表达 我们令
       enc.yzww 等价于 enc.gbaa
       enc.gbaa*kEncodeBit;//其实是将当前通道缩放回小数状态 
       //g*kEncodeBit;得到的是 g 保存的小数在 r 通道中的大小,因为 G 是将 r 放大 255 倍之后获取的小数,所以 将 G 缩小 255 倍,就是这部分小数在 r 中的实际大小 ,
       //依次类推,每一个通道除以255(也就是乘以kEncodeBit)都是为了得到当前数据在前一通道的实际小数值
       enc -= enc.gbaa*kEncodeBit  展开写为
       enc.rgba=enc.rgba-enc.gbaa*kEncodeBit;//实际上就是从前一个通道要保存的数据 移除后一面通道的数据,这样后期解码的时候才可以简单缩放相加得到原始的值
       //带入数据
       enc.gbaa*kEncodeBit的结果为float4(0.00143,0.00029,0.00049,0.00049)//小数数很长没有全写上,便于表示完整数据,我们使用分数形式来写
       enc.rgba=float4(0.523 , 0.365 , 0.075 ,0.125) - float4(0.365/255  ,0.075/255 ,0.125/255 ,0.125/255)
       enc.rgba=float4(0.523-0.365/255 , 0.365-0.075/255 , 0.075-0.125/255 , 0.125-0.125/255)
     //这里有一个点,就是 A 通道的数据减了一次自己缩小了255的值,这里正常来讲A 应该是不做操作,它应该包含最后的所有小数部分。在解码的时候我们可以观察到 其确实有损失,但是非常小,就0.523来说大概在小数点后12位会有一点大的偏差,官方可能是为了性能考虑,没有单独再处理 A 分量的问题
    

解码操作

  • DecodeFloatRGBA(float4(0.523-0.365/255 , 0.365-0.075/255 , 0.075-0.125/255 , 0.125-0.125/255) )

  • float4 kDecodeDot = float4(1.0, 1/255.0, 1/65025.0, 1/16581375.0);//声明一个缩放常数,用来将每个通道的数值缩放回其正常大小值

  • return dot( enc, kDecodeDot ); //做点积操作,然后返回最终结果,这里的最终结果就是解码结果

      //先看一下dot的计算方式
      float4 A ,B  //AB是两个四维变量
      dot(A,B)=A.x*B.x+A.y*B.y+A.z*B.z+A.w*B.w
      //带入数值 计算 dot( enc, kDecodeDot )
     dot( enc, kDecodeDot )=(0.523-0.365/255)+(0.365-0.075/255)/255+( 0.075-0.125/255 )/65025+(0.125-0.125/255)/16581375
      =0.523-0.365/255+0.365/255-0.075/255/255+0.075/65025-0.125/255/65025+0.125/16581375-0.125/255/16581375
      =0.523-0.365/255+0.365/255-0.075/65025+0.075/65025-0.125/16581375+0.125/16581375-0.125/255/16581375
      =0.523-0.125/255/16581375
      =0.52299999997043694636715154509083//这里有损失 很小的一个损失
    

图解原理

下方图片来自 https://blog.csdn.net/manipu1a/article/details/121914335
原图作者使用的是2幂次来做的解释,个人感觉和255并没有完全对上,对于这一点还是感觉使用移除正数保留小数部分的说法更准确,如有更好的解释,欢迎留言

float的组成

float4 enc = float4(1.0, 255.0, 65025.0, 16581375.0) * v;

得到的结果

frac 操作去除正数部分

Frac 后小数部分是重叠的

enc -= enc.yzww * float4(1.0/255.0,1.0/255.0,1.0/255.0,0.0);

上图作者是处理了W分量的

你可能感兴趣的:(float 编码为 RGBA8)