体探针漏光解决方案

体探针漏光解决方案

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


问题描述

在全局照明领域,体探针插值漏光是存在多年的顽疾。比如:GAMES202的LPV(Light Propagation Volumes);Unity的LPPV(Light Probe Proxy Volume);GDC上各种GI分享(某版COD);还有之前我给项目实现的类似UE VLM的功能,都存在这个问题。和很多技术人员一样,一看到行业困难的技术点,就会自己动脑想一套更好的方案来解决它,因为这样可以名正言顺的造轮子。


GAMES202里GI部分提及的漏光问题


Unity LPPV 漏光问题

基础思路


被墙隔离的探针关系

根本的原因当然是探针的密度不可能无限密,而且分空间的表面可以没有厚度。而一般的探针信息里并没有描述空间划分的逻辑,上图中只有红色探针往左和绿色探针往右2个区域是正确的,中间的插值就出错了。所以,所有的思路都是去描述空间的划分,或表面上一个像素与体素的归属关系修复上,就是让A点只采样红色探针,B点只采样绿色探针,因为他们的空间归属如此。

方案1:Bent Normal偏移

我没有把简单Normal偏移作为一种方案,因为这种太基础了。而我在写初版VLM的时候就顺手写了出来,基本是非常容易想到的,而且效果也不够好。看下图,AB有个简单的区分方式,就是它们法线不同,沿着各自法线方向去取探针,会得到正确的结果,也让它们跳出了中间的错误插值地段。但是,墙面不会总是简单的一个方向,有些横梁等结构会像黄色一样,法线偏移后依然是错误的探针。因为AB两个面的这种结构法线完全一样,无法区分彼此,还有更严重的问题是,当角色从A面靠近墙面的时候,靠墙的这部分角色法线是朝右边的,所以导致A处的角色显示绿色的GI。


法线方向偏移采样的问题

所以,我想到了用更大尺度的法线来描述总体法线朝向(或可见度方向)。首先想到是屏幕空间Normal的Mipmap,但是屏幕空间有相机外丢失信息的天然弊端,所以后面选择了离线制作场景的Bent Normal,正确做法应该是对应Lightmap,用UV2解析。但为了演示方便我写到了顶点上。


计算Bent Normal到顶点上

测试下效果,用2个非垂直(垂直问题小很多)交叉隔开的4个空间,分别打3个不同颜色的灯,不打灯的空间为黑色换角色,然后每0.5米一个探针,均匀放置探针,并创建出3DTexture。可以看到Normal偏移在非交叉处是对的,偏移方向就刚好是法线方向。


测试的模型


Bent Normal与Normal的效果对比

Bent Normal算法
Bent Normal的离线计算是比较常见的技术。原理就是对某个点,法线所在的半球方向随机发出很多射线,没有发生碰撞的那些方向全部加起来,然后归一化。很像初中物理里求一堆力的合力,简单相加符合的力就可以,但是这种简单的计算依然会出现小的漏光,加强版里解决。


Bent Normal基础版算法


基础版 发射射线

这个图里,黑色的Mesh与绿色Mesh相交,那么露出的顶点显示的红色Bent Normal方向是对的,但是最右边的顶点因为在物体内部,所以它没有计算出Bent Normal,这样渲染的黄色部分的Bent Normal是左右2个顶点插值的结果,就导致黄色的Bent Normal偏移角度不够,依然会插值到绿色框内部或右面的探针而产生漏光。

具体的改善是不要原点发射方向,而是沿着射线方向偏移一点再发射。如下图,再考虑射线的双向碰撞检测,就可以抛弃蓝色碰撞部分,获得绿色部分的合成,这样得到顶点Bent Normal(黄色),会更符合环境描述,使红黄2个插值的结构也更正确。


高级版发射射线


Bent Normal 高级版算法

该方法最终效果,关于动态对象,不能取这份数据,需要逐对象分帧每帧发射一条射线,累积附近的偏移方向判断(实时Bent Normal计算)

方案2:DistanceProbe

同样是空间划分的描述,我们可以记录下碰撞体距离,根据是否大于距离判断是否取对面,总之在这种过渡位置不插值,要么取x处,要么取x+1处探针,取哪个根据ShadingPoint与Probe的距离是否大于提前记录的Probe到障碍物距离来定。

下图所示,绿色圈表示当前ShadingPoint位置;2个红圈表示用来插值的2个Probe颜色。为了直观只演示x方向上的判断。

  1. 左边红圈处没有记录往右存在1米内的碰撞,所以直接取x与x+1,2个红圈位置的Probe颜色来插值。


直接用2个红圈插值

  1. 右边存在碰撞,但与ShadingPoint距离小于与碰撞体的距离,那么就直接取x处Probe颜色为ShadingPoint颜色。


红圈右边存在碰撞,但与ShadingPoint距离小于与碰撞体的距离

  1. X处右边存在碰撞,与ShadingPoint距离大于与碰撞体的距离,那么就直接取x+1处Probe颜色为ShadingPoint颜色。


x右边存在碰撞,与ShadingPoint距离大于与碰撞体的距离

所以只要把偏移数据再创建一个3DTexture来存储,设置为Point Filter,即可实现功能。

看下效果(每米增加到8个Probe的效果)。

这种方式有2个问题:

  1. 需要较高的摆放密度。这是因为同一个空间内比如方案1里的(0.5x0.5x0.5)内,只记录一个到x+方向的float距离,(y+、z+同理都是一个float),但是一个空间内不同位置的距离应该是不同的,所以需要尽量让这个单位空间去小的范围。
  1. 这种距离固定是只出现在碰撞面刚好与xyz坐标轴垂直的情况下,如果表面不是轴对齐, 那么一个小空间内也不能用一个float描述距离,这时候需要记录平面方程来判断空间划分,而不是一个float的Distance,存储容量x4,而实际工程中一定是需要支持非轴对齐的情况的,除非无限细分空间。


左边是非轴对齐表面,右边是轴对齐表面

关于动态对象,这个算这个方案的优势之处,因为存储的是空间信息,所以任何ShadingPoint都能根据WorldPosition直接获取偏移量,得到正确数据。

方案3:Decal Offset

前面2种方案都需要受到体素/空间划分密度影响,这是因为它们都是均匀摆放,导致一些位置受限。所以还有一种更直观的想法,就是我可以把某个不规则空间内应该往哪个方向偏移记录起来,等采样的时候访问。这种记录可以在任意位置(不需要均匀间隔),任意形状大小(为了方便用Cube形状,但支持任何大小)。至于如何区分哪些是框住的部分,只要类似延迟贴花,算对象空间内坐标是否<0.5即可(Cube中心对象空间内xyz任何一方>0.5都不在Cube包含范围内)。

从顶视图看,我们为其中一个空间摆放3个偏移Cube,预计算每个Cube中心的非遮挡范围的中心方向(可见度Cone的轴线方向)为箭头所示,并把这个方向当作颜色为3个Cube渲染到单独RendererTexture。


为3个位置摆放Cube,确保Cube中心的可见度/非遮挡方向为所覆盖像素的偏移方向


3个Cube 把自己中心可见度Cone轴线方向作为颜色渲染到RT上

该方案最终效果:

可以看到,漏光基本修复了,但是Cube之间有接缝,这个需要多摆放一份Cube让偏移方向可以过渡,或者对相邻Cube做Fade,类似Reflection Probe的2个Fade。

这种方案,因为Cube数量多而材质统一顶点数少,在正式工程,肯定用GPU Culling来做,HZB等做剔除后,绘制Instance性能很高。对于动态物体因为也是描述空间而非表面,所以可以直接支持。

3种方案Demo的源文件工程:
https://github.com/jackie2009/Probe_Light_Leak


这是侑虎科技第1570篇文章,感谢作者jackie 偶尔不帅供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者主页:https://www.zhihu.com/people/jackie-93-85-85

再次感谢jackie 偶尔不帅的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)