在当前的游戏引擎中,使用Light Probe来计算全局环境光对于动态物体的影响是一种很主流的方法。在预处理阶段生成完场景的Light Probe之后,传统的方法采用查找最近的8个相邻的Probe然后使用三线性的方式(Trilinear Interpolation)进行插值,但是这样的插值代价稍大,不过一个可行的优化就是尽可能地减少插值中使用的Probe的数量,比如由8个减少到4个不等。但是这时就不能再用三线性插值,而需要用其它的插值方法比如Inverse Distance等,不过这样就会带来另外一个问题,那就是使用的Probe一般是最近的几个(如三个到四个),如此一来当物体移动之时得到的最近Probe变化可能会比较明显,进而得到的插值结果也会不太连续,影响效果。今年的GDC上Unity的一篇Presentation提到了使用四面体来进行无序Probe的插值,稍花了些时间简单实验了下,还是蛮不错的。文章可以在这里看到:Slide,其最大的一个特点就是Probe可以无序地随机分布,这样在编辑器里边添加以后对美工还是蛮友好的。
四面体网格的生成
为了对Probe所对应的点集使用基于四面体的插值,首先就需要生成这堆点集所对应的四面体集合,这个问题属于传统几何图形学的典型问题,解决方法也很多,比如广泛用在三角细分算法中的Delaunay方法(Paper)。使用Delaunay的方法可以实现对三维空间点集的四面体化操作,不过鉴于此操作不是所关注的问题的关键所在,可以借助于已有的代码来完成该步骤。比如这里就有一个开源的代码库:TetGen(作者应该是一个身在德国的中国哥们)可以完成点集的四面体切分,当然这个里边还有其它不少更高级的功能,感兴趣也可以学习一下。它支持输入很简单的点集描述,然后在一些简单的配置参数下计算得到该点集的四面体化结果。比如对一堆随机生成的点集的四面体化效果如下所示:
四面体重心坐标的计算
要使用四面体对一个给定的目标点进行插值,就要计算其相对于该四面体的重心坐标,也即是其相对于四个顶点的权重比值。这个操作要在每次Probe进行更新的时候都需要进行,因而就要求效率尽可能地高,之前的blog中介绍了使用有向平面距离的方法来计算四面体重心坐标的方法,效率还可以。
其中,这里边的有向距离1 / D(a,PLbcd), 1 / D(b,PLacd), 1 / D(c,PLabd), 1 / D(d,PLabc)对于每个四面体来说均是固定的,因而就可以存储到每个体数据中以便加快效率。
空间数据结构的组织
由于常规的Probe分布是由对应的AABB或OBB来进行定位的,这样在查找目标点所对应的邻近Probe时可以直接进行对应。因为一个区域内的Probe一般是在三个方向上均匀分布的,也就相当于一个三维的数组结构,这样给定一个目标点的位置,就可以直接根据Probe的密度、各个方向上的size及step直接定位到其周边的相邻Probe (AABB 的操作最直接,对于OBB则需要做一个局部坐标系的转换,之后就跟AABB的计算相同),因而这种情况下对于整个Probe数据的组织就比较简单。但是使用基于四面体的方式之后,由于原始Probe的无序,因而这里的数据结构组织就较常规的方法有些区别。首先,对于四面体组织的网格,需要知道四面体间的拓扑信息,也即通过一个四面体能够找到其邻接的四面体,如果用ID来对应每个四面体的话那么在每个四面体中就需要添加四个存储其相邻体的ID数组。另外,由于四面体插值的频繁进行,而这里的插值方法又是使用面的方程来进行,因而就可以将每个面的描述信息也进行存储,这样来提高效率。但是四面体的面是可以公用的,虽然一个面最多只能被两个四面体共用,但还是有必要在存储前来删除冗余。这样其实最终的数据结构就可以如下描述:
- 点:点的位置;关联的Probe的ID或指针;
- 面:关联的三个点的ID;面方程;
- 体:关联的四个面的ID
当然,在上述描述的具体实现过程中还会有其它若干细节,比如要注意组成四面体的点与面之间的对应关系,以便正确找到下一个关联的四面体等;不过这些都比较琐碎且也是具体实现相关。
查找与插值的处理
由于在上述数据结构中存储了四面体之间的拓扑结构,因而在进行目标点对应的四面体定位时就可以使用这些拓扑信息。首先,对于点与四面体的关系及其对应的重心坐标可知:如果一个点在四面体内部,那么它对应的重心坐标均属于[0,1];否则,就可以通过重心坐标的越界方式找到下一个其可能位于的四面体,如此进行若干次迭代之后就可以正确定位到其所在的四面体,或是到达所有四面体集合的边缘。为了快速定位一个点对应的四面体,可以有多种方法:
- 选择所有四面体中处于较中心位置上的一个四面体作为Spawn点,来进行目标体的查找(Presentation中使用的方法)
- 随机选择一个四面体作来初始点,来进行目标体的查找
- 使用各种空间分割方法,如BVH等来对四面体集进行空间组织后加速目标体的查找
当然,在使用上述方法的过程中又会有多种优化方式,比如一些信息的Cache,像记录上一次的目标四面体作为下次查询的初始点等。
对于目标点位于四面体集合外侧的情况,使用边界扩展的方法来进行插值计算。如上图所示(以2D情形为例),其中的黑色线条为体集合的外缘,而绿色区域为每个三角面向外侧的扩充;其中的绿色线条方向为交界顶点处的法向量(这其实也相当于计算每个外表面对应的Voronoi区域)。然后,对于每个目标点P通过投影得到其对应的原始三角面中的位置P‘,以P‘计算其对应三角形的重心坐标后用此三角形上的三个点来插值得到P点的最终结果值。但是这种方法会在集合外侧得到与到集合表面距离无关的插值结果,而实际情况中想要的应该是:外侧体面对于目标点的影响应该随着该点到面的距离增大而减弱。不过这个也比较简单,可以对最终的外缘插值结果再增加一个针对距离d的衰减变化即可。
与常规方法的比较
最后,将基本四面体的Probe插值方法与传统的插值方法进行对比。整体来说,这种方法还是不错的,如果引擎中有之前的均匀分布的Probe系统之后,修改到这种基于四面体插值还是比较容易的。由于Probe的均匀分布,因而可以不错助于其它方式(如TetGen)而直接生成其所对应的四面体网格,而对于数据结构的修改也不是很大。对于某一个物体记录四面体Cache之后的查找更新操作也是比较快的。就算不带来效率提升也不至于也不用损失太多的性能(具体数据还木有来得及对比)。不过使用基于四面体的方法之后最大的一个优势就是可以在引擎中将此前以Probe集合为基本单位的Probe系统转化为以单个Probe为基本单位的新系统,Probe的生成与控制方式更加灵活,Unity里应该就是这种方法。
其它的一些优化
- 在边界插值时可以将距离变化影响也考虑在内,这样只需针对d施加一个合适的衰减函数即可(线性或非线性的)。但是要注意此函数在d=0点处的值应为1,以保证落在边缘表面上的点的插值结果的正确性。
- 可以使用Least Square的方法并结合四面体的插值描述来对整个Probe系统进行优化,这样在给定一定的误差阈值之后,就可以减少Probe的数量。
下述为一些实验效果:其中的Probe是在一个指定的区域范围内以均匀的分布方式随机生成,数量为512个;Probe的球谐值采用球面蒙特卡洛采样方式计算得到;每个Probe球面上采样1024个点,场景采用BVH空间分割来加速求交测试:
其中的黑色线框即为当前角色中心所处于的四面体,从中可以看出:两个邻近不同位置的目标点对应了两个相邻的不同四面体。
转:http://blog.csdn.net/bugrunner/article/details/7485475