Rendering on Game Engine

引言

本课程将介绍现代游戏引擎所涉及的系统架构,技术点,引擎系统相关的知识。通过该课程,你能够对游戏引擎建立起一个全面且完整的了解。本节主要是介绍游戏引擎的渲染实践。

Rendering System in Games

从游戏刚刚开始的时候,我们就和绘制渲染走在一起。

最早的游戏需要显示在示波器上,而那时我们就已经想努力表现画面了。到了红白机时代即使硬件只能显示几种颜色,人们依旧构建了绘制系统奉献了诸多优秀的游戏。实际上现代游戏的绘制系统越来越丰富,越来越复杂,很多人提到游戏引擎下意识会说:“游戏引擎是不是就是绘制引擎?”。游戏引擎显然不止绘制,但它从技术难度和专业壁垒来讲确实是非常重要的一环。

那么这里顺带一提,没有渲染系统的游戏也是存在的:文字游戏。

Rendering on Graphics Theory

GAMES101 课程给大家系统地讲了计算机图形学的一些理论,这些算法是游戏渲染的基础。但同时游戏的渲染也存在一些区别:

  • 图形学解决的问题是明确的
  • 图形学不会特别关注硬件是如何实现的,而更关注算法和数学上的正确性
  • 实时 Realtime(30 FPS)/ 交互 Interactive(10 FPS)/ 离线渲染 Offline Rendering

Challenges on Game Rendering

在游戏中的绘制系统我们遇到的挑战都有什么呢?

第一个挑战是游戏中需要处理的物体对象和各种效果是极其复杂的。举个例子,在我的场景中有成千上万的对象,并且每个对象的形式都不尽相同:植被、角色、天空云……我们需要用到的算法也随之不同,整体上还要加大量的后处理和光照运算,所以游戏的绘制系统是一个 All in One 的组合。

第二个挑战是我们并不是跑在一个理想的设备上,我们的任务不是验证其正确性而是让它跑在现代的设备上。所有的算法必须基于这些 PC、主机或者小小的 Switch 上面,所以需要我们对硬件的了解非常深,适配当代的硬件。

第三个挑战是在图形学效果不错的帧率放到游戏中可能就不够看了,并且帧率必须要稳定。近年的游戏给我们的时间越来越少,画面要求却越来越高。

最后一个挑战是显卡可以 100% 吃掉,但 CPU 的利用率却提不起来。所以在真实的游戏引擎中我们不能像计算机图形学项目那样把所有计算资源全部吃掉,需要留给前面说的游戏逻辑、物理、人工智能等等系统。

Rendering on Game Engine

在讲之前需要说明,接下来要讲的所有东西它不是一个理论模型,它是一个工程实践经过这个行业近三十年迭代优化的软件工程系统,属于实践科学。

Outline of Rendering

如何把游戏引擎的渲染系统简化到四节课中呢?

  1. Basics of Game Rendering

    基础 GPU 硬件、渲染数据组织结构、可见性裁剪等基础内容

  2. Materials, Shaders and Lighting

    现代游戏的光照模型、标准的材质系统、着色模型等行业标杆内容

  3. Special Rendering

    地形系统、天空/烟雾系统以及后处理系统等重要的引擎子系统内容

  4. Pipeline

    渲染管线以及前沿渲染技术

What is not Included

卡通化渲染、二维渲染引擎、毛发皮肤材质渲染等等由于课程安排原因暂时无法涉及。

Building Blocks of Rendering

Rendering Pipeline and Data

渲染其实看起来也没那么难,就是空间上的顶点连城一个个的面,这些面经过一个投影矩阵投到我的屏幕上,再光栅化成一个个小像素点,在每个像素点上找到对应的材质、纹理渲染成各种各样的颜色,最后做出我们的效果。

Computation - Projection and Rasterization

这里面我们可以看到绘制最核心的工作是 Computation,最基础的则是投影和光栅化。这些在 101 课程中讲得比较多,我们找一个相机位置进行(正交/透视)投影,然后得到屏幕空间的三角形之后把它光栅化成一个一个的像素,这就是我们的投影。

Computation - Shading

投影光栅化之后就开始对它进行着色绘制。这里我们取一段简单的代码示例,可以发现它的运算也就几种:从常量里取常量,例如屏幕的长宽像素都是常数;中间会做大量加减乘除运算,例如 Phong 模型需要知道法线在哪里、光线在哪里、眼睛在哪里就可以算出来光有多少会被衰减掉;图中的小球上有很多花纹需要存到二维的贴图上……这些东西就像炒一盘菜,用这几种数学运算、常数变量的访问才能得到想要的结果。

Computation - Texture Sampling

这个东西听上去很简单,但这里面 Texture 的过程十分的复杂。

我们在人身上贴个花纹做个砖墙,离你很近的时候会看到一个一个的像素,但离你非常非常远的时候在屏幕上看到的像素其实隔了很多像素。这个时候如果我们不去对它低频滤波的话,那么当砖墙由近及远移动的时候画面会一直在抖动:走样。

在纹理上我们每一张贴图会存很多很多层,当我作为一个屏幕上小小的像素点取一个纹理的时候实际上要取四个点对它进行插值,然后还要每层三次双线性差值。图中也表明了要取 8 个像素点,所以纹理映射一次需要 3 + 3 + 1 = 7 次插值。

Understand the Hardware

所以要隆重介绍今天的主角:GPU 或者大家平时讲的显卡。

显卡是一个非常了不起的发明创造,现代很多引擎能够有突飞猛进的变化就是因为独立显卡的出现许多复杂的运算可以通过更高效的机器处理,这样极大地解放了 CPU,游戏画面也越来越精细。

如果你想成为一名游戏引擎的图形程序员,那这是你最好的伙伴。

SIMD and SIMT

要想了解显卡,首先要建立两个概念:SIMD(Single Instruction Multiple Data)和 SIMT(Single Instruction Multiple Threads)。

听上去很高大上,其实 SIMD 就是 CPU 广泛使用的单指令多数据数据运算,例如做一次加法运算它的 xyzw 四个坐标同时运算,一个指令完成四个加法或者四个减法。想象一下我们在渲染的时候就是有大量的矩阵、坐标运算,SIMD 就是我们最好的朋友。

但是在现代显卡里 SIMT 的概念更有趣值得了解。它的想法是如果我的计算内核做的很小但有很多个,这样一条指令可以在很多核上做同样的指令操作。假设我有 100 个核,一条指令就可以做 100 个数的加减乘除,我的计算效率可以 * 4 * 100 = 400。现代显卡类似于一个大蜂巢,里面包含了无数个小小的核,也是为什么现代显卡算力强的原因。

SIMT 结构给我们一个启示,在做绘制渲染的时候要尽可能用同样的代码让大家一起跑,每个人分别访问自己的数据,这样会在显卡上跑得非常快远远超过在 CPU 上算出来的东西,这是做图形程序最重要要掌握的概念。

GPU Architecture

有了这些概念就可以窥探一下现代 GPU 的架构了。

这张图是十年前 N 卡的费米架构。一块显卡上放了很多的内核,但它分成一组一组的,每一组叫作 GPC(Graphics Processing Cluster)图形处理集群。在图形处理集群里面可以看到很多的 SM(Streaming Multiprocessor),这些 SM 包含了许多小内核,N 卡一般放的是 CUDA 内核。这些内核会处理大量的数学运算,其专门的硬件会支持前面提到的纹理采样的工作,包括比较复杂的数学运算也是由 RT Core 去做支撑。

Data Flow from CPU to GPU

我们的数据在计算机里流动的时候是有成本的。

从计算机诞生开始的时候用的是冯诺依曼架构:计算和数据分开。这个架构会让硬件设计变得比较简单,但带来的问题是我每一个计算都需要去找数据,而找数据并在计算单元中搬来搬去这件事又特别特别慢。

所以游戏引擎设计中有一个原则就是数据尽量单向传输,CPU 送到显卡,尽可能不要从显卡里读数据。

Be Aware of Cache Efficiency

下一个概念是 Cache 缓存。

缓存对现代计算的影响是非常非常大的,可能远远超过大家的想象。在 CPU 上如果做一次加减乘除,可能一个 clock 就做完了;但是 A + B 的 A 找不到了,要从内存里取数据实际上要等 100 多个时钟周期。

做计算的时候如果数据恰好都在缓存上,我们称为 cache-hit 缓存被命中;数据不在缓存中则称为 cache-miss。所以在计算机图形中如果纹理没做好老是让计算机产生 cache-miss 效率会直线下降。

支持游戏的硬件结构一直在变,从 DX11 时代就可以做细分曲面、更加灵活的 Shader,到今天可以更灵活处理的 Mesh Shader。

主机是 UMA 内存共享的架构,对于引擎而言又是另一种情况。

移动端的游戏则更加考虑手机处理芯片的能力,许多采用的是 Tiled-Based Rendering。游戏引擎的架构都是和硬件的架构息息相关的,所以在讲渲染之前特别希望先了解一下显卡是如何工作的。

Renderable

Mesh Render Component

上节课我们讲到 GameObject 构建了这个世界,但需要区分逻辑表达的游戏对象和真实绘制的东西。

在 Mesh Render Component 中一般会存 Renderable 可绘制物体,这是我们绘制系统的核心数据对象。

Building Blocks of Renderable

那么一个 Renderable 是怎么来的呢?

先来看物体本身吧,以图中这个士兵角色为例,他有 Mesh 网格和几何形体,每一个 Mesh 有各种各样的金属皮肤材质,这些材质上还有许多花纹 Texture,其实还有 Normal 去表达 Mesh 表达不了的细节……这就是 Renderable 最简单的一个 Building Block。

Mesh Primitive

Mesh 在引擎中是怎么表达呢?

首先我们需要先定义一个 Mesh Primitive,最简单的顶点应该包含位置、法线朝向、uv 以及其他各种各样的属性。而每三个顶点放在一起则构成了一个三角形,许多三角形放在一起就形成了表面外观。

struct Vertex
{
Vector3 m_position;
// other data
UByte4 m_color;
Vector3 m_normal;
};

struct Triangle
{
Vertex m_vertex[3];
};

Vertex and Index Buffer

但是上面的存储方式很笨,如果同学们写过 OpenGL 或者 DirectX 基础的图形学代码就会知道我们会把数据用 Index Data 和 Vertex Data 去定义。我们把所有的顶点放在一起,三角形不会再把顶点数据再存一遍,而只存顶点的索引值。

实际模型中会发现很多顶点是被很多三角形公用的,数学也可以算出来顶点数量大约是三角面的一半,而三角形又有三个顶点,理论上使用 Index 这种存储方法可以节约六倍以上。

Why We Need Per-Vertex Normal

为什么每个顶点都要存一个法向?

这里面有一个很简单的数学知识,虽然三角形三个顶点可以算出顶点朝向在大部分情况下是对的,但一旦表面是硬表面有一条折线的时候,我们会发现两个顶点的位置是重合的法向却完全不一样。

所以在写游戏引擎的绘制系统定义顶点数据时一定要单独定义法向方向。

Material

材质系统来自于我们真实的生活,表达上接近于我们在物理世界对物体的感知。

这些材质系统需要和后面物理系统的物理材质做一些区分,物理材质更多的是表达物体的摩擦系数、弹性等等……和这里的材质相近,但在现代引擎的绘制系统定义的只是视觉材质,物理材质则会单独定义。

Famous Material Models

材质系统经过图形学发展,从经典的 Phong 模型到现在的 Physically Based Rendering PBR 模型,包括一些特殊的效果如半透明材质,我们其实已经积累了很多非常优质的材质模型。

Various Texture in Materials

有了这些材质模型,我们需要纹理。

表达材质的时候纹理扮演了非常非常重要的作用,现代引擎里你觉得材质像金属还是非金属表面并不是由材质的参数决定的,很多时候是由纹理决定的。

Variety of Shaders

那是不是我们有了材质的表达、纹理、Mesh 就能绘制出我的东西呢? 这里面有个无名英雄:Shader。

Render Objects in Engine

Coordinate System and Transformation

先来做个投影变换吧。

首先给物体乘模型矩阵从自身的坐标系移到世界坐标系;之后确定相机位置用一个视图矩阵再投影到相机坐标系;最后再选择正交或者透视投影变成屏幕坐标系里的东西。

Object with Many Materials

接下来先把 Vertex Buffer、Index Buffer 这些 Mesh 的东西提交上去,然后把材质参数、Texture 和 Shader 提交给显卡,一个物体就被绘制出来了。

这样我们就从一个抽象的 GameObject 游戏对象变成了一个 Renderable 绘制物体。

How to Display Different Textures on a Single Model

但有些聪明的小伙伴会发现,这个物体并不是单一材质纹理的,这些数据又该如何去整理呢?这里我们想引入一个非常重要的概念:SubMesh。

现代游戏引擎中对一个 GO 会根据它材质的不同切分成子 Mesh,每个 SubMesh 会对应自己的材质纹理和着色器代码。但是一般会把它的顶点、三角形全部放到一个大的 Buffer 里面去,每个 SubMesh 只需要存一个偏移量就好了。

但在绘制多个对象的时候如果每一个 GO 都存储整套 Mesh 和材质,这个数据量会非常的大。仔细观察这些 Mesh 和贴图,你会发现它们其实都是一样的,那我们怎么节约这个空间呢?

Resource Pool

现代引擎中我们一般的做法是建立一个 Pool,把所有的 Mesh、Texture 放在一起。

Instance: Use Handle to Reuse Resources

这样的话当我绘制一个拥有各种各样小兵的场景,你会发现它只是通过一个指引指向了各自所需要的材质、Mesh 网格。这是游戏引擎很经典的架构:Instance 实例化。

Sort by Material

Initalize Resource Pools
Load Resource

Sort all Submeshes by Materials

for each Materials
Update Parameters
Update Textures
Update Shader
Update VetexBuffer
Update IndexBuffer
for each Submeshes
Draw Primitive
end
end

GPU Batch Rendering

实际上很多子物体是一模一样的,这些物体依次设置 VertexBuffer、IndexBuffer 也是很浪费的。所以我们现在可以一个 DrawCall 设置一次 VB、IB 把成百上千个东西全部创建出来,这就是 GPU Batch Rendering 的思想。

总结下来就是现在的游戏引擎架构中,我们会尽可能把绘制计算交给 GPU 而非 CPU。

Visibility Culling

现在我们已经可以绘制画面了,但它并不高效。

事件锥实际上是一个锥形的东西,绝大部分东西我们是看不见的。如图当相机开始移动的时候,其实 7~80% 的空间我们是看不见的,那里面的物体、地形、粒子效果可以不绘制。可见性裁剪 是游戏系统中很基层的底层系统。

Culling One Object

那这个可见性裁剪如何去实现呢?

上节课我们讲过每个物体都有包围盒,当我们给一个四棱锥的 view frustum 可以判断包围盒是不是在事件锥里面,这其实就是 Culling 一个最基础的思想。

Using the Simplest Bound to Create Culling

包围盒是一个非常重要的概念。

它有很多种形式,最基础的是把整个物体包围进球里的 Bounding Sphere,也有更常用的 AABB 轴对称包围盒,存储两个端点就可以构建出 AABB Box,而且它的计算效率也是最高的。 有的时候我们的包围盒是贴着物体,被称为 Oriented Bounding Box:OBB;还有用凸包 Convex Hull 的包围盒……

所以其实无论我们在做什么,对物体形状的表达是非常复杂的。例如一个角色包含几万个面,不可能和这几万个面进行一一计算,而包围盒大致表真判断结果是很多计算的基础。

Hierarchical View Frustum Culling

上节课我们还讲了对空间里的物体进行各种各样的划分,诸如四叉树、BVH 可以让 Culling 运算一层层去问,计算复杂度也会下降非常多。

Construction and Insertion of BVH in Game Engine

BVH 算是现代游戏引擎最普遍的选择。

虽然它很简单也不是最高效的算法,但现代游戏里”动”的东西很多,当 BVH 构建好但节点发生变动,重新构建的成本要尽可能的低。

PVS(Potential Visibility Set)

其中我想介绍一个非常有意思的算法:PVS。

我们首先用 BSP-Tree 把空间划分成一个个小的格子,每个格子通过 Portal 连接。假设现在我们身处豪华的大平层,有很多房间,那么身处任意一个房间通过 Portal 看到的房间是固定的。

Portal and PVS Data

for each portals
getSamplingPoints();
for each portal faces
for each leaf
do ray casting between portal face and leaf
end
end
end

所以 PVS 的想法非常淳朴,身处某个房间通过门或窗最多能看到哪几个房间。如图你站在七号房间,最多只能看到 6-1-2-3 四个房间,意味着只需要渲染这四个房间。

The Idea of Using PVS in Stand-alone Games

for each GreenBoxs
for each BlueCells
do ray casting between box and cell
end
end

其实真正应用 PVS 的已经不多了,但这个思想非常有用,帮助我们对资源进行调度。

GPU Culling

随着硬件性能发生突飞猛进的变化,越来越多的 Culling 已经不再用这些传统的算法裁剪,GPU Based Culling 就可以完成。

Texture Compression

Texture Compression

游戏引擎里一般不会以图片的形式存储纹理,取而代之会把纹理进行压缩。 绘制系统中不能用那些非常好的算法,因为这些算法不能随机访问,而且计算复杂度非常高。

Block Compression

我们一般采取的策略是 Block Based,把图片切成一个个小块再压缩。

这里介绍一个非常经典的算法,假设我有一个 4x4 的小色块,找出颜色最鲜艳和最暗淡的点,我们认为其他点都是这两个点的差值。因为很多图片相邻像素都有关联度,所以可以通过两个极值的比例关系近似表达这个像素的颜色。

其实整个计算机图形学的 Texture Compression 都是基于这个这个 Block Compression 思想。

Authoring Tools of Modeling

Modeling - Polymodeling

构建一个模型最经典的是 3DMAX、MAYA 以及现在越来越流行的 BLENDER,点线面去构建各种各样酷炫的模型。

Modeling - Sculpting

ZBrush 雕刻性的素材生成工具正在替代传统建模方式。

真实世界塑造形体的时候雕刻家会不断把材料切削掉成自己的形状,实际上这种雕刻行为在计算机里可以更自由,构建更加自由的形体。

Modeling - Scanning

实体扫描建模得益于深度学习算法的提升,可以得到高精度细腻的网格,远远超过手工构建。

Modeling - Procedural Modeling

通过一些算法或者规则自动生成网格的程序化建模可以把我们的艺术家从繁琐的细节工作解放出来,专注于创意本身。

Comparison of Authoring Methods

这四种方法各有利弊。

Cluster-Based Mesh Pipeline

Sculpting Tools Create Infinite Details

Cluster-Based Mesh Pipeline

Cluster-Based Mesh Pipeline 是一种新的模型表达管线。

它的基本思想是当我面对一个非常精细的模型时,我把它分成一个一个的小 Cluster。这样做的目的在于现代计算机显卡已经能够基于数据非常高效的创建几何细节,而传统管线则要预先把 VertexBuffer、IndexBuffer 构建好。

Programmable Mesh Pipeline

随着硬件的发展,MeshShader 可以用一个算法基于数据凭空生成很多的几何,还可以根据相机的远近选择精度。

GPU Culling in Cluster-Based Mesh

Nanite

Nanite 则是在此基础上更符合工业化更加成熟,也是现代游戏引擎发展的一个重要方向。

Take Away

  1. 游戏引擎的绘制系统是工程科学,非常依赖于现代图形硬件的理解

  2. 游戏引擎需要解决的核心是模型、材质和网格这些数据之间的关系

  3. 绘制时尽可能通过一些运算把绘制减到越少越好

  4. 绘制和一些复杂处理尽可能从 CPU 移到 GPU

PILOT

Pilot Engine - Editor and Game

Pilot Engine - Source Code

Pilot Engine Download

Homework

Q&A


GAMES104_Lecture_04