在进行PBR运算(几何运算)时能更精确地处理好浮点误差。
IEEE 浮点
极大量的当代GPU/CPU都按照IEEE规定的标准实现浮点数。
以float为例,float有32位,其中1位(最高位)为符号,接下来的8位为指数e,剩下的是小数c,那么原数n等于
(符号)2^(e-127)*1.c(2)
所以说1的二进制表示是01111111100000000000000000000000
当e=0时,需要特殊对待,此时n等于
(符号)2^(e-127)*0.c(2)
当e=255时,也需要特殊对待,如果e等于255,若c不为0,那么就是nan,否然是无穷(带符号)
比较有意思的一点就是nan与任何浮点进行任何比较都是false,!(x==x)可以验证x是否为nan。nan是“感染式”传递的,一顿操作猛如虎,最后输出nan,那么就一定是中间在哪儿出了茬子。
IEEE 运算
加减乘除与开根号这几种运算,IEEE规定实现上必须满足“结果最接近精确值”,这意味着(n-n')<0.5e,其中e为结果的指数级别下最小的浮点单位,n为真实结果,n'为计算得到的结果。如果刚好卡在两个数中间,默认会向偶数舎入。
听起来很牛叉,实现起来想必也不简单,不过那不是咱们考虑的东西辣(哈哈哈哈和)。
误差分析
令p为2^(-24)(相对误差下的0.5e),假设a,b,c,d都是float,考虑计算a+b+c+d,那么a+b+c+d最大可为
(((a+b)(1+p)+c)(1+p)+d)(1+p)
展开有
(a+b)(1+p)^3+c(1+p)^2+d(1+p)
由于p很小,那么可以放缩:
(1+p)^m < 1+(m+1)p
其实因为p实在是太小了,(1+p)^m和1+mp已经无限接近了,这里的1+(m+1)p是一个极度保守的估计。
那么刚才的计算中,最大误差为4p|a+b|+3p|c|+2p|d|。
容易发现这个误差和计算的顺序有关,所以渲染中设计核心算式时可以将较小的数字放在前面,能够减少误差,这个trick很鬼畜吧...
计算顺序和误差是有关的,比如这样:
((a+b)+(c+d))
算出来的误差是
3p|a+b|+3p|c+d|
如果有一个先验:a和-b比较接近,那么误差就会极大减小,所以如果精心设计算式,误差是可以扑通扑通地下降的。不过这种技巧已经出离鬼畜了...
这种技巧有个名称,叫做“前向误差分析”(forward error analysis),与之相对的是数值算法中更喜欢用的“后向误差分析”(这种东西可以逆推得到若要输入数据输入算法得出相同结果,输入数据所允许的误差范围)在计算方法李或许会学?
优化
刚才误差估计中,很让人不爽的一点是(1+p)^n放缩太狠了。加强放缩!
(1+p)^n =
1+np+n(n-1)p^2+...+n!p^n <=
1+np+(np)^2+...+(np)^n <
1/(1-np) = 1+np/(1-np)
令 r(n) = np/(1-np)
那么上文中a+b+c+d的误差上界是r(4)|a+b|+r(3)|c|+r(2)|d|,小很多了!
更进一步,(1+-p)^n/(1+-p)^m这种形式的误差也可以用类似方法去放缩,得到r(n+m)。
更多误差分析
考虑乘法
由于IEEE和我们🐮🍺的微电子工程师,咱们的乘法损失精度也是p(但是是在a*b尺度下的p),然后考虑已经产生了精度损失的两个数作乘法:
(a(1+r(n)))(b(1+r(m))) = ab(1+r(n+m))
为啥呢?
1+r(n) = 1/(1-np)
1+r(m) = 1/(1-mp)
(1+r(n))(1+r(m)) = 1/(1-(n+m)p+(nm)p^2) < 1-(1-(n+m)p) = 1+r(n+m)
好了,由于乘法还要多损失一个精度,所以实际上精度损失是r(n+m+1)