今天给大家分享的是Siggraph 2015上的一篇实时体渲染技术方案,源码(unity/shadertoy)与PPT原文在这里。
常见的体渲染方案都是通过RayMarching完成的,这里我们主要关心的是各个采样点的位置,常见做法是固定相邻两个采样点之间的距离,这个距离我们称之为March Step,在每个采样点位置,我们都需要评估此点的visibility信息,然而,要想得到一个较高质量的显示效果,通常每条射线上的采样数目要足够多,而这对于性能就是一个极大的负担,而如果降低采样点数目,就有可能导致渲染质量降低,这是一个两难的问题。
这里给出的渲染效果是按照每条射线上的采样点数目为32得到的,质量还是非常不错的,这个渲染结果有什么问题呢?
问题就是,这种方案只能用于静止或者移动十分缓慢的相机场景中,如果相机采用一个游戏中正常的速度进行移动的话,就可能导致较强的闪烁锯齿等效果。之所以会出现这个表现,是因为出于渲染性能的考虑,每条射线上的采样点数目严重不足,因此相机稍微移动一下,同一射线上采样的结果就截然不同,从而导致闪烁。
这里就引出一个问题,是否可以在保持采样数目不变的前提下减轻乃至消除锯齿呢?本文给出的方案就是基于这个设想而展开的,这里先给出结论,实验结果证明,通过一些技术手段可以大幅降低锯齿效果,下面我们一起来看下,具体是怎么做到的。
首先,我们用一张示意图来解释锯齿产生的原因。如上图所示,中间的倒三角表示的是相机的frustum,向上的箭头表示的是raymarching的方向,三角形中间的水平线条表示的是各条射线采样点的位置。如前面所述,由于采样点比较稀疏,当相机向前移动时,就会导致采样点也跟着往前移动,从而导致采样结果与上一帧的采样结果发生较大偏差,从而产生锯齿。
这里一个非常直观的解决方案是,保证相机移动后,前后两帧的采样点基本上能够重合在一起(比如每次移动的距离恰好等于一个step之类),对于这里给出一系列技术方案,我们这里将之用Pinning来表征。
其实,说起来高大上,做起来非常简单,那就是不再保持采样点相对相机静止,而是根据相机的移动距离进行调整补偿,从而保证前后两帧采样点的位置基本一致,这样最终渲染的结果就相当于是静止的了,也就能解决前面提出的问题,效果(向前移动Forward Motion)如下面的动图所示:
这种方案的实现非常简单,大概只需要一两行shader代码就可以搞定了,但是问题是,这种方案是否可以用于任意形式的相机移动呢。
下面是横向移动(Strafing Motion)的表现,对于这种情况而言,采样点位置无需做任何的补偿:
然而,当相机进行旋转的时候,这种做法就不再有效了,如下图所示,在屏幕中心区域,还可以得到不错的表现,越靠近屏幕边缘,抖动锯齿就越明显:
而之所以会出现这种情况,是因为如下图所示,在相机旋转的时候,采样点的移动方向实际上是跟采样slice相垂直的,因此就没有办法保证下一帧的采样点跟上一帧的采样点都能够很好的在同一个slice上重合起来:
解决这个问题的方法也很简单,那就是将原来的水平slice,切换成如下图所示的圆形slice,这样在相机旋转的时候,还依然能够保持前后两帧之间的重合关系:
下图是采用这种做法后的表现动图,可以看到问题已经得到了解决:
但是,显而易见,使用这种采样点layout,对于前面的Motion而言可能就不太友好了。那么有没有一套可以兼顾两种变换的sample layout呢,这篇文章给出了一种比较黑科技的实现方案,如下图所示:
红色线框中覆盖的是Motion时对应的采样slice,而绿色线框对应的是旋转时对应的采样slice,通过将两者衔接起来以实现两种运动切换时的自然过渡(相当于前后两帧重叠部分继续使用上一帧的采样点布局,只有当前帧新增部分才会按照相机的移动规律开辟新的采样点布局),从而解决Motion与Rotation之间切换导致的锯齿,下面是这种方案的实施效果:
作者给这种方案起了一个名字叫做advection(对流),起因于这种方案是通过对射线scale进行advecting以保持原有采样点可用。
由于相机的运动是无迹可寻的,因此这里并不能给出一套固定的解决方案,需要在实现的时候根据相机运动的情况来设计slice曲线。另外,除了前面我们阐述时的gather方法(对于每条射线,搜集其与slice的交点作为采样点,将各个采样点的数据累加输出结果),我们还可以使用scatter方法(对于上一帧的采样slice,将与这一帧重叠的部分采样点数据逐一写入到对应各个像素的Framebuffer中)
说到具体实现,对于当前帧的每个采样点,我们需要去搜索前一帧中的匹配点,这里用的是一种非常简单但是十分稳定的解决方案——Fixed Point Iteration(FPI,含义暂不明确),整个方案只需要定义一套前向映射关系Forward Mapping,这种映射关系指明了上一帧的各个采样点在当前帧的对应位置,在计算当前帧射线上各个采样点的位置的时候,只需要根据Forward Mapping进行反向映射就能找到上一帧中对应的位置数据,这样就能保证采样结果的稳定。
这种方法有一个限制,就是需要避免slice曲线出现较高的梯度(变化率),高梯度意味着在突变的地方会存在相邻像素之间采样scale存在较大差异的情况,这会使得采样结果被揪在一起(pinch),如上图箭头所指的位置,为了消除这个限制,作者给出了一种对高梯度采样区域进行放松调整(relaxing)的方案,如下图所示,随着时间变化,对高梯度区域进行平滑处理:
不过这里需要注意的是,因为这种放松算法会打破之前重用上一帧采样点的规则,因此可能会导致锯齿的增加,因此在实际使用中需要做一个平衡,而由于相机的移动会加剧锯齿的发生,因此这里的做法是当相机静止下来后才逐步加强这套放松算法的力度(相机移动状态下,即使有轻微的瑕疵也可以忽视,毕竟人眼也不能一直保持在同一个位置)。
这种方案的另一个弊端在于,当相机同时存在平移与旋转的时候,可能会导致slice曲线发生扭曲,如上图所示,这种扭曲在相机围绕着slice进行旋转的时候尤为明显。扭曲对于此前重用上一帧的采样点数据来降低锯齿并没有影响,不过就又一次出现前面说的高梯度高突变,从而使得渲染结果揪在一起。
作者对这种问题给出的解决方案是对scale的幅度进行clamp,降低扭曲的幅度,当然这种做法可能会导致锯齿增强(作者推测可能会有其他更好的解决办法):
另外,作者发现,advection实际上是可以直接通过半径计算得到的,因此不需要对每个slice都进行一遍advection计算,只需要计算出最近跟最远的slice的advection结果,之后在两者之间进行插值就能得到很好的结果了。
前面说的是固定采样分布情况下的处理情况,下面介绍一下在任意的采样点分布情况下实现上述稳定采样结果的实现方案。
通常来说,为了实现高效采样,我们不会采用固定步长的采样方式,而是会根据距离不断加长采样步长(我们用revert-z采样来指代这种方案),如下图所示:
之所以要采用这种采样方式,是因为游戏场景通常是使用透视相机进行渲染的,而在透视投影的情况下,人眼感知到的深度变化率是与成正比的,因此距离越远越不敏感,要想得到同样的感知幅度变化,就需要加大刺激程度,也就是增加步长。但是这种做法在相机进行前向移动的时候,可能就无法重用上一帧的采样结果了。
前面已经介绍过固定步长的采样分布是如何对上一帧的采样结果进行重用的——因为任意两个相邻采样点之间的距离是固定的,因此随时可以通过错位采样来保证前后两帧采样点之间的大面积重合,但这种做法对于相邻采样点之间的距离会变化的revert-z采样方式可能就不再可用了。
如上图所示,作者通过一种方法可以保证在revert-z采样方式下依然可以得到稳定的采样结果(这种方法被称之为adaptive采样方法),那么具体是怎么做到的呢?
如上图所示,其奥秘就在于依然以固定step作为采样点之间的距离,只不过随着距离相机越来越远,部分采样点就直接跳过,且距离相机越远,跳过的采样点就越多,这样既利用了固定采样step的稳定性,同时又保证了稀疏采样的高效率。
前面说的跳过部分采样点只是一种虚指的说法,实际上这里只需要将采样点设计成POT步长即可,之后计算出各个采样点数据之后将之与对应位置的density相乘就能得到对应采样点的visibility信息,而各个采样点的density/weights可以提前在CPU中算好,在GPU中只需要直接取用就可以了(理论上最佳的使用方式是传入数组,但是如果不支持数组传入的话,也可以使用贴图,另外最差的情况就是直接将权重数组写入到shader代码中)。
这里给出了一个相机运动的测试用例与对应的slice曲线输出,下面一起来欣赏下最终的结果输出:
网友评论