再谈Unity中的光照探针技术
在我刚入行的时候,曾经读过Unity Technologies的《Light probe interpolation using tetrahedral tessellations》,当时也没有什么特殊的感觉,只是普普通通的觉得习得算法+1。时隔两年,有机会去重新实现这套算法,让我确实有了更深刻的理解。恰巧中文社区对这项技术没有什么讨论,是故撰文以作记录,谓之再谈。本文仅讨论光照探针的插值和搜索技术,对插值过后,如何进行光照计算不作讨论。文章分为两个部分,一,算法原理介绍。二,算法的优缺点。
算法介绍
首先,确认一个问题,光照探针技术到底在做什么?光照探针技术本质上是属于Irradiance Volume系列的技术,其算法目的是记录空间中一点的光照情况,即Radiance的分布情况。Radiance在空间中的分布是位置和方向的函数,可以记作 https://www.zhihu.com/equation?tex=L%28p%2C%5Comega%29 .一旦我们拥有了Radiance函数,就可以在任意物体表面的任意一点根据渲染方程进行着色。
https://www.zhihu.com/equation?tex=L_o%3D%5Cint%7BL_ifcos%5Ctheta%7Dd%5Comega
虽然Radiance函数是一个连续函数,但是并没有解析式。所以,如果我们想知道空间中任意一点,任意一个方向的Radiance值 ,只能通过采样和重采样来解决。回到Unity的光照探针的这个例子上来,在场景中摆放的光照探针打位置,便是对Radiance函数的采样点,而运动物体的着色点的位置则是重采样点。无论是Unity还是Unreal,在重采样的时候均选择线性插值,即,认为Radiance函数在采样点之间线性变化。尽管这个假设并不正确,但是依旧被工业界广泛采用。
插值技术
上文说到,要想知道空间中任意一点,任意一个方向的Radiance值 ,只能通过对现有采样点进行插值来获得。而Unity采用的方案是四面体插值。
四面体插值
这是Unity PPT中的原图,很好的解释了什么是四面体插值。对于空间中一点P,可以用四面体的四个顶点坐标的线性组合来表示,即, https://www.zhihu.com/equation?tex=P%3DaP_0%2BbP_1%2BcP_2%2BdP_3 其中 https://www.zhihu.com/equation?tex=a%2Bb%2Bc%2Bd%3D1 。而a,b,c,d的解法也在上图的矩阵公式里。在我们解得a,b,c,d之后,且点P确实在四面体之中,便可以用它们来对Radiance函数进行重采样,即
https://www.zhihu.com/equation?tex=L%28P%2C%5Comega%29%3DaL%28P_0%2C%5Comega%29%2BbL%28P_1%2C%5Comega%29%2BcL%28P_2%2C%5Comega%29%2BdL%28P_3%2C%5Comega%29
四面体外插值
然而空间中一点P,并非总是在四面体之中的,那Unity如何插值呢?在Unity的PPT中描述到,Unity会将P点投影到四面体的外壳表面上进行插值。如下图所示。
然而这里采用的投影,并不是垂直投影。而是将壳表面沿着顶点法线匀速向外扩,得到一个经过P点的新的三角形 https://www.zhihu.com/equation?tex=%28%5Cvec%7BP_0%7D+%2Bt%5Cvec%7BV_0%7D%2C%5Cvec%7BP_1%7D+%2Bt%5Cvec%7BV_1%7D%2C%5Cvec%7BP_2%7D+%2Bt%5Cvec%7BV_2%7D%29 ,再去求P点在这个新三角形下的重心坐标.如下图所示
而投影点 https://www.zhihu.com/equation?tex=%5Cvec%7BP%27%7D%3Da%5Cvec%7BP_0%7D%2Bb%5Cvec%7BP_1%7D%2Bc%5Cvec%7BP_2%7D .最终的插值则使用投影点的插值, https://www.zhihu.com/equation?tex=L%28P%2C%5Comega%29%3DaL%28P_0%2C%5Comega%29%2BbL%28P_1%2C%5Comega%29%2BcL%28P_2%2C%5Comega%29 .
Unity在PPT中谈到为什么要做这种投影,目的是为了保证投影的连续性。然而我通过测试发现,这个投影并非连续。这个在后面来谈。
很显然,四面体外插值的系数a,b,c要比四面体内插值系数难求解的多。因为我们需要先求解t。合并一下方程。
https://www.zhihu.com/equation?tex=a%28%5Cvec%7BP_0%7D-%5Cvec%7BP2%7D%2Bt%28%5Cvec%7BV_0%7D-%5Cvec%7BV_2%7D%29%29%2Bb%28%5Cvec%7BP_1%7D-%5Cvec%7BP2%7D%2Bt%28%5Cvec%7BV_1%7D-%5Cvec%7BV_2%7D%29%29-%28%28%5Cvec%7BP%7D-%5Cvec%7BP_2%7D%29-t%5Cvec%7BV_2%7D%29%3D0
https://www.zhihu.com/equation?tex=%5Cbegin%7Bbmatrix%7D++%5Cvec%7BP_0%7D-%5Cvec%7BP_2%7D%2Bt%28%5Cvec%7BV_0%7D-%5Cvec%7BV_2%7D%29+%26++%5Cvec%7BP_1%7D-%5Cvec%7BP_2%7D%2Bt%28%5Cvec%7BV_1%7D-%5Cvec%7BV_2%7D%29+%26+%5Cvec%7BP%7D-%5Cvec%7BP_2%7D-t%5Cvec%7BV_2%7D+%5C%5C+%5Cend%7Bbmatrix%7D++%5Cbegin%7Bbmatrix%7D+a+%26+b+%26+-1%5C%5C+%5Cend%7Bbmatrix%7D+%3D0
Unity的工程师认为方程左边的矩阵行列式为0。这样可以构造一个关于t的三次方程且t有唯一正根。然而,根据我的测试发现,该方程存在无根和有多个正根的情况。这反过来说明这个投影并非连续。
在求得t之后,就可以通过常规方法再去解系数a,b,c了。
搜索技术
前文介绍了一个点在四面体内和四面体外如何对Radiance函数进行插值。那如何找到这个点在哪个四面体或者四面体外的哪个空间呢?Unity在它们的PPT中这样介绍到:给定一个点P,再任意找一个四面体,求出P在该四面体下的重心坐标,如果P的重心坐标的4个分量均大于0,则点P在这个四面体内,否则,沿着最小的重心坐标所以对应的方向搜索下一个四面体。Unity在它的PPT中给出了一个在二维空间搜索的例子。
小黄脸所在的位置就是现在的P点。最开始从浅色小黄脸所在的三角形中开始搜索。算出来小黄脸的重心坐标是(-0.7,0.5,1.2)。那下一步则搜索-0.7这个点所对着的三角形。
小黄脸在这个三角形下的重心坐标是(-0.2,0.6,0.6)。则继续沿着-0.2所对着的三角形搜索。
到第三个三角形,得到重心坐标(0.4,0.4,0.2)。则表示已经找到了小黄脸所在的三角形了。
上面这个例子是二维情况下的三角形搜索。那三维空间下的四面体搜索也是一样的。
至此Unity中光照探针的插值和搜索技术就已经介绍完了。现在来总结一下这项技术优缺点以及使用时的注意事项。
算法的优缺点
缺点
我们先从缺点开始。这个算法的缺点主要是由四面体外插值造成的。
算法不完备
前文说到,在四面体外插值的时候,需要将四面体的壳表面沿着顶点法线向外扩,直到经过顶点P为止。
而这需要先求解方程,这是一个关于t的最高次数为3次的方程。。而这样一个方程可能有0,1,2,3个根。
如果这个方程有0个根,这意味着我们沿着顶点法线走,无法找到一个过点P的平面。这也意味着,这个算法是不完备的,在这种情况下,我们无法为P点进行插值。但是好在并非没有破解之法。我们可以简单的将P点垂直投影到壳表面上,以这个投影点 https://www.zhihu.com/equation?tex=P%27 作为插值点。然而这么做会打破插值函数的连续性,并非明智之举,实属没有办法的办法。从视觉上来讲,就是当一个玩家在Unity光照探针范围之外活动,走到某个特定的位置的时候身上的亮度会突然改变。而这种突变的明显程度受最外层光照探针的亮度差影响。如果最外层的光照探针,一个特别亮,一个特别暗,那么玩家触发的这个亮度突变会特别大。但是好消息是在实际游戏项目中,大部分探针都会摆放的比较相近,因此他们的亮度也会比较相近。而导致这种亮度的突变其实不易察觉。但是为了避免触发算法本身的这个问题,在Unity中摆放光照探针的时候应该尽可能覆盖整个场景和玩家活动区域。
同样的,如果方程有两个根,或者3个根,我们也无法直接为P点进行插值。通常会选择最小的t值去生成插值。同样的这种情况下也会造成玩家身上的亮度突变。同样的,避免方法也是摆放光照探针的时候应该尽可能覆盖整个场景和玩家活动区域,尽量不要让玩家走到光照探针的四面体之外。
数值不稳定
Unity的算法除了不完备以外,还存在数值不稳定的问题。即,当我们求解方程的时候,本来真值 https://www.zhihu.com/equation?tex=t%3Dt_0 。但是由于计算机精度不够,计算出来的值是 https://www.zhihu.com/equation?tex=t%3Dt%27_0 。这就导致插值出来的结果与真实值有一定的偏差。虽然这在视觉上这不会有太明显的差异。但是这种偏差会对搜索造成一定的影响。举一个二维空间的例子
此时P点位于空间EACF内,它的真实的插值坐标应该是(0.001,0.999).但是由于计算精度不够,导致计算出来的结果是(-0.001,1.001)。这个时候搜索程序会认为P点在EABD空间内。于是将P置于EABD空间内计算重心坐标。这个时候就会计算出来(1.001,-0.001)。这个时候搜索程序又会认为点P在EACF内,重新进行搜索,如此循环往复,直到消耗完最大搜索次数为止。很显然,这会造成非常明显的卡顿现象,且当玩家从一个四面体外空间进入到相邻的外空间的时候容易触发这一问题。自然这也没有什么好的解决方法。只能通过摆放光照探针的时候应该尽可能覆盖整个场景和玩家活动区域,尽量不要让玩家走到光照探针的四面体之外来避免。
很可惜,Unity算法中的这两个问题都无法在这个算法框架内解决,只能通过设计新的外插值方案来解决。在更好的光照探针插值方案被设计出来之前,我们能做的只能是去尽可能的避免它。
四面体外插值计算开销大
Unity算法的第3个缺点就是当玩家位于四面体之外时,计算开销会比较大。其主要原因是由于当玩家位于四面体之外,需要求解一个关于t的最高次数为3次的方程。至于与四面体内部相比差距有多大,则因计算设备性能而异。自然这也没有什么好的解决方法。只能通过摆放光照探针的时候应该尽可能覆盖整个场景和玩家活动区域,尽量不要让玩家走到光照探针的四面体之外来避免。
优点
很显然,Unity算法的优点正如同它在PPT中所说,这套算法支持随意摆放光照探针的位置。这有两个好处
[*]探针可以贴墙摆放,解决墙另一侧探针的光会透过墙,影响到墙内侧的问题。
[*]在空旷,光照情况变化比较平缓的地方可以少放置探针,减少存储数据需要的开销。
总结
根据上面的分析,可以得出结论,Unity这套算法的主要问题来自于四面体外插值方案,而这些问题均可以通过摆放光照探针的时候应该尽可能覆盖整个场景和玩家活动区域,尽量不要让玩家走到光照探针的四面体之外来避免。
<hr/>PS:灵犀互娱内推直通车。panyuanzhi.pyz@alibaba-inc.com
页:
[1]