渲染流水线:
1、首先相机摆放到场景一个位置和角度,场景的各个物体也已经被摆放好
2、曲面细分
3、拿到场景物体的顶点信息,根据顶点信息构成图元
4、经过透视投影,将图元转化为2*2*2的正方体,再把2*2*2的正方体扩展到屏幕大小
5、对图元进行采样,仅采样会导致抗锯齿,所以这里做MSAA
对图片进行模糊再采样,然后将一个像素也模拟成n*n个虚拟像素,观察图元内包含的虚拟像素来决定颜色的深度,然后再做平均(卷积)
6、经过Z-Buffer,进行深度测试,处理Z通道中的遮挡关系
7、对最后的图元中的像素信息进行着色,shader着色(考虑环境光、漫反射光、强光影响),将RGB颜色画在各个像素上完成渲染管线生成最后的屏幕图片
在Games101中,提到过的抗锯齿方法在UE5里面是都有的:
MSAA:
将一个像素分为多个采样点,按照图元占用采样点个数来算百分比,将颜色乘以百分比来作为当前像素的颜色
FXAA:
- 读取上下左右4个方向 + 自身的像素亮度
- 筛选出其中的最大值lumaMax和最小值lumaMin
- 令对比度lumaContrast = lumaMax - lumaMin
- 当对比度超过一定阈值时,便认为当前像素为边缘像素
寻找混合方向:
往上下和左右分别去计算,哪一个的差值更大,当这个差值大到一定的系数,则为边缘像素
算法为:
如果像素上下差值更大且差值大到一定系数,那么就向水平方向进行像素混合
光这样还是不行,由于是垂直方向差值过大,沿水平方向做抗锯齿,这里同样也沿水平方向找直到找到锯齿边界
找到边界后就做融合
就此做完了FXAA的全过程
TAA:
对镜头进行抖动,渲染场景至主缓存中;如果模型发生运动,需要将渲染运动模型的运动矢量存至速度缓存。在一个TAA的后处理历程中,通过重投影获取当前帧像素在历史帧中的位置,获取历史帧像素,验证并修正历史帧像素
参考:
Ray Traced:
简单来说就是屏幕中各个像素的位置射出一条射线进行反向追踪,计算各种因素的光照信息
如图,经过反射光,折射光从而把这些光线亮度累加在一起,就是这个像素最终亮度的值
但是只是做每个像素的反射和折射计算累加会消耗很多性能,这里采用了包围盒的概念来提速,如果射线连包围盒都没有进入,那么他是不会和包围盒内的物体有反射或者折射效果的,如果光线进入了包围盒,再来计算其是否与包围盒内的物体有进行反射或者折射
再进行优化,就是对场景空间分成很多个立方体的包围盒,一个包围盒又可以细化为多个子集的包围盒,以此类推变成树状结构,光线先射进大的包围盒中,看是否包围盒内有物体,若有,则对大包围盒细化为小的包围盒,看小的包围盒内是否也有光线射中的物体,以此去判断,具体的方法就是Oct-Tree KD-Tree BSP-Tree
通常光线追踪会产生一个致命的问题,就是图像Noise的问题
Noise的根本原因是由于采样不足,采样过多影响性能,导致不能做到实时
Ray trace的反射点,不足以达到整个场景的每一个细节完全覆盖
解决这个问题是通过Temporal,通过时间去降噪
通过motion vector找到上一帧的颜色信息
再使用上一帧的颜色信息和当前帧的颜色信息进行线性插值
Temporal的方式通常都会带来一些问题:
1、突然切换画面:这样的话上一帧就不适用了。
解决方法:淡入淡出,切了画面过后,一般场景颜色由暗到亮进行一个过渡,从而得到前几帧像素的储备,从而解决这个问题
2、向后退:导致原来的信息从画面边缘上增多,上一帧画面,没有这一帧画面多出来的信息,不能进行处理
3、深度遮挡的信息:上一帧被遮挡的物体表面信息,这一帧被遮挡了,或者是,上一帧没被遮挡的物体,这一帧被遮挡了,无法处理上一帧与这一帧中凭空出现或者消失的信息。
这样就会造成拖影的结果:
解决方法:
1、Clamping
将上一帧的颜色信息向当前帧颜色信息靠近,比如当前帧为黄色,上一帧为绿色,那么将上一帧往黄色上靠,例如白色,淡黄色等等
2、Detection
Detection是去识别物体的边缘,边缘内的设置物品id,如果上一帧和这一帧的位置的object ID不一样了,则调整α的值,让值更趋近于当前帧的像素
4、第四个问题就是,当物体不变,光源变,那么motion vector不变,阴影位置变了,那么通过Motion vector去找上一帧的值就不准了
Shadow:
在光源位置生成一张深度图,再从相机视角出发,观察到的位置深度比光源处生成的深度图深度大,则该位置存在阴影,反之则没有阴影
上面实现的事硬阴影,如何实现软阴影?(Soft Shadow)
PCSS(Percentage Closer Soft Shadow)
在摄像机看到的像素对周围做N * N的卷积,在最外侧的阴影则会和没有阴影的一起做平均,则产生了软阴影,N值越大,性能消耗越高,软阴影效果越明显
VSSM(Variance Soft Shadow Mapping)
对最开始的阴影生成一张深度图,再对这个深度图做平方再存放起来,然后做以下公式
用二维前缀和计算出大概的期望,用这个期望值来代表该像素的阴影数值,X为Depth数值
这个方法以显存换性能
由于PCSS的第一步和第三步都是n平方的操作,所以性能不达标,所以就有VSSM的操作
1、计算灯光位置对场景的一个深度采样
2、计算一个深度的平均值的平方,和深度平方的平均值来计算方差
3、通过概率论可以计算出任意一个深度值在第一部的深度采样图的概率排名,通过这个排名就可以直接计算出O(1)大概多少是在阴影内的,根据概率就可以算出对应阴影卷积后的平均值
DFSM(Distance Field Shadow Mapping)
已知o是光源,P1是o最近的物体位置,P2是P1最近物体位置,P3是P2最近物体位置,通过距离场:oP1是已知的,P1P2 P2P3是已知的
通过近似的算法直接算出来对应的数值SDF(p)就是P1P2或者P2P3,p-o就是oP1 oP2 oP3这样的值,这样的比例去做近似表达角度大小的数值,角度越大,值越大,阴影越柔软,K值是为了让更多位置大于1,更多位置为硬阴影,k值越大,阴影外轮廓柔度越低,1为最黑的阴影,0~1根据值的大小,线性决定阴影强弱
每个像素,根据求出来的球谐函数的值进行计算
Wavelet对光照的计算
通过Wavelet的各个形状来替换图片上的各个形状
Wavelet不能处理快速旋转的场景,没有Wavelet的图可以快速进行替换,而SH可以对当前这一层的图片进行旋转计算匹配,支持快速旋转的场景
Wavelet不动的时候效果好很多,但是快速旋转不行
所以前向渲染都是通过一个主要光源计算像素
四个次要光源计算顶点
其余光源用球谐函数来计算,依然能达到一个很好的效果
VSM:虚幻用的Virtual Shadow Map
对阴影贴图进行分区
和Virtual Texture的思想类似,将阴影分块,视野外或者距离外的卸载掉(Load),视野内的距离内的加载出来
进行分区显示,也做了类似MipMaps的操作,越近,分的区越细腻,阴影贴图展现的精度也越高,如果视口上没有显示的分区,不进行整体渲染,只渲染视口上显示的分区
Cascade Shadow(级联阴影)
根据距离做不同密度的Shadow
软阴影做边界的Blend
3A常用的阴影是Cascade Shadow + VSSM
RSM(Reflective Shadow Map):这个其实不是做阴影的,只是利用贴图计算反射光对场景的贡献
将阴影贴图中,所有的像素都是间接光源,再以间接光源去做阴影贴图,从来达到反射光做出阴影效果
优化:将阴影贴图,离中心位置越近,间接光源越小,离中心位置越远,间接光源越大,并不是取所有的点,而是随机一部分点,从而达到性能优化的效果
LPV(Light Propagation Volumes)
在RSM中,我们通过shadow map找到并定义了一系列虚拟点光源,LPV的第一步仍是如此,在找到虚拟点光源后,考虑整个场景分成的一个个小格子,在对应的小格子中,可以找到其中的虚拟点光源
分成了体素,注入光源,将体素进行前后左右上下进行传播,最后实现了光照的反射的计算
当然LPV也是有缺点的,当体素大小不合理时,我光照明明遮住了,光线并不会传播过去,但是由于LPV不对可视性考虑,所以依然能传到被遮挡的体素上去,变得十分不合理
VXGI(Voxel Global IIIumination)
做成Oct-Tree类似的形式,将被照到的位置的法线信息,和面向光源的方向信息给存储起来
基于追踪出的圆锥面的大小,对格子的层级进行查询,就是对场景中的所有体素都要判断是不是与这个锥形相交,如果相交的话就要把对于这个点的间接光照的贡献算出来
AO(环境光遮蔽)【Ambient Occlusion】:
SSAO(Screen Space Ambient Occlusion)
先在屏幕表面的球形范围内随机生成一系列点,根据深度图,判断这些点深度是否小于深度图深度,再按照比例若小于深度图的点大于一半,则这部分不做AO,若小于一半,则做AO,取其中有绿色点的一半,若四个红色,一个绿色,则该点的暗面程度为1\5,值越小,颜色越黑
SSAO也有一个明显的缺陷,就是凹形的时候可能它没在物体内部,但是仍被认为深度大于深度图,从而造成计算不精准
另外,还有桌子明明没有和地面贴合,但是计算的时候不考虑是否贴合,只考虑球形范围内的点,是否深度小于深度图,来计算该点暗值,AO是贴近的缝之间存在暗面的效果,但SSAO明显这一点没做到
SSDO(Screen Space Directional Occlusion)
SSDO的检测采样做法和SSAO完全一样,如上图所示,在法线方向半球,(这里我们就假设已经知道法线了),撒点,然后根据深度图和投影到Camera的深度的对比来判断是否能对P点产生遮挡。
如果被遮挡,如上图第二幅图所示,根据法线计算它们对P点的贡献之和。其次图1的C点没有被挡到,于是我们可以直接采样环境光的直接光照,也就是说SSDO还提供了一种做环境光照的方法
当然Screen Space都存在一个严重的问题,就是屏幕外的没法渲染,因为只拿到屏幕上有限的内容去做操作,那么假如一个镜子,它朝向一个远方(屏幕上没有的地方),那么远方的内容不会被反射到镜子里面,这是个很严重的问题
以及屏幕上的内容,无法反射出三维信息的内容,从而三维反射不真实的情况,时常发生
SSR(Screen Space Reflection)
用MipMap取2*2深度的最小值,若深度最小值都没达到,则光线不可能会接触到物体表面
若光线到达这个最小值,则找到上一层的MipMap(更精细信息的MipMap),再去观察是否有到达物体表面,打到物体表面就将这一块渲染到反射面上,就达到反射的效果了
前向渲染和延时渲染
前向渲染有一个问题就是无效渲染太多,比如场景中有四个物体,互相之间存在叠压关系,按照前向渲染的流程,先前渲染了一个物体之后,它的一部分被后一个渲染的物体挡住了,那么被挡住的这部分就是无效的计算,毕竟我们在屏幕上是看不到这部分的。
另一个问题在于难以支持过多的光源,对于每个需要逐像素计算的光源,渲染一个几何体的时候需要逐个做一次光照计算。如果有一个场景,其中有10个几何体需要进行渲染,有四个光源对整个场景产生影响,那么渲染整个场景需要进行40次光照计算。而且其中还有很多的计算被挡住了。
延迟渲染可以支持大量的实时光照,所以现在的大型电脑游戏基本都已经是延迟渲染了。
延迟渲染就是把光照计算延迟到深度测试之后的渲染方式,延迟着色适合在场景中实时光照很多的情况下使用,而且延迟着色可以对每个光源都按逐像素的方式计算。那Z-prepass也是先渲染出深度缓存进行深度测试后再计算光照的,和延迟渲染有什么不同?最大的不同点就在于G-buffer ,Z-prepass在深度测试后也还是按照一个几何体渲染完再进行下一个这种方式来渲染的 ,延迟渲染是几何体的信息传递到G -buffer之后就和几何体没多大关系了,接下来的操作都是对G-buffer进行的。
如果说前向渲染中几何体数量N和光照数量M产生的计算量是N*M的话延迟渲染产生的计算量就是N+M 。这是因为延迟渲染的思路就是先把几何体的信息都渲染到二维空间中(G-Buffer),然后把G-Buffer整体进行光照计算,G-Buffer中存在的信息都是会最终呈现在屏幕上的,不会有无效计算。
那么G-Buffer中到底会储存哪些信息?不同的引擎对于G-Buffer的处理也是不一样的,对于一个针对PBR渲染的引擎来说,至少会有深度、模板、颜色、法线、世界位置、金属、粗糙、高光这些信息。
参考:https://www.cnblogs.com/HalfDog/p/18039916
Lumen原理:
光追分为三步:
1、通过屏幕空间找到反射信息
通过SSR类似的,但不只是SSR,SSR只追随一条反射光线去找物体表面信息,而Lumen的屏幕空间反射是考虑到了Diffuse\Glossy\Mirror的形式去找,但是每个点反射出很多条光线实时渲染30帧的要求肯定是达不到的
所以在屏幕空间中进行采样点,将光线打到采样点后,根据表面法线方向发射固定数量的反射光线继续去进行追踪,从而提高性能效率
根据motion vector找到上一帧位置的颜色信息给到当前帧位置上的颜色即可
2、在屏幕空间外要获取的反射信息则在全局场景里面去获取光照信息
1.8m内运用了Mesh Distance Field(这个范围内的是高精度的距离场),1.8m外运用Global Distance Field(这个范围是低精度的距离场)
Ray Marching也是Lumen的运用的主要技术之一
表面缓存(Surface Cache)也是Lumen非常重要的原创技术
通常Surface Card是对一个物体的上下前后左右分别拍出照
将场景里面拍好的物体(Surface Card)放到Surface Cache里面,进的用精细的图片存放,远的用粗糙的图片存放
但整体上采样在屏幕中的大小基本一致(平均)
距离场(SDF)有个致命的问题就是只能判断相交,无法获取材质信息,所以无法给三角面着色。那么UE5就引入了表面缓存(Surface Cache),打到那个物体表面时,就从Surface Cache里面获取各类信息(颜色,法线等等)
3、将1、2步的信息进行汇总,汇总的结果也是光追通常伴有噪点的图像信息,降噪的主要手段就是Temporal!用时间滤波去降噪
Nanite:
将生成的Mesh分成很多Cluster(簇),每个簇又生成各自的碰撞盒子,这样如果实现看不到打不到的簇就会被隐藏不被渲染,如果移动相机视角看见了,就直接通过Streaming加载出来
在这里首先要补充一下V-Buffer(Visibility-Buffer)的概念
Visibility Passes: 对场景进行光栅化,将Primitive ID和Instance ID(或Material ID)保存到ID Texture里(顺手做个Depth Prepass),也就是说只有可见的Primitive才会进入后续的阶段;
Worklist Pass:构建并Worklist,这一步是为了驱动下一步,将屏幕划分成很多tile,根据使用到某个Material ID的tile加到该Material ID的Worklist里,作为下一步的索引;
Shading Passes : 使用Compute Shader对每个Material ID进行软光栅,获取顶点属性并插值,然后再进行表面着色。
从而解决G-Buffer带宽增加的问题
Visibility Buffer的基本思路是在前一个Pass中生成一个类似于GBuffer的全屏Buffer,其中的每个texel存储的是一个索引值。这个索引值通常为PrimitiveID。在之后的Pass中可以通过这个PrimitiveID来索引到所在图元,进而得到与之关联的所有属性值,法线、粗糙度等等。有点类似C++里的指针。这样做的目的是避免像传统的延迟渲染那样需要在前一个Pass生成多个buffer来存储不同的属性值,并且需要传给后一个负责渲染的Pass。由于存储和传递的只是ID索引从而极大的降低带宽的压力。带来的额外工作是需要自己手动进行原本由硬件光栅化自动完成的插值
参考:UE_Visibility Buffer & Deferred Material_webgis 获取管线buffer-CSDN博客
Nanite是用V-Buffer和G-Buffer进行一个混合使用,从而达到更好的一个效果
首先Nanite和以前的LOD的最大的区别是,Nanite可以在一个模型中分为很多不同级别精度的渲染,而LOD只能做一个模型整体精度的渲染
Nanite的思想是保持边界不动,分成不同级别个数的簇Group,再在不同级别的Group中保持边缘不动,减少三角形个数,切记!!!这个Nanite它不是树状结构,Group在不同级别下的簇,不一定是包含关系,是依赖关系!!!