深入了解unreal实时渲染

unreal引擎官方给出了unreal-4如何渲染一帧的介绍,结合renderDoc插件,分析渲染过程,并且给出各个环节的性能瓶颈与解决方案。

本文用以记录,学习,分享。如有错误,烦请指正。

原文链接

An In-Depth Look at Real-Time Rendering

概述

概述之概述

这个系列课程旨在揭示和可视化整个渲染过程,对unreal所有的特性、挑战以及解决方案做一个全局的探讨,使得读者能理解unreal的设计蓝图,也能理解这一套工作流的利弊。

当什么都没有的时候,实时渲染RTR是最高效的。RTR主要讨论的是一种平衡,性能与渲染质量的平衡。站在项目角度,应当在开发前明确目标帧率,在目标帧率的约束下,尽量用最少的性能换取最高的画面表现。画面表现,性能,渲染特性,三者有得必有失。

现实是非常复杂:
1.所有细节都需要被高效的绘制
2.需要一个稳定的工作流程和限制条件
3.结合使用预计算来离线渲染以提高效率
4.多种方法的耦合

unreal用到的特性有:

其中可见哪些是预计算的,哪些是实时计算的。几种计算反射与光照的方法在离线阶段完成。

由点到面

一切渲染的源头,是由三点构成的三角面片,点标志着三角形的位置,面片显示三角形的颜色与图案,虚幻世界的一切模型都是由这样的三角面片构成的。

计算机的两个核心元件,CPU与GPU共同肩负着渲染的责任。他们处理渲染流水的不同工作,都具有其瓶颈。

unreal采用的渲染流水线

unreal游戏场景视口的渲染主要采用延迟渲染管线(Deffered Rendering),在这个流程中的某些环节采用前向渲染管线(Forward Rendering)。

前向渲染

每进行一次完整的前向渲染,都需要渲染该对象的渲染图元,并计算两个缓冲区的信息,一是颜色缓冲区,二是深度缓冲区。利用深度缓冲区来决定一个片元是否可见,如果可见即更新颜色缓冲区中的颜色值。乐乐用下面的伪代码解释这一过程[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Pass{
for(each primitive in this model){
for(each fragment covered by this primitive){
if(failed in depth test){
//如果没有通过深度测试,则该片元不可见
discard;
}
else{
//如果可见则进行光照计算
float4 color = Shadeing();
writeFrameBuffer();
}
}
}
}

延迟渲染

延迟渲染包括两个过程,在第一部分,不进行任何光照计算,仅仅计算哪些片元是可见的(主要通过深度缓冲实现)。如果发现一个片元可见,则把相关信息存储到G缓冲区(信息包括表面法线,视角方向,漫反射系数等),然后在第二部分中,利用Gbuffer中的信息进行真正的光照计算[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Pass1{
for(each primitive in this model){
for(each fragment covered fy this primitive){
if(failed in depth test){
//如果没有通过深度测试,则该片元不可见
discard;
}
else{
//如果可见则写进GBuffer
writeGBuffer(materialInfo, norml, pos);
}
}
}
}
Pass2{
for(each pixel in the screen){
if(the pixel i valid){
readGBuffer();
float4 color = shading();
writeFrameBuffer(pixel ,color);
}
}
}

利弊

延迟渲染与前向渲染


渲染准备

要将画面从数据呈现到屏幕中,需要CPU与GPU的交替配合,流水线使得工作效率更高。

CPU与GPU的逐帧执行流水线

在Frame0时:
完成场景中所有物体的位置计算;

在Frame1时:
依据远近、相机视锥盒子、遮挡关系进行剔除,得到最终需要渲染的物体,并将渲染信息发送给GPU;

在Frame2时:
GPU进行绘制,先计算深度缓冲Zbuffer,再进行GBuffer的计算,渲染各种特性。理解draw call,GPU的性能与draw call紧密联系。通常场景中的同种材质的图元 在一个draw call中调用渲染。


几何渲染

draw call对性能的影响

Drawcall就是一个命令,cpu发起,gpu接收,这个命令仅仅指向一个需要被渲染的图元列表。DrawCall发出后,一般影响的是GPU端的绘制,因为draw的过程是图形的准备及绘制过程,大部分在GPU中处理(设置绘制上下文--绘图方式--顶点坐标--绘制)。

draw call调用次数对游戏性能的影响,大于模型面数对性能的影响。比如有时候 ,50000个三角形会比5000 00000个三角形渲染起来更慢(如果draw call次数过多的话)。因为draw call有着基本的性能消耗,所以将一个低量级的三角形数量优化到一个超低量级的数量往往对性能提升没什么用。

如果一个actor有多个component,那么每个component单独作一次draw call,一个接着一个的渲染,所以把很多component从多个actor,集合到一个actor中,并不会提升性能

降低draw call的一种方法是使用更少的大模型代替很多的小模型。于是推荐使用 Modular Meshes 技术合并网格,同时利用Statistics工具和Stat命令,但是一旦合并就很难再恢复原样,所以尽量在项目的后期优化时再合并网格。如果面对目标机型为低端机的,可以利用合并网格技术合并几乎所有网格以提升性能。
另一种降低draw call的方法是Level of Detail(LOD):给定条件下简化模型,比如随着与相机的距离变化简化模型。unreal采用了HLOD,成组的简化模型。

Shaders之始,Vertex shader

shader(着色器)是跑在GPU上的小程序,贯穿渲染流水线的整个过程,shader的种类有很多,渲染流水线执行的第一个shader便是 vertex shader。

主要作用是将输入的顶点从局部坐标系,变换到世界空间坐标系。还有别的用途,比如处理顶点着色,也用作顶点位置的额外偏移,比如草地与水的偏移。

值得注意的是,顶点坐标便宜并不会改变其实际的坐标信息,只是视觉上的一种变幻而已,

vertex shader作为渲染的一部分,也对性能有着影响。着色器越复杂,运行越慢;顶点越多运行越慢。因此高量级的模型只应该参与简单的vertex shader。

光栅化与GBuffer

光栅化与OverShading

光栅化就是将图元渲染到像素构成的网格中。 每次draw call都要完成一次光栅化。
一个像素点只能代表一个三角形。同时一个像素点甚至可以代替一个面数非常大的模型(模型离相机非常非常远的情况下)

当光栅化结束,每个像素点都使用pixel shader来进行更精确的像素计算,这个过程需要更多的图元信息,比如纹理。(OpenGL中,使用fragment shader,Direct中叫pixel shader),将在下文介绍。

OverShading在两个地方出现。第一处是光栅化的时候,unreal对像素点的管理采用以2X2的模块单位进行分组,这意味着如果三角形某个点只占用2X2中的1个的时候,也会会整个2X2进行计算。第二处是当模型分布不合理时,导致某一处像素组,发生大量重复的计算与替换。

Gbuffer

GBuffer是延迟渲染管线中的关键概念之一。在这一过程中,将画面的多种信息计算加载到不同的缓存中,最后合成一帧。

GBuffer包含的信息图:Normal(A), metalic value(B-r), specular value(B-g), roughness value(B-b), 
without lighting value(C), special pixels(D), depth buffer(E)

unreal游戏场景视口中可看见Gbuffer信息

像素着色器和材质

纹理

说到纹理,就要说纹理压缩技术,每个平台的压缩技术都不一样,DXTC(又叫BC)常见于PC平台。法线贴图常使用BC5压缩技术。

纹理影响存储和带宽,而并不影响渲染性能,于是要考虑压缩技术。

为了最大化纹理的利用效率,大部分引擎包括虚幻,都是用mipmaps技术(MIPS),即多级纹理。与LOD原理差不多,MIPS会依据距离加载同一张纹理的不同的级别显示。

pixel shader

像素着色器是渲染管线的核心,是GPU运行的对像素进行着色的一系列计算。像素着色器驱动着虚幻引擎整个材质系统,也驱动着光线,雾,反射,屏幕后处理,等特性。

像素着色器可以自由选择屏幕中需要着色的像素点,依据mask图像。

像素着色器用shader语言书写,每个平台的语言都不一样,比如DirectX API使用的着色器语言是HLSL

材质

在ue中,材质系统是除了几何体数据外的所有其他数据。包括光照模型,分布函数,各种渲染状态,各种渲染分支,以及一个提供给用户的节点图等等。
材质系统的很大一部分特性是基于PBR的。PBR使用Specular/Metallic/Roughness来处理所有的着色。

材质系统有个设置,shading model,是一些mask用于确定哪些像素使用非PBR着色模型,然后这些像素点采用另外的渲染路径。

UE4 Shader生成分两部分,第一部分是把材质编辑器中的节点图编译成HLSL代码,这一部分是通过FHLSLMaterialTranslator来完成的。
UE4 Shader生成的第二部分是把HLSL生成多平台的Shader代码,如Windows上的HLSL,Android上的GLSL,IOS上的MetalShader。

一个材质/着色器的最大纹理采样器使用数量通常是16,其中通常有13个可以使用。开发者可以使用128个共享采样器(DX1以上);

纹理大小主要导致延迟,而不是帧率损失;

像素着色器影响很大,因其对游戏运行十分重要;

分辨率越高,复杂材质对性能的影响越大

一个材质会编译生成很多个shader

从render的角度, 具有同样材质实例的mesh 会在同一批次渲染,即在一个draw call被绘制,即使是不同skeletal mesh 上的材质。但就算是同一个skeletal mesh的不同材质,他们母材值一样,但是材质实例不一样,也是会调用不同的draw call。所以为了尽量减少draw call,合并texture使得不同的mesh用同一个材质实例是一个方法。


反射

实时计算反射非常难,unreal提供了三种工具来计算反射,皆有其利弊,但是好好利用这三种技术,将反射与渲染结果相结合的话,会提升效果表现。

反射捕捉

  • 在一个特定的位置捕捉一张静态的cubemap
  • 预计算
  • 非常快
  • 不精确
  • 只能够用于捕捉点位置的反射效果

屏幕反射 Planar Reflection

Screen Space Reflections

  • 并不常用,发生在平面上的捕捉
  • 非常耗
  • 对于需要精确反射效果的光滑平面适用,其他情况就不太适用
  • 只在有限区域适用

屏幕空间反射SSR

  • 虚幻默认的反射系统
  • 实时计算,影响场景中的所有地方
  • 准确的反射
  • 输出信息有噪声
  • 中等消耗
  • 只能显示 当前可见的集合体的反射信息
  • 在”post process volume”组件中

总结

所有这三种反射一起影响最终的反射环境

如果项目没有为分发进行烘培的话,反射捕捉会在关卡加载的时候进行。因此太多的反射捕捉会降低loading的速度(反射捕捉超过千个的话,引擎直到最后烘培前都不well)

反射捕捉的精度可以在引擎中调整

天空盒是一个性价比很高的选择

平面反射仅推荐在非常需要的时候使用

当项目对硬件的要求不高时,建议关闭SSR;如果电脑性能跟得上,那么SSR质量调整随意


静态光源与静态阴影

虚幻主要有两种方式处理光照和阴影,针对静态光照的方式 与 针对动态光照的方式,或者说是预计算与实时 两种方式。光照和阴影一般是分开处理的。

方法的利弊

处理静态光照,是在编辑器中预计算,然后存在光照贴图中;

非常快,但是消耗存储

预计算时候花费较长的时间

只要场景中有东西改变,都要重新渲染一遍

模型需要光照贴图的UV,这个额外的映射也需要时间

质量的利弊

有效的处理辐射和全局光照

产生具有软阴影的物理阴影

质量依赖于lightmap的分辨率和UV映射

lightmap分辨率是有上限的

非常大的模型不会有足够的lightmap uv 空间

一旦预计算完成,光线和阴影不会实时的变化与更换

Lightmass

Lightmass是生成Lightmaps的进程,是一个独立的工具,支持基于网络的分布式渲染。烘培质量由Light Build Quality还有每个关卡的Lightmass决定。

Indirect Lighting Cache

为了解决动态模型的光线预计算。ILC在场景中基于光线缓存,体在每个单位上放置了向量。每个向量都存了当前位置的光线强度。在运行时,距离动态模型最近的5X5X5个向量将会被考虑。

静态光源的表现及影响

静态光源经常以用同一种速度渲染

烘培之后,一个光源 还是五万个光源,对于性能表现不大

lightmap分辨率影响的是存储和文件大小,并不影响帧率

提升烘培时间可以考虑:

 a. lightmap 分辨率
 b. 模型/光源的数量
 c. 具有大衰减半径或源半径的光

动态光源与实时阴影

unreal项目中对性能影响最大的几个因素:

  1. drawcall的数量,几何体渲染
  2. 透明物体渲染
  3. 像素着色器
  4. 动态阴影

方法的利弊

  1. 使用GBuffer进行实时渲染
  2. 光源可以被任意更改/移动/替换
  3. 不需要任何特别的模型上的准备
  4. 阴影是最耗的
  5. 有多种方法渲染动态阴影,依据使用场景的不同来设计应用阴影的解决方案

质量的利弊

  1. 因为阴影对性能影响很大,所以经常损失质量来弥补性能
  2. 对于大部分资源,都不会进行辐射计算和全局光照计算
  3. 动态光源比静态光源更尖锐和显眼
  4. 动态阴影通常是大小中立的,不像静态阴影计算方法那样依赖模型大小
  5. 动态阴影很难做好

实时阴影

为了计算阴影,需要知道两点的位置,为了得到距离集合体表面的距离,需要查询和比较。这是很慢的。

实时阴影对性能影响明显,关掉一些光源的”shadow casting”选项对性能有显而易见的提升。

unreal引擎中主要有四种动态阴影的方法,以及一些不常用的方法:

  1. Regular Dynamic Shadows - 最常用的方法
  2. Per Object Shadows - stationary light shadows
  3. cascaded shadow maps (CSM) - Directional light shadowing
  4. Distance field shadows - 使用距离场信息进行计算,而不是实时追踪集合体

不常用的有:

  1. inset shaodows
  2. contact shadows
  3. capsule shadows

渲染过程

在像素着色器中计算,动态光源作为一个球体来渲染,这个球的作用就像mask一样。任何在球内的物体都需要一个像素着色器操作来混合动态光源信息。

实时光照的影响

  1. 实时光源在延迟渲染管线中影响不大,但在前向渲染管线中影响很大

  2. 实时光源的计算,会激发更多的像素着色器,而成本取决于像素着色器操作,像素越多,速度就越慢

  3. 光源离相机越近,照亮的像素就越多,运行就越慢

  4. 光源的半径最好尽可能的小

  5. 避免重复的光范围覆盖,引起更多的像素着色器计算。

  6. 如果不是一定需要的话,把阴影关掉

  7. 几何体的三角面片数量也影响动态阴影

  8. 尽量考虑用距离场来简化计算

  9. 距离场最适合用于具有硬直边几何形状的实体模型

  10. 当距离较远时,淡化或关闭阴影

对于阴影方案的选择

  1. 静态阴影与动态阴影相结合是最好的方案
    1. 弱光 离得比较远的光源 用静态
    2. 相机附近的间接光照 用静态
    3. 在静态灯光之上使用动态灯光,以更好地突出阴影和阴影,并在静态结果之上提供一个互动层

综合,有两个基础准则

  1. 只有在需要尽可能高的性能时,才使用静态
  2. 只有当你需要能够在任何时候自由地修改灯光时,才使用动态

雾与透明物体

fog

透明物体

延迟渲染管线在处理透明物体时显现了弊端(延迟渲染只有GBuffer信息,而计算透明物体需要更多的表面信息),因此透明物体都是在最后一个stage再开始处理,或者单独用前向渲染计算,最后与延迟渲染的结果合并。

透明效果的影响:

  1. 当以最好的质量渲染时,透明物体的需要计算更多的像素着色器
  2. 当许多层覆盖同一个像素时,透明物体的计算更加高昂。
  3. 除了像素着色器的消耗,透明物体的渲染顺序也是个麻烦的事情,很容易出错

建议:

  1. 如果非得使用透明物体的话,建议不要把view mode 设置为 default lit,改为 Unlit,会节约很多性能。如果非得需要绝佳的性能,把lighting mode设置为 Surface ForwardShading
  2. 预计透明材料覆盖的像素越多,一般来说,它的材料应该更简单
  3. 讲光线考虑进效果的透明材质,比不发光unlit的透明材质,消耗更多性能。因此,透明物体,能避免光线就避免光线。

更多

  • 次表面渲染
  • 折射
  • displacement mapping
  • 屏幕环境遮罩 SSAO
  • 交互界面 UI
  • 贴花 Decals

后处理

后处理在渲染流水线的最后计算,再次依赖于像素着色器,基于合成并重用GBuffer来计算其效果。

常见的后处理效果:

  • light bloom
  • depth of field/Blurring
  • 镜头光斑
  • 光束
  • vignette 晕影相机?
  • 颜色矫正
  • 曝光
  • motion blur

参考及推荐阅读

1. Unity Shader 入门精要

2.《大象无形-虚幻引擎程序设计浅析-罗丁力》