目录
17.2 光线追踪基础 17.3 光线追踪技术 17.3.2 场景加速结构 17.3.3 光线追踪阴影17.3.4 光线追踪AO17.3.5 光线追踪反射17.3.6 光线追踪折射17.3.7 光线追踪间接漫反射17.3.8 光线追踪半透明17.3.9 降噪技术 17.3.10 光线追踪优化 17.3.11 综合技术 17.4 图形API和GPU 17.5 UE光线追踪 17.5.2 光线追踪 17.6 UE光线追踪源码分析 17.6.3 UE光追渲染流程17.6.4 UE光追光影 17.6.5 UE光追天空光 17.6.6 UE光追GI 17.7 本篇总结特别说明参考文献
17.1 本篇概述 17.1.1 本篇内容
UE的光线追踪一直是童鞋们呼吁比较高的一篇,虽然多年前博主已经在探究光线追踪技术及UE4的实现阐述过,但内容较基础和片面 。那么,此篇就针对UE的实时光线追踪进行更加系统、全面、深入地分析 。本篇主要阐述UE的以下内容:
与传统的扫描线或光栅化渲染方式不同 , 光线追踪(Ray )是三维计算机图形学中的特殊渲染算法,追踪从摄像机发出的光线而不是光源发出的光线 , 通过这样一项技术生成编排好的场景的数学模型显现出来 。
文章插图
利用光线追踪技术渲染出的照片级画面 。
与传统方法的扫描线技术相比,这种方法有更好的光学效果 , 例如对于反射与折射有更准确的模拟效果,并且效率非常高,所以当追求高质量的效果时经常使用这种方法 。
在物理学中,光线追迹可以用来计算光束在介质中传播的情况 。在介质中传播时,光束可能会被介质吸收,改变传播方向或者射出介质表面等 。我们通过计算理想化的窄光束(光线)通过介质中的情形来解决这种复杂的情况 。
在实际应用中 , 可以将各种电磁波或者微小粒子看成理想化的窄波束(即光线) , 基于这种假设,人们利用光线追迹来计算光线在介质中传播的情况 。光线追迹方法首先计算一条光线在被介质吸收 , 或者改变方向前,光线在介质中传播的距离,方向以及到达的新位置,然后从这个新的位置产生出一条新的光线,使用同样的处理方法,最终计算出一个完整的光线在介质中传播的路径 。
17.1.2 光线追踪和光栅化
光栅化渲染管线( )是传统的渲染管线流程 , 是以一个三角形为单元,将三角形变成像素的过程(下图左),在目前图像API和显卡硬件有着广泛的支持和应用 。
光线追踪渲染管线(Ray)则是以一根光线为单元,描述光线与物体的求交和求交后计算的过程(下图右) 。和光栅化线性管线不同的是,光线追踪的管线是可以通过递归调用来衍生出另一根光线,并且执行另一个管线实例 。
文章插图
更详细的对比表:
关键概念光栅化光线追踪
基本问题
几何体覆盖了哪些像素?
什么物体对光线可见?
关键操作
测试像素是否在三角形内
光线-三角形相交测试
如何流化工作
流化三角形(每个测试像素)
流化光线(每条测试交点)
低效率
每个像素对多个三角形着色(过绘制)
每条光线对多个三角形相交测试
加速结构
(层级)Z-
层次包围盒(BVH)
劣势
非一致性查询难以实现
遍历内存非常不一致
17.1.3 光线追踪简史
光线追踪渲染技术从自然界中的光线简化、光线投射算法、光线追踪算法等一步步演变而来 。
UE5的核心技术之一便是Lumen,实现了实时可信的全局光照效果,它支持软件光线追踪和硬件光线追踪两种模式 。
文章插图
UE的SSR(左)和光追反射(右)对比图 。
文章插图
UE5的远场的大规模GI效果 。它支持硬件光线追踪模式 。
17.2 光线追踪基础 17.2.1 数学基础
已知原点oo(为了防止博客园语法解析错误,去掉了箭头,下同)和方向dd,则射线是半无限的直线:
→p(t)=→o+t→d(t≥0)p→(t)=o→+td→(t≥0)
已经有3个顶点aa、bb、cc,它们可以组成一个三角形 , 该三角形的法线可通过叉乘计算而得:
→n=(→b?→a)×(→c?→a)n→=(b→?a→)×(c→?a→)
射线和三角形的交点必须沿着射线,并且必须在三角形的平面内,因此必须满足:(p?a)?n=0(p?a)?n=0,结合射线公式 , 可计算出tt值:
t=(→a?→o)?→n→d?→nt=(a→?o→)?n→d→?n→
以上公式需要处理一种特殊的情况,那就是d?n=0d?n=0 , 即射线平行三角形的平面 。如果计算的tt值是负数,说明不在三角形内,可以拒绝该点 。
给定任意的距离tt,可以很方便地通过射线的公式计算得到射线和三角形平面的交点 。接下来 , 我们必须检查该点是在三角形内还是在三角形外,可以通过计算质心坐标(u,v)(u,v)来实现这一点 。质心坐标定义为:
→p=→a+u(→b?→a)+v(→c?→a)p→=a→+u(b→?a→)+v(c→?a→)
对应的图例如下:
文章插图
对于四边形(双线性面片),质心坐标更加复杂:
→p=(1?u)(1?v)→a+u(1?v)→b+(1?u)v→c+uv→dp→=(1?u)(1?v)a→+u(1?v)b→+(1?u)vc→+uvd→
对应图例:
文章插图
以上是针对四个顶点位于同一个平面的情况,但实际上 , 它们可能不在同一个平面 , 会产生两个平面 。
文章插图
由两个三角形近似的四边形 。
包围盒( box)对于加速复杂场景的光线追踪非常有用 。一般盒子由一个顶点和三个向量定义(下图) 。直接的相交测试将测试六个面中的每个面是否相交 。通过仅需要测试面向射线原点的三个面,可以实现更快的测试 。
文章插图
(a)具有一般方向的盒;(b) 轴对齐盒 。
轴对齐的盒子可以更有效地进行交叉测试 。轴对齐盒由xy、xz和yz平面中的每个平面中的两个矩形组成 。轴对齐盒由其最小和最大顶点pmin以及pmax定义,如上图(b)所示 。我们可以将盒子视为三块无限大空间板的交点 。Smits描述了一种非常有效的射线交叉测试 , 利用IEEE浮点约定优雅而有效地处理0的除法 , 从而简化了代码 。
如果盒用作边界盒,我们不需要知道最近的交点和法线,我们只需要知道光线是否与盒相交 。
二次曲面()由圆盘、球体、圆柱体、圆锥体、椭球体、抛物面和双曲面组成 。
圆盘由其中心c、法线n和半径r定义 。寻找射线-圆盘交点与射线-三角形交点测试非常相似 。我们首先计算射线-平面交点p,并检查距离t是否为正且小于之前的最近交点 , 如果(p?c)2≤r2(p?c)2≤r2 , 则交点在圆盘上 。圆盘在实际渲染中被大量使用,例如用于渲染粒子系统 。
球体由其中心c和半径r定义 。如果存在交点,则交点必须位于射线的某个位置,并且必须位于球体的表面上 。为了找到交点,我们将射线方程代入球体方程(p?c)2=r2(p?c)2=r2:
0=(→p?→c)2?r2=→p2?2(→p?→c)+→c2?r2=(→o+t→d)2?2(→o+t→d)?→c+→c2?r2=→o2+2t(→o?→d)+t2→d2?2(→o?→c)?2t(→d?→c)+→c2?r2=→d2t2+2→d?(→o?→c)t+(→o?→c)2?r20=(p→?c→)2?r2=p→2?2(p→?c→)+c→2?r2=(o→+td→)2?2(o→+td→)?c→+c→2?r2=o→2+2t(o→?d→)+t2d→2?2(o→?c→)?2t(d→?c→)+c→2?r2=d→2t2+2d→?(o→?c→)t+(o→?c→)2?r2
tt存在两个解:t1=?B+D2At1=?B+D2A和t2=?B?D2At2=?B?D2A,其中A=d2,B=2d?(o?c),C=(o?c)2?r2,D=√B2?4ACA=d2,B=2d?(o?c),C=(o?c)2?r2,D=B2?4AC 。对于判别式DD,存在3种情况:
给定交点距离tt , 我们可以计算出交点pp,交点上的法线是n=p?cn=p?c 。
还有其它形式的二次曲面,本文就不再解析 , 有兴趣的同学可以自行寻找资料 。
隐式曲面( )由函数ff定义:曲面是点pp的集合,其中函数的值为0,f(p)=0f(p)=0 。因此,为了找到射线-曲面交点,我们必须确定沿射线的(最近的)点p)p),其中f(p)f(p)为0:
f(→o+t→d)=0f(o→+td→)=0
它可以使用例如-迭代或其他迭代方法来完成,描述了一种有效的算法 。交点处的曲面法线由该点处函数的梯度给出:
→n=?f(→p)=(?f(→p)?x,?f(→p)?y,?f(→p)?z)n→=?f(p→)=(?f(p→)?x,?f(p→)?y,?f(p→)?z)
还有NURBS曲面、细分曲面、位移曲面、盒体等,本文不再详述 。
光线微分(Ray )尽管是光线的基本属性,但用于光线追踪还是相对较新的,对于包括纹理过滤和曲面细分在内的许多应用程序都很有用 。光线微分描述了光线与其真实或虚拟“相邻”光线之间的差异 。如下图所示,微分给出了每条射线所代表的光束大小的指示 。
光线和光束 。
Igehy的光线微分方法追踪光线传播、镜面反射和折射时的光线微分 。曲面交点处的曲率决定了在镜面反射和折射后光线差及其相关光束的变化 。例如,如果光线击中高度弯曲的凸面 , 镜面反射光线将具有较大的差异(表示高度发散的相邻光线) 。
下图显示了光线追踪的镜面反射 。在左图像中,不计算光线微分,并且纹理滤波器宽度为零,因此产生锯齿瑕疵 。在右图中,光线微分用于确定适当的纹理过滤器大小 。为了清楚地显示差异,图像的分辨率非常低(200×200像素),每个像素仅拍摄一条反射光线,并关闭像素滤波 。
文章插图
反射:(a)不使用光线微分,(b) 使用 。
和将光线微分推广到光泽和漫反射 。对于漫反射或环境遮挡的分布光线追踪,光线微分对应于半球的一部分 。从同一点追踪的光线越多,对端半球部分越小 。如果半球分数()非常?。氏喙匚⒎郑ㄈ缇得娣瓷洌┙贾鞯嫉匚?。
17.2.2 浮点数
浮点数的实数必须近似,包含浮点数(-point )、定点数(Fixed-point,亦即整数)、有理数( ,齐次表示) 。IEEE-754单精度的数据布局是:1位符号、8位指数(偏置)、23位分数(带隐藏位的24位尾数) , 其图例如下:
文章插图
其表示的数值公式是:
V=(?1)s×(1.f)×2e?127V=(?1)s×(1.f)×2e?127
这是一种标准化格式,IEEE-754可表示的数字如下表:
序号指数()分数()符号(Sign)值
V=(?1)s×(1.f)×2e?127V=(?1)s×(1.f)×2e?127
e=0e=0
f=0f=0
s=0s=0
V=0V=0
e=0e=0
f=0f=0
s=1s=1
V=?0V=?0
e=0e=0
f≠0f≠0
V=(?1)s×(0.f)×2e?126V=(?1)s×(0.f)×2e?126
e=255e=255
f=0f=0
s=0s=0
V=+InfV=+Inf
e=255e=255
f=0f=0
s=1s=1
V=?InfV=?Inf
e=255e=255
f≠0f≠0
V=NaNV=NaN
上表补充以下几点说明:
涉及、的运算称为无穷算术( ,IA) 。IA是鲁棒性错误的潜在来源!+Inf+Inf和–Inf–Inf比较是正常的,但比较却无法预料:
但IA也提供了一个很好的功能,允许不必测试除零操作 , 从内循环(inner loop)中删除测试分支 , 对SIMD代码有用 。(尽管同样的方法通常也适用于非IEEE CPU 。)
IEEE-754的特殊表达方式 , 导致了不规则数字线——即距离零越远,间距越大,指数k+1的数字范围的间距是指数k的两倍,从一个指数到另一个指数的等同于多个可表示数 。(下图)
文章插图
不规则间距的后果:
因此 , 会导致非结合律:(a+b)+c≠a+(b+c)(a+b)+c≠a+(b+c),错误层出不穷!所有离散表示都有不可表示的点:
文章插图
在浮点运算中,由于间距不规则,行为会根据位置而变化!
文章插图
例如,-裁剪算法(多边形分割的算法之一):
文章插图
进入浮点错误:
文章插图
ABCD相对于平面拆分:
文章插图
当然,可以用厚平面来解决:
文章插图
厚平面也有助于限制错误:
文章插图
ABCD相对于厚平面拆分:
文章插图
顺序不一致导致的裂纹:
文章插图
另一个示例是BSP树鲁棒性 , 存在以下方面的稳健性问题:
实现稳健性的方法:保守插入图元,考虑查询和插入错误,然后可以忽略查询问题 。
浮点值误差的示例还有射线和三角形的检测 。常用方法:计算光线R与三角形T平面的交点P,测试P是否位于T的边界内 。然而,这是非鲁棒性的!以下图举例:
文章插图
R与一个平面相交:
文章插图
R与另一平面相交:
文章插图
稳健测试必须共享共享的边缘AB的计算,直接在3D中执行测试,过程如下:
仍然存在错误,但可控 。胖(fat)射线测试也很鲁棒!
文章插图
实现鲁棒性的方法有:
公差比较包含以下几种方式:
警告:英特尔内部使用80位格式,除非另有说明 。错误取决于生成的代码,在调试和发布时给出不同的结果 。
接下来介绍精确算术(Exact ,半精确同样) 。
整数算术是精确的,只要没有溢出() , 在+、–、和*下是封闭的,但对/不是,通常可以通过叉乘(cross )删除除法 。示例:C如何投影到AB上?
文章插图
用浮点和整数的运算如下:
// floatfloat t = Dot(AC, AB) / Dot(AB, AB);if (t >= 0.0f && t <= 1.0f)... /* do something */// integerint tnum = Dot(AC, AB), tdenom = Dot(AB, AB);if (tnum >= 0 && tnum <= tdenom)... /* do something */
测试(Test)是布尔值,可以精确计算 。构造()是非布尔型 , 无法精确执行 。测试通常表示为行列式,例如:
P(u,v,w)□∣∣ ∣ ∣∣∣∣ ∣ ∣∣≥0?u?(v×w)≥0P(u,v,w)?||≥0?u?(v×w)≥0
使用扩展精度算法(EPA)进行估算,EPA开销昂贵,通过“浮点过滤器”限制EPA的使用,常用的滤波器是区间计算( ) 。区间计算的样例:x = [1,3] = { x ∈R | 1 ≤ x ≤ 3 },其规则如下:
区间计算的间隔必须向上/向下四舍五入到最接近的机器表示数,是可靠的计算 。
17.2.3 隐式函数
球体追踪是光线追踪的诸多形式的其中一种,是隐式函数的理想选择,不是光栅化或体素的替代品 。很低效,但是很简单,并且非常灵活 。球体追踪只需要4步:
通过以上几个步骤,就可以实现复杂而有趣的场景(来自):
文章插图
显式数据作为独立值存储在存储器中 , 如网格顶点、纹理像素等…从存储器中读取数据 。隐式数据的代码是数据,一切都是程序性的,通过计算访问数据 。
文章插图
通过简单的加减乘除和mod、min、max、noise等操作可以实现复杂、自然的模型:
文章插图
关于距离和噪音,有一些事情需要注意 。大多数情况下,需要很多步骤才能找到物体表面,这也是个问题 。追踪正弦曲线时,空间不是线性的 。Mod和噪声操作同样如此(下图) 。
文章插图
文章插图
噪声还会引起渲染瑕疵:
文章插图
有人说你应该不惜一切代价避免噪声,因为它会破坏渲染,其他人则不同意,下面是非噪声和添加噪声的场景对比:
文章插图
17.2.4 采样方式
在图形学中,采样是个有意思却蕴含着丰富的技术,包含了各式各样的方式 。在光线追踪中,常见的采样有均匀、随机、低差异序列、重要性等方式 。
均匀采样( )是不区分光源重要性的平均化采样,生成的光线样本在各个方向上概率都相同,并不会对灯光特殊对待,偏差与实际值通常会很大 。蒙特卡洛采样(Monte Carlo )着重考虑了光源方向的采样,能突出光源对像素的贡献量,但会造成光源贡献量过度 。重要性采样( )则加入概率密度函数pdf,通过缩小采样结果,防止光源的贡献量太大 。
文章插图
左:完全伪随机序列生成的采用点;右:低差异序列生成的采样点 。可以看出右边的更均匀 。
文章插图
蒙特卡洛采样使用随机样本来数值计算该积分 。重要性采样的思想是尝试生成与具有类似形状的被积函数的概率密度函数(PDF)成比例的随机样本 。
UE在实现TAA时采用了、Sobal等序列:
文章插图
相比随机采样,获得的采样序列更加均匀,且可以获得没有上限的样本数(UE默认限制在8以内) 。除此之外,还有Sobel、、等低差异序列算法,它们的比较如下图:
文章插图
所有采样技术都基于将随机数从单位平方扭曲到其它域,再到半球、球体、球体周围的圆锥体,再到圆盘 。还可以根据BSDF的散射分布生成采样,或选择IBL光源的方向 。有许许多多的采样方式,但它们都是从0到1之间的值开始的,其中有一个很好的正交性:有“你开始的那些值是什么”,然后有“你如何将它们扭曲到你想要采样的东西的分布,以使用第二个蒙特卡罗估计” 。
文章插图
对应采样方式,常用的有均匀、低差异序列、分层采样、元素区间、蓝噪点抖动等方式 。低差异类似广义分层 , 蓝色噪点类似不同样本之间的距离有多近 。过程化模式可以使用任意数量的前缀,并且(某些)前缀分布均匀 。
文章插图
方差驱动的采样——根据迄今为止采集的样本,周期地估计每个像素的方差,在差异较大的地方多采样,更好的做法是在方差/估计值较高的地方进行更多采样,在色调映射等之后执行此操作 。离线(质量驱动):一旦像素的方差足够低,就停止处理它 。实时(帧率驱动):在方差最大的地方采集更多样本 。计算样本方差(样本方差是对真实方差的估计):
float SampleVariance(float samples[], int n) {float sum = 0, sum_sq = 0;for (int i=0; i
样本方差只是一个估计值,大量的工作都是为了降噪,MC渲染自适应采样和重建的最新进展 。总体思路:在附近像素处加入样本方差,可能根据辅助特征(位置、法线等)的接近程度进行加权 。高方差是个诅咒,一旦引入了一个高方差样本,就会有大麻烦了,可以考虑对数据进行均匀采样 。
此外,对于不同粗糙度的表面,所需的光线数量和方向亦有所不同:
文章插图
在计算阴影、AO等通道中,也使用了重要性采样来生成光线,相同视觉质量需要的光线更少 。重要性采样过程中使用了半球、余弦采样、距离采样:
文章插图
从左到右:半球、半球+余弦、半球+余弦+距离 。
更进一步的 , 存在多重要性采样(,MIS) , 以便同时考量光源、BRDF、PDF等因素的影响,对采样的方向和位置等有所偏倚 。
文章插图
多重要性采样公式:
1nfnf∑i=1f(Xi)g(Xi)wf(Xi)pf(Xi)+1ngng∑j=1f(Yj)g(Yj)wg(Yj)pg(Yj)1nf∑i=1nff(Xi)g(Xi)wf(Xi)pf(Xi)+1ng∑j=1ngf(Yj)g(Yj)wg(Yj)pg(Yj)
其中:nknk是从某个 PDF 中提取的样本数pkpk,加权函数wkwk采用可能生成样本的所有不同方式,并且wkwk可以通过幂启发式计算:
wk(x)=(nsps(x))β∑i(nipi(x))βwk(x)=(nsps(x))β∑i(nipi(x))β
除了以上方式,还有方差、域扭曲、准随机序列、低差异、分层等等采样方式 。准蒙特卡罗(QMC)的特点是确定性、低差异序列/集合(、、-)比随机的收敛速度更好,例如Sobol或(0-2)序列不需要知道样本数量 , 奇妙的分层特性 。
文章插图
若是继续拓广之,可以以任意形状任意数量的tap去采样,如双边、蓝噪声、棋盘、星状等,或者它们之间的结合:
文章插图
为了避免阶梯式瑕疵,使用了随机采样(蓝色噪点 + TRAA) 。
文章插图
双边上采样的其中一种模式 。
更有甚者,可以通过旋转、升至更高维度以获得更多样本和低噪点:
文章插图
文章插图
文章插图
总之,目前存在诸多采样方式,目的都是为了让光线追踪更快地收敛到准确结果,从而降低噪点,提升渲染性能 。
17.2.5 体素化
对于任意连续的函数f(x,y,z)f(x,y,z),隐式地将体积定义为f(x,y,z)>0f(x,y,z)>0 , 表面是f(x,y,z)=0f(x,y,z)=0的水平集 。
文章插图
只需要一个连续的函数,任意的代数函数、有向距离?。–SG树,在网格三线性采样)、密度函数(在网格三线性采样) 。
文章插图
使用密度()要容易得多(局部更改) , 但在距离场上的一些有用操作(如放大、缩小体积、更高质量的渐变计算)上会失败,可以将密度视为距离?。?夹紧距离约为一个采样单元格 。
将隐式曲面或参数化网格体素化的过程:
1、在网格上采样 。
2、近似每个单元格中的表面 。
3、确保表面与单元边界对齐 。
体素化的理想特征是易于实现、局部独立、平滑、自适应/适合LOD、最小化三角形条形、保留锐利和薄的特征 。
对于简单的立方体,在每个网格单元的中心采样f(x,y,z)f(x,y,z) , 在具有不同符号的单元格之间绘制一个面 。
文章插图
这种表达方式在体素化的理想特征的优劣如下表:
易于实现局部独立平滑自适应LOD最小化三角形锐利薄
++
步进立方体( Cube):在每个单元格的角落采样f(x,y,z)f(x,y,z) , 在三角形拓扑中使用角的符号 , 沿着边缘在插值的0点处定位顶点 。
文章插图
这种表达方式在体素化的理想特征的优劣如下表:
易于实现局部独立平滑自适应LOD最小化三角形锐利薄
超体素()算法:在每个单元格的角落采样f(x,y,z)f(x,y,z),允许细分一次单元格的边(以缝合相邻的LOD级别) , 为三角形拓扑使用采样点的符号,沿边在插值的0点处定位顶点 。
文章插图
是一种允许行进立方体跨越不同LOD级别的方法 。总共71种拓扑方式,用于处理任意边组合的细分 。
这种表达方式在体素化的理想特征的优劣如下表:
易于实现局部独立平滑自适应LOD最小化三角形锐利薄
双重轮廓(Dual ):在每个单元格的角落采样f(x,y,z)f(x,y,z),在每个边交叉点位置采样f′(x,y,z)f′(x,y,z),在轮廓上的每个单元格内找到一个理想点,连接相邻单元格的两点(支持多个LOD分辨率) 。
文章插图
这种表达方式在体素化的理想特征的优劣如下表:
易于实现局部独立平滑自适应LOD最小化三角形锐利薄
双重行进立方体(DualCube):在精细网格上采样f(x,y,z)f(x,y,z),找出误差最小的点(QEF),如果误差 >εε , 则在该点细分八叉树 。重复前面的步骤,直到索引的误差 0时,光线追踪结果普遍存在 。插值因子可视化:
文章插图
L = saturate( BD / WSS * PHS ) L: Lerp factor BD: Blocker distance (from ray origin) WSS: World space scale – chosen based upon model PHS: Desired percentage of hard shadow FS = lerp( RTS, PCSS, L ) FS: Final shadow result RTS: Ray traced shadow result (0 or 1) PCSS: PCSS+ shadow result (0 to 1)
使用收缩半影过滤,否则,光线追踪结果将无法完全包含软阴影结果,将导致在两个系统之间执行lerp时出现问题 。
文章插图
效果对比:
文章插图
不同图元复杂度的效果、消耗及性能如下:
文章插图
目前仅限于单一光源,不能扩大到适用于整个场景,存储将成为限制因素,但最适合最接近的模型:当前的焦点模型、最近级联的内容 。总之 , 解决传统的阴影贴图问题,AA光线追踪硬阴影的性能非常好,混合阴影结合了这两个世界的优点 , 无需重新编写引擎,游戏速度足够快!
在2017年,坦克世界就已经通过各种优化手段在 11及以上的图形平台实现了光线追踪阴影 。他们实现了实时光线追踪物理正确的软阴影,不需要硬件RT Core,使用了用于构建BVH的Intel ,使得坦克世界成为第一款在D3D11中使用实时RT阴影的游戏 。
文章插图
坦克世界开启(左)和关闭(右)光线追踪软阴影的对比图 。
在实现光追软阴影时,分为CPU侧和GPU侧逻辑 。其中CPU侧包含两级加速结构:
CPU BVH占CPU帧时间的2.5%,使用TBB线程,SSE 4.2(比原始WoT内部BVH 快5.5倍) , 每帧更新高达约5mb的GPU数据,高达72mb的静态GPU数据 。下图是CPU侧的各个阶段消耗:
文章插图
GPU侧执行像素着色或计算着色:
下图是GPU侧的各个阶段消耗:
文章插图
坦克世界对光追阴影进行了优化:RT阴影只能由坦克投射 , 不支持alpha测试的几何体,BLAS使用LOD,每像素只发射1根射线 。如果出现以下情形之一 , 则不追踪光线的像素:
利用此法实现的实时光追阴影的性能参数如下:
文章插图
实现的光追阴影和常规的 Map阴影对比如下:
文章插图
1080p上的每像素单根光线只耗费小于4ms,下图是每像素单根光线的局部放大图:
文章插图
使用了软阴影球体追踪 , 用柔和的半影扩大阴影 , 沿光线步进SDF近似最大圆锥体覆盖率,圆锥体覆盖近似:
c = min(c, light_size * SDF(P) / time);
文章插图
并且对软阴影进行了改进 , 即三角测量最近距离 , =单个样本(最?。?,三角测量cur和prev样本 , 更少条带 。抖动阴影光线,UE4时间累积 , 隐藏剩余的带状瑕疵,较宽的内半影 。
文章插图
改进前后对比:
文章插图
以往的LTC并不能处理遮挡的光照,但更真实的光影应该具备:
文章插图
之前有文献提出了仅光追的软阴影,做法是平均可见性:
文章插图
但如果使用BRDF获得直接光,再乘以光追的平均可见性的软阴影,将得到错误的结果:
文章插图
正确的做法应该如下图右边所示:
文章插图
也可以采用随机化的方式,但必须强制BRDF的所有项都是随机化的:
文章插图
随机化的结果是过多噪点和过于模糊:
文章插图
所以仅光追的软阴影和完全随机化的两种方案都将获得错误或不良的结果 。正确的软阴影算法应该如下所示:
文章插图
从数学上讲,我们可以看到事情显然是正确的:a?b/a=ba·b/a=b:
文章插图
对应的正确随机化公式:
文章插图
更加准确的方法推导如下:
文章插图
文章插图
正确降噪的各个频率的函数如下:
文章插图
降噪图例:
文章插图
在采样方面,使用了多重要性采样:
文章插图
对于电介质(非金属) , 使用了电解质多重要性采样:
文章插图
最终效果对比:
文章插图
渲染通道和流程如下:
文章插图
总之 , 比率估计器:无噪声有偏分析+无偏噪声随机,作为稳健噪声估计的总变化(非方差),由分析着色驱动的阴影多重要性采样 。实时光线追踪GPU的注意事项包含活动状态、延迟和占用率、多重要性采样的分支、波前与内联,混合的光线+光栅图形示例 。
17.3.4 光线追踪AO
环境遮挡可以被认为是由非常大面积的光源(即每个点上方的整个半球)进行的照明 , 类似于阴天的室外照明 。下图(a)显示了来自两个表面点的环境遮挡光线 。在左侧点,大部分光线击中对象,因此遮挡较高;在正确的点上,几乎没有光线击中对象,因此几乎没有遮挡 。图(b)显示了茶壶场景中的环境遮挡 。此图显示纯环境遮挡;当然 , 可以与表面颜色、纹理等相结合 。
文章插图
实现的SSAO和光追AO的对比图如下:
文章插图
在不同的rpp(每像素光线数量)上 , 光追AO效果也有所不同:
文章插图
在表面法线方向构造圆锥体,加上随机变化+时间累积,AO射线使用低SDF mip,更好的GPU缓存位置和更少的带宽,软远程AO 。也使用UE4的SSAO,小规模的环境遮挡 。
文章插图
上:SSAO;下:SSAO + RTAO 。
17.3.5 光线追踪反射
当光线击中完美镜面反射表面时,它是否以与入射角相同的角度反射,基本物理定律最早由欧几里德在公元前3世纪编纂 。在现实世界中,反射对象很常见,不仅仅是金属球!
对于光追反射,从反射表面发射一条额外光线 , 反射光线的方向使用反射定律从入射光线方向计算 。光线照射场景中的对象时,使用与直接可见表面相同的照明计算对该表面进行着色 。
文章插图
间接光的光泽反射可以通过在光泽反射分布的方向内发射光线来计算 。对于给定的入射方向和一对随机数,反射模型提供反射方向 。下图显示了两个茶壶中的光泽反射 , 使用Ward(各向同性)光泽反射模型计算反射 。
文章插图
类似地 , 可以通过围绕折射方向分布光线来计算光泽折射,可以产生轻微磨砂玻璃的外观 。
SSR和光追反射也有明显的区别 , SSR无法反射物体背面和屏幕以外的集合体,而光追反射没有此限制:
文章插图
实现的光追反射的各分量和合成效果如下:
文章插图
文章插图
对于不同的亮度,采用了不同的rpp,其中对亮度较高的像素采用更多的光线,且使用了抑制因子 , 最后结合阴影图做优化 。其组合过程图例如下:
文章插图
文章插图
文章插图
文章插图
17.3.6 光线追踪折射
早在 2008,周昆团队就已经在研究基于光线追踪的折射效果,实现了令人瞠目结舌的折射、反射、焦散、多重折射、阴影、色散等等效果(下图),该成果发表成论文of。
文章插图
其实现流程主要是将物体体素化 , 生成八叉树结构体来加速遍历,然后采用自适应光子追踪,从而实现高效且逼真的光线追踪效果 。
文章插图
体素化的过程将三角形网格转换为体积数据:
文章插图
体素化使用GPU Gems III[Crane 07]的技术,仅在表面附近添加了超采样 , 添加高斯平滑 。
八叉树构建时 , 使用密集3D数组代替稀疏树,考虑折射率和消光系数,构造类似于 。
文章插图
生成光子时,在边界框上生成光子,在折射对象的表面上生成光子,周围体积必须为空,需要阴影贴图来完成遮蔽 。
文章插图
将辐射直接存入体素 , 为每个光子步骤使用线段:
文章插图
然后根据表面积的大小使用不同精度的数据(即八叉树的不同节点数据):
文章插图
文章插图
不同追踪技术的效果对比:
文章插图
在view pass中,曲线观察光线的轨迹,聚集光辉,考虑散射、衰减 。同时忽略八叉树,原因是图像对步长敏感,性能已经足够好 。
文章插图
在2008年前后,采用128 x 128 x 128体积分辨率、1024 x 1024初始光子、640 x 480图像分辨率,使用8800 Ultra渲染,可达到2到7fps 。
17.3.7 光线追踪间接漫反射
Ward等人使用宽分布光线追踪来计算间接漫射光 。反射光线的分布覆盖了每个点上方的整个半球,具有余弦加权分布,因此在朝向极点的方向上追踪的光线多于赤道附近的方向(下图) 。在该图像中,没有环境光源;阴影区域中的任何光都是由于间接光的漫反射 。请特别注意,白色棋盘格是如何在茶壶底部反射的 , 以及右茶壶上的壶嘴是如何将光线投射到茶壶主体的附近部分的 。这种效果通常被称为颜色溢出(尽管在这种情况下“颜色”是白色) , 并可以用附近对象的颜色对表面着色 。
文章插图
在实现间接漫反射上,与AO类似,有许多非相干光线,GI存储在稀疏网格体积中 。基于静态几何图形和静态灯光集计算的辐照度,动态几何体可以接收光,但不影响计算的辐照度,动态几何没有贡献,三线性采样创建阶梯样式,薄几何体会导致光照泄漏,通过采样余弦分布上的辐射来收集照明,考虑丢失的几何图形(下图) 。
文章插图
直接采样和AO、光追收集的效果如下:
文章插图
文章插图
17.3.8 光线追踪半透明
真正的透明度不是alpha混合!在现实光学中,当光穿过半透明物体时,一些光被吸收,一些光不被吸收 。从表面的背面发射光线 , 像反射光线一样的阴影,顺序独立 。
文章插图
当阴影光线击中透明对象时,它将继续朝向灯光 。撞击透明对象的阴影光线应进行着色并重新发射 , 就像它是非阴影光线一样 。阴影光线将穿过表面的完全透明区域,阴影光线从半透明对象获取颜色 。
文章插图
除了以上特性,光线追踪还可以实现互反射()、溢色、焦散、色散、DOF、运动模糊、复杂半透明、体积光雾、参与介质等等效果 。
文章插图
透过雾照在球体上的聚光灯 。请注意,由于参与介质中的附加散射,聚光灯照明分布的形状和球体阴影清晰可见 。
17.3.9 降噪技术
降噪技术只用于BRDF的可见项(光照项采用解析近似):
文章插图
在实时光线追踪领域,降噪算法有很多,诸如使用引导的模糊内核的滤波,机器学习驱动滤波器或重要采样,通过更好的准随机序列(如蓝色噪声和时空积累)改进采样方案以及近似技术,尝试用某种空间结构来量化结果(如探针、辐照度缓存) 。
下面抽取部分重要的降噪技术来剖析 。
17.3.9.1 SVGF / A-SVGF
时空方差引导滤波器(SVGF)[ 2017]是一种降噪器 , 使用时空重投影以及特征缓冲器(如法线、深度和方差计算)驱动双边滤波器模糊高方差区域 。
SVGF将噪点输入转换为完整图像,通常需要10毫秒才能运行,因此将其集成到实时光线追踪器中可能没有好处 。深度学习过滤器可能在更短的时间内完成类似的任务 。然而 , 该技术非常擅长重建最终图像,尤其是调整版本(A-SVGF,自适应SVGF) 。声称,与之前的交互式重建滤波器相比,提供了大约10倍的时间稳定结果 , 匹配参考图像的效果更好5-47%(根据SSIM),并且在分辨率的现代图形硬件上仅运行10毫秒(15%以内误差) 。
自适应时空方差引导滤波(A-SVGF)[等人2018]是一种较新的技术 , 在SVGF基础上进行了改进,消除了闪烁等问题,通过自适应地重用根据时间特征(如编码在矩缓冲器中的方差、视角等的变化)在空间上重新投影的先前样本 , 并使用快速双边滤波器对其进行滤波,改进了SVGF 。因此,与基于历史长度累积样本不同,矩缓冲区充当替代色调,使用方差的变化来驱动旧样本和新样本的比例,从而减少重影 。虽然SVGF仅使用矩缓冲器来驱动模糊,但A-SVGF将其用于滤波和累积步骤 。
虽然引入力矩缓冲区有助于消除时间延迟,但并不能完全消除时间延迟 。具有大量累积样本的区域和新区域之间可能存在亮度差异 。这在光线追踪场景的较暗区域(如室内)中尤其明显 。为了缓解这种情况,最好在场景的黑暗区域使用2 spp,而不是使用每像素1个采样(1 spp) 。
Quake 2 RTX使用A-SVGF作为其去噪解决方案 。
文章插图
路径追踪器/光线追踪器提供直接和间接照明,经过重建滤波器并被合并 。然后,对结果应用色调映射和TAA(可以替换为色调映射+DLSS) 。
文章插图
g)
如上图所示 , 重建滤波器使用时间累积来确定积分颜色/矩,并使用方差估计来获得滤波后的颜色 。意味着我们需要历史缓冲区(来自先前的帧重建),需要光栅化来提供法线、反照率、深度、运动矢量和网格id 。
RTX使用特殊形式的SVGF,添加了辐照度缓存,使用光线长度更好地驱动反射 , 并对透射表面(如水)进行分割渲染 。SVGF虽然非常有效,但确实引入了在游戏中可能注意到的时间延迟 。
17.3.9.2
多光线追踪的时空重要性重采样()[等人2020]试图将实时降噪器的时空重投影步骤移到渲染的早期,重用来自相邻采样概率的统计数据 。本质上是一篇早期论文的结合,讨论了重采样重要性采样,并添加了时空去噪器引入的思想 。
将可用于的RTXDI SDK 。已被NV实现在UE的分支中,源码在 。详细的原理参见() -and Basic。
17.3.9.3 DLSS
在DLSS面世之前,NV已经有AI超采样:
文章插图
DLSS利用AI的学习能力,将低分辨率的输入画面 , 上采样成高清(接近原生分辨率)的画面:
文章插图
文章插图
利用DLSS2.0将1080P上采样到4K比原生4K有了巨大的性能提升(2x到5x):
文章插图
传统初级的抗锯齿算法是通过插值低分辨率像素重建高分辨率图像,常见的选择是双线性、双三次、 , 对比感知锐化,深度神经网络可以根据先验或训练数据在现有像素的基础上产生幻觉() 。它们与原生高分辨率图像相比,生成的图像缺少细节 。由于幻觉 , 图像可能与原生渲染不一致,且时间不稳定:
文章插图
文章插图
进一步的算法 , 如TAAU等使用邻域截?。?对高频信号、新出现的信号重建后方差大,从而导致摩尔纹、闪烁、模糊、重影等瑕疵:
文章插图
实时超分辨率的挑战是:对于单帧方法,模糊图像质量 , 与原生渲染现不一致 , 时间不稳定;对于多帧方法,用于检测和纠正跨帧变化的启发式方法,启发式的局限性导致模糊、时间不稳定和重影 。
在重建信号时 , 和传统的抗锯齿等算法不同 , DLSS 2.0是基于DL(深度学习)的多帧重建,使用从成千上万的高质量图像中训练的神经网络,神经网络比手工制作的启发式更强大,使用来自多帧的样本进行更高质量的重建,从而获得更为精准的重建信号(下图) 。
文章插图
左:原始的高频信号;中:非DL技术的重建,和原始信号方差大;右:DLSS重建方法,更加精准贴合原始信号 。
DLSS 2.0在相同的渲染消耗下,获得更佳的分辨率和图像质量:
文章插图
以下是1080P+TAA、540P+DLSS 2.0、540P+TAAU的画面对比:
文章插图
如果游戏引擎需要集成DLSS,其步骤概览如下:
文章插图
/阶段,因为TAA被DLSS取代 , 依赖TAA的去噪器需要改进降噪器或在降噪后添加专用的TAA通道:
文章插图
DL 需要输入以下信息,通过NGX SDK处理成降噪和抗锯齿后的画面:
文章插图
Post(后处理)阶段采用放大后的分辨率渲染,引擎需要处理与几何体和着色不同的后处理分辨率:
文章插图
值得一提的是,DLSS的主要研发者是闫令琪的师弟文刀秋二——跟博主一样是个热爱摄影的人
文章插图
。
17.3.9.4 降噪实现
理想的降噪器结合了最新技术论文中的想法 , 图例和步骤如下所示:
文章插图
1、
计算场景的NDC空间速度,写入常见的G-附件,如反照率、法线等 。可能还需要这些缓冲区的第一次反弹版本,将需要基于光线追踪的 ,而不是基于光栅的。
文章插图
在降噪之前,重要的是使用某种通用通道(G通道)对材质信息进行编码 , 如法线、反照率、深度/位置、对象ID、粗糙度/金属度等 。此外,访问速度可以将以前的采样转换到当前位置 。可以通过确定渲染的每个顶点的先前和当前NDC空间坐标位置,并取两者的差来计算速度缓冲区 。
→V=→?→→=NDC→cur?NDC→prev
因此,需要对象的前一帧矩阵,以及该对象的动画顶点速度,即当前和先前动画采样之间的位置差 。
文章插图
// NDC space velocityfloat3 ndc = inPosition.xyz / inPosition.w;float3 ndcPrev = inPositionPrev.xyz / inPositionPrev.w;outVelocity = ndc.xy - ndcPrev.xy;
可以进一步使用此概念,例如使用运动矢量进行第一次反弹光泽反射,使用阴影运动矢量在对象移动时进行更好的阴影重投影,甚至使用双运动矢量进行遮挡 。[Zeng等人,2021]
2、Ray Trace
使用[等人2018年][等人2020年]的人工智能自适应采样和样本映射,以更好地确定哪些区域应接收更多样本,通常是高光/阴影,以帮助避免盐巴/胡椒(salt/)等瑕疵,并随时间保持亮度 。将镜面反射和全局照明写入单独附件的分离降噪器比较理想,因为反射降噪将更好地处理第一次反弹数据,全局照明、环境遮挡、阴影可以基于较少的数据使用更简单的时空累积 。
3、(累积)
尽可能频繁地使用时空重投影,对于朗伯数据(如全局照明/环境遮挡)更容易实现,而对于镜面反射数据(如反射)则更难实现 。为了获得更好的结果,使用启发式数据(如法线/反照率/对象ID)将之前的样本转换为当前位置,以及第一次反弹数据(如视图方向、第一次反弹法线、反射率等) 。然后,任何成功的重投影都可以用于重要性样本[等人2020],或将其辐射编码到辐射历史缓冲区[等人2018] 。
时空重投影是重复使用来自前一帧的数据,将其空间重投影到当前帧 。将以前的采样转换为当前帧需要首先在视图空间中找到以前帧数据的坐标,可以通过添加速度缓冲区来完成 。通过比较此屏幕空间坐标的当前位置/法线/对象ID/等与其上一个坐标之间的差异,可以判断对象是否已被遮挡,现在是否在视图中,或者重用以前的采样 。
文章插图
在执行时空重投影时,具有描述给定样本必须累积的时间的缓冲区非常有价值,即历史缓冲区 。它可以用于驱动滤波器在具有较少累积样本的区域中模糊更强,或者用于估计当前图像的方差(较高的历史将意味着较小的方差) 。
outHistoryLength = successfulReprojection ? prevHistoryLength + 1.0 : 0.0;
然后,历史长度可以用作累积因子αα,即当前采样对最终辐射的贡献因子 。
outColor = lerp(colorPrevious, colorCurrent, accumulationFactor);
虽然历史缓冲区是一个有用的东西,但有更好的方法来确定累积因子,而不是成功重投影的比率,我们可以使用统计分析来防止时间延迟 。
4、 (统计分析)
估计当前光线追踪图像的方差,计算亮度/速度的方差变化,并使用其驱动时空重投影和滤波 。尝试使用该差异信息拒绝萤火虫(,即异常亮点) 。
σ2=∑(x?^x)2nσ2=∑(x?x^)2n
方差是信号平均值(均值)的平方差 。我们可以取当前信号的平均值,然后用3x3高斯核(本质上是张量),然后取两者的差 。
σ2=∑x2n?^x2σ2=∑x2n?x^2
const float radius = 2; // 5x5 kernelfloat2 sigmaVariancePair = float2(0.0, 0.0);float sampCount = 0.0;for (int y = -radius; y <= radius; ++y){for (int x = -radius; x <= radius; ++x){// Sample current point data with current uvint2 p = ipos + int2(xx, yy);float4 curColor = tColor.Load(p);// Determine the average brightness of this sample// Using International Telecommunications Union's ITU BT.601 encoding paramsfloat samp = luminance(curColor);float sampSquared = samp * samp;sigmaVariancePair += float2(samp, sampSquared);sampCount += 1.0;}}sigmaVariancePair /= sampCount;float variance = max(0.0, sigmaVariancePair.y - sigmaVariancePair.x * sigmaVariancePair.x);
在A-SVGF中将空间方差估计为边缘避免滤波器(类似于A-trous引导滤波器)的组合,并在反馈回路中使用它来驱动时空重投影期间的累积因子 。除了管理累积,估计方差还可以让我们在时间上降低过滤器的权重,[等人2020]使用类似于A-SVGF的泊松圆盘过滤器,以更好地渲染接触阴影 。
/*** Variance Estimation* Copyright (c) 2018, Christoph Schied* All rights reserved.* Slightly simplified for this example:*/// Setupfloat weightSum = 1.0;int radius = 3; // ? 7x7 Gaussian Kernelfloat2 moment = tMomentPrev.Load(ipos).rg;float4 c = tColor.Load(ipos);float histlen = tHistoryLength, ipos, 0).r;for (int yy = -radius; yy <= radius; ++yy){for (int xx = -radius; xx <= radius; ++xx){// We already have the center dataif (xx != 0 && yy != 0) { continue; }// Sample current point data with current uvint2 p = ipos + int2(xx, yy);float4 curColor = tColor.Load(p);float curDepth = tDepth.Load(p).x;float3 curNormal = tNormal.Load(p).xyz;// Determine the average brightness of this sample// Using International Telecommunications Union's ITU BT.601 encoding paramsfloat l = luminance(curColor.rgb);float weightDepth = abs(curDepth - depth.x) / (depth.y * length(float2(xx, yy)) + 1.0e-2);float weightNormal = pow(max(0, dot(curNormal, normal)), 128.0);uint curMeshID =floatBitsToUint(tMeshID, p, 0).r);float w = exp(-weightDepth) * weightNormal * (meshID == curMeshID ? 1.0 : 0.0);if (isnan(w))w = 0.0;weightSum += w;moment += float2(l, l * l) * w;c.rgb += curColor.rgb * w;}}moment /= weightSum;c.rgb /= weightSum;varianceSpatial = (1.0 + 2.0 * (1.0 - histlen)) * max(0.0, moment.y - moment.x * moment.x);outFragColor = float4(c.rgb, (1.0 + 3.0 * (1.0 - histlen)) * max(0.0, moment.y - moment.x * moment.x));
萤火虫抑制( )可以通过多种方式进行,从调整光线追踪期间的采样方式,到使用过滤技术或关于输出辐射亮度的 。
// 增加每次弹跳的粗糙度//https://twitter.com/YuriyODonnell/status/1199253959086612480//http://cg.ivd.kit.edu/publications/p2013/PSR_Kaplanyan_2013/PSR_Kaplanyan_2013.pdf//http://jcgt.org/published/0007/04/01/paper.pdffloat oldRoughness = payload.roughness;payload.roughness = min(1.0, payload.roughness + roughnessBias);roughnessBias += oldRoughness * 0.75f;// 截取拒绝// Ray Tracing Gems Chapter 17float3 fireflyRejectionClamp(float3 radiance, float3 maxRadiance){return min(radiance, maxRadiance);}// 方差拒绝// Ray Tracing Gems Chapter 25float3 fireflyRejectionVariance(float3 radiance, float3 variance, float3 shortMean, float3 dev){float3 dev = sqrt(max(1.0e-5, variance));float3 highThreshold = 0.1 + shortMean + dev * 8.0;float3 overflow = max(0.0, radiance - highThreshold);return radiance - overflow;}
5、(过滤)
可以使用à-Trous双边滤波器快速完成,根据想要的模糊强度重复此步骤3到5次,每次将步长减小2的幂(因此,在3次迭代的情况下,顺序为4、2、1) 。或者 , 可以使用降噪自动编码器 , 该编码器速度较慢,但可以产生更好的过滤结果 。然后,该结果可以输入一个超级采样自动编码器,该编码器可以上采样结果 , 类似于的DLSS 2.0 。
à-Trous避免了以略微抖动的模式采样,以覆盖比3x3或5x5高斯核通常可能的更宽的半径,同时具有重复多次的能力,并避免由于不同输入的数量而在边缘模糊 。可以结合以下方法进行:
6、 Blit(历史拷贝)
写入当前预处理数据,如反照率、深度等 , 以便重新投影下一帧 。
发布了一个使用的类似降噪器的示例实现[Wyman等人2021] 。下图是NV的AI降噪 , 可利用1采样高噪点图,通过降噪算法,获得良好的降噪结果 。
文章插图
文章插图
上:1次采样的原始噪点图;下:开启了降噪处理的画面 。
文章插图
顶部图像中的环境遮挡使用每像素一条光线进行光线追踪,然后进行降噪 。缩放图像从左至右显示:基准真相、屏幕空间环境遮挡、光线追踪环境遮挡 。其中光线追踪环境遮挡每帧每像素一个样本,并从每像素一样本进行降噪 。降噪图像不会捕获所有较小的接触阴影,但仍然比屏幕空间环境遮挡更接近基准真相 。(提供)
所有这些类型的算法都依赖于重用数据,因此 , 当重用数据不可用时,例如在快速移动的对象、高度复杂的几何体或历史信息很少的区域,每个方法的质量都会下降 。有一些方法可以利用一些缓存数据来帮助避免这种情况 , 例如使用辐照度缓存来获得更好的默认颜色,如 RTX 。
文章插图
对于反射,时空重投影也非常困难,因此通常情况下 , 降噪器将依赖于第一次反弹数据,其中反射表面的法线、位置数据等基于第一次反射,而不是原始表面 。
降噪可以通过时空重投影重新使用以前的样本——自适应地重新采样辐射或统计信息以进行重要性采样 , 并使用快速高斯/双边滤波器等滤波器或人工智能技术(如去噪自动编码器和通过超采样进行放大),帮助弥合低样本/像素图像与基准真相之间的差距 。
虽然降噪并不完美,因为时间技术可能会引入辐射滞后,并且任何滤波器都会由于试图模糊原始图像而导致锐度损失,但引导滤波器可以帮助保持锐度,并且自适应采样或增加每帧像素的样本数可以使去噪图像和地面真实图像之间的差异可以忽略不计 。尽管如此 , 每像素采样率更高是无法替代的,因此使用不同的每像素采样(spp)计数来试验这些技术 。
一个健壮的降噪器应该考虑使用所有这些技术,但取决于应用程序的权衡和需求 。最近的研究侧重于通过改进采样方案和使用缓存信息重新采样像素,将降噪移到渲染的早期,而之前的研究则侧重于过滤、机器学习中的自动编码器、重要性采样以及当前在商业游戏和渲染器中生产的实时方法 。
UE大量综合使用了滤波、采样的若干种技术(双边滤波、空间卷积、时间卷积、随机采样、信号和频率等等),而不仅仅限于光线追踪,还用于包含SSGI、SSR、SSAO等屏幕空间技术 。下图是UE的SSGI在经过时间累积之后,可以看到画面的噪点更少且不明显了:
文章插图
17.3.9.5 降噪文献
降噪是未来值得深究的课题和领域,希望童鞋们有志参与其中,现推荐部分降噪相关的样例、论文、演讲和文献:
Talks
17.3.10 光线追踪优化
光线追踪的实际场景可能很复杂:数千个光源,超过内存容量的纹理 , 超过内存容量的几何图形(以镶嵌形式),非常复杂的可编程着色器,用于位移、照明和反射 。
17.3.10.1 光源优化
在光源方面 , 直接照明的主要消耗通常是阴影 。如果使用阴影贴图,则必须为每个光源渲染和管理阴影贴图 。如果使用光线追踪,并且我们必须为每个光源追踪至少一条阴影光线,则渲染时间将长得无法接受 。幸运的是,可以通过基于潜在照明对光源进行排序来处理来自许多光源的阴影 。有些光源太远,照明太暗,因此可以非常粗略地近似 。在每个表面点处 , 计算每个光源的直接照明,然后根据照明强度对灯光进行排序,最后对要计算阴影的灯光、要计算无阴影的灯光和要跳过的灯光进行概率选择 。
17.3.10.2 纹理优化
在纹理方面 , 当渲染图像所需的纹理超过可用内存时 , 必须按需从磁盘读取纹理,仅以所需分辨率读取纹理 , 并将纹理缓存在内存中 。
可以使用纹理和纹理分块 。分块纹理以便将相邻像素组一起从磁盘读取到存储器,下图显示了平铺纹理MIP贴图的三个级别 。在本例中,每个分块包含16×16像素 。最粗糙的MIP贴图级别(级别0-3)可以压缩到单个块中(此处未显示),MIP映射级别4由单个块组成,下一层有2×2个分块(每个分块中仍有16×16个像素),下一层有4×4个分块,依此类推 。
文章插图
此外 , 还可以使用多分辨率纹理分块缓存 。多分辨率纹理分块缓存的纹理访问对于直接可见几何体的渲染具有高度一致性,缓存大小为总纹理大小的1%就足够了 。当光线微分用于为纹理查找选择适当的MIP贴图级别时,观察到光线追踪的类似结果,选择纹理像素与射线束横截面大小大致相同的级别 。非相干光线具有较宽的光线束,因此将选择粗略的MIP贴图级别 。更精细的MIP图级别将仅由具有窄射线束的射线访问;幸运的是,这些光线是相干的 , 因此生成的纹理缓存查找也将是相干的 。
17.3.10.3 几何优化
对于复杂场景,可以使用实例化、光线重新排序和着色缓存(Rayand)、几何替身( stand-ins)、多分辨率曲面细分等技术 。
在光线重新排序和着色缓存方面,Toro渲染器对光线进行了重新排序,以增加几何相干性,使得光线追踪大于计算机主内存的场景成为可能 。对射线重新排序要求每个射线的图像贡献是线性的 , 这种要求对于真实的物理反射是正确的,但对于电影制作中使用的非常艺术化的可编程着色器通常不是正确的 。Razor项目受到用于扫描线渲染的REYES算法的启发,一次性对曲面点的整个网格进行着色,并存储着色结果中与视图无关的部分 。如果以下某些光线击中同一曲面面片,则可以重复使用着色结果 。
几何替身的预计算是一个相当长的过程,但一旦完成 , 就可以交互式地对场景进行光线追踪 。
对于多分辨率曲面细分,在实际应用中,对曲线、面片、NURBS曲面、细分曲面和任何具有位移的曲面进行细分,而不是用数值方法计算光线曲面交点是有利的 。这些曲面被分割为大小可控的较小曲面面片,对应于纹理的分块 。直接可见曲面面片的镶嵌率应取决于观察距离和曲面曲率,还可选择地取决于观察角度 。对于反射或阴影 , 我们通常可以使用更粗糙的曲面细分 。下图显示了曲面面片的五个细分的示例,最精细的细分率为14×11,较粗的层次由最精细细分的顶点子集组成 , 最粗糙的细分只是面片的四个角,可以将不同级别的细分视为细分几何体的MIP贴图 。
文章插图
曲面面片的多分辨率细分示例:14×11、7×6、4×3、2×2和1个四边形 。
Pharr和缓存了置换曲面的曲面细分几何体,但没有利用多分辨率曲面细分 。根据需要以所需的分辨率细分曲面面片(然后在适当的情况下置换顶点) , 并将细分存储在缓存中 。由于细分的大小相差很大,缓存可以存储比精细细分多得多的粗细分 。
对于射线相交测试,可以选择四边形与射线束横截面大小大致相同的细分 。对精细和中等镶嵌的访问通常是非常一致的,对粗细分的访问相当不一致 , 但粗细分的缓存容量很大,而且这些细分无论如何都很快重新计算 。精细细分仅用于直接可见的几何体、平面的镜面反射和折射以及光线原点附近的漫反射和环境遮挡光线,对于所有其他光线,光线束较宽,并使用中等和粗糙曲面细分 。(类似不同粗糙度对应不同纹理的等级?。?
17.3.10.4 并行计算
光线追踪似乎非常适合并行加速:每个像素的计算独立于所有其他像素,导致人们普遍认为光线追踪是“令人尴尬的平行” 。但是 , 只有当场景数据适合主内存时,这才是成立的!如果场景较大,则必须非常小心地维护和利用数据访问一致性,安排执行顺序以使后续光线趋向于遍历相同的几何体并访问相同的纹理,从而确保良好的缓存行为 。此举非常值得,可提升缓存命中率 。
现代CPU有SIMD指令(英特尔上的SSE、IBM/上的、AMD上的3dNow),可以并行执行四种操作 。利用这些指令对平行于一个三角形的四条射线进行交叉测试 。如果光线是相干的,将提供良好的加速,对于可见性光线 , 典型的加速比约为3.5倍 。
利用SIMD指令的另一种方法是对平行的四个三角形进行一条射线的交叉测试 。如果三角形是相干的(就像它们来自细分曲面上的相邻位置一样) , 会提供良好的加速 , 并且不需要光线是相干的 。SIMD指令的另一个用途是平行交叉测试轴对齐包围盒的所有三个平面 。
17.3.10.5 GPU加速
、RTX等系列GPU已经新增了光线追踪的硬件单元,从而加速了实时光线追踪的到来 。此外 , 在GPU内如何提升光线、纹理等数据的一致性也是提升光线追踪的首要问题 。内置了一致性引擎,用来收集和处理相关性高的光线(下图) 。
文章插图
此外,GPU需要考虑SIMD、SIMT、连贯性、内存合并、核心占用、管线瓶颈、同步方式甚至物理温度(防止降频)等,更多可参阅:。
在实现时,需要注意或使用自相交、数据精度、面片(patch)相交、加载均衡、多相交、LOD等问题或技巧 。更多可参阅:
17.3.11 综合技术 17.3.11.1 Lumen GI
以往的实时研究有辐照度?。?)、屏幕空间降噪器( Space )等方式 。而UE5的Lumen使用了屏幕空间降噪器( Space ) 。
文章插图
下采样入射辐射 , 入射光是相干的 , 而几何法线不是,以全分辨率积分BRDF上的输入照明:
文章插图
在辐射缓存空间中过滤,而不是屏幕空间(下图左) 。首先要进行更好的采样——重要的是对入射光进行采样(下图中) 。稳定的远距离照明和世界空间辐射缓存(下图右) 。
文章插图
最终收集管线:
文章插图
其中屏幕空间的辐照率缓存可以细分成以下阶段:
文章插图
屏幕探针结构体:带边框的八面体图集,通常每个探针8x8个 , 均匀分布的世界空间方向,邻域有相同的方向,二维图集中的辐射率和交点距离:
文章插图
屏幕探针放置:分层细化的自适应布局[K?ivánek等人2007],迭代插值失败的地方,最终级别的地板填充(Flood fill) 。
文章插图
采用自适应采样——实时性需要上限,不希望在处理自适应探针时遇到额外障碍,将自适应探针放在图集底部:
文章插图
屏幕探针抖动——时间抖动放置网格和方向,直接放置在像素上,没有泄露,屏幕单元格内的遮挡差异必须通过时间过滤来隐藏:
文章插图
面距离加权,防止前台未命中泄漏到后台,插值中的抖动偏移 , 只要还在同一个面上,在空间上分布探针之间的差异,通过扩展TAA 3x3的邻域达到时间稳定最终照明 。
还使用了重要性采样——对于入射辐射率Li(l)Li(l) , 重投射最后一帧的屏幕探针的辐射率!不需要做昂贵的搜索,光线已按位置和方向索引,回退到世界空间探针上 。对于BRDF,从将使用此屏幕探针的像素累积,更好的是,希望采样与入射辐射率Li(l)Li(l)和BRDF的乘积成比例 。
结构重要性采样()——将少量样本分配给概率密度函数(PDF)的层次结构区域 , 实现良好的全局分层,样本放置需要离线算法 。
文章插图
完美地映射到八面体mip四叉树!
文章插图
集成到管线中——向追踪线程添加间接路径,存储、,追踪后,将组合进均匀的探针布局 , 以进行最终集成:
文章插图
光线生成算法——计算每个八面体纹理的BRDF的PDF x 光照的PDF,从均匀分布的探针射线方向开始,需要固定的输出光线计数——保持追踪线程饱和 。按PDF对光线进行排序,对于PDF低于剔除阈值的每3条光线,超级采样以匹配高PDF光线 。
文章插图
改进的点是不允许光照PDF来剔除光线,光照PDF为近似值,BRDF为精确值,借助空间过滤可以更积极地进行剔除 , 具有较高BRDF阈值的剔除,在空间过滤过程中减少剔除光线的权重 , 修复角落变暗的问题 。
文章插图
重要性采样回顾:使用最后一帧的光照和远距离光照引导此帧的光线,将射线捆绑到探针中可以提供更智能的采样 。
接下来聊空间过滤的技术 。
辐射缓存空间中的过滤:廉价的大空间滤波,探针空间为32x32 , 屏幕空间为,可以忽略空间邻域之间的发现差异,仅深度加权 。从邻域收集辐射率——从相邻探针中匹配的八面体单元收集 , 误差权重——重投影的相邻射线击中的角度误差 , 过滤远处的灯光,保留局部阴影 。
文章插图
对于平坦表面的效果是良好的 , 但对于几何接触的地方 , 存在漏光的问题:
文章插图
保持接触阴影——角度误差偏向远光等于泄漏,远距离光没有视差,永远不会被拒绝 。解决方案是在重投影之前,将邻域的命中距离截取到自己的距离 。
文章插图
接下来聊世界空间的辐射缓存 。
远距离光存在问题,微亮特征的噪点随着距离的增加而增加,长而不连贯的追踪是缓慢的,远处的灯光正在缓慢变化——缓存的机会 , 附近屏幕探针的冗余操作,解决方案是对远距离辐射进行单独采样 。用于远距离照明的世界空间辐射缓存(The[ 2015]的技术),自世界空间以来的稳定误差——易于隐藏,就像体积光照图一样 。
文章插图
管线集成——在屏幕探针周围放置,然后追踪计算辐射,插值以解决屏幕探测光线的远距离照明 。
文章插图
世界探针射线必须跳过插值足迹以避免自光照:
文章插图
屏幕探针光线必须覆盖插值足迹+跳过距离:
文章插图
还存在漏光的问题,世界探针的辐射应该被遮挡,但不是因为视差不正确 。
文章插图
解决方案是简单的球面视差,重投影屏幕探针光线与世界探针球相交 。
文章插图
稀疏覆盖——以摄像头为中心的3d 网格将探针索引存储到图集中,分布保持有限的屏幕大小 。
文章插图
八面体探针图集存储辐射、追踪距离,通常每个探针为32x32的辐射率:
文章插图
放置和缓存——标记将在后面的间接中插入的任何位置 , 对于每个标记的世界探针:重用上一帧的追踪,或分配新的探针索引,重新追踪缓存命中的子集以传播光照更改 。
文章插图
依然存在的问题是高度可变的成本,快速的摄像机移动和不连续需要追踪许多未经缓存的探针 。解决方案是全分辨率探针的固定预算,缓存未命中的其它探针追踪的分辨率较低,跳过照明更新的其它探针追踪 。
BRDF的重要采样的做法是从屏幕探针累积BRDF,切块(Dice )探针追踪分块,根据BRDF生成追踪分块分辨率 。超采样近的相机,高达64x64的有效分辨率 , 4096条追踪!非常稳定的远距离照明 。
探针之间的空间过滤——再次拒绝邻域交点 , 问题是不能假设相互可见性 。理想情况下,通过探测深度重新追踪相邻射线路径,单次遮挡试验效果良好,几乎免费——重复使用探针深度 。
文章插图
文章插图
世界空间辐射缓存还用于引导屏幕探针重要性采样、头发、半透明、多反弹 。
回到积分,现在已经在屏幕空间的辐射缓存中以较低的分辨率计算了入射辐射,需要以全分辨率进行积分,以获得所有的几何细节 。
文章插图
重要性采样BRDF会导致不一致的获取,8spp*4相邻探针方向查找,可以使用mips(过滤重要性采样),但会导致自光照,尤其是在直接照明区域周围 。将探针辐射转换为三阶球谐函数:SH是按屏幕探针计算的,全分辨率像素一致地加载SH,SH低成本高质量积分 。
文章插图
对于高粗糙度下的光线追踪反射,在漫反射上聚集 。重用屏幕探针——从GGX生成方向,采样探针辐射 , 自动利用已完成的探针采样和过滤!下采样追踪会丢失接触阴影 。使用全分辨率弯曲法线——使用快速屏幕追踪进行计算,与屏幕探针之间的距离耦合的追踪距离,约16像素 。与屏幕空间辐射缓存积分——将屏幕探针GI视为远场辐照度 , 全分辨率弯曲法线表示场的数量,基于水的间接照明,多重反弹似给出场辐照度 。
文章插图
文章插图
接着使用时间过滤——抖动探针位置需要可靠的时间过滤,使用深度剔除,结果稳定,但对光线变化的反应也很慢 。追踪过程中追踪命中速度和命中深度,属于快速移动对象的投影面积 。当追踪击中快速移动的对象时,切换到快速更新模式 , 降低时间过滤 , 提高空间过滤 。
文章插图
最终收集性能:
文章插图
文章插图
文章插图
未来的工作是降噪质量、高动态场景中的时间稳定性、将屏幕空间辐射缓存应用于Lumen的表面缓存以实现多反弹GI 。
Cache只是Lumen的一小部分技术,Lumen还涉及表面缓存、软件射线追踪、硬件光线追踪、反射、透明GI等内容 。关于Lumen的源码剖析可参见:剖析虚幻渲染体系(06)- UE5特辑Part 2(Lumen和其它) 。
17.3.11.2GI
即表面元素( ),一个由位置、半径和法线定义,并近似了给定位置附近表面的一个小邻域(下图) 。
文章插图
从中生成面元 , 当几何图形进入视图时填充屏幕,在世界空间中持久存在,累积和缓存辐照度 。迭代屏幕空间填充,将屏幕拆分为16x16块,找到覆盖率最低的tile,应用面元覆盖率和追踪权重,如果tile超过随机阈值 , 则生成 。
文章插图
除了支持刚体,还支持蒙皮骨骼的面元化 。由于所有东西都假设是动态的,所以蒙皮几何体和移动几何体都与解决方案的其余部分交互,就像静态几何体一样 。
文章插图
面元根据屏幕空间投影进行缩放,生成算法确保覆盖范围在任何距离,由非线性加速度结构支撑 。
文章插图
所有东西都有固定大小的缓冲区,可预测的预算,固定数量的面元,固定的加速度结构,回收未使用的面元 。
文章插图
让相关的面元保持活跃,最后一次见到时追踪,如果在间隙检测期间看到,则重置,位置更新期间增加 。启发式基于激活的面元总数、自从见过的时间、距离、覆盖率 。下图是距离启发式:
文章插图
为了应用光照,对每个像素:查找表面网格单元,从单元格里取N个面元,累积表面辐照度,按距离和法线加权,如果辐照度权重
文章插图
修复前后对比:
文章插图
积分辐照度图示:
文章插图
修正指数移动平均估值器[Barré] , 追踪短期均值和方差估计值,使用短期估计器调整混合因子,能够快速响应变化,同时收敛到低噪点 。基于短期方差的偏差光线计数,使用射线计数通知相对置信度的多尺度均值估计器 , 反馈回路对变化和变化做出快速反应,在稳定的情况下保持光线计数小 。
文章插图
累积漫反射辐照度,假设是兰伯特BRDF,通过对余弦叶进行重要采样来生成光线:
文章插图
采用了光线引导:
文章插图
每个在其半球上生成一个移动平均6x6亮度图,存储在单个4K纹理中(可支持所有的7x7),每个纹理8位+每个纹理单个16位缩放,规范化每帧函数 。
文章插图
有了重要性采样变量,函数的每个离散部分都将根据其值按比例选取,还有它的概率密度函数,也就是函数在那个位置的值 。
文章插图
文章插图
利用附近的面元数据 , 允许查找相邻的辐射,结构化加速 , 使用与 VPL相同的权重、距离、深度函数 。
文章插图
辐照度共享前后对比:
文章插图
还可以使用BF5方法对光线进行排序,按位置和方向排列的箱射线,12位表示空间,4位表示方向,空间散列的单元定位,射线方向定向,计算箱子总计数和偏移量,根据光线索引和以前计算的面元偏移对光线重新排序 。
文章插图
多光源采样使用了重要性采样(随机光源分割、储备采样) 。随机光源切割是小样本快速收敛,需要预先构建的数据结构,采样可能开销很大 。
文章插图
蓄水池采样( )示意图:
文章插图
光线追踪探针示意图:
文章插图
透明对象需要大屏幕支持,例如不透明对象,是满足需求的最佳选择:保持近距离的细节 , 支持大规模场景,具有低内存成本的稀疏探针放置 , LOD的变速率更新 。
文章插图
计算更新方向和距离,复制移位后有效的探针数据,用更高级别的探针初始化新创建的探针 。
文章插图
4级的放置示意图:
文章插图
采样过程如下:
文章插图
进一步的采样优化是使用蓝色噪声梯度抖动采样 。
文章插图
一帧概览:
17.3.11.3 收集与合成
过滤通常被认为是取平均值的过程 , 用于产生模糊像素的相邻像素的加权平均值,我们称这种方法为聚集(也称收集,),许多像素被聚集在一起以产生一个输出 。在几何体交点上采样照明,最终的光追各分量和组合效果如下:
文章插图
文章插图
总之,通过DXR轻松访问最先进的GPU光线追踪,性能正在达到目标,易于不适合光栅化的原型化算法,可与现有低频结构相结合 。
文章插图
在光追的各项时间消耗如下表:
文章插图
SDF到网格的转换使用双通道近似,多个三角形指向同一个粒子 , 首先需要生成粒子 。输出用于PBD模拟器的线性粒子数组(表面)和三角形渲染的索引缓冲区 。使用单个间接绘制调用绘制的所有网格 。转成粒子使用的、4x4x4的线程组,过程如下:
转成三角形使用的、4x4x4的线程组,过程如下:
异步计算:
工作立即提交 。
文章插图
异步计算可以让fps提升19%+ 。
集成到UE4渲染器:
阴影遮蔽( mask)组合 。
UE4 RHI定制:
清除RT/而不进行隐式同步 。缺少异步计算功能 。计算着色器索引缓冲区写入 。
文章插图
此外,额外定制了UE4 RHI,使用GPU->CPU缓冲区回读 , UE4仅支持2d纹理回读而不停顿 , 其它 API会让整个GPU陷入停顿,缓冲区可以有原始视图和类型化视图,宽原始写入等于高效填充窄类型缓冲区 。
其它的UE4优化:允许间接分派/提取的重叠,允许清除和复制操作重叠,允许不同RT的绘制重叠,减少GPU缓存刷新和停顿(下图) , 优化的暂存缓冲区 , 快速清晰的改进 。优化屏障和栅栏,优化纹理数组子资源屏障,更好的3d纹理GPU分块模式 , 改进的部分2d/3d纹理更新 , 5倍更快的直方图+眼睛适应着色器,4倍更快的离线CPU SDF生成器(烘焙) 。
文章插图
物理数据存储在一个大的原始缓冲区中,宽加载4/指令(16字节),位压缩:粒子位置:16位范数、粒子速度:fp16、粒子标志(活动、碰撞等)的位字段,基准工具: 。
内存是一个巨大的性能利器 , SDF生成、网格生成、物理 , 重复加载相同数据时使用 。标量加载是AMD在性能上的一大胜利,用例:常量索引原始缓冲区加载,用例:基于的原始缓冲区加载,存储到SGPR的负载获得更好的占用率 。
17.3.11.4 光子映射
光子映射综合来看,分为两个Pass:
光子追踪过程的目的是计算漫反射表面上的间接照明 , 是通过从光源发射光子、在场景中追踪光子并将其存储在漫反射表面来实现的 。
从光源发射的光子应具有对应于光源发射功率分布的分布 , 以确保发射的光子携带相同的通量 , 即我们不会在低功率光子上浪费计算资源 。
来自漫射点光源的光子从该点以均匀分布的随机方向发射 。来自平行光的光子都沿同一方向发射 , 但来自场景外部的原点 。来自漫反射正方形光源的光子从正方形上的随机位置发射,方向限于半球 。发射方向从余弦分布中选择:在平行于正方形平面的方向上发射光子的概率为零,在垂直于正方形的方向上的发射概率最高 。
通常,光源可以具有任何形状和发射特性——发射光的强度随原点和方向而变化 。例如,灯泡具有非平凡的形状,从其发出的光的强度随位置和方向而变化 。光子发射应遵循此变化,因此通常,发射概率根据光源表面上的位置和方向而变化 。下图显示了这些不同类型光源的发射:
文章插图
光源发光:点光源、定向光源、方形光源、普通光源 。
光源的功率必须分布在从光源发射的光子之间 。如果光源的功率为且发射光子的数量为nene , 则每个发射光子的功率是:
==
下面给出了漫射点光源光子发射的简单示例的伪代码:
文章插图
为了进一步减少计算的间接照明(在渲染期间)的变化 , 希望尽可能均匀地发射光子 。例如,可以使用分层或者低差异准随机采样 。
在具有稀疏几何体的场景中,许多发射的光子不会击中任何对象,发射这些光子将浪费很大时间 。为了优化发射,可以使用投影图( map) 。投影图只是从光源看到的几何图形的图 , 由许多小单元格(cell)组成 。如果在该方向上有几何图形,则单元格为“开”,如果没有,则为“关” 。例如,投影贴图是点光源的场景的球形投影,是平行光的场景的平面投影 。为了简化投影,可以方便地围绕每个对象或对象簇投影边界球体 。此举也大大加快了投影图的计算 , 因为不必检查场景中的每个几何元素 。投影图最重要的方面是 , 它给出了从光源发射光子所需方向的保守估计 。如果估计不是保守的(例如,可以先用几个光子对场景进行采样),可能会丢失重要的效果,例如焦散 。
使用投影图发射光子非常简单 。可以在包含对象的单元格上循环,并向单元格所表示的方向发射随机光子 。然而,这种方法可能会导致稍微有偏差的结果,因为光子图可能在访问所有单元格之前“已满” 。另一种方法是生成随机方向,并检查对应于该方向的单元是否有任何对象(如果没有,则应尝试新的随机方向) 。这种方法通常效果良好,但在稀疏场景中可能代价高昂 。对于稀疏场景,最好为具有对象的单元随机生成光子 。一种简单的方法是选择具有对象的随机单元,然后为该单元的发射光子选择随机方向 。在所有情况下,都必须根据投影图中的活动单元格数量和发射的光子数量来缩放存储光子的能量 。因此需要修改光子能力的公式:
= withof = withof cells
投影图的另一个重要优化是识别具有镜面反射特性的对象(即可以生成焦散的对象) 。如后所述,焦散是单独生成的 , 由于镜面反射对象通常稀疏分布,因此使用焦散投影图非常有益 。
文章插图
场景中的光子路径:(a)两次漫反射后被吸收;(b)镜面反射后转为两次漫反射;(c)两次镜面透射后被吸收 。
光子发射后,将使用光子追踪在场景中进行追踪(也称为“光线追踪”、“反向光线追踪”、“正向光线追踪”和“反向路径追踪”) 。光子追踪的工作方式与光线追踪完全相同,只是光子传播通量 , 而光线收集辐射 。这是一个重要的区别 , 因为光子与材质的相互作用可能不同于射线的相互作用 。一个值得注意的例子是折射,其中根据相对折射率改变辐射亮度的情况不会发生在光子上 。
当光子击中物体时,它可以被反射、透射或吸收——根据表面的材质参数概率而定 。用于确定交互类型的技术称为俄罗斯轮盘赌——掷骰子,决定光子是否应该存活并被允许执行另一个光子追踪步骤 。
光子仅存储在它们撞击漫反射表面(或更准确地说,非特殊表面)的位置 。原因是,在镜面反射表面上存储光子不会提供任何有用的信息:从镜面反射方向具有匹配入射光子的概率为零,因此,如果我们想要渲染精确的镜面反射,最好的方法是使用标准光线追踪沿镜面方向追踪光线 。对于所有其他光子-表面相互作用,数据存储在全局数据结构(光子图)中 。注意,每个发射的光子可以沿其路径存储多次 。此外,有关光子的信息存储在其被吸收的表面(如果该表面是漫反射的) 。
对于每个光子-表面相互作用,存储位置、入射光子功率和入射方向(实际还会为每个光子数据集保留了一个标记空间,该标记在光子图中的排序和查找过程中使用) 。
struct Photon {floatx,y,z;// positioncharp[4];// power packed as 4 charscharphi, theta; // compressed incident directionshortflag;// flag used in kdtree};
再次考虑上图中的简单场景,(a)显示了该场景的传统光线追踪图像(直接照明和镜面反射和透射),(b)显示了为该场景生成的光子图中的光子 , 玻璃球下光子的高浓度是由玻璃球聚焦光子引起的 。
数据存储还可以扩展到参与介质 , 以及多重散射、各向异性散射和非均匀介质 。
光子仅在光子追踪过程中生成,在渲染过程中,光子图是一种静态数据结构,用于计算场景中许多点处的入射通量和反射辐射的估计 。为此 , 需要在光子图中定位最近的光子 。这是一个非常频繁的操作,因此需要在渲染过程之前优化光子图,以便尽可能快地找到最近的光子 。
首先,我们需要选择一个好的数据结构来表示光子图 。数据结构应紧凑,同时允许快速最近邻搜索 。它还应该能够处理高度不均匀的分布——在焦散光子贴图中非常常见 。处理这些需求的自然候选者是平衡kd树 。用于平衡光子图的伪代码:
文章插图
光子映射方法的一个基本组成部分是计算任何给定方向上任何非镜面反射表面点处的辐射估计的能力 。光子辐射亮度估算可由经典的BRDF推导而成:
Lr(x,→ω)=∫Ωxfr(x,→ω′,→ω)Li(x,→ω′)∣∣→nx?→ω′∣∣dω′i,Li(x,→ω′)=d2Φi(x,→ω′)cosθidω′idAi,Lr(x,→ω)=∫Ωxfr(x,→ω′,→ω)d2Φi(x,→ω′)cosθidω′idAi∣∣→nx?→ω′∣∣dω′i=∫Ωxfr(x,→ω′,→ω)d2Φi(x,→ω′)dAi.Lr(x,→ω)≈∑np=1fr(x,→ωp,→ω)ΔΦp(x,→ωp)ΔA.Lr(x,ω→)=∫Ωxfr(x,ω→′,ω→)Li(x,ω→′)|n→x?ω→′|dωi′,Li(x,ω→′)=d2Φi(x,ω→′)cos?θidωi′dAi,Lr(x,ω→)=∫Ωxfr(x,ω→′,ω→)d2Φi(x,ω→′)cos?θidωi′dAi|n→x?ω→′|dωi′=∫Ωxfr(x,ω→′,ω→)d2Φi(x,ω→′)dAi.Lr(x,ω→)≈∑p=1nfr(x,ω→p,ω→)ΔΦp(x,ω→p)ΔA.
这个过程可以想象为围绕xx展开一个球体,直到它包含nn个光子(见下图),然后使用这nn个光子来估计辐射亮度 。
文章插图
使用光子图中最近的光子估计辐射亮度 。
上图使用了球体 , 通过假设曲面在xx周围局部平坦,我们可以通过将球体投影到曲面上并使用所得圆的面积来计算该面积(即上图中的阴影区域),等于:
△A=πr2△A=πr2
其中rr是球体的半径,即xx和每个光子之间的最大距离 。使用光子图计算表面处反射辐射的公式变成了以下等式:
Lr(x,→ω)≈1πr2N∑p=1fr(x,→ωp,→ω)ΔΦp(x,→ωp).Lr(x,ω→)≈1πr2∑p=1Nfr(x,ω→p,ω→)ΔΦp(x,ω→p).
该估计基于许多假设 , 精度取决于光子图和公式中使用的光子数 。由于球体用于定位光子,因此很容易在估计中包括错误的光子,特别是在物体的角和锐边 。边和角也会导致面积估计错误 。发生这些误差的区域的大小在很大程度上取决于光子图和估计中的光子数量 。随着估算和光子图中使用更多光子 , 公式变得更精确 。如果我们忽略由于位置、方向和通量表示的有限精度而导致的误差,那么我们可以达到极限并将光子数量增加到无穷大 。将给出了以下有趣的结果,其中NN是光子图中的光子数:
limN→∞1πr2?Nα?∑p=1fr(x,→ωp,→ω)ΔΦp(x,→ωp)=Lr(x,→ω)forα∈??0,1[limN→∞1πr2∑p=1?Nα?fr(x,ω→p,ω→)ΔΦp(x,ω→p)=Lr(x,ω→)forα∈]0,1[
该公式适用于位于表面局部平坦部分上的所有点xx,其中BRDF不包含狄拉克δδ函数(不包括完美镜面反射) 。上面等式中的原理是,不仅将使用无限量的光子来表示模型内的通量,而且还将使用无限数量的光子来估计辐射 , 并且估计中的光子将位于无穷小的球体内 。不同的无限度由项NαNα控制,其中α∈]0,1[α∈]0,1[ , 确保了估计中的光子数量将无限小于光子图中的光子数 。
上述公式意味着我们可以通过使用足够的光子获得任意好的辐射估计!在基于有限元的方法中,获得任意精度更为复杂,因为误差取决于网格的分辨率、辐射的方向表示的分辨率和光模拟的精度 。
上图显示了定位最近的光子如何类似于围绕x展开球体并使用该球体内的光子 。在此过程中 , 可以使用球体以外的其他体积 。人们可以使用立方体,圆柱体或圆盘 。这可能有助于获得定位最近光子更快的算法,或者在选择光子时可能更准确 。如果使用不同的体积,则?等式中的A应替换为体积与在x处接触表面的切面之间的交点面积 。
球体具有明显的优点,即投影面积和距离计算非常简单,因此计算效率高 。通过将球体沿x处表面法线方向压缩(如下图所示),将球体修改为圆盘(椭球体),可以获得更精确的体积 。使用圆盘的优点是,在边缘和拐角处的估计中使用更少的“假光子” 。例如,在房间的边缘效果非常好 , 因为可以防止墙壁上的光子泄漏到地板上 。然而,仍然存在的一个问题是,面积估计可能是错误的 , 或者光子可能泄漏到它们不属于的区域 。这个问题主要通过使用过滤来解决 。
文章插图
使用球体(左)和圆盘(右)来定位光子 。
如果光子图中的光子数太低,则辐射亮度估计在边缘处变得模糊 。当光子图用于估计分布射线追踪器的间接照明时,这种伪影可能令人满意,但在辐射估计表示焦散的情况下 , 这种伪影是不需要的 。焦散通常具有锐利的边缘,在不需要太多光子的情况下保留这些边缘会很好 。
为了减少边缘的模糊量,对辐射估计进行滤波 。滤波背后的思想是增加接近感兴趣点x的光子的权重 。由于我们使用球体来定位光子 , 因此自然会假设滤波器应该是三维的 。然而,光子存储在二维表面上 。面积估计也基于光子位于表面的假设 。因此,我们需要在光子定义的区域上归一化的2d滤波器(类似于图像过滤器) 。
过滤焦散可以使用两个径向对称过滤器:锥形过滤器、高斯过滤器及专用微分过滤器( ) 。前面两个过滤器是老调重弹了,下面重点说说微分过滤器 。
基于微分检查的过滤器的思想是在估计过程中检测边缘附近的区域,并在这些区域中使用更少的光子 。这样,我们可能会在估计中得到一些噪声,但通常比模糊边缘更好 。基于以下观察修改辐射估计:在边缘附近向估计添加光子时,估计的变化将是单调的 。也就是说 , 如果我们刚好在焦散线之外,并且我们开始将光子添加到估计中(通过增加包含光子的以x为中心的球体的大?。敲纯梢怨鄄斓剑孀盼颐翘砑痈喙庾?,估计值正在增加;反之亦然 , 当我们在焦散中时 。基于此观察,可以将微分检查添加到估计中-如果我们观察到随着更多光子的添加,估计值不断增加或减少,则停止添加光子并使用可用的估计值 。
定位最近的光子需要一种高效的算法,下面是其中一种的伪代码:
文章插图
对于该搜索算法 , 需要提供初始最大搜索半径 。选择好的半径可以很好地减少搜索,减少测试的光子数量 。另一方面,太小的最大半径将在光子图估计中引入噪点 。可以基于误差度量或场景的大小来选择半径 , 误差度量例如可以考虑所存储光子的平均能量 , 并根据该平均能量计算最大半径,假设辐射估计中存在一些允许误差 。
文章插图
可以添加一些额外的优化,例如,将最大堆的构建延迟到找到所需光子数的时间,在所请求的光子数量较大时特别有用 。也可以初始最大搜索半径被设置为非常低的值,如果该值太低 , 则使用更高的最大半径执行另一次搜索 。搜索例程的另一个更改是使用前面描述的磁盘检查,有助于避免不正确的颜色溢出,并且在不使用收集步骤且光子直接可视化的情况下特别有用 。
接下来就是渲染部分了 。
使用分布光线追踪来渲染最终图像,其中通过对多个样本估计求平均来计算像素辐射亮度,每个样本包括从眼睛通过一个像素追踪光线进入场景 。可将照光拆分为直接光、镜面和光泽反射、焦散、多重漫反射以及参与介质等部分 。它们和传统的PBR比较类似,本文就忽略研讨之 。
文章插图
光子映射的效果图 。
for Light提出了一种新的光子收集方法,以有效地实现光子映射的无偏倚渲染 。不像经典光子映射那样将收集的光子收集到估计的密度中 , 而是单独处理每个光子 , 并将相应的光子路径与生成聚集点的眼睛子路径连接 , 从而创建无偏路径样本 。通过以严格和无偏的方式评估所有相关项来计算此类路径样本的蒙特卡洛估计 , 从而形成一种独立的无偏采样技术 。该文进一步开发了一组多重要性采样(MIS)权重,允许文中方法与双向路径追踪(BDPT)进行最佳组合,从而产生一种无偏渲染算法,该算法可以有效地处理各种光路,并与以前的算法相比较 。实验证明了该方法的有效性和鲁棒性 。
文章插图
随机渐进光子映射(SPPM)、统一路径采样/顶点连接和合并(UPS/VCM)和该文的无偏光子采集与双向路径追踪(UPG+BDPT)在渲染1小时后的比较 。SPPM利用偏置光子映射来产生低方差结果,代价是过度模糊锐利特征 。UPS/VCM从BDPT中获得额外的好处,但顶点合并部分仍有偏差 。文中的方法既无偏又稳健,产生了与参考最相似的结果 。请注意,左插图设置为曝光1=64,以使HDR阴影细节可见 。
17.3.11.5 综合实现
当前阶段 , 光栅化仍然比光线追踪“快”,而光线追踪可以比光栅化更好地处理某些效果,如反射、软阴影、全局照明等 。目前通常采用混合射线追踪,例如仅反射使用光线追踪而光栅化其他所有内容(包括主光线) 。主流的GPU已基本支持光栅化、计算、光线追踪甚至深度学习等管线混合计算:
文章插图
确定游戏开发人员在集成到现有游戏引擎基础设施时必须解决的问题,因为游戏引擎是为GPU设计和优化的 , 包含艺术资源和材质着色器 。
传统的渲染管线如下图上所示,其中蓝色部分和间接光无关,可以忽略 。下图下的红色是和间接光相关的阶段 。
文章插图
对于下图的黄色步骤,解决方案是多次反弹或近似 。接下来要看的是透明度,它似乎是光线追踪的一个很好的候选者,对吗?
文章插图
事实证明,屏幕空间照明问题同样适用于透明材质(多维性、性能、过滤) 。当前已经在探索SSS的体积解决方案,但没有正确的SSS体积解决方案 。混合渲染管线的流程如下:
文章插图
对于非直接光照,分裂和近似Karis 2013有助于减少方差,蓝色是预先计算的,使用光栅化或光线追踪进行评估 。
文章插图
在RTX的渲染流程如下:
文章插图
随机化的区域光渲染流程如下:
文章插图
V的光线追踪包含了GPU光线追踪管线、DXR的引擎集成、GPU性能等 。
文章插图
简单光线追踪管线:
文章插图
生成管线阶段,读取的纹理 , 使用随机光栅化来生成光线:
文章插图
float4 light(MaterialData surfaceInfo , float3 rayDir){foreach (light : pointLights)radiance += calcPoint(surfaceInfo, rayDir, light);foreach (light : spotLights)radiance += calcSpot(surfaceInfo, rayDir, light);foreach (light : reflectionVolumes)radiance += calcReflVol(surfaceInfo, rayDir, light);(...)}
然而这种简单的光追管线渲染出来的画质存在噪点、低效、光线贡献较少等问题:
文章插图
可以改进管线 , 在生成射线时加入可变速率追踪:
文章插图
可变速率追踪的过程如下:
文章插图
可变速率追踪使得水上、掠射角有更多光线 。但依然存在问题:
文章插图
可以加入Ray (光线箱化),将屏幕偏移和角度作为bin的索引 。
文章插图
文章插图
文章插图
文章插图
依次可以加入SSR混合(SSR )、碎片整理()、逐单元格光源列表光照、降噪(BRDF降噪、时间降噪)等优化 。
文章插图
SSR 的过程和结果 。
文章插图
逐单元格光源列表光照 。
文章插图
BRDF降噪过程 。
最终形成的新管线和时间消耗如下:
文章插图
渲染效果:
文章插图
DXR基?。?
文章插图
DXR的性能优化包含减少实例数、使用剔除启发法、接受(一些)小瑕疵 。剔除启发法假设远处的物体并不重要,除了桥梁、建筑等大型物体物,需要一些测量 。投影球体包围盒,如果θθ小于某个阈值,则剔除:
文章插图
不同阈值的效果:
文章插图
剔除结果是:使用4度剔除,每帧5000->400 BLAS、20000->2800个TLAS实例的重建 , TLAS+BLAS构建(GPU)从64毫秒降到14.5毫秒,但引入了偶尔跳变及物体丢失等瑕疵 。
BLAS更新依旧开销大,可以采用以下方法优化:
TLAS+构建耗时从14.5毫秒降低到1.15毫秒,(GPU)从0.71毫秒降低0.81毫秒(使用交错重建+标志) 。
不透明物体应该总是使用着色器,仅对Alpha 物体使用Any Hit着色器 , 对蒙皮、破坏使用计算着色器 。
射线有效载荷(RAY )在ray交点出返回,与 RTV的格式相同 , 包含材质数据、法线、基础色、平滑度等 。
struct GbufferPayloadPacked{uint data0; // R10G10B10A2_UNORMuint data1; // R8G8B8A8_SRGBuint data2; // R8G8B8A8_UNORMuint data3; // R11G11B10_FLOATfloat hitT; // Ray length};
还可以验证正确性,即光栅化输出,向场景中发射主要光线,将有效载荷与进行比较,如果是非零输出 , 则有bug!需要修正错误 。
文章插图
是Intel开发的光线追踪开源库,其核心特点是:
它的技术上的特点是:
硬件方面的优化实现 。
支持的特性如下所示:
文章插图
其系统概览如下:
文章插图
它已成功在World Of Tank等游戏中应用 。
利用GPU硬件加速的光线追踪步骤和图例如下:
文章插图
基于现代光栅化游戏引擎的光追实现流程如下:
文章插图
结合信息之后,由此产生了混合渲染管线:
文章插图
下面是光栅化和光追的效果对比图:
文章插图
在2021年11月,Tech发布了IMG CXT系列及其突出功能: 架构,提供超高效的混合光线追踪,可提供7nm、5nm甚至3nm工艺设计 。其特性包括基于贴图的延迟渲染、专用图像压缩、超宽ALU、超标量ALU处理、广泛的异步机制、基于固件的GPU、去中心化的多核等硬核技术 。其中该架构添加了并发异步光线追踪 , 意味着CXT GPU现在可以有多达五种不同的任务类型在GPU内并发执行:几何、片段/像素、计算、2D和光线追踪 。
文章插图
上图中可以看到IMG CXT GPU的高级视图 。GPU的主要组件包括:
文章插图
与B系列GPU类似,CXT GPU也具有多核能力,可扩展到四个核 。在上述“超越桌面”配置中,设计还包括额外的可选IP块:
从3D的早期开始,传统的渲染就使用光栅化进行,即使用三角形网格构建对象的几何体,然后“着色”以创建其外观 。然而,通过光栅化,世界的照明方式只能近似 。光线追踪是不同的,它模拟了光在真实世界中的工作方式 , 其中光子从光源发射并在场景周围反弹 , 直到到达观看者的眼睛 。光线追踪将光线从观察者(屏幕)发送到场景、对象上,并从那里发送到光源 。当灯光与对象交互时,它会被对象阻挡、反射或折射 , 这取决于其材质属性,从而创建阴影和反射,甚至是屏幕外对象 。一旦光线射入场景,照明过程自然发生,意味着开发人员不必花费时间创建“假”照明效果 。这种优雅的照明场景方法有助于提供更逼真的图形,改善游戏和视觉应用程序,同时简化内容创建者的照明过程 。
根据不同的级别 , 存在6种光线追踪级别系统(Ray,RTLS):
文章插图
对于Level 2 , 添加长方体/三角形测试器:
文章插图
对于级别3,是全硬件的BVH遍历:
文章插图
对于 , 支持Level 4 RTLS 。体系架构旨在实现智能手机功率和带宽预算中的光线追踪,还允许将这种效率扩展到移动以外的市场 。光线追踪的核心问题是缺乏一致性,因为射线可以、也会引入随机方向,会与传统GPU中设计的并行性相冲突 。解决此问题的最佳解决方案是关注工作负载,为此,引入了一致性收集单元 。
文章插图
有了这个单元,BVH行走仍然是完全卸载的,但它现在变成了一个调度问题 。可以存储许多射线,然后相干单元将射线分组成类似的包或束,例如,通过BVH加速结构的类似路径的射线——这些被称为“相干” 。虽然它们从一条射线到下一条射线可能是非相干的,但在多条射线上求平均值时,总是可以利用相似性和相关性,这正是 体系结构所做的 。
在 中,光线被分组成处理包,不仅在处理中,而且在存储器访问中都将实现高效率 。这种排序给了我们另一个好处:与MIMD架构不同,返回到GPU内部常见的高效处理方法:许多单元都做相同的事情 。
因此,可以利用并行性,因为不只是针对一个方框检查一条光线,可以针对同一方框检查多条光线 。此举带来了显著的效率提高,并减少了对缓存和内存子系统的压力 。对于三角形交点也是如此:可以同时针对多个三角形检查光线 。
因此,架构有四个基本好处:
下表是不同的级别和对应设计的特性支持情况:
Level4
2020 game
2021
CXT
ALU
Full
Full
HW Box
HW
HW BVH
HWSort
Cache Hit Rate
Low
Low/
High
Low
Low
High
Low(SIMT )
Low(MIMD)
High
Power
早在1996年就开创了基于分块的延迟渲染(TBDR) 。TBDR的重点是处理效率和带宽 。基于分块的渲染通过在渲染之前将所有三角形几何体排序到屏幕空间平铺区域中来实现 。这不同于即时模式渲染(IMR),其中每个三角形都被变换并立即绘制 。对所有几何体进行排序,然后按屏幕空间分块区域(通常为16x16或32x32像素)进行渲染的好处是,可以仅使用用于深度/模板缓冲区和颜色缓冲区的片上内存来完成分块区域的渲染 。IMR将所有这些带宽推离芯片 , 并依赖缓存命中来减少带宽,但由于几何体提交在屏幕空间中的空间不一致,这种缓存方法通常会失败,导致高带宽、延迟敏感性和低功率效率 。
因此,通过首先对几何体进行排序,缓存命中率实际上变为100% 。此外,深度和模板缓冲区通常只使用一次,因此可以丢弃 。使用和MRT渲染,许多MRT“颜色”目标仅用于中间暂存数据,只需要将一个颜色缓冲区写入内存 。使用TBDR,所有这些都可以在芯片上完成,节省内存占用和大量带宽 。TBDR在处理抗锯齿方面也具有显著优势 。由于过采样缓冲区仅存在于片上存储器中,因此仅写入下采样颜色目标,再次节省了内存占用和带宽 。
光线追踪体系结构在许多方面与 TBDR体系结构相同,因为还进行了空间排序,只是将光线分成沿类似路径通过BVH的包,而不是在2D屏幕空间中 。这里的好处与一致性排序类似——显著的缓存效率和减少的带宽,同时处理保持SIMD/SIMT性质,确保逻辑和整体处理的高功率效率 。
体系结构在 GPU中添加了一个新块,称为光线加速集簇(RAC) , 负责 GPU上的所有光线追踪活动,包括整个过程:从发射光线(从着色器/内核)到将命中(或未命中)结果返回给ALU进行处理 。
文章插图
当光线由图形着色器或计算内核程序生成并处理结果时,RAC与GPU的ALU引擎紧密耦合 。虽然这些装置与交换射线和命中/未命中信息密切相关,但它们在技术上完全“解耦”,意味着两个装置同时运行 , 以实现最高的效率和利用率 。RAC有效地处理整个BVH遍历,包括计算非常密集的盒/射线和三角形射线交叉,以及效率优化 , 如相干排序 。RAC与当前光线追踪API公开的所有模式和功能完全兼容,包括 ?扩展和 光线追踪 。
RAC是一个可扩展单元,支持多个性能点(例如,RAC的1x、0.5x、0.25x)以及多核可扩展性(2x及以上),其中多个RAC可以放置在ALU单元旁边 。在当前的 GPU设计中,RAC由两个128宽的ALU单元共享,从而提高了RAC、ALU和纹理处理单元(TPU)的利用率 。具有调度逻辑和其他固定功能支持的RAC、两个ALU和两个TPU单元的组合称为可扩展处理单元(SPU) 。这些构成了构建CXT GPU系列的基本单元,从每个GPU核心一个到四个SPU单元,然后由于分散多核系统 , 可以进一步扩展 。
下表总结了不同级别及对执行效率的影响,以及由此产生的对功率、性能和带宽的影响 。
GPU Block Ray1234 RTLS
ALU
Full
High
Low
Low
ALU
Low
Low
High
Box/Tri
N/A
High
Full
BVH
Yes
Yes
Yes
Yes
No
No
No
Yes
Cache Hits
Low
Low
Low/
High
Usage
High
High
Low
Power
Very Low
Low
High
光线查询也称为 光线追踪(DXR)下的内联光线追踪,非常容易理解,因为本质上任何着色器或内核(计算)都可以发出光线查询,该查询将启动整个光线追踪过程 。在该系统中 , 生成的命中/未命中信息返回到必须处理它的同一着色器/内核 。因此,光线追踪非常简单 , 根据DXR名称样式 , 它实际上是一个内联过程 。
一个简单的例子就是阴影光线 。在这里 , 场景被渲染为正常,但现在在片段/像素着色器中,光线朝光源发射,当光源被击中时,我们知道当前像素被照亮,可以在着色器中执行正确的代码 。如果击中场景中的任何其他对象,可以知道它在阴影中,并且再次,可以在着色器中执行正确的代码 。在该方案中 , 反射将更加困难,因为当反射对象被击中时,必须触发大量复杂度,以确定如何为该反射对象渲染正确的颜色,而这一切都必须在原始投射着色器中处理 。
文章插图
对于大多数初始渲染算法,将推荐使用光线查询,更容易添加到现有游戏引擎中,并且也可能在实现中提供更可预测的性能 。
文章插图
参考了加速结构和边界体积层次结构,用来剔除光线盒和光线三角形测试数量的高级结构,如下所示:
文章插图
如图所示,边界体积层次结构提供了一种加速机制 , 可以系统地检查边界框,如果遗漏了一个框,我们知道可以忽略该级别下的所有框/三角形 。这使得它成为一种加速结构,将射线测试过程尽可能减少到最小 。这种结构以及在其创建中使用的质量和启发式方法,将对硬件的效率产生重大影响 , 因为最佳结构可以比简单、构造差的结构更有效地减少工作量 。因此,API公开了生成此加速结构的快速和慢速方法 。
快速构建算法对于被动画化并在帧与帧之间广泛变化以保持高帧速率的对象至关重要 。对于静态对象,应在加载时(甚至在开发期间离线)使用慢速构建方法,静态对象将在其整个生命周期中使用,因此应尽可能优化 。它们由两个元素组成,一个顶层加速结构(TLAS)和多个底层加速结构(BLAS) 。上面描述的更多的是BLAS,因为它包含一个对象的加速度结构 , 例如示例中的兔子,而TLAS由多个BLAS结构组成 。
构建加速结构的步骤如下所示:
文章插图
在进入RAC之前,GPU内部需要各种其他处理步骤,对于使用光线查询的混合渲染工作负载,可以总结如下:
应用程序通过发出API调用来渲染场景,这些API调用由GPU驱动程序在内存中构造命令缓冲区和数据结构(纹理、着色器、缓冲区)来处理 。驱动程序还将启动硬件,可能会将其从节能模式中唤醒,或者只是标记有更多的工作可供处理 。此触发触发嵌入式固件处理器,该处理器将处理所有内部活动管理 , 并确保所有作业遵守设置的优先级 。
典型的首先要做的是启动几何处理,意味着绘制调用将成为GPU内的任务,每个任务都在GPU内进行调度 , 并旨在在USC内保留所需的资源进行处理 。然后将提取顶点/几何体数据,当数据可用时,任务变为活动状态并执行着色器程序 。这将生成输出几何图形,然后输出几何图形将命中一系列固定功能块,如剔除、剪裁、平铺和几何图形压缩,然后将中间参数数据写入内存 。
该参数数据是每个分片的几何体链接列表,在每个分片中都可能可见 , 从而使基于分块的延迟渲染发挥其魔力 。所有这些工作都是处理的第一阶段,通常将其称为几何阶段或分块加速器(TA)阶段 。此阶段与下一个渲染阶段同时运行 。
文章插图
基于分块的延迟渲染架构中的3D处理从HSR开始 。所有的3D处理都是一块一块地完成的,意味着使用参数数据链表结构获取位置数据 。对于分块深度/模板内的所有几何数据,执行测试,在标记缓冲区内生成可见性列表 , 该列表指示每个像素的可见对象 。一旦处理了所有几何体,就有了按像素标记的可见性列表,从逻辑上讲,它是一个单一的不透明对象(因为它后面的所有东西都将被隐藏/移除),并且在不透明对象前面有几个Alpha混合层 。
然后按正确的深度顺序开始渲染,并按每个着色器进行排序,每个着色器代表一个任务 。任务处理意味着,首先,调度器在USC内保留所需的资源进行处理,然后在任务变为活动状态并执行正确的着色器程序指令之前预取任务和数据 。如果任务中的着色器程序包含光线查询调用,则将在此处触发RAC 。
对于具有光线查询调用的着色器,该任务不仅将请求USC资源 , 还将请求RAC资源 。当着色器使用USC/ray接口(URI)将所需的光线信息发射到RAC时,执行实际光线追踪 , 并且该信息存储在光线存储中 。
与纹理操作类似,在将所需光线信息传输到RAC之后,USC将将任务置于非计划等待状态 , 意味着在RAC执行其工作时,USC会开始处理其他任务/作业 。可以想象 , 所有这些工作都是大规模并行的,因为不仅仅处理一个片段/工作项或射线,而是在每个任务(warp)中并行处理多个线程 。硬件还将执行许多此类任务,以确保延迟吸收和高利用率 。RAC将有效地存储许多需要处理的射线 。
此时,光线参考计数器会追踪每条光线,该计数器会随着所需的每次测试而增加 。根据加速度结构,这些测试从一开始 , 随着更多的盒子相交而增加,从而触发更多的盒子测试 。射线处理在相干组中进行 , 意味着分组相干收集块将扫描射线,以构建相干地穿过结构的射线分组 。当数据包填满时 , 它们将被执行,根据需要运行射线穿过盒子和/或三角形和/或基本测试仪 。此处理通过专用加速结构缓存(ASC)运行,确保数据也在数据包中重复使用 。
当然,ASC只是一个缓存级别 。进一步的缓存将在整个GPU内存层次结构中发生,包括最大的SLC缓存级别,甚至可能是SoC级别的系统级缓存 。当该处理完成时,射线参考计数器(RRC)将随着测试的调度和完成而递增和递减,直到当参考计数达到零并且射线的结果准备就绪时,处理结束 。
此时,一条或多条光线将被调度为将控制权返回给USC进行进一步的着色器处理,意味着USC任务将恢复 。然后,USC可以通过URI从为所有处理保留了资源的光线存储读取生成的光线数据 。
在这个阶段 , 着色器的处理将继续正常进行,直到通过执行具有和不具有光线查询的着色器/内核的混合来完全绘制分块 。在此过程中 , 其他固定功能块(如纹理处理单元)将用于执行着色器 。
重要的是要认识到,此时的执行是许多任务的混合:几何体将在处理 , 计算任务可能在运行,RAC将追踪光线并查找命中/未命中,而着色器核心将执行代码作为所有这些操作的一部分 。2D和内务任务也可以用于复制数据或生成 。对于如此多样的作业,目标是在所有处理单元中获得最大效率,并确保任何处理任务和内存访问的延迟通过处理其他独立任务完全隐藏 。
一旦分块完成,将触发像素后端,将完成的分块写入内存,可能使用想象图像压缩(IMGIC)帧缓冲区压缩 。
光线追踪时隐藏的一致性
虽然光线追踪在本质上是“令人尴尬的平行”,但实时光线追踪之所以花了这么长时间才变得实用,原因之一是 , 尽管存在并行性,但它通常是发散的和非相干的 。可以从下图中加以理解 。
文章插图
在现实世界中,材质具有不同的属性——有些是平滑的 , 但大多数是粗糙的——因此,对于真实曲面,光线不会以相同的方式反射,而是在不同的方向上反弹 。结果是发散 , 例如光线从一个像素反弹到下一个像素 , 光线沿不同方向传播 。因此,光线将沿着不同的路径穿过BVH框,从而导致不同的内存访问,从逻辑上讲,沿不同方向传播的光线也将与不同的三角形相交,从而触发不同的着色器程序,从而导致着色器执行的差异 。
发散对GPU是不利的,因为尽管它们非常擅长处理高度并行的工作负载,但它们的SIMD架构只有在这些工作负载一致且相似的情况下才有意义 。如果每个像素都想做一些不同的事情,那么GPU所依赖的高执行和带宽效率的技巧就会失败 。意味着最终会采用暴力方法(即使用大量ALU和光线追踪单元),需要在处理流程难以有效使用它们时进行补偿(即尽管理论上的峰值吞吐量很高 , 但在实际使用中,低利用率会导致低吞吐量) 。
然而,虽然从一个像素到下一个像素的光线可能是发散的 , 但并不意味着在四处反弹的光线束之间没有“相干” 。同样,这在下图中得到了最好的说明 。下面的反射形状显示了从该对象反射的光线中隐藏的相干 。例如,你可以看到穿黄色衣服的人被多次反射 , 意味着这些光线进入同一方向,实际上是相干的 。更重要的是,如果我们能将这些光线分组,它们将沿着类似的路径通过BVH,为我们提供高速缓存命中率和数据重用率 。它们也将最终命中并与相同的三角形相交,并且可能还执行相同或类似的着色器程序,从而在传统的并行GPU ALU管线中提供高效率 。
文章插图
大约10年前,多通道光栅化达到了临界点,对于艺术家来说,迭代时间长,工作流程笨拙,从可视性角度渲染瑕疵近似值 , 预烘焙和缓存照明通常有效…直到它不起作用,无法按预期准确模拟光照传输 。采用路径追踪——处理一切的统一光照传输算法 , 图元包含曲面、头发、体积测量…反射包含所有类型的BSDF、…灯光包含点光源、区域光源、环境图光源…
方差的概念和公式:
文章插图
所有采样技术都基于将随机数从单位平方扭曲到其它域,再到半球、球体、球体周围的圆锥体 , 再到圆盘 。还可以根据BSDF的散射分布生成采样,或选择IBL光源的方向 。有许许多多的采样方式 , 但它们都是从0到1之间的值开始的,其中有一个很好的正交性:有“你开始的那些值是什么”,然后有“你如何将它们扭曲到你想要采样的东西的分布 , 以使用第二个蒙特卡罗估计” 。
文章插图
对应采样方式 , 常用的有均匀、低差异序列、分层采样、元素区间、蓝噪点抖动等方式 。低差异类似广义分层,蓝色噪点类似不同样本之间的距离有多近 。过程化模式可以使用任意数量的前缀,并且(某些)前缀分布均匀 。
文章插图
方差驱动的采样——根据迄今为止采集的样本,周期地估计每个像素的方差,在差异较大的地方多采样,更好的做法是在方差/估计值较高的地方进行更多采样,在色调映射等之后执行此操作 。离线(质量驱动):一旦像素的方差足够低 , 就停止处理它 。实时(帧率驱动):在方差最大的地方采集更多样本 。计算样本方差(重要提示:样本方差是对真实方差的估计):
float SampleVariance(float samples[], int n) {float sum = 0, sum_sq = 0;for (int i=0; i
样本方差只是一个估计值,大量的工作都是为了降噪 , MC渲染自适应采样和重建的最新进展 。总体思路:在附近像素处加入样本方差,可能根据辅助特征(位置、法线等)的接近程度进行加权 。高方差是个诅咒,一旦引入了一个高方差样本 , 就会有大麻烦了,例如考虑对数据进行均匀采样:
f(x)={
s
ne
ight
ses
对应的截帧如下:
文章插图
由于无法截取硬件光线追踪的详情,博主本想通过 PIX截帧 , 但发现PIX有BUG(也可能是驱动或UE的),无法正常截取UE5.0.3,截帧数据非常不完整:
文章插图
如果想在UE5中启用PIX截帧调试,可参阅:
光追相关的步骤简要说明如下:
:
ne:
:
ight:
:
:
:
如果需要降噪,则对路径追踪结果执行降噪 。
执行全屏绘制,以显示路径追踪结果 。
:
主要的流程和步骤已经阐述完毕 。下面小节将对部分重要的特性进行剖析 。
17.6.4 UE光追光影 17.6.4.1
光追光影的渲染过程集成在了::中:
// LightRendering.cppvoid FDeferredShadingSceneRenderer::RenderLights(FRDGBuilder& GraphBuilder, ...){(...)// 非合批光源,处理RHI预处理阴影遮罩纹理PreprocessedShadowMaskTextures 。if (RHI_RAYTRACING && bDoShadowBatching){(...)// 分配PreprocessedShadowMaskTexturesif (!View.bStatePrevViewInfoIsReadOnly){View.ViewState->PrevFrameViewInfo.ShadowHistories.Empty();View.ViewState->PrevFrameViewInfo.ShadowHistories.Reserve(SortedLights.Num());}PreprocessedShadowMaskTextures.SetNum(SortedLights.Num());(...)}(...)for (int32 LightIndex = UnbatchedLightStart; LightIndex < SortedLights.Num(); LightIndex++){(...)// 确定此灯光是否还没有预处理阴影 , 如果需要 , 执行批处理以摊销成本.if (RHI_RAYTRACING && bWantsBatchedShadow && (PreprocessedShadowMaskTextures.Num() == 0 || !PreprocessedShadowMaskTextures[LightIndex - UnbatchedLightStart])){(...)// 处理降噪批次.const auto QuickOffDenoisingBatch = [&]{(...)TStaticArray Outputs;// 降噪阴影遮罩纹理.DenoiserToUse->DenoiseShadowVisibilityMasks(GraphBuilder, View, ...);for (int32 i = 0; i < InputParameterCount; i++){const FLightSceneInfo* LocalLightSceneInfo = DenoisingQueue[i].LightSceneInfo;int32 LocalLightIndex = LightIndices[i];FRDGTextureRef& RefDestination = PreprocessedShadowMaskTextures[LocalLightIndex - UnbatchedLightStart];check(RefDestination == nullptr);RefDestination = Outputs[i].Mask;DenoisingQueue[i].LightSceneInfo = nullptr;}};// 光线追踪需要的光线阴影,并快速关闭降噪批次 。for (int32 LightBatchIndex = LightIndex; LightBatchIndex < SortedLights.Num(); LightBatchIndex++){const FSortedLightSceneInfo& BatchSortedLightInfo = SortedLights[LightBatchIndex];const FLightSceneInfo& BatchLightSceneInfo = *BatchSortedLightInfo.LightSceneInfo;// 降噪器不支持纹理矩形光源的重要性采样 。const bool bBatchDrawShadows = BatchSortedLightInfo.SortKey.Fields.bShadowed;(...)// 如果降噪器不支持此光线追踪配置,则不值得进行批处理并增加内存压力 。if (bRequiresDenoiser && DenoiserRequirements != IScreenSpaceDenoiser::EShadowRequirements::PenumbraAndClosestOccluder){continue;}(...)// 执行光线追踪阴影.FRDGTextureUAV* RayHitDistanceUAV = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(RayDistanceTexture));{// 光线追踪不透明几何体投射到发丝几何体上的阴影. 注意:此输出不需要降噪器,因为发丝具有几何噪声,因此很难降噪.RenderRayTracingShadows(GraphBuilder, SceneTextureParameters, View, BatchLightSceneInfo, BatchRayTracingConfig, DenoiserRequirements, LightingChannelsTexture, RayTracingShadowMaskUAV, RayHitDistanceUAV, SubPixelRayTracingShadowMaskUAV);(...)}bool bBatchFull = false;// 将光线追踪从排队取出以对阴影降噪 。if (bRequiresDenoiser){for (int32 i = 0; i < IScreenSpaceDenoiser::kMaxBatchSize; i++){if (DenoisingQueue[i].LightSceneInfo == nullptr){DenoisingQueue[i].LightSceneInfo = &BatchLightSceneInfo;DenoisingQueue[i].RayTracingConfig = RayTracingConfig;DenoisingQueue[i].InputTextures.Mask = RayTracingShadowMaskTexture;DenoisingQueue[i].InputTextures.ClosestOccluder = RayDistanceTexture;LightIndices[i] = LightBatchIndex;// 如果此灯类型的队列已满,则快速批处理 。if ((i + 1) == MaxDenoisingBatchSize){QuickOffDenoisingBatch();bBatchFull = true;}break;}else{check((i - 1) < IScreenSpaceDenoiser::kMaxBatchSize);}}}else // 不需要降噪, 直接存到处理预处理阴影遮罩纹理数组中.{PreprocessedShadowMaskTextures[LightBatchIndex - UnbatchedLightStart] = RayTracingShadowMaskTexture;}// 如果填充的降噪批次或达到最大光批次 , 则终止批次.ProcessShadows++;if (bBatchFull || ProcessShadows == MaxRTShadowBatchSize){break;}}// 确保处理所有降噪队列 。if (DenoisingQueue[0].LightSceneInfo){QuickOffDenoisingBatch();}}(...)}(...)}
对光追阴影补充以下几点说明:
17.6.4.2 ows
本小节阐述光追阴影的具体过程 。
// RayTracingShadows.cppvoid FDeferredShadingSceneRenderer::RenderRayTracingShadows(FRDGBuilder& GraphBuilder, ...)#if RHI_RAYTRACING{FLightSceneProxy* LightSceneProxy = LightSceneInfo.Proxy;(...)// 阴影遮挡的光线生成Pass 。{(...)// 填充FOcclusionRGS参数 。FOcclusionRGS::FParameters* PassParameters = GraphBuilder.AllocParameters();PassParameters->RWOcclusionMaskUAV = OutShadowMaskUAV;PassParameters->RWRayDistanceUAV = OutRayHitDistanceUAV;PassParameters->RWSubPixelOcclusionMaskUAV = SubPixelRayTracingShadowMaskUAV;PassParameters->SamplesPerPixel = RayTracingConfig.RayCountPerPixel;PassParameters->NormalBias = GetRaytracingMaxNormalBias();PassParameters->LightingChannelMask = LightSceneProxy->GetLightingChannelMask();(...)// FOcclusionRGS的shader 。TShaderMapRef RayGenerationShader(GetGlobalShaderMap(FeatureLevel), PermutationVector);// 清理无用的RDG资源 。ClearUnusedGraphResources(RayGenerationShader, PassParameters);(...)// 增加RayTracedShadow的通道 。GraphBuilder.AddPass(RDG_EVENT_NAME("RayTracedShadow (spp=%d) %dx%d", RayTracingConfig.RayCountPerPixel, Resolution.X, Resolution.Y),PassParameters,// Pass标记是Compute 。ERDGPassFlags::Compute,[this, &View, RayGenerationShader, PassParameters, Resolution](FRHIRayTracingCommandList& RHICmdList){FRayTracingShaderBindingsWriter GlobalResources;SetShaderParameters(GlobalResources, RayGenerationShader, *PassParameters);FRHIRayTracingScene* RayTracingSceneRHI = View.GetRayTracingSceneChecked();// 启用光追材质.if (GRayTracingShadowsEnableMaterials){// 向RHI派发光追命令.RHICmdList.RayTraceDispatch(View.RayTracingMaterialPipeline, RayGenerationShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, Resolution.X, Resolution.Y);}// 不启用光追材质.else {// 初始化光追管线状态.FRayTracingPipelineStateInitializer Initializer;Initializer.MaxPayloadSizeInBytes = RAY_TRACING_MAX_ALLOWED_PAYLOAD_SIZE; FRHIRayTracingShader* RayGenShaderTable[] = { RayGenerationShader.GetRayTracingShader() };Initializer.SetRayGenShaderTable(RayGenShaderTable);FRHIRayTracingShader* HitGroupTable[] = { View.ShaderMap->GetShader().GetRayTracingShader() };Initializer.SetHitGroupTable(HitGroupTable);// 禁用SBT索引,以便对场景中的所有几何体使用相同的命中着色器 。Initializer.bAllowHitGroupIndexing = false; FRayTracingPipelineState* Pipeline = PipelineStateCache::GetAndOrCreateRayTracingPipelineState(RHICmdList, Initializer);// 向RHI派发光追命令.RHICmdList.RayTraceDispatch(Pipeline, RayGenerationShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, Resolution.X, Resolution.Y);}});}}
光追阴影的着色器是,其对应的文件是GS.usf 。下面对其进行分析:
// RayTracingOcclusionRGS.usfRAY_TRACING_ENTRY_RAYGEN(OcclusionRGS){uint2 PixelCoord = DispatchRaysIndex().xy + View.ViewRectMin.xy + PixelOffset;FOcclusionResult Occlusion = InitOcclusionResult();FOcclusionResult HairOcclusion = InitOcclusionResult();const uint RequestedSamplePerPixel = ENABLE_MULTIPLE_SAMPLES_PER_PIXEL ? SamplesPerPixel : 1;uint LocalSamplesPerPixel = RequestedSamplePerPixel;if (all(PixelCoord >= LightScissor.xy) && all(PixelCoord <= LightScissor.zw)) // 确保不越界{// 随机序列.RandomSequence RandSequence;uint LinearIndex = CalcLinearIndex(PixelCoord);RandomSequence_Initialize(RandSequence, LinearIndex, View.StateFrameIndex);FLightShaderParameters LightParameters = GetRootLightShaderParameters(PrimaryView.PreViewTranslation);// 获取GBuffer数据.float2 InvBufferSize = View.BufferSizeAndInvSize.zw;float2 BufferUV = (float2(PixelCoord) + 0.5) * InvBufferSize;float3 WorldNormal = 0;uint ShadingModelID = SHADINGMODELID_UNLIT;(...)// 屏蔽掉无限远的深度值.float DeviceZ = SceneDepthTexture.Load(int3(PixelCoord, 0)).r;const bool bIsDepthValid = SceneDepthTexture.Load(int3(PixelCoord, 0)).r > 0.0;const bool bIsValidPixel = ShadingModelID != SHADINGMODELID_UNLIT && bIsDepthValid;const uint LightChannel = GetSceneLightingChannel(PixelCoord);const bool bTraceRay = bIsValidPixel && (LightChannel & LightingChannelMask) != 0;if (!bTraceRay){LocalSamplesPerPixel = 0;}(...)// 计算遮挡.Occlusion = ComputeOcclusion(PixelCoord, ShadingModelID, RAY_TRACING_MASK_SHADOW | RAY_TRACING_MASK_THIN_SHADOW, DeviceZ, WorldNormal, LightParameters, TransmissionProfileParams, LocalSamplesPerPixel);(...)}(...)// 计算遮挡到阴影.const float Shadow = OcclusionToShadow(Occlusion, LocalSamplesPerPixel);// 根据不同的降噪输出维度, 保存不同的结果. if (DIM_DENOISER_OUTPUT == 2){RWOcclusionMaskUAV[PixelCoord] = float4(Shadow, Occlusion.ClosestRayDistance, 0, Occlusion.TransmissionDistance);}else if (DIM_DENOISER_OUTPUT == 1){float AvgHitDistance = -1.0;if (Occlusion.HitCount > 0.0){AvgHitDistance = Occlusion.SumRayDistance / Occlusion.HitCount;}else if (Occlusion.RayCount > 0.0){AvgHitDistance = 1.0e27;}RWOcclusionMaskUAV[PixelCoord] = float4(Shadow, Occlusion.TransmissionDistance, Shadow, Occlusion.TransmissionDistance);RWRayDistanceUAV[PixelCoord] = AvgHitDistance;}else{const float ShadowFadeFraction = 1;float SSSTransmission = Occlusion.TransmissionDistance;// 0为阴影 , 1为非阴影,除非写入SceneColor,否则不需要RETURN_COLOR.float FadedShadow = lerp(1.0f, Square(Shadow), ShadowFadeFraction);float FadedSSSShadow = lerp(1.0f, Square(SSSTransmission), ShadowFadeFraction);// 通道指定记录在ShadowRendering.cpp(寻找光衰减信道分配).float4 OutColor;if (LIGHT_TYPE == LIGHT_TYPE_DIRECTIONAL){OutColor = EncodeLightAttenuation(half4(FadedShadow, FadedSSSShadow, 1.0, FadedSSSShadow));}else{OutColor = EncodeLightAttenuation(half4(FadedShadow, FadedSSSShadow, FadedShadow, FadedSSSShadow));}RWOcclusionMaskUAV[PixelCoord] = OutColor;}}
上述涉及了几个重要的函数,继续分析之:
// 遮挡变成阴影,就是可见样本/总样本 。float OcclusionToShadow(FOcclusionResult In, uint LocalSamplesPerPixel){return (LocalSamplesPerPixel > 0) ? In.Visibility / LocalSamplesPerPixel : In.Visibility;}// 计算光线遮挡.FOcclusionResult ComputeOcclusion(...){FOcclusionResult Out = InitOcclusionResult();const float3 WorldPosition = ReconstructWorldPositionFromDeviceZ(PixelCoord, DeviceZ);(...)uint TimeSeed = View.StateFrameIndex;// 根据不同的样本数量进行可见性测试.#if ENABLE_MULTIPLE_SAMPLES_PER_PIXELLOOP for (uint SampleIndex = 0; SampleIndex < LocalSamplesPerPixel; ++SampleIndex)#else do if (LocalSamplesPerPixel > 0)#endif{// 处理随机序列.RandomSequence RandSequence;#if ENABLE_MULTIPLE_SAMPLES_PER_PIXELRandomSequence_Initialize(RandSequence, PixelCoord, SampleIndex, TimeSeed, LocalSamplesPerPixel);#elseRandomSequence_Initialize(RandSequence, PixelCoord, 0, TimeSeed, 1);#endiffloat2 RandSample = RandomSequence_GenerateSample2D(RandSequence);// 生成光线.RayDesc Ray;bool bIsValidRay = GenerateOcclusionRay(LightParameters, ...);uint Stencil = SceneStencilTexture.Load(int3(PixelCoord, 0)) STENCIL_COMPONENT_SWIZZLE;bool bDitheredLODFadingOut = Stencil & 1;(...)BRANCHif (!bIsValidRay && (DIM_DENOISER_OUTPUT == 0)){// 降噪器仍然必须追踪无效的光线,以获得正确的最近命中距离.continue;}else if (bApplyNormalCulling && dot(WorldNormal, Ray.Direction) <= 0.0){continue;}// 衰减检测.if (LightParameters.InvRadius > 0.0){const float MaxAttenuationDistance = 1.0 / LightParameters.InvRadius;if (Ray.TMax > MaxAttenuationDistance){continue;}}uint RayFlags = 0;// 如果不在双面阴影投射模式中使用,则启用背面剔除 。if (bTwoSidedGeometry != 1){RayFlags |= RAY_FLAG_CULL_BACK_FACING_TRIANGLES;}(...)uint RayFlagsForOpaque = bAcceptFirstHit != 0 ? RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH : 0;// 追踪可见光线.FMinimalPayload MinimalPayload = TraceVisibilityRay(TLAS, RayFlags | RayFlagsForOpaque, RaytracingMask, PixelCoord, Ray);(...)Out.RayCount += 1.0;// 有命中物体.if (MinimalPayload.IsHit()){float HitT = MinimalPayload.HitT;Out.ClosestRayDistance = (Out.ClosestRayDistance == DENOISER_INVALID_HIT_DISTANCE) || (HitT < Out.ClosestRayDistance) ? HitT : Out.ClosestRayDistance;Out.SumRayDistance += HitT;Out.HitCount += 1.0;if (ShadingModelID == SHADINGMODELID_SUBSURFACE || ShadingModelID == SHADINGMODELID_HAIR){(...)}}// 未命中物体.else{Out.ClosestRayDistance = (Out.ClosestRayDistance == DENOISER_INVALID_HIT_DISTANCE) ? DENOISER_MISS_HIT_DISTANCE : Out.ClosestRayDistance;Out.TransmissionDistance += 1.0;Out.Visibility += 1.0;}}(...)// 输出结果.if (ENABLE_TRANSMISSION && LocalSamplesPerPixel > 0 && ShadingModelID == SHADINGMODELID_SUBSURFACE_PROFILE){(...)}else if (ShadingModelID == SHADINGMODELID_SUBSURFACE || ShadingModelID == SHADINGMODELID_HAIR){(...)}else{Out.TransmissionDistance = (LocalSamplesPerPixel > 0) ? Out.Visibility / LocalSamplesPerPixel : Out.Visibility;}return Out;}
下面对生成随机序列、生成光线、追踪可见光线进行分析:
// PathTracingRandomSequence.ush// 生成二维随机序列 。float2 RandomSequence_GenerateSample2D(inout RandomSequence RandSequence){float2 Result;// 纯随机.#if RANDSEQ == RANDSEQ_PURERANDOMResult.x = Rand(RandSequence.SampleSeed);Result.y = Rand(RandSequence.SampleSeed);// Halton随机序列.#elif RANDSEQ == RANDSEQ_HALTONResult.x = Halton(RandSequence.SampleIndex, Prime512(RandSequence.SampleSeed + 0));Result.y = Halton(RandSequence.SampleIndex, Prime512(RandSequence.SampleSeed + 1));RandSequence.SampleSeed += 2;// Sobol随机序列.#elif RANDSEQ == RANDSEQ_OWENSOBOLResult = SobolSampler(RandSequence.SampleIndex, RandSequence.SampleSeed).xy;#endifreturn Result;}// RayTracingDirectionalLight.ush// 生成平行光遮挡光线.void GenerateDirectionalLightOcclusionRay(...){// 绘制随机变量并在单位圆盘上选择一个点.float2 BufferSize = View.BufferSizeAndInvSize.xy;float2 DiskUV = UniformSampleDiskConcentric(RandSample) * LightParameters.SourceRadius;// 在单位球体上按用户定义的半径排列灯光方向.float3 LightDirection = LightParameters.Direction;float3 N = LightDirection;float3 dPdu = float3(1, 0, 0);if (dot(N, dPdu) != 0){dPdu = cross(N, dPdu);}else{dPdu = cross(N, float3(0, 1, 0));}float3 dPdv = cross(dPdu, N);LightDirection += dPdu * DiskUV.x + dPdv * DiskUV.y;RayOrigin = WorldPosition;RayDirection = normalize(LightDirection);RayTMin = 0.0;RayTMax = 1.0e27;}// RayTracingPointLight.ush// 生成点光源遮挡光线.bool GeneratePointLightOcclusionRay(...){float3 LightDirection = LightParameters.Position - WorldPosition;float RayLength = length(LightDirection);LightDirection /= RayLength;// 定义光线时应用法线扰动.RayOrigin = WorldPosition;RayDirection = LightDirection;RayTMin = 0.0;RayTMax = RayLength;return true;}// RayTracingSphereLight.ush// 用区域采样生成球体光源遮挡光线.bool GenerateSphereLightOcclusionRayWithAreaSampling(...){float4 Result = UniformSampleSphere(RandSample);float3 LightNormal = Result.xyz;float3 LightPosition = LightParameters.Position + LightNormal * LightParameters.SourceRadius;float3 LightDirection = LightPosition - WorldPosition;float RayLength = length(LightDirection);LightDirection /= RayLength;RayOrigin = WorldPosition;RayDirection = LightDirection;RayTMin = 0.0;RayTMax = RayLength;float SolidAnglePdf = Result.w * saturate(dot(LightNormal, -LightDirection)) / (RayLength * RayLength);RayPdf = SolidAnglePdf;return true;}// 用立体角采样生成球体光源遮挡光线.bool GenerateSphereLightOcclusionRayWithSolidAngleSampling(...){(...)// 确定着色点是否包含在球体灯光中.float3 LightDirection = LightParameters.Position - WorldPosition;float RayLength2 = dot(LightDirection, LightDirection);float Radius2 = LightParameters.SourceRadius * LightParameters.SourceRadius;BRANCHif (RayLength2 <= Radius2){return GenerateSphereLightOcclusionRayWithAreaSampling(...);}// 围绕与z轴对齐的圆锥体均匀采样.float SinThetaMax2 = Radius2 / RayLength2;float4 DirAndPdf = UniformSampleConeConcentricRobust(RandSample, SinThetaMax2);float CosTheta = DirAndPdf.z;float SinTheta2 = 1.0 - CosTheta * CosTheta;RayOrigin = WorldPosition;// 将光线方向投影到世界空间 , 使z轴与光照方向对齐.float RayLength = sqrt(RayLength2);LightDirection *= rcp(RayLength + 1e-4);RayDirection = TangentToWorld(DirAndPdf.xyz, LightDirection);RayTMin = 0.0;// 裁剪到与球体最近交点的长度.RayTMax = RayLength * (CosTheta - sqrt(max(SinThetaMax2 - SinTheta2, 0.0)));RayPdf = DirAndPdf.w;return true;}// RayTracingOcclusionRGS.usf// 生成遮挡光线.bool GenerateOcclusionRay(...){// 根据不同光源类型生成光线.#if LIGHT_TYPE == LIGHT_TYPE_DIRECTIONAL{GenerateDirectionalLightOcclusionRay(...);}#elif LIGHT_TYPE == LIGHT_TYPE_POINT{if (LightParameters.SourceRadius == 0){return GeneratePointLightOcclusionRay(...);}else{float RayPdf;return GenerateSphereLightOcclusionRayWithSolidAngleSampling(...);}}#elif LIGHT_TYPE == LIGHT_TYPE_SPOT{return GenerateSpotLightOcclusionRay(...);}#elif LIGHT_TYPE == LIGHT_TYPE_RECT{float RayPdf = 0.0;return GenerateRectLightOcclusionRay(..);}#endifreturn true;}// RayTracingCommon.ushvoid TraceVisibilityRayPacked(inout FPackedMaterialClosestHitPayload PackedPayload, ...){const uint RayContributionToHitGroupIndex = RAY_TRACING_SHADER_SLOT_SHADOW;const uint MultiplierForGeometryContributionToShaderIndex = RAY_TRACING_NUM_SHADER_SLOTS;const uint MissShaderIndex = 0;// 通过启用最小有效载荷模式,忽略所有其他有效载荷信息,意味着这些功能不需要有效载荷输入.PackedPayload.SetMinimalPayloadMode();PackedPayload.HitT = 0;PackedPayload.SetPixelCoord(PixelCoord);// 追踪光线(图形API内建函数).TraceRay(TLAS, RayFlags, InstanceInclusionMask, RayContributionToHitGroupIndex, MultiplierForGeometryContributionToShaderIndex, MissShaderIndex, Ray, PackedPayload);}FMinimalPayload TraceVisibilityRay(in RaytracingAccelerationStructure TLAS, ...){FPackedMaterialClosestHitPayload PackedPayload = (FPackedMaterialClosestHitPayload)0;if ((PayloadFlags & RAY_TRACING_PAYLOAD_INPUT_FLAG_IGNORE_TRANSLUCENT) != 0){PackedPayload.SetIgnoreTranslucentMaterials();}// 追踪可见性光线.TraceVisibilityRayPacked(PackedPayload, TLAS, RayFlags, InstanceInclusionMask, PixelCoord, Ray);// 解压负载.FMinimalPayload MinimalPayload = (FMinimalPayload)0;// 理论上,由于FPackedMaterialClosestHitPayload源自FminiMallPayLoad,因此不需要此解包setp , 但编译器目前不喜欢它们之间的直接转换 。此外,如果将来HitT以不同的方式打包,并且FMinimalPayload不是直接从中继承的,则需要更改 。MinimalPayload.HitT = PackedPayload.HitT;return MinimalPayload;}
以上可知,追踪阴影的过程比较复杂,下面直接画个流程图,以便更加清晰明了:
AL
Rand
cked
on
17.6.4.3 光追阴影降噪
光追阴影的降噪器根据不同的降噪类型而定:
// LightRendering.cpp(...)const int32 DenoiserMode = CVarShadowUseDenoiser.GetValueOnRenderThread();const IScreenSpaceDenoiser* DefaultDenoiser = IScreenSpaceDenoiser::GetDefaultDenoiser();const IScreenSpaceDenoiser* DenoiserToUse = DenoiserMode == 1 ? DefaultDenoiser : GScreenSpaceDenoiser;(...)const auto QuickOffDenoisingBatch = [&]{(...)// 执行降噪处理 。DenoiserToUse->DenoiseShadowVisibilityMasks(GraphBuilder, View, &View.PrevViewInfo, SceneTextureParameters, DenoisingQueue, InputParameterCount, Outputs);(...)};(...)
由此可知,有两种阴影降噪器:::()和 。不过博主搜索了整个UE工程 , 发现它们其实都是同一个类型: , 下面对它进行分析:
// ScreenSpaceDenoise.cppclass FDefaultScreenSpaceDenoiser : public IScreenSpaceDenoiser{public:virtual void DenoiseShadowVisibilityMasks(FRDGBuilder& GraphBuilder, const FViewInfo& View, ...) const{// 设置渲染纹理.FViewInfoPooledRenderTargets ViewInfoPooledRenderTargets;SetupSceneViewInfoPooledRenderTargets(View, &ViewInfoPooledRenderTargets);FSSDSignalTextures InputSignal;// 设置降噪数据.DECLARE_FSSD_CONSTANT_PIXEL_DENSITY_SETTINGS(SSDShadowVisibilityMasksEffectName);Settings.SignalProcessing = ESignalProcessing::ShadowVisibilityMask;(...)// 批处理ID.for (int32 BatchedSignalId = 0; BatchedSignalId < InputParameterCount; BatchedSignalId++){Settings.MaxInputSPP = FMath::Max(Settings.MaxInputSPP, InputParameters[BatchedSignalId].RayTracingConfig.RayCountPerPixel);}// 降噪历史数据.TStaticArray PrevHistories;TStaticArray NewHistories;for (int32 BatchedSignalId = 0; BatchedSignalId < InputParameterCount; BatchedSignalId++){(...)}(...)FSSDSignalTextures SignalOutput;// 恒定像素密度下的信号降噪.DenoiseSignalAtConstantPixelDensity(GraphBuilder, View, SceneTextures, ViewInfoPooledRenderTargets, InputSignal, Settings, PrevHistories, NewHistories, &SignalOutput);// 保存输出数据.for (int32 BatchedSignalId = 0; BatchedSignalId < InputParameterCount; BatchedSignalId++){Outputs[BatchedSignalId].Mask = SignalOutput.Textures[BatchedSignalId];}}};
以上代码涉及的非常复杂,下面简单地阐述其主要步骤:
static void DenoiseSignalAtConstantPixelDensity(FRDGBuilder& GraphBuilder, const FViewInfo& View, ...){(...)// 创建内部降噪缓冲区的描述符和缓冲区.bool bHasReconstructionLayoutDifferentFromHistory = false;TStaticArray InjestDescs;TStaticArray ReconstructionDescs;TStaticArray HistoryDescs;(...)// 设置公共着色器参数.FSSDCommonParameters CommonParameters;{Denoiser::SetupCommonShaderParameters(View, SceneTextures, ...);(...)}// 设置所有元数据以进行空间卷积 。FSSDConvolutionMetaData ConvolutionMetaData;if (Settings.SignalProcessing == ESignalProcessing::ShadowVisibilityMask){for (int32 BatchedSignalId = 0; BatchedSignalId < Settings.SignalBatchSize; BatchedSignalId++){FLightSceneProxy* LightSceneProxy = Settings.LightSceneInfo[BatchedSignalId]->Proxy;(...)ConvolutionMetaData.LightPositionAndRadius[BatchedSignalId] = FVector4f(TranslatedWorldPosition, Parameters.SourceRadius);ConvolutionMetaData.LightDirectionAndLength[BatchedSignalId] = FVector4f(Parameters.Direction, Parameters.SourceLength);GET_SCALAR_ARRAY_ELEMENT(ConvolutionMetaData.HitDistanceToWorldBluringRadius, BatchedSignalId) = FMath::Tan(0.5 * FMath::DegreesToRadians(LightSceneProxy->GetLightSourceAngle()) * LightSceneProxy->GetShadowSourceAngleFactor());GET_SCALAR_ARRAY_ELEMENT(ConvolutionMetaData.LightType, BatchedSignalId) = LightSceneProxy->GetLightType();}}// 压缩元数据以实现更低的内存带宽、半分辨率的一致内存访问和更低的VGPR占用空间.ECompressedMetadataLayout CompressedMetadataLayout = GetSignalCompressedMetadata(Settings.SignalProcessing);if (CompressedMetadataLayout == ECompressedMetadataLayout::FedDepthAndShadingModelID){CommonParameters.CompressedMetadata[0] = Settings.CompressedDepthTexture;CommonParameters.CompressedMetadata[1] = Settings.CompressedShadingModelTexture;}else if (CompressedMetadataLayout != ECompressedMetadataLayout::Disabled){(...)FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("SSD CompressMetadata %dx%d", ...);}FSSDSignalTextures SignalHistory = InputSignal;// 在重建过程中预计算重建过程的某些值.if (SignalUsesInjestion(Settings.SignalProcessing)){(...)FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("SSD Injest(MultiSPP=%i)", ...);SignalHistory = NewSignalOutput;}// 使用比率估计器进行空间重建,以在历史剔除中更精确.if (Settings.bEnableReconstruction){(...)TShaderMapRef ComputeShader(View.ShaderMap, PermutationVector);FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("SSD Reconstruction(MaxSamples=%i Scissor=%ix%i%s%s)", ...);SignalHistory = NewSignalOutput;}// 空间预卷积.for (int32 PreConvolutionId = 0; PreConvolutionId < Settings.PreConvolutionCount; PreConvolutionId++){(...)TShaderMapRef ComputeShader(View.ShaderMap, PermutationVector);FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("SSD PreConvolution(MaxSamples=%d Spread=%f)", ...);SignalHistory = NewSignalOutput;}(...)// 时间Pass.// 注意:即使没有ViewState,也总是这样做,因为它已经不是降噪质量的理想情况,因此并不真正关心性能,并且重建可能具有与时间累积输出不同的布局 。if (bHasReconstructionLayoutDifferentFromHistory || Settings.bUseTemporalAccumulation){FSSDSignalTextures RejectionPreConvolutionSignal;// 时间拒绝可能利用可分离的预卷积.if (SignalUsesRejectionPreConvolution(Settings.SignalProcessing)){(...)TShaderMapRef ComputeShader(View.ShaderMap, PermutationVector);FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("SSD RejectionPreConvolution(MaxSamples=5)"), ...);}(...)TShaderMapRef ComputeShader(View.ShaderMap, PermutationVector);(...)// 设置信号的前一帧历史缓冲区.for (int32 BatchedSignalId = 0; BatchedSignalId < Settings.SignalBatchSize; BatchedSignalId++){FScreenSpaceDenoiserHistory* PrevFrameHistory = PrevFilteringHistory[BatchedSignalId] ? PrevFilteringHistory[BatchedSignalId] : &DummyPrevFrameHistory;(...)PassParameters->HistoryBufferScissorUVMinMax[BatchedSignalId] = FVector4f(float(PrevFrameHistory->Scissor.Min.X + 0.5f) / float(PrevFrameBufferExtent.X),float(PrevFrameHistory->Scissor.Min.Y + 0.5f) / float(PrevFrameBufferExtent.Y),float(PrevFrameHistory->Scissor.Max.X - 0.5f) / float(PrevFrameBufferExtent.X),float(PrevFrameHistory->Scissor.Max.Y - 0.5f) / float(PrevFrameBufferExtent.Y));PrevFrameHistory->SafeRelease();}// 手动清除未使用的资源,以找出着色器在下一帧中实际需要什么.{ClearUnusedGraphResources(ComputeShader, PassParameters);(...)for (int32 i = 0; i < kCompressedMetadataTextures; i++)bExtractCompressedMetadata[i] = PassParameters->PrevCompressedMetadata[i] != nullptr;}// 增加时间累积通道.FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("SSD TemporalAccumulation%s", ...);SignalHistory = SignalOutput;} // 空间过滤器,更快地收敛历史.int32 MaxPostFilterSampleCount = FMath::Clamp(Settings.HistoryConvolutionSampleCount, 1, kStackowiakMaxSampleCountPerSet);if (MaxPostFilterSampleCount > 1){(...)TShaderMapRef ComputeShader(View.ShaderMap, PermutationVector);FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("SSD HistoryConvolution(MaxSamples=%i)", ...);SignalHistory = SignalOutput;}(...)// 最终卷积/输出校正if (SignalUsesFinalConvolution(Settings.SignalProcessing)){(...)TShaderMapRef ComputeShader(View.ShaderMap, PermutationVector);FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("SSD SpatialAccumulation(Final)"), ...);}else{*OutputSignal = SignalHistory;}}
以上可知,屏幕空间降噪(SSD)过程非常复杂,涉及诸多Pass:压缩元数据、注入、重建、预卷积、拒绝预卷积、时间累积、历史卷积、空间累积等 。限于篇幅,下面选取时间累积进行分析:
// SSDTemporalAccumulation.usfvoid TemporallyAccumulate(...){(...)// 采样当前帧数据.FSSDCompressedSceneInfos CompressedRefSceneMetadata = http://www.kingceram.com/post/SampleCompressedSceneMetadata(SceneBufferUV, BufferUVToBufferPixelCoord(SceneBufferUV));(...)// 重新投影到上一帧.float3 HistoryScreenPosition = float3(DenoiserBufferUVToScreenPosition(SceneBufferUV), DeviceZ);bool bIsDynamicPixel = false;float4 ThisClip = float4(HistoryScreenPosition, 1);float4 PrevClip = mul(ThisClip, View.ClipToPrevClip);float3 PrevScreen = PrevClip.xyz * rcp(PrevClip.w);float3 Velocity = HistoryScreenPosition - PrevScreen;float4 EncodedVelocity = GBufferVelocityTexture.SampleLevel(GlobalPointClampedSampler, SceneBufferUV, 0);bIsDynamicPixel = EncodedVelocity.x> 0.0;if (bIsDynamicPixel){Velocity = DecodeVelocityFromTexture(EncodedVelocity);}HistoryScreenPosition -= Velocity;// 采样多路复用信号.FSSDSignalArray CurrentFrameSamples;FSSDSignalFrequencyArray CurrentFrameFrequencies;SampleMultiplexedSignals(SignalInput_Textures_0, SignalInput_Textures_1, ...);// 采样历史缓冲区.FSSDSignalArray HistorySamples = CreateSignalArrayFromScalarValue(0.0);{float2 HistoryBufferUV = HistoryScreenPosition.xy * ScreenPosToHistoryBufferUV.xy + ScreenPosToHistoryBufferUV.zw;float2 ClampedHistoryBufferUV = clamp(HistoryBufferUV, HistoryBufferUVMinMax.xy, HistoryBufferUVMinMax.zw);bool bIsPreviousFrameOffscreen = any(HistoryBufferUV != ClampedHistoryBufferUV);BRANCHif (!bIsPreviousFrameOffscreen){FSSDKernelConfig KernelConfig = CreateKernelConfig();// 内核的编译时配置.KernelConfig.SampleSet = CONFIG_HISTORY_KERNEL;KernelConfig.bSampleKernelCenter = true;(...)// 在进行历史记录的双边拒绝时允许有一点错误,以容忍每帧TAA抖动.KernelConfig.WorldBluringDistanceMultiplier = max(CONFIG_BILATERAL_DISTANCE_MULTIPLIER, 3.0);// 设置双边预设.SetBilateralPreset(CONFIG_HISTORY_BILATERAL_PRESET, KernelConfig);// 内核的SGPR配置.KernelConfig.BufferSizeAndInvSize = HistoryBufferSizeAndInvSize;KernelConfig.BufferBilinearUVMinMax = HistoryBufferUVMinMax;(...)// 内核的VGPR配置.KernelConfig.BufferUV = HistoryBufferUV + BufferUVBilinearCorrection;KernelConfig.bIsDynamicPixel = bIsDynamicPixel;(...)// 计算随机信号.KernelConfig.Randoms[0] = InterleavedGradientNoise(SceneBufferUV * BufferUVToOutputPixelPosition, View.StateFrameIndexMod8);FSSDSignalAccumulatorArray SignalAccumulators = CreateSignalAccumulatorArray();FSSDCompressedSignalAccumulatorArray UnusedCompressedAccumulators = CreateUninitialisedCompressedAccumulatorArray();// 累积内核.AccumulateKernel(KernelConfig, PrevHistory_Textures_0, ...);// 从累加器导出历史样本.for (uint BatchedSignalId = 0; BatchedSignalId < CONFIG_SIGNAL_BATCH_SIZE; BatchedSignalId++){(...)}(...)// 拒绝历史. (跟上面类似, 忽略)#if (CONFIG_HISTORY_REJECTION == HISTORY_REJECTION_MINMAX_BOUNDARIES || CONFIG_HISTORY_REJECTION == HISTORY_REJECTION_VAR_BOUNDARIES){(...)}// 屏蔽应该输出的内容,以确保编译器编译出最终不需要的所有内容 。uint MultiplexCount = 1;FSSDSignalArray OutputSamples = CreateSignalArrayFromScalarValue(0.0);FSSDSignalFrequencyArray OutputFrequencies = CreateInvalidSignalFrequencyArray();{MultiplexCount = CONFIG_SIGNAL_BATCH_SIZE;for (uint BatchedSignalId = 0; BatchedSignalId < MultiplexCount; BatchedSignalId++){OutputSamples.Array[BatchedSignalId] = HistorySamples.Array[BatchedSignalId];OutputFrequencies.Array[BatchedSignalId] = CurrentFrameFrequencies.Array[BatchedSignalId];}}// 不需要保持DispatchThreadId , 而SceneBufferUV处于最高VGPR峰值,因为内核的中心 。uint2 OutputPixelPostion = BufferUVToBufferPixelCoord(SceneBufferUV);if (all(OutputPixelPostion < ViewportMax)){OutputMultiplexedSignal(SignalHistoryOutput_UAVs_0, ...);}}
上述的降噪过程和6.6.1Super 、7.4.8.2 SSGI降噪比较相似,综合使用了滤波、采样的若干种技术(双边滤波、空间卷积、时间卷积、随机采样、信号和频率等等) 。
17.6.5 UE光追天空光
启用Cast Ray并指定 Type时,天空照明支持软环境阴影 。天光捕捉关卡的距离部分,并将其作为光源应用于场景中 。
文章插图
17.6.5.1 ight
ight是渲染天空光的主逻辑,其C++侧逻辑如下:
// RaytracingSkylight.cppvoid FDeferredShadingSceneRenderer::RenderRayTracingSkyLight(FRDGBuilder& GraphBuilder, ...){(...)// 填充天空光参数if (!SetupSkyLightParameters(GraphBuilder, Scene, Views[0], bShouldRenderRayTracingSkyLight, &SkylightParameters, &SkyLightData)){(...)return;}(...)// 如果解耦采样生成, 则单独生成天空光可见光线.if (CVarRayTracingSkyLightDecoupleSampleGeneration.GetValueOnRenderThread() == 1){GenerateSkyLightVisibilityRays(GraphBuilder, Views[0], SkylightParameters, SkyLightData, SkyLightVisibilityRaysBuffer, SkyLightVisibilityRaysDimensions);}(...)for (FViewInfo& View : Views){(...)TShaderMapRef RayGenerationShader(GetGlobalShaderMap(FeatureLevel), PermutationVector);(...)GraphBuilder.AddPass(RDG_EVENT_NAME("SkyLightRayTracing %dx%d", ...){FRayTracingShaderBindingsWriter GlobalResources;SetShaderParameters(GlobalResources, RayGenerationShader, *PassParameters);FRayTracingPipelineState* Pipeline = View.RayTracingMaterialPipeline;if (CVarRayTracingSkyLightEnableMaterials.GetValueOnRenderThread() == 0){FRayTracingPipelineStateInitializer Initializer;Initializer.MaxPayloadSizeInBytes = RAY_TRACING_MAX_ALLOWED_PAYLOAD_SIZE;// 着色器表.FRHIRayTracingShader* RayGenShaderTable[] = { RayGenerationShader.GetRayTracingShader() };Initializer.SetRayGenShaderTable(RayGenShaderTable);// 命中组.FRHIRayTracingShader* HitGroupTable[] = { View.ShaderMap->GetShader().GetRayTracingShader() };Initializer.SetHitGroupTable(HitGroupTable);Initializer.bAllowHitGroupIndexing = false;Pipeline = PipelineStateCache::GetAndOrCreateRayTracingPipelineState(RHICmdList, Initializer);}FRHIRayTracingScene* RayTracingSceneRHI = View.GetRayTracingSceneChecked();// 派发光追.RHICmdList.RayTraceDispatch(Pipeline, RayGenerationShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, RayTracingResolution.X, RayTracingResolution.Y);});// 降噪.if (GRayTracingSkyLightDenoiser != 0){// 使用默认降噪器(即屏幕空间降噪器)const IScreenSpaceDenoiser* DefaultDenoiser = IScreenSpaceDenoiser::GetDefaultDenoiser();const IScreenSpaceDenoiser* DenoiserToUse = DefaultDenoiser;(...)IScreenSpaceDenoiser::FDiffuseIndirectOutputs DenoiserOutputs = DenoiserToUse->DenoiseSkyLight(GraphBuilder, ...);}(...)}}
降噪过程和阴影一样,之后就不再阐述 。下面分析其使用的代码:
// RayTracing\RayTracingSkyLightRGS.usfRAY_TRACING_ENTRY_RAYGEN(SkyLightRGS){(...)// 获取GBuffer数据.FScreenSpaceData ScreenSpaceData = http://www.kingceram.com/post/GetScreenSpaceData(UV);FGBufferData GBufferData = GetGBufferDataFromSceneTexturesLoad(PixelCoord);float DeviceZ = SceneDepthTexture.Load(int3(PixelCoord, 0)).r;float3 WorldPosition;float3 CameraDirection;ReconstructWorldPositionAndCameraDirectionFromDeviceZ(PixelCoord, DeviceZ, WorldPosition, CameraDirection);float3 WorldNormal = GBufferData.WorldNormal;float3 Albedo = GBufferData.DiffuseColor;(...)// 遮罩无限远的深度值bool IsFiniteDepth = DeviceZ> 0.0;bool bTraceRay = (IsFiniteDepth && GBufferData.ShadingModelID != SHADINGMODELID_UNLIT);uint SamplesPerPixel = SkyLight.SamplesPerPixel;if (!bTraceRay){SamplesPerPixel = 0;}// 评估表面点处的天空光const bool bGBufferSampleOrigin = true;const bool bDecoupleSampleGeneration = DECOUPLE_SAMPLE_GENERATION != 0;float3 ExitantRadiance;float3 DiffuseExitantRadiance;float AmbientOcclusion;float HitDistance;// 估算天空光.SkyLightEvaluate(DispatchThreadId, ...);// 预除以反照率,在合成中恢复.DiffuseExitantRadiance.r = Albedo.r > 0.0 ? DiffuseExitantRadiance.r / Albedo.r : DiffuseExitantRadiance.r;DiffuseExitantRadiance.g = Albedo.g > 0.0 ? DiffuseExitantRadiance.g / Albedo.g : DiffuseExitantRadiance.g;DiffuseExitantRadiance.b = Albedo.b > 0.0 ? DiffuseExitantRadiance.b / Albedo.b : DiffuseExitantRadiance.b;DiffuseExitantRadiance.rgb *= View.PreExposure;RWSkyOcclusionMaskUAV[DispatchThreadId] = float4(ClampToHalfFloatRange(DiffuseExitantRadiance.rgb), AmbientOcclusion);RWSkyOcclusionRayDistanceUAV[DispatchThreadId] = float2(HitDistance, SamplesPerPixel);}
下面分析:
// RayTracingSkyLightEvaluation.ushvoid SkyLightEvaluate(...){// 初始化数据.float3 CurrentWorldNormal = WorldNormal;(...)// 在策略之间分割样本 , 除非天空光pdf由于MIS(多重要性采样)而为0(意味着恒定贴图).const float SkyLightSamplingStrategyPdf = SkyLight_Estimate() > 0 ? 0.5 : 0.0;// 迭代到请求的样本计数.for (uint SampleIndex = 0; SampleIndex < SamplesPerPixel; ++SampleIndex){RayDesc Ray;float RayWeight;if (bDecoupleSampleGeneration){// 从预计算的可见性光线缓冲区中获取当前采样的可见性光线const uint SkyLightVisibilityRayIndex = GetSkyLightVisibilityRayTiledIndex(SampleCoord, SampleIndex, SkyLightVisibilityRaysDimensions.xy);FSkyLightVisibilityRays SkyLightVisibilityRay = SkyLightVisibilityRays[SkyLightVisibilityRayIndex];Ray.Origin = WorldPosition;Ray.Direction = SkyLightVisibilityRay.DirectionAndPdf.xyz;Ray.TMin = 0.0;Ray.TMax = SkyLight.MaxRayDistance;RayWeight = SkyLightVisibilityRay.DirectionAndPdf.w;}else // 非解耦样本生成模式.{RandomSequence RandSequence;RandomSequence_Initialize(RandSequence, PixelCoord, SampleIndex, View.StateFrameIndex, SamplesPerPixel);// 确定天光或朗伯光线.float2 RandSample = RandomSequence_GenerateSample2D(RandSequence);// 为当前采样生成可见性光线.float SkyLightPdf = 0;float CosinePdf = 0;BRANCHif (RandSample.x < SkyLightSamplingStrategyPdf){RandSample.x /= SkyLightSamplingStrategyPdf;// 采样光源.FSkyLightSample SkySample = SkyLight_SampleLight(RandSample);Ray.Direction = SkySample.Direction;SkyLightPdf = SkySample.Pdf;CosinePdf = saturate(dot(CurrentWorldNormal, Ray.Direction)) / PI;}else{RandSample.x = (RandSample.x - SkyLightSamplingStrategyPdf) / (1.0 - SkyLightSamplingStrategyPdf);// 余弦采样半球.float4 CosSample = CosineSampleHemisphere(RandSample, CurrentWorldNormal);Ray.Direction = CosSample.xyz;CosinePdf = CosSample.w;// 计算pdf.SkyLightPdf = SkyLight_EvalLight(Ray.Direction).w;}Ray.Origin = WorldPosition;Ray.TMin = 0.0;Ray.TMax = SkyLight.MaxRayDistance;// MIS / pdfRayWeight = 1.0 / lerp(CosinePdf, SkyLightPdf, SkyLightSamplingStrategyPdf);}(...)// 基于采样世界位置是否来自GBuffer,应用深度偏移.float NoL = dot(CurrentWorldNormal, Ray.Direction);if (NoL > 0.0){if (bGBufferSampleOrigin){ApplyCameraRelativeDepthBias(Ray, PixelCoord, DeviceZ, CurrentWorldNormal, SkyLight.MaxNormalBias);}else{ApplyPositionBias(Ray, CurrentWorldNormal, SkyLight.MaxNormalBias);}}else{ApplyPositionBias(Ray, -CurrentWorldNormal, SkyLight.MaxNormalBias);}NoL = saturate(NoL);(...)// 追踪一条可见性光线.FMinimalPayload MinimalPayload = TraceVisibilityRay(TLAS, RayFlags, InstanceInclusionMask, PixelCoord, Ray);(...)if (MinimalPayload.IsHit()) // 如果命中了物体, 说明该光线不能触达到天空盒.{RayDistance += MinimalPayload.HitT;HitCount += 1.0;}else // 没有命中物体, 则说明该光线命中了天空盒.{BentNormal += Ray.Direction;// 估算材质.const half3 N = WorldNormal;const half3 V = -ViewDirection;const half3 L = Ray.Direction;FDirectLighting LightingSample;if (GBufferData.ShadingModelID == SHADINGMODELID_HAIR){(...)}else{FShadowTerms ShadowTerms = { 0.0, 0.0, 0.0, InitHairTransmittanceData() };// 计算BxDFLightingSample = EvaluateBxDF(GBufferData, N, V, L, NoL, ShadowTerms);}float3 Brdf = LightingSample.Diffuse + LightingSample.Transmission + LightingSample.Specular;// 计算天空光.float3 IncomingRadiance = SkyLight_EvalLight(Ray.Direction).xyz;ExitantRadiance += IncomingRadiance * Brdf * RayWeight;float3 DiffuseThroughput = LightingSample.Diffuse;if (SkyLight.bTransmission){DiffuseThroughput += LightingSample.Transmission;}DiffuseExitantRadiance += IncomingRadiance * DiffuseThroughput * RayWeight;}} // for// 样本数的平均值if (SamplesPerPixel > 0){const float SamplesPerPixelInv = rcp(SamplesPerPixel);ExitantRadiance *= SamplesPerPixelInv;DiffuseExitantRadiance *= SamplesPerPixelInv;AmbientOcclusion = HitCount * SamplesPerPixelInv;}(...)// 如果碰撞到任何遮挡几何体,则计算碰撞距离.if (HitCount > 0.0){HitDistance = RayDistance / HitCount;}(...)}
上述代码调用了两次,第一次为了计算天空光的pdf,第二次为了计算辐射率 。的解析如下:
// MonteCarlo.ush// 逆向的等面积球面映射.// Based on: [Clarberg 2008, "Fast Equal-Area Mapping of the (Hemi)Sphere using SIMD"]float2 InverseEquiAreaSphericalMapping(float3 Direction){float3 AbsDir = abs(Direction);float R = sqrt(1 - AbsDir.z);float Epsilon = 5.42101086243e-20;float x = min(AbsDir.x, AbsDir.y) / (max(AbsDir.x, AbsDir.y) + Epsilon);// Coefficients for 6th degree minimax approximation of atan(x)*2/pi, x=[0,1].const float t1 = 0.406758566246788489601959989e-5f;const float t2 = 0.636226545274016134946890922156f;const float t3 = 0.61572017898280213493197203466e-2f;const float t4 = -0.247333733281268944196501420480f;const float t5 = 0.881770664775316294736387951347e-1f;const float t6 = 0.419038818029165735901852432784e-1f;const float t7 = -0.251390972343483509333252996350e-1f;// Polynomial approximation of atan(x)*2/pifloat Phi = t6 + t7 * x;Phi = t5 + Phi * x;Phi = t4 + Phi * x;Phi = t3 + Phi * x;Phi = t2 + Phi * x;Phi = t1 + Phi * x;Phi = (AbsDir.x < AbsDir.y) ? 1 - Phi : Phi;float2 UV = float2(R - Phi * R, Phi * R);UV = (Direction.z < 0) ? 1 - UV.yx : UV;UV = asfloat(asuint(UV) ^ (asuint(Direction.xy) & 0x80000000u));return UV * 0.5 + 0.5;}// RayTracingSkyLightCommon.ushfloat4 SkyLight_EvalLight(float3 Dir){// 利用逆向的等面积球面映射算出天空光的UV,并采样出天空光纹理的颜色.float2 UV = InverseEquiAreaSphericalMapping(Dir.yzx);float4 Result = SkylightTexture.SampleLevel(SkylightTextureSampler, UV, 0);float3 Radiance = Result.xyz;// 计算pdf.#if USE_HIERARCHICAL_IMPORTANCE_SAMPLINGfloat Pdf = Result.w > 0 ? Result.w / (4 * PI * SkylightPdf.Load(int3(0, 0, SkylightMipCount - 1))) : 0.0; #elsefloat Pdf = 1.0 / (4.0 * PI);#endifreturn float4(Radiance, Pdf);}
17.6.5.2
是组合ight计算的结果到场景颜色中,其C++侧逻辑如下:
// RaytracingSkylight.cppvoid FDeferredShadingSceneRenderer::CompositeRayTracingSkyLight(FRDGBuilder& GraphBuilder, ...){for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++){const FViewInfo& View = Views[ViewIndex];(...)GraphBuilder.AddPass(RDG_EVENT_NAME("GlobalIlluminationComposite"), ...){// VS和PS实例.TShaderMapRef VertexShader(View.ShaderMap);TShaderMapRef PixelShader(View.ShaderMap);(...)// 叠加性(Additive)混合模式.GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI();(...)DrawRectangle(RHICmdList, ...);});}}
下面直接进入PS使用的代码:
// CompositeSkyLightPS.usfvoid CompositeSkyLightPS(in noperspective float2 UV : TEXCOORD0, out float4 OutColor : SV_Target0){// 获取GBuffer数据.FGBufferData GBufferData = http://www.kingceram.com/post/GetGBufferDataFromSceneTextures(UV);float3 Albedo = GBufferData.StoredBaseColor - GBufferData.StoredBaseColor * GBufferData.Metallic;// 从天空光纹理采样出数据.float4 SkyLight = SkyLightTexture.Sample(SkyLightTextureSampler, UV);// 降噪后应用反照率SkyLight.rgb *= Albedo;OutColor = SkyLight;}
17.6.6 UE光追GI 17.6.6.1 UE光追GI开启条件
UE 5.0.3的标准光追GI已被Lumen硬件光追取代(下图),而Lumen的全局光照支持两种光线追踪模式:软件光线追踪(需要在项目设置中开启 Mesh)和硬件光线追踪(需要在项目设置中开启Ray ) 。后面只分析Lumen硬件光线追踪 。
文章插图
其中决定是否使用Lumen GI的代码如下:
void FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder){(...)InitViews(...);// 计算并提交渲染器的整个依赖拓扑的最终状态 。CommitFinalPipelineState();(...)}void FDeferredShadingSceneRenderer::CommitFinalPipelineState(){(...)CommitIndirectLightingState();(...)}// IndirectLightRendering.cppbool ShouldRenderLumenDiffuseGI(const FScene* Scene, const FSceneView& View, bool bSkipTracingDataCheck, bool bSkipProjectCheck) {// 是否可启用Lumen特性.return Lumen::IsLumenFeatureAllowedForView(Scene, View, bSkipTracingDataCheck, bSkipProjectCheck)// 动态全局光照方法是否Lumen&& View.FinalPostProcessSettings.DynamicGlobalIlluminationMethod == EDynamicGlobalIlluminationMethod::Lumen// 控制台变量是否开启.&& CVarLumenGlobalIllumination.GetValueOnAnyThread()// 视图家族的GI标记是否开启.&& View.Family->EngineShowFlags.GlobalIllumination && View.Family->EngineShowFlags.LumenGlobalIllumination// 是否使用硬件光追探针收集或者支持软件光追.&& (bSkipTracingDataCheck || Lumen::UseHardwareRayTracedScreenProbeGather() || Lumen::IsSoftwareRayTracingSupported());}void FDeferredShadingSceneRenderer::CommitIndirectLightingState(){for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++){const FViewInfo& View = Views[ViewIndex];TPipelineState& ViewPipelineState = ViewPipelineStates[ViewIndex];EDiffuseIndirectMethod DiffuseIndirectMethod = EDiffuseIndirectMethod::Disabled;EAmbientOcclusionMethod AmbientOcclusionMethod = EAmbientOcclusionMethod::Disabled;EReflectionsMethod ReflectionsMethod = EReflectionsMethod::Disabled;IScreenSpaceDenoiser::EMode DiffuseIndirectDenoiser = IScreenSpaceDenoiser::EMode::Disabled;bool bUseLumenProbeHierarchy = false;// 检测是否使用Lumen GI.if (ShouldRenderLumenDiffuseGI(Scene, View)){DiffuseIndirectMethod = EDiffuseIndirectMethod::Lumen;bUseLumenProbeHierarchy = CVarLumenProbeHierarchy.GetValueOnRenderThread() != 0;}else if (ScreenSpaceRayTracing::IsScreenSpaceDiffuseIndirectSupported(View))(...)}}
代码如下:
// LumenScreenProbeHardwareRayTracing.cppbool UseHardwareRayTracedScreenProbeGather(){#if RHI_RAYTRACING// 光线追踪是否开启.return IsRayTracingEnabled()// 是否使用硬件光线追踪.&& Lumen::UseHardwareRayTracing()// Lumen的屏幕探针收集硬件光追的控制台变量不为0&& (CVarLumenScreenProbeGatherHardwareRayTracing.GetValueOnAnyThread() != 0);#elsereturn false;#endif}
17.6.6.2
一旦满足所有条件,则Lumen的硬件光追GI会在和之间调用渲染相关GI:
void FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder){(...)RenderBasePass(...);(...)RenderDiffuseIndirectAndAmbientOcclusion(GraphBuilder, ...);(...)RenderLights(...);(...)}
下面进入分析和Lumen GI相关的逻辑:
// IndirectLightRendering.cppvoid FDeferredShadingSceneRenderer::RenderDiffuseIndirectAndAmbientOcclusion(FRDGBuilder& GraphBuilder, ...){(...)for (FViewInfo& View : Views){const FPerViewPipelineState& ViewPipelineState = GetViewPipelineState(View);(...)else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen){FLumenMeshSDFGridParameters MeshSDFGridParameters;LumenRadianceCache::FRadianceCacheInterpolationParameters RadianceCacheParameters;// 渲染Lumen屏幕探针收集.DenoiserOutputs = RenderLumenScreenProbeGather(GraphBuilder, ...);if (ViewPipelineState.ReflectionsMethod == EReflectionsMethod::Lumen){DenoiserOutputs.Textures[2] = RenderLumenReflections(GraphBuilder, View, ...);}// Lumen需要它自己的深度历史,因为像半透明速度这样的东西会写入深度.StoreLumenDepthHistory(GraphBuilder, SceneTextures, View);if (!DenoiserOutputs.Textures[2]){DenoiserOutputs.Textures[2] = DenoiserOutputs.Textures[1];}}(...)// 将漫反射间接和环境光遮挡应用于场景颜色 。if (... ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen ...){FDiffuseIndirectCompositePS::FParameters* PassParameters = GraphBuilder.AllocParameters();(...)else if (ViewPipelineState.DiffuseIndirectMethod == EDiffuseIndirectMethod::Lumen){PermutationVector.Set(4);PermutationVector.Set(ScreenBentNormalParameters.UseScreenBentNormal != 0);DiffuseIndirectSampling = TEXT("ScreenProbeGather");}(...)FPixelShaderUtils::AddFullscreenPass(GraphBuilder, View.ShaderMap,RDG_EVENT_NAME("DiffuseIndirectComposite(DiffuseIndirect=%s%s%s%s) %dx%d", ...);}(...)} // for}
17.6.6.3
下面对的硬件光追部分进行分析:
// LumenScreenProbeGather.cppFSSDSignalTextures FDeferredShadingSceneRenderer::RenderLumenScreenProbeGather(FRDGBuilder& GraphBuilder, ...){(...)if (GLumenIrradianceFieldGather != 0){return RenderLumenIrradianceFieldGather(GraphBuilder, SceneTextures, FrameTemporaries, View);}(...)auto ComputeShader = View.ShaderMap->GetShader(0);// 增加全局探针下采样的Pass.FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("UniformPlacement DownsampleFactor=%u", ScreenProbeParameters.ScreenProbeDownsampleFactor), ...);(...)if (ScreenProbeParameters.MaxNumAdaptiveProbes > 0 && AdaptiveProbeMinDownsampleFactor < ScreenProbeParameters.ScreenProbeDownsampleFactor){ uint32 PlacementDownsampleFactor = ScreenProbeParameters.ScreenProbeDownsampleFactor;do{PlacementDownsampleFactor /= 2;FScreenProbeAdaptivePlacementCS::FParameters* PassParameters = GraphBuilder.AllocParameters();(...)auto ComputeShader = View.ShaderMap->GetShader(0);// 增加自适应探针放置的Pass.FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("AdaptivePlacement DownsampleFactor=%u", PlacementDownsampleFactor), ...);}while (PlacementDownsampleFactor > AdaptiveProbeMinDownsampleFactor);}(...)auto ComputeShader = View.ShaderMap->GetShader(0);// 设置自适应探索非直接参数的Pass.FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("SetupAdaptiveProbeIndirectArgs"), ...);(...)// 生成BRDF的pdf.GenerateBRDF_PDF(GraphBuilder, View, SceneTextures, BRDFProbabilityDensityFunction, BRDFProbabilityDensityFunctionSH, ScreenProbeParameters);(...)if (LumenScreenProbeGather::UseRadianceCache(View)){(...)// 渲染辐射率缓存.RenderRadianceCache(GraphBuilder, ...);(...)}// 生成重要性采样的光线.if (LumenScreenProbeGather::UseImportanceSampling(View)){GenerateImportanceSamplingRays(GraphBuilder, View, ...);}(...)// 追踪屏幕探针.TraceScreenProbes(GraphBuilder, Scene, ...);FScreenProbeGatherParameters GatherParameters;// 过滤屏幕探针.FilterScreenProbes(GraphBuilder, View, SceneTextures, ScreenProbeParameters, GatherParameters);(...)// 在屏幕空间探针中插值并集成.InterpolateAndIntegrate(GraphBuilder, ...);(...)// 降噪.if (GLumenScreenProbeTemporalFilter){if (GLumenScreenProbeUseHistoryNeighborhoodClamp){(...)auto ComputeShader = View.ShaderMap->GetShader(0);// 生成压缩的GBuffer数据.FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("GenerateCompressedGBuffer"), ...);(...)// 对非直接探针层级进行降噪.DenoiserOutputs = IScreenSpaceDenoiser::DenoiseIndirectProbeHierarchy(GraphBuilder, View, ...);bLumenUseDenoiserComposite = true;}else{// 更新历史屏幕探针收集.UpdateHistoryScreenProbeGather(GraphBuilder, View, ...);DenoiserOutputs.Textures[0] = DiffuseIndirect;DenoiserOutputs.Textures[1] = RoughSpecularIndirect;}}(...)return DenoiserOutputs;}
17.6.6.4
从上可知,Lumen的GI使用了屏幕空间的光照探针 , 其中和硬件光追相关的是,其它和软件光追应该是一样 。下面就只分析:
// LumenScreenProbeTracing.cppvoid TraceScreenProbes(FRDGBuilder& GraphBuilder, const FScene* Scene, ...){(...)// 清理追踪结果.auto ComputeShader = View.ShaderMap->GetShader(0);FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("ClearTraces %ux%u", ...);(...)// 追踪屏幕空间的探针.auto ComputeShader = View.ShaderMap->GetShader(PermutationVector);FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("TraceScreen(%s)", ...);(...)// 是否使用硬件光线追踪.const bool bUseHardwareRayTracing = Lumen::UseHardwareRayTracedScreenProbeGather();if (bUseHardwareRayTracing){FCompactedTraceParameters CompactedTraceParameters = CompactTraces(GraphBuilder, View, ...);// 硬件追踪屏幕探针.RenderHardwareRayTracingScreenProbe(GraphBuilder, Scene, ...);}else{// 软件追踪屏幕探针.(...)}(...)// 屏幕空间追踪体素, 也分硬件和软件模式.PermutationVector.Set< FScreenProbeTraceVoxelsCS::FTraceVoxels>(!bUseHardwareRayTracing && Lumen::UseGlobalSDFTracing(*View.Family));auto ComputeShader = View.ShaderMap->GetShader(PermutationVector);FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("%s%s", ...);}
17.6.6.5
从上得知如果是硬件光追模式 , 则会进入:
// LumenScreenProbeHardwareRayTracing.cppvoid RenderHardwareRayTracingScreenProbe(FRDGBuilder& GraphBuilder, const FScene* Scene, ...){(...)// 转换光线分配器TShaderRef ComputeShader = View.ShaderMap->GetShader();FComputeShaderUtils::AddPass(GraphBuilder,RDG_EVENT_NAME("FConvertRayAllocatorCS"), ...);(...)// 【近场(near-field)】、提取表面缓存和材质id的默认追踪.PermutationVector.Set(true);PermutationVector.Set(false);if (bInlineRayTracing){DispatchComputeShader(GraphBuilder, Scene, ...);}else{DispatchRayGenShader(GraphBuilder, Scene, ...);}(...)// 使用【远场】进行屏幕探针采集if (bUseFarFieldForScreenProbeGather){// 硬件压缩光线, 以提升缓存一致性和命中率, 提升效率.LumenHWRTCompactRays(GraphBuilder, Scene, ...);(...)PermutationVector.Set(false);PermutationVector.Set(true);if (bInlineRayTracing){DispatchComputeShader(GraphBuilder, Scene, ...);}else{DispatchRayGenShader(GraphBuilder, Scene, ...);}}}
以上需要执行两次光线追踪,第一次是追踪近?。∟ear Field),第二次是追踪远?。‵ar Field) 。追踪时支持两种模式:使用 的内联模式和使用Ray 的硬件模式 。下面分析它们的区别,先分析 模式:
// LumenScreenProbeHardwareRayTracing.cppvoid DispatchComputeShader(FRDGBuilder& GraphBuilder, const FScene* Scene, ...){(...)TShaderRef ComputeShader = ...;(...)GraphBuilder.AddPass(RDG_EVENT_NAME("HardwareInlineRayTracing %s %s", ..., ERDGPassFlags::Compute,[PassParameters, &View, ComputeShader, DispatchResolution](FRHIRayTracingCommandList& RHICmdList){(...)if (IsHardwareRayTracingScreenProbeGatherIndirectDispatch()){// 非直接模式,注意参数是PassParameters->CommonParameters.HardwareRayTracingIndirectArgsDispatchIndirectComputeShader(RHICmdList, ComputeShader.GetShader(), PassParameters->CommonParameters.HardwareRayTracingIndirectArgs->GetIndirectRHICallBuffer(), 0);}else{(...)// 直接模式.DispatchComputeShader(RHICmdList, ComputeShader.GetShader(), GroupCount.X, GroupCount.Y, 1);}(...)});}
以上可知,CS模式又支持非直接和直接两种,注意它们虽然使用同一个,但的参数不一样!非直接的开启条件如下:
// LumenScreenProbeHardwareRayTracing.cppbool IsHardwareRayTracingReflectionsIndirectDispatch(){return GRHISupportsRayTracingDispatchIndirect && (CVarLumenReflectionsHardwareRayTracingIndirect.GetValueOnRenderThread() == 1);}// WindowsD3D12Device.cppif (D3D12Caps5.RaytracingTier >= D3D12_RAYTRACING_TIER_1_1){GRHISupportsRayTracingDispatchIndirect = true;}
也就是说需要D3D12光线追踪Tier 1.1以上(其它图形API暂不支持)以及相关控制台变量为1才开启 。
相关说明可参见DX 12光追说明文档:和 。
非直接模式相当于异步模式,可以提升GPU的并行度,通常效率更高 。
17.6.6.6
下面继续分析使用Ray 的硬件模式:
void DispatchRayGenShader(FRDGBuilder& GraphBuilder, const FScene* Scene, ...){(...)// 生成非直接参数.DispatchLumenScreenProbeGatherHardwareRayTracingIndirectArgs(...);// 设置屏幕追踪参数.SetLumenHardwareRayTracingScreenProbeParameters(...);(...)TShaderRef RayGenerationShader = ...;(...)GraphBuilder.AddPass(RDG_EVENT_NAME("HardwareRayTracing %s %s", ...{(...)// 非直接模式if (IsHardwareRayTracingScreenProbeGatherIndirectDispatch()){RHICmdList.RayTraceDispatchIndirect(Pipeline, ...);}// 直接模式.else{RHICmdList.RayTraceDispatch(Pipeline, ...);}});}
以上可知,硬件光追也支持非直接和直接模式,如果支持非直接 , 则优先用之 。下面分析gRGS的:
// LumenScreenProbeHardwareRayTracing.usfLUMEN_HARDWARE_RAY_TRACING_ENTRY(LumenScreenProbeGatherHardwareRayTracing){// 计算线程组和线程id.uint ThreadIndex = DispatchThreadIndex.x;uint GroupIndex = DispatchThreadIndex.y;#if DIM_INDIRECT_DISPATCHuint Iteration = 0;uint DispatchedThreads = RayAllocator[0];#elseuint DispatchedThreads = ThreadCount * GroupCount;uint IterationCount = (RayAllocator[0] + DispatchedThreads - 1) / DispatchedThreads;// 直接模式则需要用for循环来实现迭代多条光线.for (uint Iteration = 0; Iteration < IterationCount; ++Iteration)#endif{uint RayIndex = Iteration * DispatchedThreads + GroupIndex * ThreadCount + ThreadIndex;if (RayIndex >= RayAllocator[0]){return;}// 获取追踪数据.#if (DIM_LIGHTING_MODE == LIGHTING_MODE_HIT_LIGHTING) || ENABLE_FAR_FIELD_TRACINGFTraceData TraceData = http://www.kingceram.com/post/UnpackTraceData(RWRetraceDataPackedBuffer[RayIndex]);uint RayId = TraceData.RayId;#elseuint RayId = RayIndex;#endif(...)// 创建追踪光照上下文.FRayTracedLightingContext Context = CreateRayTracedLightingContext(TLAS, ...);(...)// 执行小误差追踪.FRayTracedLightingResult Result = EpsilonTrace(Ray, Context);// 如果没有命中物体if (!Result.bIsHit){Ray.TMin = max(Ray.TMin, AvoidSelfIntersectionTraceDistance);Ray.TMax = Ray.TMin;// 通过近场的球体包围盒裁剪TMaxif (length(Ray.Origin - LWCHackToFloat(PrimaryView.WorldCameraOrigin)) < MaxTraceDistance){float2 Hit = RayIntersectSphere(Ray.Origin, Ray.Direction, float4(LWCHackToFloat(PrimaryView.WorldCameraOrigin), MaxTraceDistance));Ray.TMax = (Hit.x> 0) ? Hit.x : ((Hit.y > 0) ? Hit.y : Ray.TMin);}// 处理辐射度缓存命中.bool bIsRadianceCacheHit = false;#if DIM_RADIANCE_CACHE{float ClipmapDitherRandom = InterleavedGradientNoise(ScreenTileCoord, View.StateFrameIndexMod8);FRadianceCacheCoverage Coverage = GetRadianceCacheCoverage(Ray.Origin, Ray.Direction, ClipmapDitherRandom);if (Coverage.bValid){Ray.TMax = min(Ray.TMax, Coverage.MinTraceDistanceBeforeInterpolation);bIsRadianceCacheHit = true;}}#endif// 设置远场上下文特例化.Context.FarFieldMaxTraceDistance = FarFieldMaxTraceDistance;Context.FarFieldReferencePos = FarFieldReferencePos;#if DIM_LIGHTING_MODE == LIGHTING_MODE_SURFACE_CACHEResult = TraceAndCalculateRayTracedLightingFromSurfaceCache(Ray, Context);#if DIM_PACK_TRACE_DATARWRetraceDataPackedBuffer[RayIndex] = PackTraceData(CreateTraceData(RayId, ...));#endif#endif }// 写入最终光照结果.#if DIM_WRITE_FINAL_LIGHTINGbool bMoving = false;if (Result.bIsHit){float3 HitWorldPosition = Ray.Origin + Ray.Direction * Result.TraceHitDistance;bMoving = IsTraceMoving(...);}RWTraceRadiance[ScreenProbeTraceCoord] = Result.Radiance * View.PreExposure;RWTraceHit[ScreenProbeTraceCoord] = EncodeProbeRayDistance(...);#endif}}
下面进入光线追踪的调用栈:
// LumenScreenProbeHardwareRayTracing.usfFRayTracedLightingResult EpsilonTrace(RayDesc Ray, inout FRayTracedLightingContext Context){FRayTracedLightingResult Result = CreateRayTracedLightingResult();#if ENABLE_NEAR_FIELD_TRACINGuint OriginalCullingMode = Context.CullingMode;Context.CullingMode = RAY_FLAG_CULL_BACK_FACING_TRIANGLES;Ray.TMax = AvoidSelfIntersectionTraceDistance;if (Ray.TMax > Ray.TMin){// 第一次追踪: 启用背面剔除的短距离,以避免在追踪几何体与GBuffer中的几何体不匹配的情况下自相交(Nanite、光线追踪LOD等).#if DIM_LIGHTING_MODE == LIGHTING_FROM_SURFACE_CACHE{Result = TraceAndCalculateRayTracedLightingFromSurfaceCache(Ray, Context);}#else{Result = TraceAndCalculateRayTracedLighting(Ray, Context, DIM_LIGHTING_MODE);}#endif}Context.CullingMode = OriginalCullingMode;#endifreturn Result;}
以上的和会进入复杂的Luman Card追踪和采样逻辑,此文就不继续分析了,可以参看6.5.6 Lumen场景光照和6.5.7 Lumen非直接光照 。
此外,UE硬件光追的反射、AO、半透明等特性也杂糅在Lumen当中 , 形成了相辅相成、耦合性较高且极其复杂的渲染体系,从而呈现出精彩纷呈的电影级别的实时渲染画质 。
17.7 本篇总结
本篇主要阐述了UE的硬件光线追踪的渲染流程和主要算法,使得读者对此模块有着大致的理解 , 至于更多技术细节和原理,需要读者自己去研读UE源码发掘 。
正如毛星云(再次惋惜、缅怀以及RIP)在实时光线追踪(real-time ray )技术还有哪些未攻克的难题?中提及的,实时光追渲染领域还存在诸多悬而未决的问题:
但即便如此,基于硬件光线追踪的渲染体系技术肯定是不久将来的主流,值得我们深入探究和挖掘 。
希望童鞋们能够踏实地扎根于图形渲染技术,力争做到客观公正、实事求是、以德服人、以技服人(反面教材——【猪门马保国】),一起提升国内图形渲染技术的综合实力 , 缩小国际之间的差距 。共勉 。
【17剖析虚幻渲染体系- 实时光线追踪】特别说明参考文献
- Docker -- Docker底层原理深度剖析
- ae渲染完后怎么导出 ae渲染完后怎么导出视频
- Python生成高级圣诞树-代码案例剖析【第16篇—python圣诞节系列】
- 泰安泰燃天然气有限公司 泰安天然气的剖析
- 智慧消防综合管理平台的发展现状剖析
- 草图大师怎么渲染出图 草图大师怎样渲图
- 3dmax怎么渲染?超详细的效果图渲染VR文字版教程来了
- 如何渲染线框模型 3dmax渲染线框模型
- 剖析Docker文件系统:Aufs与Devicemapper
- [转载] 剖析Docker文件系统:Aufs与Devicemapper