最近发现移动平台上经常出现不明黑色色块,像是除零错误。排查之后定位到GGX函数上:



inline half GGXTerm(half NdotH, half roughness)
{
        half a = roughness * roughness;
        half a2 = a * a;
        half d = NdotH * NdotH * (a2 - 1.0f) + 1.0f;
        return UNITY_INV_PI * (a2 / (d * d));
}



果然roughness为0的时候d就可能为0,继而出现除零错误。同样代码PC上就不会出现肉眼可见的问题,可能是tile中一个像素出错会导致整个tile都输出黑色,看来mobile gpu真的很敏感。看起来只要限制最小粗糙度*或者在除数上附加一个简单的小数就能修复这个问题了:



inline half GGXTerm(half NdotH, half roughness)
{
        half a = roughness * roughness;
        half a2 = a * a;
        half d = NdotH * NdotH * (a2 - 1.0f) + 1.0f;
        return UNITY_INV_PI * (a2 / (d * d + EPSILON));
}



意料之外的是:真机测试上,即使EPSILON大到0.01,仍然有除零错误。再看一遍代码,作为除数的d竟然为粗糙度的8次方,这对精度提出了相当的要求。是否移动平台上half的精度和pc上有巨大差异呢?

 

上网搜索一番,发现已经有人严谨的研究过这个问题,并且制作了有趣的试验,让精度问题能以图形化的方式在不同设备上可视化(见参考资料)。文章中提到了三个影响精度的因素:

1)有效位数

2)取整方式(RTZ和RNE,后者提供双倍精度)

3)0洞和对Subnormal(低于最小精度)的支持

在PC和mobile上重新进行Tom Olson的试验发现:

PC对half和float的表示是一致的,都是8位指数,23位有效位;

mobile上的float是23位有效位,half是5位指数,10位有效位。

平台

取整方式

Subnormal

PC(GTX1080)

RTZ

支持

红米3s(Adreno 430)

RTZ

不支持

荣耀5C(Mali T830MP2)

RTZ

支持

Iphone5s

RNE

支持

Iphone6

RNE

不支持

 

 

 

 

苹果竟然开倒车,5s还是实行高标准的RNE+Subnormal,6竟然不支持Subnormal了。

指定在计算过程中使用float可以避免这个问题**,如下所示。



inline half NN4_GGXTerm(half NdotH, half roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float d = NdotH * NdotH * (a2 - 1.0f) + 1.0f;
    return (half)(a2 / (d * d));
}



 

 

附注:

*

不支持Subnormal的half能支持的最小粗糙度数值:

5位指数位,可以表示的范围是[-14,15].

按d=roughness8计算,d的最小精度为2-14. 粗糙度的最小值为0.2973.而测试结果表明可以支持比这低得多的数值。

再看一遍代码注意到:



half a2 = a * a;
        half d = NdotH * NdotH * (a2 - 1.0f) + 1.0f;
        return UNITY_INV_PI * (a2 / (d * d));



其实可以先计算a/d,然后再平方。a/d的最小精度为2-14,计算出粗糙度的最小值为2-14/4=0.08838835,与测试结果完全吻合。

注意!

如果使用



return UNITY_INV_PI * (a2 / (d * d + EPSILON));



这种优化形式,编译器就无法先算a/d,再平方了。也就大大限制了粗糙度的能取的最小值。

 

**

事实上,即使使用float计算ggx,对normal的精度要求也是16位的half无法满足的。如果在计算ggx时使用32位float,但normal和view还是16位精度,某些平台会出现下图这样的噪点,完全无法表现ggx的高光形状。

 

android手机GPU使用率监测 手机gpu使用率0_最小值

 

 

 

***

写到这里的时候我已经把distribution项换成blinn-phong了。还有ggx anisotropic也换成了ward。没办法ggx的高光形状在粗糙度很小时非常敏感,美术经常想调到0.1以下。还有某些奇葩的平台要兼容呢(说的就是你,红米3s)。还有还有据说移动平台上float转half挺费的。

 

****

补充:D term的输出结果也要限制一个最大值。不然在hdr模式下可能非常大,然后toonmapping里一算倒数就进0洞了。。。

 

 

参考资料:

https://en.wikipedia.org/wiki/Half-precision_floating-point_format

https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Exponent_encoding

https://community.arm.com/cn/b/blog/posts/gpu-1253895321

https://community.arm.com/cn/b/blog/posts/gpu---2-1049657424

https://community.arm.com/cn/b/blog/posts/gpu---3-792964848