实时渲染基础
实时渲染是一个混合了多种不同解决方案的复杂过程,从本质上可以将其看作两个阶段:预计算阶段和具体的实时渲染阶段。
要了解会影响实时渲染性能的因素,首先需要清楚以下的基础概念:
目标帧率与毫秒
与目标帧率相关的一个重要概念是ms(毫秒),它代表渲染一帧所用的时间,数值越低则帧率越高。如30FPS对应的ms即为1000/30 = 33.33毫秒,60FPS对应的ms即为1000/60 = 16.67毫秒。在引擎的各种性能工具显示中都以ms为标准。
在评估性能时,应去掉引擎限制的帧率上限。可以用以下控制台命令来解除限制。
t.MaxFPS 600
另外,通过GPU Visualizer工具(快捷键Ctrl+Shift+,)可以查看渲染中的哪些过程耗费了大量的时间。
帧时间与GPU/CPU
使用stat unit
命令时,主要关注Game和GPU这两个的渲染时长,效率最低的一方会决定游戏的性能。
从上图可以看出,CPU(Game)处理的主要是逻辑和变换方面的计算,而GPU处理的主要是渲染方面的计算,二者并行(后文有详解)。
用stat rhi
、stat engine
、stat scenerendering
等命令可以看到更细致的性能数据。
最常见的四大性能问题
-
半透明
半透明物体由于要进行多层像素的叠加绘制,会导致较大开销。当半透明物体在屏幕空间上占比越大时,消耗也越大。 -
绘制调用(Draw Call)
Draw Calls绘制调用是实时引擎的渲染方式,它是逐对象渲染而非逐面渲染,所以如果场景中网格体数量很多,即使单个网格体面数很低,渲染消耗依旧很大,相比于场景中只有一个面数极高的网格体渲染要慢很多。同时,单个对象上材质数量越多,draw calls也越多。 -
动态阴影
要注意性能开销大的是动态阴影而非动态光照,当开启动态阴影时,保持场景中对象数量不变,增加每个网格体的面数,帧率会大幅下降,这是因为网格体的多边形数量会影响阴影的计算,所以动态光照下模型面数也是影响性能的一大因素。(当使用静态光照时面数并不会影响性能) -
像素/顶点着色器
引擎要依赖像素着色器完成几乎所有的渲染工作,不光材质,包括光照、反射和雾等都由像素着色器驱动。输出分辨率越高,材质越复杂,性能影响就越大。
实时渲染深入探究
首先了解一下实时渲染的整体流程:
注意上述步骤并不是完全依次执行的,有些会同步执行,如步骤3、4、5、6。
另外要记住,实时渲染性能最好的时候是当场景里什么都没有的时候。所以,实时渲染流程的本质是管理性能的损耗。(RTR = Real Time Rendering)
有获得必有牺牲,功能Features、品质Quality、性能Performance三者不可兼得。所以制定严格的资源和内容规范(如模型面数、贴图尺寸)就成为了很重要的一件事,因为可以提前预估将会产生的性能损耗。
延迟渲染与前向渲染
延迟渲染的特点:
前向渲染的特点:
从游戏角度看:
前向渲染适合制作简单的手游或VR游戏,且能够提供更好的效果,但当功能和场景越来越复杂时,每加入一些新资源都会导致性能大幅下降。
延迟渲染则更适合制作一些复杂的大型游戏,虽然一开始性能就会低一截,但再继续添加内容时,性能基本保持稳定,不会下降很多。
渲染之前和遮挡
CPU和GPU工作流
上文提到,CPU和GPU是并行工作,下面就详细解释一下:
由图可知,CPU和GPU会顺序对每一帧的画面进行处理,但二者之间会存在一定的时间差。
- 首先CPU进行计算,计算内容包括所有逻辑和变换,因为知道一切对象所在的位置是渲染对象的前提;
- 然后CPU和部分GPU会计算遮挡过程,剔除所有不可见对象,将所有可见对象记录在一个列表中。剔除方法包括距离剔除、视锥剔除、预计算可视性、遮挡剔除。这四种剔除方式性能消耗依次增大,引擎会优先使用消耗低的剔除方式来节约性能;
(注:预计算可视性需要在世界设置中勾选Precompute Visibility,并使用PrecomputeVisibilityVolume,它是通过构建光照将场景划分为许多个立方体空间,在每一个立方体内根据玩家或摄像机的位置,记录哪些Actor可见而哪些不可见,即把Actor位置的可视性状态存储在场景中)
- GPU开始实际的渲染过程
补充:遮挡剔除属于硬件剔除,部分移动端设备无法支持,则可以通过模型LOD实现软件剔除,对应模型属性中的LOD For Occluder Mesh,此功能并不常用。
遮挡相关性能提升须知
- 始终开启距离剔除(用CullDistanceVolume),例如当制作一个大楼的室内场景时,视线距离本身受限,此时是否用距离剔除虽然看不出差别,但如果不用,引擎就会使用其他消耗更高的剔除方式如遮挡剔除,产生不可见的性能损耗。
- 当场景中的对象数量超过1-1.5万时,基本就会开始对性能造成可见的损耗(与计算机硬件性能有关),达到3-6万个对象时,则性能损耗会很明显。因为无论此时屏幕空间中有多少可见对象,只要是有那么多的对象存在于场景中,引擎就会耗费性能去计算是否要剔除这些对象,所以这个计算过程就将对性能产生影响。用
stat initviews
命令可以看到这些开销。 - 上述消耗主要在CPU上,也有部分在GPU上。
- 大型开放场景无法很好地遮挡,因为很容易看到所有对象。
- 几乎任何可见对象都能被遮挡,包括粒子(通过Bounding Box计算遮挡)。
- 大型模型很少会被遮挡,因此会增加GPU的损耗。
- 将小模型合并起来变为大模型虽然会降低CPU的损耗,因为减少了引擎计算是否遮挡的次数,但是一旦屏幕空间中出现合并后模型的哪怕一个像素,这一整个模型都会被渲染。
以上是预计算阶段,下面将开始真正的实时渲染阶段
几何结构渲染
在正式渲染几何体之前,还要处理另外一个问题,即模型的渲染顺序。
前期Z通道
因为渲染是按模型逐个进行的,而非逐像素或逐线条渲染,所以当模型产生重叠的时候,会渲染大量冗余的像素。而解决方法则是依靠一个前期的深度Z通道进行测试。用很少量的信息去预先渲染环境,预先渲染几何体,知道每一个对象占画面的具体像素位置,由此来得到一个遮罩信息,其余被挡在后面的模型就根本不会渲染这块区域了。
绘制调用Drawcalls
- 一组共享相同属性的多边形(即一个对象)称作为一个drawcall。
- 一个对象有几种材质就会有几个drawcalls。
- 蓝图中每个静态网格体组件都要算作一个drawcall。
- 开启灯光也会增加drawcalls以及三角面。
- drawcall会成为最大的性能损耗,所以数量必须合理,2000-3000较为合理。
stat rhi
命令中的DrawPrimitiveCalls就是Drawcalls。
下面是一个简单场景的drawcall绘制流程,总共分了6步,即6个drawcalls。
- UE4会逐对象渲染,并且输出到GBuffer,然后输出到画面上。
- drawcalls的渲染顺序也由很多因素决定。上图的渲染顺序是因为它会按材质类型对场景对象进行分类,同样材质的放在一起渲染,减少切换材质渲染导致的硬件开销。
- Drawcalls所产生的消耗远比多边形面数要大得多,因为每渲染完一个drawcall,计算机就需要停下等待下一条渲染指令,而这之间的停顿时间就成了性能消耗的最大因素。
Drawcalls相关性能提升须知
- 渲染多面数的消耗一般来说比渲染多drawcalls的消耗低。
- 5万三角面根据具体实现方式可能性能比5千万三角面还要差。
- drawcalls有一个基础消耗,当模型三角面在1000-4000之间,再降低模型面数几乎不会影响性能了(渲染一个drawcall的时间一般以微秒 10-6 s为单位),当然开启动态阴影除外。
虽然合并模型可以减少drawcalls,但会对其他许多方面造成影响。
- 可能很难被遮蔽并被剔除。
- 光照贴图纹理所占用内存空间有上限,分辨率上限也会低。
- 碰撞信息会复杂,计算量会加大。
- 模型本身占用的内存大,会对流送产生影响。
所以最好使用模块化的流程来搭建场景,如果一定要合并模型(多选模型右键Merge Actors)来平衡性能和品质,也要留到最后面再去进行。
合并模型最好遵循以下的原则:
当然,如果性能问题不大,最好不要花费无谓的时间去合并模型!
补充:同一个模型在内存中会实例化,只占用一小块内存空间,但在渲染中相同模型并不会实例化,因为对于少量模型使用实例化静态网格体渲染方式,反而会增加性能损耗。实例化静态网格体渲染可以用于渲染海量对象,而且几乎不会有性能损耗,用于UE4的植被系统。
LOD
- 确保每个LOD的面数至少减为上一级别的50%。
- HLOD(Hierarchical LOD)是将对象编组,在远距离时可以将多个小模型合并为一个,同时减少面数和drawcalls。
着色器Shader
- UE4中有多种不同着色器:顶点着色器(Vertex)、像素着色器(Pixel)、 外壳着色器(Hull)、域着色器(Domain)等等。
- 着色器的用处可以简单理解为对输入变量进行计算得到对应的输出结果。
- 着色器系统的存在是为了解决CPU和GPU的自身架构对大量并行的简单运算性能不够的问题。
顶点着色器
- 顶点着色器将本地坐标中的顶点位置转换成世界坐标位置:一个模型,它的顶点信息都是以local坐标存储的,而local坐标的原点就是枢轴的位置,转换成world坐标就是把每个顶点相对于枢轴的坐标加上枢轴在世界坐标中的位置信息。
- 顶点着色器会处理着色:它会处理平滑过渡,处理边缘和柔化边缘,处理顶点的颜色。
- 顶点着色器能够应用偏移:世界坐标偏移(World Position Offset)。CPU并不知道这额外的偏移,所以也不会影响物理,只能用于视觉层面。布料、水波、植被风力,都是WPO的应用。
顶点着色器相关性能提升须知
模型顶点越多,应用顶点偏移动画越影响性能,所以高模只应该使用简单的顶点偏移动画,并且在远距离最好关闭动画。
光栅化、过度着色和GBuffer
光栅化
光栅化(Rasterisation)是把3D数据转换为像素的计算过程。
如果像素格中间点在三角形(蓝线)内部,则该像素会被上色。
需要注意的是,一个像素只可能表示一个多边形(面),而不会是不同多边形混合的颜色。
如下图所示为很小的三个重叠的三角形被光栅化后的结果,不同三角形的像素点会清晰地被划分开来。
当UE4渲染这些像素时,它们使用像素着色器来精确计算着色。纹理、材质、顶点着色信息都会影响最终像素的颜色。
过度着色
由于硬件设计的原因,这个着色过程并不是一个一个像素进行的,而是一个2×2的像素块来进行的。但是如果一个多边形特别小,无法占据4个像素格,这就是第一次过度着色(Overshading)。
如下图所示,灰色格子为像素格,橙色格子为像素块,一个三角形光栅化后本应只有绿色区域着色(绿色区域并不正确,根据像素中间点判断应只有右下角那块着色),但实际橙色区域全都着色了。
此时旁边又出现一个多边形,而这时候中间两块2×2的像素块又会重新着色一次,这就是第二次过度着色。
在UE4中,可以切换到Optimization Viewmodes里的Quad Overdraw模式,就可以看到哪些像素被过度着色了。一般来说过度着色不会超过4次,超过的一般就是透明对象。
对象离得越远,体积越小,越容易被过度着色。这意味着远距离观察的对象实际上渲染速度更慢。
过度着色相关性能提升须知
- 多边形如果密度越大,渲染起来损耗就更大。
- 从远处观察,密度会自动变大。
- LOD和距离剔除都可以改善这种多边形密集的情况。
- 初始像素着色器通道越复杂,过度着色损耗就越大。前向渲染的像素着色器通道损耗比延迟渲染更大,所以其渲染效率受过度着色影响更大。
注:在延迟渲染中,过度着色会增加损耗,但并不会太大,大部分情况下可以忽略。但对于使用前向渲染的手游或VR游戏,在性能优化时需要关注。
补充:这也是为什么在游戏制作中,模型的三角面应该保持均匀,避免出现过于上图所示细长的三角面,原因就是会造成大量过度着色。(现今硬件性能的提升,让这个问题基本可以忽略)
GBuffer
从这步开始,引擎不再依靠几何体计算结果,而只用存储在图片中的信息进行后续的合成处理。
GBuffer(Geometry Buffer)可以存储各种单独的通道:如世界空间法线、金属度、高光度、粗糙度、漫反射等等。(ZDepth从技术角度上不属于GBuffer)
一种特殊的GBuffer:自定义深度(Custom Depth),任意模型都可以开启该属性(在细节面板勾选Render CustomDepth Pass),引擎便会用单独的GBuffer来渲染模型,可以用于实现如轮廓特效或绿幕抠图等效果。
GBuffer相关性能提升须知
对于GBuffer其实并无太多用户可以优化的点,需要记住的是它对内存和带宽有很大的消耗,所以在渲染输出时会有限制。
渲染和纹理
纹理压缩
纹理在导入UE4时总会被压缩,压缩格式根据平台会有所不同,windows下为BC(Block Compression 块压缩)格式。
法线贴图使用BC5格式,可以仅存储红绿通道,并由此来计算蓝通道。
BC3(DXTC5)用于带Alpha通道的纹理,BC1(DXTC1)用于不带Alpha通道的纹理。
注意谨慎使用带有Alpha的纹理,因为Alpha通道基本不会被压缩,会导致纹理体积很大。
纹理压缩的原因:
- 内存和带宽有限制。
- 纹理分辨率会影响内存和带宽。虽不会影响渲染效率,但会导致延迟和卡顿。
多级渐进纹理Mipmaps
Mipmaps由许多只有原纹理1/4大小的纹理组成,它们都存储在纹理本身内并根据摄像机距离自动调用。
注:只有分辨率为2n的纹理才会自动生成Mipmaps,可以不是正方形。
使用Mipmaps既因为高清纹理在远处会产生大量噪点,同时还因为UE4要处理纹理流送。
纹理流送就是确定引擎在某一时刻根据玩家所处位置和摄像机角度需要加载哪张纹理和哪张mip,因为如果直接导入所有可能需要的纹理,会导致内存和带宽过载,且界面加载时间十分长。
着色器和材质
像素着色器
像素着色器和顶点着色器类似,是一组由GPU执行的运算。它可以同时执行大量简单运算来修改像素的颜色,对渲染管线极为重要,被用于渲染的每一个步骤中。
像素着色器在不同的平台上用不同的着色器语言编写,在DirectX上用HLSL编写。
补充:UE4在生成着色器时(编译材质),实际上不止是为每种材质只生成一个着色器,而是为材质的每种用途都生成一个着色器。在材质编辑器的Details面板的Usage标签下有许多种不同用途,所以默认情况下对材质做出修改后经常会看到UE4要编译大量的着色器。
下图完整描述了从输入几何体信息到通过各种着色器最终输出图片的过程:
PBR材质
这里不深入解释什么是PBR材质,而是去讨论为什么要用PBR材质。
主要原因是因为PBR规范很统一,方便预测使用效果以提升效率,并且它适用于GBuffer的工作流程。
着色模型Shading Model
着色模型是由G缓冲生成的纹理,是一系列遮罩图像,可以识别哪些像素要使用PBR之外的其他着色模型,用于分离与PBR不同的布料、毛发等专门的表面着色类型。
材质相关性能提升须知
- 材质/着色器的纹理采样器有最大上限,通常为16个,且经常只有13个可以用。但使用共享采样器时可以使用多达128张不同的纹理(只限DX11或DX12平台)。
- 纹理尺寸主要会引起延迟或卡帧,而不是帧率丢失。在游戏刚加载完时,可能会发现有的贴图由模糊变成清晰,这是因为电脑没有足够带宽或内存来快速传输完整分辨率的纹理,而使用了一张低分辨率的mipmap来代替。可以通过
r.Streaming.PoolSize
设定纹理池(Streaming Pool)大小来增加分配给纹理的内存空间解决该问题,纹理模糊说明已经超出了纹理池的大小。另外为了防止UI界面的纹理出现模糊情况,应把对应纹理的Texture Group改为UI,这样会自动禁用其Mipmaps。 - 像素着色器对于性能有很大影响,因为它涉及渲染流程的方方面面。材质复杂度越高,越会影响性能。通常来说一个材质100-200条指令才是合理范围。
- 像素着色器的损耗取决于屏幕上的具体像素数量,分辨率越高,损耗也会越大。
反射
实时反射计算很难实现,因为从某种程度上来说,每出现一次反射都需要重新渲染整个场景,但考虑到硬件性能并不能这么做,所以只能通过一些trick来近似模拟。
UE4中有三种不同的反射系统协同应用,每种都有其优点和缺点,多数时候的渲染效果至少使用了其中两种作为组合,以此来弥补缺点、强化优点。三种反射分别为:
- 反射捕捉
- 平面反射
- 屏幕空间反射SSR
反射捕捉
三种反射系统会按顺序执行,首先执行的也是优先级最低的就是反射捕捉。
反射捕捉意味着在一个特定位置捕获一张预先计算出来的静态立方体贴图(Cubemap),虽然非常快速,但并不精确且只有局部效果。(注:在Epic内部,基本只用Sphere反射而不用Box反射,虽然两种都属于同一种反射机制)
如果摄像机与反射球位置(捕获反射贴图的位置)重合,此时观察场景中的反射是基本精确的,但当摄像机位置发生移动后,就会发现场景中的反射明显出现了偏移。因为反射捕捉的本质只是捕获一张360°的图片并混合到模型材质上,在编辑器中打开关卡和运行游戏时会进行更新,但如果打包成游戏则会烘焙成一张贴图且不会再更新。
反射球有其范围,场景内的任何像素都会检查附近是否有反射捕捉,如果有则获取其Cubemap并融合到场景中。如果有两个反射捕捉Actor相互重叠,它们的反射也会混合在一起。
优点:性能最好,反射非常清晰锐利
缺点:反射结果不精确
平面反射
平面反射是一种刚出现不久,并不太常用的反射系统,它也是在给定位置捕获反射内容。
平面反射在某些设置下可能损耗很大,但非常适合需要精确反射效果的表面,它只在有限范围内起作用,且不适用于任何其他非平面。
平面反射的细节面板中默认勾选了Capture Every Frame,表明它不像反射捕捉一样是静态的,而是会跟随游戏动态更新的(这会导致消耗较大)。
优点:平面反射效果好
缺点:性能消耗较大,只能用于平面
屏幕空间反射SSR
唯一默认激活的反射系统,可通过以下命令关闭。或在后期处理体积的Rendering Features标签下把SSR的Intensity改为0。
ShowFlag.ScreenSpaceReflections 0
SSR是默认的反射系统,它能反射所有对象,并且是实时反射,它很精确,但输出结果噪点很多且损耗很大,只会反射当前可见的内容(在屏幕空间内的)。
优点:反射结果精确,实时更新,没有局部范围限制
缺点:性能消耗很大,反射噪点很多,屏幕空间限制
反射总结
由于三种反射系统各有利弊,所以在实际项目中会将他们混合起来使用。
优先级最高的是SSR,其次是平面反射,最后是反射捕捉。当某一区域没有反射时,引擎会从高到低寻找对应的反射信息进行填充,形成最终的反射结果。
最终反射结果会与材质的粗糙度、高光度和金属度蒙版进行混合。
反射相关性能提升须知
- 当一个项目未经过烘焙时,反射捕捉会在关卡加载时执行捕获,所以在关卡中添加过多反射捕捉可能导致加载变慢,在最终烘焙前应解决该问题。
- 当有许多反射捕捉重叠时,损耗会变大,因为像素着色器会一遍遍进行运算。通常情况下不应该超过8次重叠。
- 反射捕捉的清晰度(分辨率)可以在项目设置中进行设置,设置越高,内存占用越多。
反射球的基本放置思路是先放置一个大型的反射捕捉来大致覆盖整个空间,然后把许多较小的反射捕捉放置在反射程度高或者需要精确反射的表面附近。
知识点补充:
- 天空光照能为整个游戏世界提供低成本的备用反射捕捉,场景中任何附近没有反射捕捉的对象都会转而使用天空光照反射。(注:Sky Distance Threshold的值为裁剪掉设定值以内的对象,目的是只保留天空球的反射来生成Cubemap)
- 平面反射应当只在有绝对需要的时候才使用。
- 硬件性能有限时,应关闭三种反射系统中消耗最大的SSR。(比如VR游戏)
- 在性能够用时,可以把SSR质量提高以减少噪点。
r.SSR.Quality 4(默认值为3)
静态光照
实时灯光和阴影计算也很难实现,它们需要占用大量硬件性能,且计算速度很慢,因此光照的部分计算量被分流到了预计算阶段,这也就是静态光照。
静态光照是指所有预先计算而非实时渲染的光照。
静态光照流程的优缺点
- 静态光照会在编辑器中进行预计算,并将大部分结果存储在光照贴图中。
- 它在性能方面非常快速,但会增加内存占用量。
- 预先计算光照需要花费很长时间。
- 每当模型有变化时,都必须重新渲染光照。
- 模型需要光照贴图UV,这个额外准备步骤要花时间。
静态光照质量的优缺点
- 静态光照可以处理辐射和全局光照。
- 可以获得真实的阴影效果,包括真实而高质量的软阴影。
- 光照质量取决于光照贴图分辨率和UV布局。
- 由于UV布局的关系,光照可能会出现接缝。
- 光照贴图分辨率有上限。
- 非常大的模型会缺乏足够的光照贴图UV空间。
- 一旦计算完毕,在运行过程中就无法移除或改变光照和阴影。
光照贴图
光照贴图本质上就是一张烘焙有光照和阴影信息的纹理,它的最大尺寸为8192,它会和底色相乘来模拟光照效果。
在UE4中,光照贴图在光照重建过程中由称为Lightmass的流程生成。引擎不会创建单独的图象,而是会把所有光照贴图自动打包到一起形成图集(可在世界设置中查看)。
Lightmass
Lightmass是独立于UE4编辑器外,一个用于处理光源渲染和光照贴图烘焙的一个渲染器应用。
它支持网络分布式渲染,烘焙质量取决于光源构建质量(在世界设置中调节)以及Lightmass各个选项级别(Production,High,Medium,Preview),并且在场景中需要放置Lightmass重要体积。
间接光照缓存ILC
间接光照缓存用来解决动态模型上的预计算光照。
设想一下,当场景中的光照已经被Lightmass烘焙成光照贴图形式,此时一个动态角色在场景中移动,他该如何知道场景中各个位置的光照信息呢?
实际上,在编辑器的Show→Visualize中勾选Volume Lighting Samples,可以看到场景中出现一堆光点,这些点存储了间接光照信息。这是Lightmass在关卡中放置的光照样本,并在光照构建中计算了其间接光照。
当角色在场景中移动时,引擎会寻找距离他最近的点,查询该点的亮度并与角色自身颜色混合。
在每个动态模型中都有一项属性称为间接光照缓存质量(Indirect Lighting Cache Quality),默认设置为ILCQ Point,意味着它会获取最近的点,如果设置为ILCQ Volume,它会取最近的5×5×5个点,以此获得更好质量。
补充:光点在Lightmass重要体积内生成,主要会分布在地面附近而空中几乎没有,需要使用Lightmass Character Indirect Detail Volume才能迫使光点出现在空中。控制光点密度的属性在世界设置中Lightmass标签下的Volume Light Sample Placement Scale。
注:新版本中体积光照贴图(Volumetric Lightmaps)逐渐取代了ILC,具体信息见官方文档
静态光照相关性能提升须知
-
静态光照总会以完全相同的速度渲染。
-
无论场景中有多少盏光源,在烘焙后性能都是一样的。
-
光照贴图分辨率影响内存和文件大小,但不会影响帧率。
-
烘焙时间会根据以下4点增加:
- 光照贴图分辨率
- 模型和灯光的数量
- 高质量设置
- 衰减半径很大或者源半径很大的光源
另外,尽量保持光照贴图尺寸的一致性。可以通过修改模型的Overridden Light Map Res属性来实现。
动态光照
动态光照流程的优缺点
- 动态光照借助GBuffer实现实时渲染。
- 光源可以被随意改变、移动、添加或删除。
- 它不需要准备任何特殊模型就可以产生效果。
- 阴影对于性能的影响非常大。
- 渲染动态阴影有很多种方法,需要时间和实践才能找到合适方法和混合方案。
动态光照质量的优缺点
- 由于动态阴影性能消耗大,所以通常会降低渲染质量作为补偿。
- 动态光照不会对大部分内容产生辐射或全局光照。
- 动态光照通常比静态光照看上去更清晰,更具有现场感。
- 动态阴影不太需要考虑模型大小。
- 动态光照不会生成软阴影,效果不真实。
补充:让动态光照也能产生GI的实验性功能有SSGI和光线传播体积(LightPropagationVolume)。
动态阴影
UE4中有四种主要的动态阴影类型,和一些不太常用的类型。
- 常规动态阴影(Regular Dynamic Shadows):这是最常用且最重要的类型,也是所有可移动光源默认带有的阴影,此类阴影边缘过于清晰锐利,且没有任何全局光照,阴影内部一片漆黑。
- 逐个对象阴影(Per Object Shadows):或称为固定光源阴影(Stationary Light Shadows),它会混合使用完全依赖光照贴图的静态光照和动态光照,此类阴影边缘依旧清晰,但没有不合理的锐利感,且能接受反射光照,阴影不会一片漆黑。固定光源一般拥有最好的品质、中等的变化程度,以及中等的性能开销。
- 级联阴影(Cascaded Shadow Maps):CSM是且仅是定向光所使用的阴影,它会级联不同的阴影贴图,随着摄像机远近在不同分辨率的阴影间切换,并在一定距离外淡出消失。
- 距离场阴影(Distance Field Shadows):使用距离场信息而非几何体信息实现超远距离的阴影,不是很精确,但消耗很低。正常投射阴影需要知道各种点之间的距离,如灯光的方位,灯光离几何体的距离,几何体距离下一个击中的点有多远等等,询问并计算这些数据是一个很慢的过程。但如果用某种方法能够存储模型之间的距离信息,就可以花费更少时间来实时计算所有这些数据。距离场就是UE4中多种能够加速该过程的方法之一,但也是目前唯一真正实用的方法。它通过创建体积纹理(Volume Texture)用于阴影计算,纹理的分辨率决定了阴影的细节程度。(注:通常情况下分辨率很低,仅用来实现远距离的阴影,且无法应用于Skeleton Mesh)
补充:体积纹理是一个被切割成多块的2D纹理,每块叠加起来形成一个立体区域,通过白色部分可以得知对象的位置。另外,在使用距离场的时候场景中的Actor不应该有过度的缩放,应尽量保持比例为1或有微小偏差,否则生成的体积纹理会不准确。
其他阴影类型:
- 插图阴影(Inset Shadows):本质上与逐个对象阴影相同,在一些动态模型上可以获得更高分辨率的阴影。角色通常会默认开启,即便附近没有合适光源也能投射出高分辨率的阴影。
- 接触阴影(Contact Shadows):能在细小物体下投射效果不错的接触阴影。
- 胶囊体阴影(Capsule Shadows):简单同时损耗很低的阴影,用来渲染模型下方的阴影。
动态光照和阴影的渲染
动态光照和阴影是通过像素着色器来计算和应用的。
动态光照渲染:
(为简单起见,主要以点光源为例来介绍)
点光源像球体一样渲染,原理就像遮罩,球体遮罩内的任何像素都会进行着色运算来混合光照,而其余区域都不需要额外计算。如果有多个光源相互重叠,在Optimization Viewmodes中,可以查看Light Complexity,不同的颜色代表光源重叠的次数,类似过度着色,灯光重叠也要引起反复的计算,增大消耗。
深度通道和灯光通道合成可以得到光所能传播的距离,世界法线通道和灯光通道合成可以得到光所能照射到的表面。
阴影渲染:
渲染阴影需要知道四个信息:
- 摄像机到几何体的距离(深度通道)
- 摄像机的位置
- 光源的位置
- 几何体到光源的距离
前三项都已知,而几何体到光源的距离计算是导致速度变慢的地方,这就是为什么所有投射阴影的光源都会导致损耗。计算方法是把光源当成摄像机,再渲染一张只有光源深度信息的Cubemap。
下方左图为摄像机视角,右图为将光源当成摄像机的视角(只展示了一个角度,实则360°都有)。
最后将计算出来的阴影信息再叠加上去得到的光照结果。
动态光照相关性能提升须知
- 动态光照本身损耗相对较低。(注:过去会在前向渲染中损耗很大,但如今在延迟渲染和前向渲染中消耗都很低)
- 损耗源于像素的着色运算,像素越多速度越慢。
- 光源距离摄像机越近,受影响的像素就越多,导致光源渲染的速度就越慢。
- 光源的半径应该尽可能缩小。(注:取消勾选Use Inverse Squared Falloff,配合调整Light Falloff Exponent)
- 防止光源重叠过多。
动态阴影相关性能提升须知
- 关闭不需要的阴影。
- 几何体的面数会影响阴影的性能。在动态阴影相当复杂的环境中,需要降低多边形数量。
- 用距离场阴影可以减少上述的影响。
- 距离场阴影最适用于边线笔直、棱角分明的几何体模型。因为距离场本质上是纹理,而纹理像素在表现斜线时效果不佳。
- 可以在模型远离时淡出或关闭阴影。(注:每个灯光的Performance标签下都有Max Draw Distance属性,用于关闭光照和阴影)
混合使用静态光照和动态光照
静态和动态光照各有优劣,混合两种光照通常是最佳的方式:
- 将静态光照用于微弱和远距离的光照。
- 用静态光照来渲染摄像机附近的间接光照。
- 在静态光照的基础上用动态光照来突出着色和阴影,即在前者结果上生成一张具有交互效果的图层(或直接用固定光照来替代这两种)。
其他两条更基础的规则:
- 如果需要尽可能高的性能,请只使用静态光照。
- 如果想随时修改光照效果,请只使用动态光照。
雾和透明度
雾
UE4有两种类型的距离雾,大气雾和高度指数雾,以及一种局部体积雾。
距离雾意味着雾随着距离消退,同时它们也是高度雾,在接近天空时消散,这些效果都是像素着色器通过深度信息和雾的颜色(基础颜色和光照影响)来生成的。
大气雾的性能更好,指数雾的效果更好。
透明度
透明度对所有延迟渲染器都造成了困难,因为在延迟渲染中只有GBuffer可以使用,但其并无法提供足够的信息来正确渲染透明度。因此对于透明表面的渲染通常推延到渲染流程中非常靠后的阶段,或者单独通过前向渲染再合并到延迟渲染流程中(这种方式损耗高但效果较好)。
在透明材质的细节面板中,会有Translucency标签,在Lighting Mode中提供了多种模式,其中Surface ForwardShading会提供最好的渲染效果,并且最好勾选上Screen Space Reflections。
透明度相关性能提升须知
- 当以最佳质量渲染透明度时,就像素着色器的运算次数而言损耗很大。
- 如果有许多图层覆盖了相同像素,并且有大量像素被覆盖时,透明度造成的损耗会非常大。
- 除了像素着色器本身的损耗外,渲染排序也会加重损耗,它很缓慢并且容错率很低。
- 透明材质覆盖的像素越多,材质就应该越简单。
- Default Lit模式的透明材质比Unlit模式的透明材质损耗大得多,Translucent模式比Masked模式性能消耗大很多。
后期处理
后期处理是基于并再度利用GBuffer去计算各种视觉效果,它也非常依赖于像素着色器。
如Bloom,Depth of Field,Color Grading实则都是通过某种方式对图片进行处理再合成的结果。
LUT(Look Up Table)
LUT是一种记录了色彩校正信息的条带,支持记录多达4096种色彩偏移并应用于图像中。
从整体看,从左到右每格蓝色依次增加,共16格。左上和右下是唯一的纯黑色和纯白色。
从局部看,从上到下每格绿色依次增加,从左到右每格红色依次增加,分别各16格。
应用方法:在原图中取任意一个像素的色彩,找到LUT中最接近的色彩,并替换回原图中,实现色彩偏移。
结尾
本文为对Sjoerd de Jong的官方教程的记录,有助于对实时渲染有较为全面深入的了解,并且掌握它对性能、功能和工作流的影响,但教程中未涉及到的知识点还有很多,如:次表面渲染(Sub Surface Rendering)、折射(Refraction)、置换贴图(Displacement Mapping)、屏幕空间环境光遮蔽(Screen Space Ambient Occlusion)、用户界面(Interface and UI Rendering)、贴花(Decals)等等,仍需读者继续去探索和学习。