游戏引擎中的渲染实践
游戏引擎中的渲染实践
理论图形学

- 游戏渲染的理论基础和图形学中的渲染是一样的。
不过渲染研究更关注于算法理论的正确性而对于实时性没有太多的要求,而在游戏渲染中实时性则至关重要。对实时渲染的关注构成了游戏渲染和渲染理论之间的主要差别。 - 这种理论模型没有特定的性能约束,根据最后的开销决定应用的场所
- Realtime 实时效果 // 20+
- interactive 可以交互的实时 // 10+
- offline rendering
out-of-core rendering // 需要花费大量时间去计算获得电影CG的效果
游戏渲染的四大挑战

- 游戏场景往往包含各种不同类型的渲染对象,同时需要在场景中实现光照、大气、天空、水面等不同的渲染效果。
→ 在短时间里处理大量的数据和构建各种不同效果.

- 在图形学中我们不太关注渲染在硬件中的实现过程。
而在游戏引擎中,为了充分利用计算资源则需要考虑渲染过程的硬件实现。
→ 需要深度适配不同的硬件架构,让算法的速度足够高效.

- 人们对于游戏画质和帧率的要求逐渐提高。现代3A大作往往需要适配4K的画幅并且保证不同场景下有着足够高的帧率(60-120FPS)。→ 在任意场景都必须保持稳定的帧率,但是预算是固定的.

- 游戏引擎除了渲染系统外还要为其它物理、逻辑系统提供支持,因此我们不能让渲染系统占掉全部的CPU计算资源。
一般来说渲染系统只能占掉10-20%的计算资源,把省下来的资源让给其它玩法系统。
→ 渲染需要CPU提交数据,但CPU预算的大部分要交给非图形学的运算.
游戏引擎的渲染
总体来看,游戏引擎中的渲染系统是一个庞大的软件工程系统。在整个游戏行业几十年的不断实践中总结出了大量有效的渲染技术。

- 引擎的渲染不是一个理论模型
- 它是通过工程实践验证,在行业中不断迭代和优化的软件工程系统,是一个实践科学。
- 随着硬件的变化会快速地更新
游戏渲染课程的内容划分

游戏渲染基础
目前游戏引擎渲染的主流方法仍是基于**光栅化(rasterization)**的渲染管线。

首先我们需要把场景中的物体投影到NDC上,然后分别计算平面上每一个像素对应的渲染对象。



- 绘制的最核心工作就是计算(computation)
- 游戏引擎的绘制系统就是做躲在像素之后的事情, 换句话说就是完成生成像素前面的工作
- 纹理采样需要采样8个像素点的数据,要做7次插值
显卡的基础架构
渲染计算的特点是有大量的像素需要进行计算,而像素之间的计算则往往是相互独立的。
因此人们设计出了GPU来执行图形渲染计算,这样还解放了CPU的计算资源。可以说现代GPU的发展也推动了整个游戏行业渲染技术的进步。

- SIMD 单指令多数据
- 一个四位向量的加法运算可以靠一个指令完成
- 现代CPU已广泛采用了SIMD的数学运算
- C++通过SSE调用SIMD加速数学运算 https://zhuanlan.zhihu.com/p/325632066
- SIMT 单指令多线程
- 通过大量的内核并行进行计算提速
- 做绘制运算时尽可能使用同样的代码一起运算,而分别访问不同的数据,这样可以利用SIMT提速

- 在现代GPU架构中有着大量重复的内核,每一组内核称为一个GPC(图形处理集群).
- 图中左下角有4个GPC
- 在每个GPC内部存放着大量的SM(图中右上角),而每个SM中还有着大量的CUDA核心用来执行数学运算,当SM接收到指令进行计算时会把运算分配给CUDA核心进行并行计算。
- GPU上还有share memory用来实现GPU上不同核心以及GPU和CPU之间的通信。
- Refer: https://zhuanlan.zhihu.com/p/393485253

- 数据流动是有成本的
- 把CPU的数据通过南桥北桥上传到显卡是非常慢的(如图)
- GPU和CPU之间通信的代价是非常大的,因此在渲染系统中会尽量把数据通信设计为单向的。
这样GPU只需要读取CPU发送的数据而无需反向传输渲染的结果。

- 确保cache hit来降低数据流动的开销

- 知道GPU的性能瓶颈在那,做对应的优化
现代硬件管线的变换

- DX11 --- 曲面细分的shader → DX12 --- mesh shader

- 不同的主机和设备上也往往有着不同于常见GPU的架构设计。
如上图---使用内存共享(Unified Memory Architecture : UMA)的架构

- 手机移动端上考虑到功耗问题,通过Tile-Base-rendering 一小块一小块的渲染出来的
Info
开发时需要关注不同平台的硬件的瓶颈
可绘制物体 Renderable
在进行渲染时我们只需要考虑那些需要进行渲染的GO,它们称为可渲染对象(renderable)。

- MeshRenderComponent里面保存了一个Renderable(可绘制物体)是绘制系统的核心数据对象。

一般来说我们可以把整个可渲染对象拆分成若干个block,每个block有着自身的网格、材质等渲染信息。


对于网格数据,我们需要存储网格上所有的顶点坐标以及每个面包含节点的编号。同时我们往往还需要为每个顶点单独存储一个法向来处理曲面发生突变的情况。

- 为什么每个顶点要单独定义法线?
- 每个三角形可以算出它的法线朝向,用临近的三角形可以算出顶点的平均朝向
- 如果表面是硬表面(有折线的时候),两点顶点的位置重合,但是法线完全不同
- 如图右所示我们需要单独记录法线,确保准确性
材质

- 来自于真实的生活,接近于对物理世界真实的感知,比如石头,金属
- 绘制系统中定义的只是视觉材质,物理材质会单独的去定义它

- 成熟的材质模型

- 表达一个材质的时候,纹理对于材质的定义以及最终渲染呈现的效果起着至关重要的作用。
→ 材质像什么大多时候并不是由材质的参数决定的, 而是纹理。
Shader

- shader是源码(需要coding),但是会作为数据处理.
- 术语:bloom(二进制的数据块)
- shader graph :
artist通过搭积木的方式把各种元素进行组合,生成一个shader代码.
这个shader代码会编译成一个数据, 这些数据会和mesh存储到一起来形成各种显示效果. - shader的代码是关键的Renderable数据
在引擎中绘制物体
接下来我们就可以对GO进行渲染了。根据光栅化的渲染管线,我们首先利用MVP变换把模型转换到屏幕空间上,然后把渲染数据提交给GPU就可以实现渲染的过程。


- 提交renderable数据和一小段代码(shader), 还需要整理数据
- 一个Gameobject有多个材质直接处理会和原版差异很大
- 然而这样的渲染过程往往不会得到令人满意的渲染结果。
实际工程中我们往往需要把一个完整的网格拆分成不同的submesh,每个submesh有着自己的材质和纹理而整个网格共享一套顶点和面片信息。
这样利用submesh的概念就可以绘制出更加逼真的图像。

- submesh :
把GO的mesh根据材质不同切分成不同的子mesh. - 每个submesh会对应自己的材质,纹理,shader.
但是顶点会统一放到大的buffer上去,通过offset去访问.

当我们需要绘制大量GO时,如果每个GO都使用单独的网格信息则会造成存储和计算资源上的浪费,实际上很多GO和submesh都共享了相同的材质、纹理甚至是shader。
因此为了更高效地利用计算资源人们还提出了**资源池(resource pool)**的概念。
在资源池中我们把所有的网格、材质、shader等资源分别集中到一起,在进行实际渲染时对每个对象分别去寻找对应的数据和资源即可。

- Mesh Pool, Texture Pool, Shader Pool 存储相同的资源.

- 将绘制物体通过submesh拆分并构建资源Pool后,再通过索引去关联Pool中的资源,最后通过实例化(instance)绘制到屏幕上.
- 实例化是常用的资源整合利用方式.

- GPU上切换数据会非常慢,对显卡的状态设置好尽量不要动。
- 为了更高效地利用GPU,我们还可以把场景中的submesh按照材质进行排序。
这样可以保证渲染时具有相同材质的submesh会放在一起进行绘制,从而降低GPU切换资源的开销。

在很多游戏场景中还存在着大量相似甚至是完全相同的GO。
对于这种情况可以通过GPU batch rendering的方法把这些GO组织在一起,然后把同一batch中的对象一次性绘制出来,进一步提升场景渲染的效率。
Visibilty Culling 可见性裁剪
在游戏场景中一种常见的情况是整个场景内有大量的可渲染对象,但在玩家视野内则只有有限数量的单位。
在这种情况下如果直接把场景中所有的可渲染对象送入渲染管线无疑会造成计算资源的浪费。

- 因此**可见性剔除(visibility culling)**是渲染系统中非常实用的技术,它的思想是在送入渲染管线前首先判断场景中的每个可渲染对象是否在相机视野中,然后只对视野范围内的对象进行渲染。
通过视锥体基于GO的包围盒进行裁剪

- visibility culling的核心是把可渲染对象使用bounding box进行表示,然后通过bounding box来迅速判断物体是否在视锥范围内。
我们通过bounding box将场景中的物体组织起来,这样在渲染时只需要通过对它们进行遍历就可以快速地实现visibility culling。 - 各种包围盒是很多计算的基础, 比如碰撞和视觉裁剪

- 计算裁剪的时候从顶层到底层去询问

- 在现代游戏引擎中,BVH是应用最为广泛的bounding box。
- BVH的一大特点是它可以在场景中物体发生运动时通过对节点的操作来动态地修改树的结构,这样无需每次都重新建树从而大大提高了计算效率。
- BVH对物体的动态添加和删除处理很高效


- PVS早期是用于可视化裁剪的
- 在游戏设计中,**PVS(potential visibility set)**是一种非常实用的技术。
它的思想是把整个场景划分为若干个相对独立的区域,不同区域之间通过portal进行连接。
当玩家在场景中进行游戏时只会在某个区域中,而这个区域内的可见性是可以事先确定的,这样就可以利用PVS来进一步剔除无需渲染的对象。 - 通过空间连接的窗口(比如门,窗户)去构建BSP-tree来进行可见性划分

- 当然随着设备计算能力的进步,PVS的应用在现代游戏中已经没有那么多了。
但是PVS的思想仍然是值得我们去学习的,实际上除了渲染之外PVS的思想在场景管理和资源调度中都有着丰富的应用。 - 通过Zoom划分来管理

- 利用现代GPU的强大计算性能我们可以通过查询的方式直接获取每个对象的可见性并以此剔除掉不可见的物体,这样的技术称为GPU based culling。
- Prez: 把场景的深度绘制出来来进行过滤
- 利用CPU的高速并行能力是实践中很重要的一环
Texture Compression 纹理压缩
我们在前面的章节介绍过纹理对于渲染出逼真的物体起着重要的作用。
通常情况下纹理会通过一张二维贴图进行表示,并且在计算机中使用JPG或是PNG这样的压缩格式进行存储。
而在游戏引擎中则无法使用这些常用的图像压缩格式,这主要是因为JPG这样的压缩算法不支持快速的随机图像坐标访问,而且它们往往具有过大的计算复杂度无法进行实时的压缩与解压。

- 游戏引擎绘制系统里的图片压缩不能使用常规图像压缩算法,因为需要随机访问图片数据。

- 在渲染系统中最常用的纹理压缩算法是block compression,它的思想是统计每个4×4区域内纹理图像最大和最小值然后通过插值的方法进行查询。
- 针对4X4的色块,找到最亮和最暗的点,其他点记录为这两个值的插值。比如DXTC压缩.
- 需要注意的是加载到显卡的纹理资源基本都是压缩后的数据格式.
Authoring Tools for Modeling 建模工具





- 从上到下分别是常规3D建模工具,雕刻构建工具,扫描生成工具,智能AI构建工具
- 最后一张介绍了它们各自的优势和不足
Cluster-Based Mesh Pipeline 基于Cluster的Mesh管线
本节课最后讨论了cluster-based mesh shader这一前沿技术的基本思想。
随着现代GPU计算能力的提高以及人们对于画质需求的不断增长,在3A大作中的模型往往都具有百万级甚至千万级的网格。

- 游戏引擎的绘制系统不是静态的,技术一直在往前推进
- 我们需要在近处看到更多的细节

- 为了渲染出具有如此高精度的网格就需要使用mesh shader相关的技术。
mesh shader的核心思想是把网格上的一小块区域视为一个meshlet,每个meshlet都具有固定数量的三角形。 - 可以利用现代显卡的优势,基于数据高效地创建更多几何细节

- mesh shader 用一个基于数据凭空生成额外几何细节的算法,通过距离相机的远近选择精度。
在进行渲染时可以通过实时生成的方法即时生成meshlet中的网格。 - 注:大小一致的小块在GPU处理更高效

- mesh shader可以生成几乎无限的细节,而且可以根据相机和物体的相对位置关系动态地调整网格的精度。
通过Mesh lite提高摄像机移动裁剪效率,如左图样例

- 虚幻5中的Nanite技术可以认为是更加成熟的mesh shader。

Info
优化就是尽可能让计算机do nothing, 少做事.
Coding/Debug:
- 实现图形学算法的时候需要逐步验证,一下子全部写完很难确认是否有效或者是否存在问题。