UE4 渲染模块概述 摘自知乎Jerry的系列文章

UE4渲染模块概述(一)---方法论、遮挡剔除、Geometry Rendering

渲染模块是自己比较薄弱的环节,但这是深入做游戏必备的核心知识,是区别一般软件开发的精髓所在。我也是本着学习的态度来更新此文,所以有理解不对的地方,还望各位同仁指正。做为渲染模块的开篇,我打算总结下我近一周学习的UE官方视频课程(这里用的是截图,因为如果打出网址,知乎会试图解析,进而转变成用户login的URL来):

本中绝大多数图片取自于视频截图,这里大多都是些概念的介绍,比较粗,所以我称之为“概述”,后面还打算出自己更加细致的研究,敬请期待。

渲染方法论

游戏中讨论的都是实时渲染(RealTime Rendering,RTR),也就是随着游戏内相机的移动,要能够尽可能无延迟地展示视口内容,效果不可能做到CG那么好。

RTR流程的本质是管理性能的损耗,以这张图为例:

X轴表示视觉质量,Y轴表示性能,那么要想获得高质量的渲染,就会带来性能的下降,这个趋势是不会变的。但做的好的优化,是可以降低这种下降的坡度,图中黄色曲线就要好于蓝色曲线。一般地,我们会根据用户体验能接受的角度,来设置一个目标帧率(一般是30帧,帧数表示一秒可以执行多少次主循环),只要能保证在这个帧率之上,就可以尽可能地把渲染视觉效果做好。

图形工程师一直在追求渲染质量、性能、功能这三者的平衡。一个空场景性能必然是最佳的,但这没有意义,另一方面若场景都用最高精度的图片或是堆砌各种功能点,游戏卡得根本玩不了,也是不行的。

在图形优化的路上有很多具体的实现,但都可以归纳成以下六点方法论:

1)流水线:并发多线程,使硬件的利用率达到最高;

2)预计算:能事先计算的就尽量事先做,降低Runtime的计算成本,比如静态光的lightmap;

3)局部化:只在可见的局部区域里做事情,比如屏幕空间反射的计算(SSR);

4)先粗后精:先用消耗低的方法缩小计算范围,再用消耗大的方法执行精确计算,比如八叉树遮挡查询;

5)时空互换:用时间换空间或是空间换时间,比如lightmap就是空间换时间的例子;

6)负担转移:使用stat unit查看如果是CPU的瓶颈,又不可进一步优化了,可以考虑将CPU的计算移至GPU计算,如骨骼动画计算可以移至GPU做。

渲染之前:遮挡查询

渲染有一个看似很简单,但却非常重要的概念:只渲染镜头可见的场景。看不见的场景并不需要浪费性能,直接剔除掉即可,这个过程就是遮挡查询,这部分发生在GPU渲染之前,是在CPU端做的逐Object的遮挡查询。

UE4使用了四种遮挡处理方法,按性能消耗由低到高的排列如下:

1)距离剔除:离镜头太远的物件直接隐去,不去绘制;

2)视锥剔除:镜头能看到的区域是一个锥体,锥体以外的部分可以不去绘制(具体是用八叉树先粗后精筛选进入视锥体的对象);

3)预计算可见性剔除:主要适用于一些室内的场景,比如透过室内的门可以看到哪些区域,这些可以预计算好,不用实时计算了;

4)遮挡剔除:最费性能,需要逐对象查询;

UE会优先使用消耗低的方法去排除掉大部分不用渲染的对象,再用遮挡剔除这种精细的方法进一步计算。

几何渲染

几何渲染可以细分成三部分:

1)GPU端像素级别的prez遮挡查询

2)解析CPU传来的draw primitive绘制指令(draw call)

3)顶点计算(vertex shader)

第一部分是GPU端prepass/prez,防止像素overdraw。举个例子,我们有一张背景图:

在背景图之前放了一盏茶壶,如下:

正常情况下我们先绘制背景图,再绘制茶壶,看似没有问题,但茶壶所在位置的像素其实被重复渲染两次了。那么如果先茶壶再是背景图如何呢?还是会遇到如下交错的情况。

为了真正解决这个问题,在渲染primitive顶点之前,需要拿到深度信息,这样我们就知道哪个顶点应该被渲,哪个不需要渲了。比如一开始的茶壶场景,渲染背景其实只要这样即可:

 

第二部分是drawcall。GPU是逐个drawcall渲染的,所谓drawcall,是指CPU向GPU发出的一次渲染指令,让GPU绘制指定的几何体。比如下图:

CPU会提交五次drawcall,一次天空,一次地面,三个柱子各一次(因为是三个独立的模型)

而下面这个图:

因为最右侧柱子上下两部分采用了不同的材质,所以需要提交两次渲染信息,这样就多一次drawcall,共计6次drawcall。

这张图展示了渲染的顺序,蓝色的是地面,因为事先做了prez,所以为地图上原来柱子的区域留空了没去绘制。

从左到右依次是:渲染地面,渲染右侧柱子的下半截,渲染中间柱子,渲染左侧柱子,渲染右侧柱子的上半截,渲染天空。

渲染顺序也就侧面反映了提交drawcall的次序,其实对于图形工程师而言,不必特别注意drawcall提交次序,只要最终结果是对的就行。不过这里也可以发现一个问题,那就是为啥没有完整地渲染完右侧的柱子,再去渲染中间的呢?那是因为渲染器会把相同材质的对象放到相邻drawcall中处理,这样可以节省让硬件切换渲染状态的耗时。可以注意到这里右侧柱子下半截的材质与其他柱子材质是一样的,所以会放到连续的drawcall中处理,而右侧柱子上半截材质不同,被放到了所有相同材质柱子渲染完成后才进行绘制。

UE4看drawcall数量的命令是stat RHI,在命令行敲入后可见:

draw primitivec calls的数量就是drawcall的数量,一般来说移动端在1000个drawcall左右比较好,PC在2000~3000左右为宜,而超过5000就有可能出性能问题了。这时需要check下出问题的区域。Triangles drawn就是这个视口中三角形面数的总量。

drawcall涉及到CPU向GPU数据的传递,这个传递的代价一般远大于三角形本身的绘制成本,可以举个对应例子,你觉得是拷贝一个1GB的文件快,还是拷贝一百万个1KB的文件快?

降低drawcall的通用处理方案是进行模型的合并,将空间邻近、相同材质、相同渲染状态的小模型合并成一个大模型,这样就只需要提交一次drawcall就可以完成绘制了。特别注意这与组件化的设计概念是不同的,不要认为将物件作为component挂在同一个Actor上就可以自动合并成同一个drawcall了,这些component仍会分开渲染。

模型合并后,虽然drawcall数量降低,但也要考虑它的副作用:

1)遮挡剔除力度减弱,哪怕是镜头锥体内只出现合并模型的一个面片,也会去渲染整个合并模型;

2)lightmap占用更多空间,这个是跟模型走的;

3)碰撞计算变得困难;

4)模型占用内存更大,传统摆一个建筑的方式是将一些局部小模型拼凑,相同的小模型在内存中只要一份就行了,但如果合并,多个相同模型顶点信息都要被完整保存下来。

所以还是推荐美术同学先使用小模型按乐高积木的方式拼接,等接近终版时再按实际性能需要进行合并(可以用3d美术工具或者UE4自带的MeregeActorTools)。

虽然面数相对于drawcall来说,影响不是那么大,但我们仍然要控制面数,可以减少后续光照计算与pixel shader的复杂度。减面的方法常用的就是LOD(Level Of Detail),它可以按镜头与物体的距离来替换不同精度的模型,比如离得很近的话用1000面的模型,离得很远的话就用100面的模型,因为离得远在屏幕上绘制的像素个数也比较少,就看不出来模型的细节了。

HLOD相对于LOD来说可以进一步优化面数和drawcall,它的原理是近处正常显示,比如4个模型就绘制成四个单独的模型,但在远距离上就用合并成一个低精度模型来替换这一组对象。

第三部分是顶点的处理,CPU传过来的顶点是三维的,但屏幕上能显示的是二维数据,因此需要降维投影,这个算法过程被称为vertex shader。shader可以理解成一种程序,它接受输入数据,再经过某种算法,给出输出。vertex shader就是将三维的顶点数据输出成屏幕空间的二维顶点,这其中要经历模型空间到世界空间,世界空间到视口空间,视口空间到屏幕空间的矩阵变换。

vertex shader的一个形象例子就是在shader里面给输入顶点一定偏移量,如下图所示:

这种看似无聊的效果,其实可以用于水流的波动,草随风摆的效果等。不过要注意的是此时只是GPU端改变了顶点显示的位置,它的实际碰撞信息(CPU端控制的)是没有变化的,也就是你不能试图蹲着从中间那个柱子经过。

UE4渲染模块概述(二)---光栅化与纹理

下面来介绍像素处理阶段。

光栅化

拿到二维的顶点并将之组合成三角形之后,就需要考虑如何绘制到屏幕上了。显示屏实际上是由一格格像素构成的,所以接下来的一步就是要计算出哪些像素被三角形覆盖了,然后再对这些被覆盖到的顶点进行着色。

对顶点/三角形像素化的过程称之为光栅化,如下图所示(还是要强调下,本文中所有图片均来自于官方课程的PPT,在前一讲给出了URL):

 

理论上对下图三角形,只需要对其覆盖的三个像素点进行着色即可,如绿色格子所示

但实际由于硬件特性,最小的绘制单位是2*2的正方形,所以实际处理的像素区域如下图橙色区域所示:

好,这里貌似并没有什么问题,只是有些像素遍历到了但没有绘制罢了。但如果换成下面示例(添加了独立的蓝色的三角形,并假定它对应不同的drawcall):

那么中间的两块2*2区域实际上在两个不同的drawcall中被绘制了两次,我们标记为红色区域:

这种现象称之为overshading/quad overdraw(注意与前面说的同一像素因为深度问题重复绘制的overdraw不同)。

在UE4引擎里,可以对这种现象可视化观察,方法如下:

选择后可见视口变化如下:

冷色调的区域表示overshading比较少,暖色甚至是白色表示overshading非常多,需要查看到底是什么问题导致的了。

一般来说,因为半透对象需要绘制前后的物理,所以overshading会较多。还有一种特殊情况如下:

注意到组成这个圆的三角形都是斜长的,而三角形其中一个顶点又都集中在圆心,那么圆心所在的2*2quad的overshading就会非常多了。

更一般的情况是三角形面数影响overshading,因为三角形越密,重合的像素就会越多,就越可能发生quad重复渲染。这也是为什么要使用LOD的原因,近处的时候三角形面数较多,overshading次数增高,但离远时,LOD切换成三角形面数较低的低模,overshading次数就会降低了。下图是一个很好的示例:

GBuffer

GBuffer是延迟渲染才有的东西,它实际是渲染出了多张不同信息量的2D图片,然后用类似photoshop的方法来处理这些图片。

使用UE4研究渲染必备的Renderdoc插件,我们可以看到GBuffer的内容。

GBufferA缓存了worldnormal,即模型在世界坐标下的法向量,用以记录朝向。

GBufferB缓存了物理渲染的PBR参数,其中:

R分量:黑白mask标识哪部分是金属 (全金属则没有漫反射,即固有色无影响)

G分量:黑白mask标识高光

B分量:黑白mask标识粗糙度(反映反射光是否有比较一致的方向性)

以B分量为例,如下图:

颜色越白表示Roughness粗糙度越大,反射的方向性就越差;反之越黑则越光滑,反射方向性越好。

GBufferC缓存了不带光照的图,这也就是viewmode里面unlit的输出了,可以认为呈现的是物体的固有色。

剩下的GBufferD、GBufferE、GBufferF都用作特殊的buffer,比如深度缓冲、标识半透物体的缓冲等。

最后合成的图像本质就是这些Buffer缓存的2D图像经过某种图像算法合成出来的,当场景简单时,这些消耗反而会超过传统的前向渲染,但如果场景复杂时,特别是多光源情况下,采用GBuffer的延迟渲染的消耗就会远低于前向渲染,再复杂的场景也会简化为2D的图像处理。

不过延迟渲染在生成这些2D缓存图像时,需要消耗大量带宽,因此不适用于移动端(不过现在基于tile的渲染可以缓解这方面的压力)。

GBuffer也可以在编辑器可视化,方法如下:

选择后可以看到视口展现了多种功能的GBuffer:

Textures

在对像素着色时,需要采样其对应的纹理。因为内存与带宽的限制,我们总是要对导入的纹理进行各种各样的压缩。下面来说下纹理的压缩格式,在编辑器里随便打开一张texture:

这里关注下红框标识的两个区域,一个是存储格式(DXT1),另一个是存储大小。这两个参量是有关系的,DXT1/BC1用于不带透明通道的有损压缩,而DXT5/BC5用于带透明通道的有损压缩,也可以选择无损压缩的

压缩质量是有提升,但相应占用存储空间也会变大:

视屏中特别强调了法线贴图应该设置为DXT5/BC5,不然质量就会很差(会有明显块状伪影)。

在游戏中应该对关键物件采用高精度的纹理(比如操作的人物模型),其他地方可以采用低精度纹理(比如场景的石块)。不当的使用纹理主要会带来内存和带宽的问题,而不是帧率,不过有时会因为加载造成画面的延迟,这些要分辨清楚。

对于游戏内使用的纹理,都会自动对其mipmap化。所谓mipmap,称为多级渐进纹理,是自动按比例缩小原图,如下图所示:

为什么要这样做?是为了匹配视点远近,需要使用的不同精度的贴图。通常摄像机都是透视投影的,也即意味着近大远小,远处的物体占用的像素其实很少,这时如果仍用高精度的贴图,那么采样UV覆盖的范围就会很少,取出来后看上去就像是噪点一般,如下图所示:

左侧采用了mipmap,右侧没有采用mipmap。引擎自动选择了合适的mipmap精度来绘制,如下图所示,每一种颜色都标识了不同的mipmap:

因为每级mipmap都是上一级长宽的一半,那就要求长宽必须是2的幂次方,才可以让引擎正常生成多级的mipmap。如果导入图片不是2的幂次方,引擎不会报错,只是不会生成mipmap了,如果这些图片用于诸如UI这些地方,也是正确的。关键要区分应用的场合。

UE4渲染模块概述(三)---Pixel Shader & Material Rendering

下面就着手介绍像素着色器(Pixel Shader)与材质渲染(Material Rendering),像素着色器是材质的基础,它的主要功能是进行逐像素的着色。

例如,可以实现对画片整体增加50%红色分量的pixel shader。这个shader跑起来会遍历2D图像中的每一个像素点(只要记住是逐像素的就行),并将它的红色提升50%,例如下图左上角前两个像素:

UE的shader使用的是HLSL语言,类似于下面这种:

float4 normal = mul(IN.Normal, ModelViewIT);
normal.w = 0;
normal = normalize(normal);
float4 light = normalize(LightVec);

我们可以在Engine/Shaders里面看到很多.usf和.ush文件里面包含类似的代码段,这些是UE提供的材质模板,那如何使用这些模板呢?随便选定一个材质,打开材质编辑器,如下:

这种连线类似于蓝图,如果熟悉PBR的概念,也能大概猜出输出结点的含义。

材质编辑器的本质仍然是HLSL代码,只是将之可视化了,它存在的意义是将用户连线出的结果,转换成HLSL代码插入到材质模板.usf文件中。这是一种非常灵活的方式,用同一份材质模板,只改变材质编辑器里面的连线,就能实现出千变万化的pixel shader。

我们将材质编辑器中操作的材质称之为母材质,既然是母亲,说明它还可以继续派生出多个子材质,这些子材质称之为材质实例。我们在母材质中预留未定的可变参数,可以进一步在材质实例中赋上具体的值来最终确定pixel shader,最后将材质实例应用于具体的网格模型(Mesh)。

这里将完整的材质应用流程总结如下图:

.usf模板文件中提供了基础的材质变量和函数,这些不能改变,但同时也把可以变化的代码段留了空,这时开发同学就可以利用材质编辑器:

自动将图形化的连线转成HLSL代码插入到.usf文件里(print到.usf中的%s处):

形成母材质,但此时仍有参数变量未定义,可以通过建立材质实例去进一步特化这些参数:

这样有具体表现力的材质实例就可以应用到具体的mesh中来了:

使用renderdoc也可以看到pixel shader里面的内容:

选中Pixel Shader,在下面绿色横线处就能看到当前使用的材质代码:

注意到这里的代码与之前的HLSL不同,更像是汇编语言,因为renderdoc里面看到的已经是编译后的shader了。对于世面上的没有原码的游戏,如果挂renderdoc截帧,pixel shader看到都是这种汇编代码,不过经验丰富的图形工程师还是可以逆向推出一些逻辑来。

PBR

UE4的材质是基于物理渲染的(Physical Based Rendering)。PBR的优势在于可以利用它在底层中建立相同的着色器系统,用于几乎所有的渲染,而且预测材质外观会更加容易。

PBR的信息来源于之前提到的GBuffer,虽然提供了粗糙度、金属色、高光三个GBuffer通道,但大部分情况只用金属色和粗糙度两个参量,下图展示了这两个参数的GBuffer分量(分别是金属色R通道与粗糙度B通道):

Pixel Shader性能

需要注意下面几点:

(1)一个材质有可引用的贴图数量上限,比如16(经常只能用到13),在DX11可以通过共享采样器做到128个贴图上限。

(2)引用贴图尺寸过大通常只会影响加载的迟延,而不会带来帧率上的减少。

(3)Pixel shaders的影响特别大,因为我们会大量使用它们。

(4)Pixel shader对性能的影响与屏幕分辨率有关,分辨率越大时,影响越为严重。

第一个问题可以通过下面的方法设置共享采样器:

为了解决第二个问题,我们常常在场景载入初期引入低分辨率纹理,但这会带来一种现象:玩家看到图片先模糊而后慢慢变清晰。

解决第三个问题,需要多多留意材质编辑器中的输出信息,如下:

这里会显示指令的个数,通常是100~200次为合理范围,像这种798的,说明Pixel Shader过于复杂,需要优化。还可以打开shader complexity的可视化窗口,如下:

视口中会出现一个红绿条,绿色复杂度低,红色表示复杂度高,白色表示极高。注意这里同时累计了vertex shader与pixel shader的复杂度,英文字母vs所在处表示顶点着色器的复杂度,而ps则表示vs+ps后的整体复杂度。

shader复杂度与复杂材质占屏幕空间的像素比例有关系,比如:

当复杂材质占的像素少时,整体像素复杂度仍落在绿色范围,表示合理,但当复杂材质所占像素多时,如下图:

就会发现整体像素复杂度已经落在红色区间了。所以视频给出的建议是如果物件有可能离玩家视口很近,则要选用简单的shader,而如果物件总是离玩家很远时,可以用稍复杂的shader。

UE4渲染模块概述(四)---反射

在光滑的地面或墙面上需要渲染物体的反射信息,实时计算很难实现 ,本文介绍UE使用的三种反射系统,这三种反射系统各有利弊,往往会将之混合起来使用。

第一种:反射捕获(Reflection Capture)

它会在指定位置计算前后左右上下六个方向的反射信息,形成一张静态的立方体图(Cubemap),这个过程是预计算的,所在实际跑起来的时候消耗很低,但也因为只是在特定位置计算的,所以当实际镜头与捕获不同时,就会看到不合理的反射图样。

UE4使用球体反射捕获对象(Sphere Reflection Capture Actor)来指定捕获的位置,如下图所示:

也有方形的版本(Box Reflection Capture Actor),如下图所示,不过比较常用的还是放置球形捕获Actor:

它本质上是捕获一张360度的图片,然后将图片混合到模型上(如上图所示的地图和茶壶身上),但只在捕获点是准确的,如果镜头移到别处,但混合的图片没有变化,就会出现下图所示的穿帮问题:

可以看到地上混合的柱子倒影不再与真实柱子吻合。反射捕获在很多游戏大作中都有使用,那为什么我们看不到这种诡异现象呢?这时因为同时还会混合其他反射处理方案(比如屏幕空间反射SSR),在某种程度上掩盖了它的瑕疵。

Reflection Capture不精确,但它的预计算的优点对性能是非常友好的,通常我们build light的时候,就会去更新捕获信息,也可以在Build这里单独进行更新。

如果打包游戏或者应用,它会将捕获纹理烘焙进游戏,这样实际跑的时候,在一定范围内(可在反射捕获Actor上配置半径)的任何像素,都会检查附近是否有反射捕获,如果有,它就向反射捕获Actor查询立方体贴图cubemap,将cubemap与物体本身贴图进行混合。

如果相近区域内有两个反射捕获Actor,如下图黄箭头标识:

那么重合的像素就会分别融合两张cubemap,但要注意的是,大量重叠的捕获范围会带来性能问题,因为要在同一个像素中计算多张cubemap融合的结果。

我们布置反射场景的基本思路,是放置一个大型的反射捕获Actor来大致覆盖整个空间,然后再把许多较小的反射捕获Actor放置在反射程度高或者需要精确反射的表面附近,尽量保证影响半径不重合,如下图所示:

当然也没有必要刻意去规避重合,只要重合数量不要超过8个都还是可以接受的。另外,可以通过设置贴图精度来改变反射图样的锐利/模糊程度。

天空光照能为整个游戏世界提供低成本的备用反射捕获,如下图所示:

游戏世界中任何附近没有球体或立文体反射捕获Actor的对象都会转而使用天空光照反射,这对于大型开放式户外环境很理想,因为我们不希望在场景中到处放置反射捕获Actor。

第二种:平面捕获(Planar Reflection)

这种在实际中用的比较少,因为它仅局限于小范围平面,也由于是实时计算,所以消耗要比反射捕获大,但优势是无论镜头如何移动,反射信息总是正确的,如下图所示:

第三种:屏幕空间反射(SSR)

这是UE4唯一默认生效的反射系统,它是实时计算的,因而位置上总是精确的,但生成的反射图样比较模糊,不如反射捕获渲染的清晰。性能消耗介于反射捕获与平面反射之间,SSR效果如下图所示(研究前要关闭捕获Actor的渲染,只开启SSR):

它的优点同时也是缺点在于:只计算屏幕空间的反射。因为计算范围受限,所以性能可以保障,但同时也由于只能采集到屏幕空间内的数据,所以只能绘制视口可见几何的反射,如下图所示:

我们知道柱子很长,在屏幕上方还有一截,但因为SSR只取了屏幕空间内的信息,所以产生的反射信息里面就没有屏幕以外的柱子了(红圈内理应有柱子倒影,但实际没有渲染)。

 

综上,三种方法各有优缺点,实际使用中是将这三种系统混合在一起。三种方法的使用优先级依次为SSR,平面反射,反射捕获。只有当硬件或者其他性能撑不住的时候,才会使用预计算的反射捕获,否则还是提供精确度相对较高的SSR和平面反射。

平面反射尽量不要使用,如果硬件不支持就要关掉SSR或者调整SSR的生成质量来平衡性能,方法是在控制台中键入r.SSR.Quality + 数字,默认质量是3,数字越大,SSR的渲染效果就越好,但相应性能就会越差。

可以在RenderDoc里面截帧分析反射信息,下图所示是屏幕空间反射SSR的渲染:

下图是SSR成像的结果:

每一个点都是投射的反射射线,因为射线密度不够,形成了较多的噪点,为了掩饰,所以SSR生成的反射图看上去是模糊的。

UE4渲染模块概述(五)---静态光照

同反射一样,光照和阴影在实时渲染中是很难计算的。因此如方法论中介绍的,能预计算的就尽量预计算,能用空间换时间的就尽量换。静态光的光照/阴影都是可以预计算的,动态光则需要实时计算。

本章讨论的是静态光照/静态阴影。静态光是指光源本身不会移动,且不会随游戏进程改变状态。静态光/阴影是在编辑器里预计算的,它们被存储于光图(lightmap)里。

使用光图的优点如下:

(1)不需要运行时计算,节省性能;

缺点如下:

(1)需占用不少内存

(2)预计算的时间较长,每移动一个物件,都需要重新build lightmap

(3)非常大的模型可能会没有足够的空间存储lightmap

这里看上去缺点很多,但都是预计算、以空间换时间必须要付出的代价,只要是运行时性能省下来了,就还是值得的。

lightmap光照贴图本质上就是一张纹理,是一张烘焙有光照和阴影信息的图片,类似于纹理的UV采样,我们也会对存于模型上的光照UV坐标进行采样,将采样结果乘以固有色basecolor得到有光照的渲染效果。图示如下:

在UE4中,会把许多光照贴图打包到一起,类似于下面:

我们可以在worldsetting里面看到这些打包好的光照贴图,但不可修改它们。

我们用LightMass来生成光照贴图,可以配置所需要的贴图精度,分散在两个地方:

(1)build光选项里面的光照质量,如preview,high,productive

(2)worldsetting里面的lightmass选项,如下图:

对于一些想要高精度lightmap的地方,可以放置一个光照重要性体积(light importance volume),UE确保体积内的任何东西都有更高质量的光照 (如战斗区域),而体积外则使用低质量光照(如人物移动不到的远景)。需要注意的是,光图的精度主要影响的内存与文件大小,而不是帧率。

对于光图的制作/烘培过程,我们可以通过下面的方法来提升烘焙速度:

(1) 降低光图分辨率

(2) 减少灯光或场景内物件数量

(3) build时仅使用preview方式

(4)灯光设置较小的衰减半径与光源半径(即使光影响范围小)

在编辑器里,可以查看光图的密度:

选中后编辑器内的效果如下:

方格越小,颜色就越绿,这表示光照贴图精度越高。对于一些镜头不常看到的地方(如空中的立柱),建议使用低细节度的光照贴图即可。

光图主要是针对直接光生成的光照纹理,对于因多次反射产生的间接光,UE是使用了一种称之为Indirect Light Cache(间接光照缓存)来进行处理。特别是对于移动中的物体(比如说游戏角色),间接光照缓存的光照信息能弥补lightmap的不足。UE4采用了布置采样点方式来生成间接光照缓存,如下图所示:

灰白色的点即为采样点,对于任何物体,都会找到离它最近的采样点,查询到它的亮度信息,然后将采样结果与物体自身颜色进行混合。可以选中物件,在下图位置找到间接光的配置项:

下拉菜单有三项可选:不使用采样点,使用采样点,使用采样体积。使用采样体积是指选用更多的采样点,比如周围5*5*5个点来计算间接光照。

间接光照缓存是在build lightmap同时做的,采样点会在lightmass importance volume范围内生成,可能会超出一点点。需要注意的是,采样点不是等密度的,一般来说地面的采样点多于空中的采样点,如果想要在空中也有较高密度的采样点,可以创建一个Lightmass Character Indirect Detail Volume(Lightmass角色间接细节体积),如下:

在worldsetting这里可以配置采样点的生成密度:

配置好就可以在空间也获得比较好的间接光质量了,如下图:

关于静态光的lightmap与Indirect lighting cache就介绍到这里

UE4渲染模块概述(六)---动态光照

知道了UE4使用lightmap存储静态光的直接光分量,用采样点存储间接光分量。那么对于位置变化或状态变化的动态光源,又是如何处理的呢?

我们先从动态阴影入手,它是动态光照的重点部分。对性能有着非常大的影响。主要有四种类型的动态阴影。

第一种:常规动态阴影(Regular Dynamic Shadows)

这是最常用的动态阴影,我们在场景中设置一个移动式的动态光源(Movable),并将之配置为投射阴影(Cast Shadows),如下:

可以看到静态网格体的边角有着不合常理的锐利程度,如下:

第二种:逐对象阴影(Per Object Shadows)

我们在场景中摆放一个固定光照(Stationary)的光源,如下:

stationary的光照会混合使用静态光照的lightmap与动态光照的实时计算,它创建的阴影如下:

可以看到阴影仍然清晰锐利,但要比纯movable光源生成的阴影更自然。

第三种:级联阴影图(Cascaded Shadow Maps,CSM)

最经常遇到的CSM就是这个了,它是方向光(Directional light)的阴影生成方式。如果将生成距离调得很小的话,可以看到阴影逐渐生成的过程,如下图:

CSM的特点就是会根据视锥体的远近生成不同精度的影图,如镜头当离得较远时,可以看到阴影边缘有一条锯齿状的线条,如下图:

当相机逼近时,会用精度更高的影图进行渲染,如下:

就几乎没有锯齿了。CSM这样做的目的尽可能降低性能影响,同时会保证好各级分辨率之间的阴影有淹平滑的过度。

第四种:距离场阴影(Distance Field Shadows)

对于开阔的大世界,CSM也会力不从心,这时需要另一种系统,能够处理长距离范围的阴影,那就是距离场阴影。

为了投射阴影,我们需要知道点与点之间的距离 ,因而需要查询几体体之间的距离信息,动态计算会很慢,如果有一种方法可以预先计算并存储好距离信息,那么就可以大大加速这一过程。距离场阴影将距离信息存储于体积纹理中(Volume Texture),纹理的精度决定了生成阴影的细节与质量。体积纹理创建后看起来像这个样子:

它将一张二维的平面纹理切成多个部分,上下堆叠,形成一块立体区域,白色的位置告诉你对象的形状,可以从这些信息里推断出一个3D的对象,像下面这样:

可以看出是一把椅子,本质上是一张纹理,只是以3D显示出来。

距离场阴影可以通过下面方式进行激活,在Project setting里面选择Rendering子项:

钩上Generate Mesh Distance Fields就可以产生距离场阴影了,不过有一点要注意,因为是在编辑器里预计算的,所以移动一小块物件也会导致build很长时间的距离场阴影,会影响开发效率,可以在场景定型后再开启预计算。build完成后,可以在编辑器里可视化距离场阴影,如下:

如果仔细观察,可以看到它的细节并不好,如下面的栏杆所示:

 

以上介绍了渲染阴影的四个主要方法,下面来看下动态光是如何计算的。动态光源被渲染成球体,这个球类似于mask的作用,任何处于球内的点都会受到融合动态光shader的影响,举个例子,在雕像附近有一盏动态光源:

从计算范围来说,这盏动态光源就是这个球体:

球体内的任何像素都相当于遮罩,白色区域覆盖的像素需要计算动态光影响,而黑色覆盖的像素则不需要。

实际光源有颜色和亮度,如下:

通过之前计算的深度,再加上这个光的颜色、强度,以及作用范围,可以得到下面的图:

再由GBuffer中缓存的world normal,可以进一步优化光照效果:

至于动态光产生的阴影,需要光源到物体的深度信息。可以类似于反射捕获时用到的六面体Cubemap,以光源为捕获点,渲染只有深度信息的立方体贴图,以下图示了cubemap的两面:

这样我们就可以生成阴影贴图shadowmap了,将之加入到前面的图中,有:

最后一步与无动态光的图片进行混合,就有了:

作为结尾,视频中说到了一些性能优化的方面:

(1)确保动态光源的半径尽可能小,这样mask的区域就会小,特别在阴影的计算上可以省不少

(2)多个动态光源的尽量不要有重合的区域,因为重合区域内的像素要分别计算不同光的影响,这一点可以通过编辑器的可视化工具,查看light complexity。如下:

与之前的可视化工具类似,冷色调复杂度低,暖色调复杂度高。

(3)非必要情况下,还是优先考虑静态光的lightmap(大世界除外,因为lightmap会超级占内存)

(4)动态阴影非常非常费,因此可以关闭部分不重要光源的cast shadow选项,或者配置好动态光源的最大绘制距离(Max Draw Distance),超过这个距离引擎就会忽略该动态光源的计算。

以上就是动态阴影/动态光的基本原理了

UE4渲染模块概述(七)---半透与后处理

作为概述的最后一部分,主要介绍下半透和后处理。

半透

半透渲染的代表就是距离雾,所谓距离雾,就是说雾气随距离衰减。UE4有两种类型的距离雾,一种是大气雾,另一种是指数雾。同样,这也是基于Pixel Shader的。距离雾的示例如下:

将雾的颜色与深度图进行混合,就能产生距离雾的效果了:

对于半透物体的渲染来说,是延迟渲染不擅长的,一般来说有两种解决方案:其一是延迟到较晚的阶段处理,其二是这部分仍采用传统前向渲染,然后两者进行混合。

半透材质的渲染除了要绘制背后的像素外,还需要进行渲染排序,这就非常费,而且材质覆盖的像素越多,就越费。可以用前几章所说的shader complexity可视化来观察像素的复杂度,比如说下面这个图:

中间这一块红色的(表示复杂度很高)是透明的门,这些像素会不断叠加材质的着色器损耗,不过也有解决方案来减少半透效果的复杂度,比如用Masked代替Translucent,或者设置成不带光照:

后处理

后处理位于渲染管线的末端,同样也是依赖于Pixel Shader的,常见的后处理特效有:

LightBloom(泛光)

Depth of Field/Blurring(景深模糊)

Lensflares(镜头光晕)

Light Shafts(光斑)

Vignetee(晕影)

Tonemapping/Color correction(色调映射/颜色校正)

Exposure(曝光)

Motion Blur(运动模糊)

下面主要介绍泛光与景深,其他部分请参照原学习视频:

(1)Bloom

泛光很简单,首先需要查询每个像素的亮度,将亮度低于一定域值的像素染成黑色,这样对比度就会变高,如下图:

再以某种方式模糊这张图像,例如可以缩小图像使之模糊,然后将之放大回原来分辨率,就会得到很模糊的效果,如下图:

最后叠加某种类似镜片类尘之类的纹理,如:

就可以得到Bloom的效果了,如下图:

(2)景深

把深度图和模糊后的成像混在一起,模糊效果就会只出现在远距离位置上了。

下图是深度图,注意是越远越亮,越近越黑:

与下面模糊的原图进行混合:

就能得到景深的效果了:

把泛光和景深的效果合到一起,就得到:

 

这部分内容就介绍到这里,这样有关渲染概述的篇章就到此作结了,还是推荐大家有时间去官网看下完整的学习视频,相信一定还会有所收获。

视频所述内容对于渲染模块来说还是太粗了,最多只能说是一个目录而已,只有静下心里钻研细节,多动手实际操练,才能成为一个渲染专家。

我自己后续会有更加细节的UE4渲染管线的分析,也会有其他UE4模块的研究,打算是尽量多贴示例图来解释算法或逻辑流程,少贴大块代码段,降低大家的学习成本。

posted @ 2020-07-21 14:59  Tonarinototoro  阅读(2119)  评论(0编辑  收藏  举报