三.游戏的绘制系统(渲染系统)
【背景】
一.绘制系统和游戏引擎区别
(1)很多人在听到“游戏引擎”的时候,会下意识地认为,游戏引擎就是指绘制引擎。
(2)游戏引擎其实并不等同于绘制系统,但绘制系统又是游戏引擎非常重要的一个部分,而且无论是从技术难度还是专业壁垒的角度来说,绘制系统也是整个游戏引擎中最高的一个部分。也是最难的部分。
(3)有没有游戏是不需要绘制系统的?实际上是有的,比如有一种叫做MUD的文字游戏。现在大家玩的绝大部分游戏都是需要绘制系统的。
二.计算机图形学和游戏引擎的区别
1.计算机图形学理论知识
GAMES101课程中,闫令琪老师系统地讲解了计算机图形学的一些理论。学到很多种算法,以及图形学的一些基础理论。这些算法和理论就构成了游戏渲染系统的基础。
2.图形学理论知识主要解决两个问题
1.明确的绘制需求
当我们需要绘制一个或者几个物体,或者实现某种特定的效果时,可以应用这些知识。
比如我们要绘制一个透明物体,需要实现透明物体的折射和反射效果。例如水面效果,或者带有次表面散射效果的半透明材质(皮肤等)。这些都是一些非常明确的需求。
2.数学上的正确性
在图形学理论中,一般不会关注具体的硬件实现,只关注图形学理论在算法或者数学上的正确性。
有一个算法叫做辐射度算法,这个算法是一个完全按照光学理论给出的数学模型。计算机需要运算几天,才能绘制出一张非常漂亮的图片。
3.图形学算法分类
(1)实时算法
如果算法的刷新率能够达到30帧以上,或者20帧以上,我们就说这是一个实时的算法。
(2)可交互的实时算法
如果只能实现10帧左右的刷新率,我们就称之为“Interactive”,即可以交互的实时算法。
(3)Out of Core Rendering 外存绘制
为了追求更高的画质,比如进行电影级的CG渲染时,我们无法实时更新画面。 我们需要花费几个小时甚至几天时间来渲染一帧画面,有时这些数据会大到连内存都无法容纳,甚至需要很多台机器离散化地进行存储。
4.图形学是理论科学
学习理论学科的同学会比较舒服。因为一般的理论知识的更新换代速度不会特别快,一些基础理论知识在近几十年只有少许调整。比如数学,很多理论知识都不需要更新换代。
5.游戏引擎是工程实践科学
(1)代表了计算机行业最前沿的制造技术,最前沿的计算机体系结构技术,最前沿的游戏引擎技术和渲染技术。
(2)这些技术的更新换代速度非常之快,因为这些技术是在工程实践中演化出来的,而且会随着硬件和整个游戏的产业环境的变化,不断进行快速的迭代和优化。
(3)下面的知识不会纠结于具体的理论模型,而是会更多地讲解工程上的最优做法,以及这样做的原因。所以,渲染系统是一个实践性的软件系统。
三.绘制系统的四个挑战
一.挑战1~复杂性问题
1.一个场景中可能存在着成千上万个物体对象,而且每个对象的形式和渲染算法都不一样。比如场景中可能同时出现水体、植被(foliage)、角色、云朵等对象。
2.大量的后处理,即光照运算等操作。
3.把所有这些计算都放在一个循环里面,我们在生成每一帧游戏画面时,都要进行上述计算。
4.绘制系统是一个非常复杂的系统,是一个实现多种复杂算法的集成系统。
二.挑战2~硬件的深度适配问题
1.游戏绘制系统并不是一个理论证明题,是一个实践性问题,它需要运行在各种性能的现代硬件上。
2.游戏绘制系统所实现的所有算法,必须要在这些现代的PC或者主机(比如Switch)上高效运行。
3.需要对这些设备的硬件进行非常深入的了解,才能充分利用各种硬件的特性,从而实现高效的算法。
比如:经典计算机的架构,南桥和北桥的概念、显卡和CPU的通信方式等。
三.挑战3~性能预算问题
1.保持稳定帧率,不能掉帧
(1)游戏画面变化可能特别大,从室内场景打开门走到室外。房间去到楼顶等等
(2)无论玩家看到的场景是大还是小,细腻还是粗糙,游戏引擎都需要保证游戏画面在1/30秒(大约33毫秒)内计算完毕。
2.现代游戏对帧率要求越来越高。
比如FPS游戏,需要帧率在60帧以上,而且现在的电竞屏要求游戏画面的刷新率能够达到120帧左右。
3.现代游戏对画幅要求越来越大
前几年主流的分辨率是1080P(即1920×1080),现在的主流分辨率已经接近4K,而即将到来的是8K的时代。
一方面是每帧的时间预算越来越少,另一方面是对画质的要求越来越高。
4.绘制系统的性能预算原则
绘制系统的计算时间必须被限定在一个固定的预算中,绘制系统的性能不能够超过这个时间预算,否则帧率就会降低,从而影响玩家的游戏体验。
四.挑战4~每帧的时间预算分配问题
1.对GPU的使用
绘制系统,可以用掉100%的显卡性能
2.对CPU的使用
(1)不能像图形学渲染算法一样用掉100%CPU计算资源
(2)工业级游戏引擎只能使用大概10%-20%左右的CPU计算资源,剩下的大部分计算资源需要分配给其他的系统,比如Gameplay、网络等模块。这也是一个硬性要求。
一.GPU硬件相关知识
一.渲染系统的对象
1.绘制的最核心工作就是计算。
涉及到上百万数量级的顶点和三角形、数千万级的像素、以及10亿级的ALU和纹理运算。这就是渲染所涉及的最基础的操作。
2.绘制的一个基本流程:
(1)游戏世界是通过顶点及其相关信息来表示的。
(2)在空间中存在着很多顶点,顶点连成一个个三角形,三角形又形成了一个个面。
(3)投影:这些面经过一个投影矩阵投影到屏幕空间上。
(4)光栅化:通过一个称为光栅化(rasterization) 的过程,将三角形光栅化成一个个像素点。
(5)着色/绘制:在每个小像素点上,去计算这个像素点对应的材质和纹理,将这个像素点渲染成各种各样的颜色。
简单的着色器代码,大概流程如下:
0)着色器代码需要通过常量获取很多数值,比如屏幕的长宽(以像素为单位)。然后进行大量加减乘除运算。
1)比如计算一个Phong模型,需要知道法线位置、光源位置、人眼位置。
2)通过这些信息,就可以计算出光线衰减的百分比。
3)纹理采样**Texturing:** 如果小球上有很多花纹,需要将花纹的纹理存储到一张2D贴图上
4)着色器代码再将自己所处理的像素点,所对应的纹理贴图上的坐标的 相应位置上的 颜色值取出。
5)通过这几种运算,即常数访问、变量访问,再加上纹理访问,我们就可以得到想要的结果。
(6)后处理/光照运算:同时考虑光照、以及物体本身的花纹等信息,并渲染出最终的效果。
3.纹理采样
性能消耗非常大,也十分复杂。
(1)走样
比如绘制一个砖墙,不对纹理进行滤波(这里的情况是低频滤波)操作,当砖墙相对于相机由近及远移动时,画面就会发生抖动。
(2)反走样
对于每一张纹理贴图,会存很多层,当着色器为屏幕上的一个像素点进行纹理采样时,采样位置可能并不一定正好位于该像素点上,因此需要取四个点,并对这四个点进行插值。同时还需要在两层纹理上按照比例采样。
(3)总结
进行一次纹理采样,需要采样2层x4个点 一共八个像素点的数据,并且进行七次插值运算。
二.GPU/显卡相关知识
1.SIMD
(Single Instruction Multiple Data)单指令多数据的数学运算
(1)广泛应用于CPU了。
(2)对于一个四维向量来说,每进行一次加法操作,它的XYZW坐标会同时进行运算。所以一条指令就能够完成四个加法或者四个减法运算。
(3)C++的SSE扩展宏,下面的代码实际上就是在调用SIMD指令
(4)渲染过程中有很多运算都适用于SIMD运算,比如矩阵运算、坐标变换运算等。
2.SIMT(显卡算力强悍的原因)
(Single Instruction Multiple Threads)单指令多线程
(1)显卡的特性。
(2)将一个计算核心做得很小,这样可以同时提供多个计算核心,并且可以同时在多个核心上执行同一条指令。
如果我们有100个计算核心,向这100个核心发送一条指令,就可以同时进行100次四维向量的加减。相当于将一条指令的计算效能放大了400倍。
(3)现代显卡如同一个蜂巢,其中内置了很多小型计算核心。NVIDIA的显卡中就内置了很多称为CUDA的计算核心。这就是现代显卡算力强悍的原因。
3.FLOPS
(floating-point operations per second)
FLOPS代表着显卡的浮点运算能力,即每秒浮点运算次数。
现代显卡一般能够达到十个以上的TFLOPS,比如Xbox或者PS5。
现代CPU的算力很难达到一个TFLOPS,显卡和CPU的算力差距已经超过了一个数量级。
4.显卡GPU的算力遥遥领先于CPU
(1)因为显卡有大量可以同时进行并行计算的小型计算核心,每个核心的功能简单,只进行简单的计算。
(2)而CPU的核心数量很少,但单个核心的计算能力很强。
(3)显卡的并行计算能力十分强大。
在设计绘制算法的时候,要尽可能地利用SIMT结构的优势,尽可能使用相同的代码进行并行计算。这样一来,每个计算核心都可以分别访问自己的数据,这样可以充分发挥显卡架构的优势。
5.GPU架构-费米架构
1.有很多重复的结构
在每个图形处理集群(上图的GPC)中,有很多流式多处理器(上图的SM),而在每个流式多处理器中,都装有很多的小型核心。它们不仅可以进行并行处理,相互之间还可以交换数据,从而进行协作。
SM流式多处理器相对于之前的架构增加了共享内存(Shared Memory)。
2.计算核心
计算机术语中称为ALU *(Arithmetic Logic Units,算术逻辑单元) *。
在NVIDIA的显卡中称为CUDA,CUDA核心负责进行数学运算。如果向流式多处理器发送一条指令,这些CUDA核心就可以同时执行同一条指令。
3.专门的硬件优化
(1)处理各种耗时的纹理采样工作,
(2)SFU(Special Function Unit,特殊功能单元),处理一些比较复杂的数学运算。比如正弦、余弦、指数、对数等超越函数运算。
(3)Tensor Core,最新的Ampere架构中,用于人工智能处理的核心。
(4)RT Core,用来加速光线追踪BVH算法的硬件逻辑电路。
6.数据在计算机中流动的成本
1.冯洛伊曼架构
(1)即将计算和数据分开。
(2)优点:让硬件设计变得非常简单。
(3)缺点1,最大问题是,每一次计算都需要去获取数据;获取数据的操作速度非常慢,而且数据在不同的计算单元中搬来搬去也是非常之慢。
(4)缺点2,容易出现数据Back Force问题。
2.数据的“Back Force”,
(1)概念
CPU已经准备好数据交给显卡去计算,但需要等显卡计算完毕之后,再将结果从显卡回读到CPU,CPU再基于显卡的计算结果进行一些判断,然后再告诉GPU如何进行绘制;
(2)对性能的影响
引擎架构中绘制和逻辑通常是不同步的。如果有一步绘制运算需要等待数据的“Back Force”,则可能会导致半帧到一帧的延迟(Latency)。
(3)绘制系统架构的一个原则
尽可能将数据单向传输。即CPU将数据单向发送到显卡,并且尽可能不要从显卡中回读数据。这也是现代计算机结构对渲染系统设计的一个限制。
7.缓存(Cache)的概念
(1)数据放到一起,是为了缓存做准备
在每次计算时,如果所需要的数据刚好都位于缓存中,则叫做缓存命中。
如果所需要的数据不在缓存中,则叫做缓存未命中。这时,CPU就需要等待很多时钟周期,才能获取到数据。
1)举例子,在现代CPU上,如果进行一次数学运算,可能一个时钟周期就做完了。
但如果这个时候,比如CPU正在进行A+B的运算,而A的数值CPU需要从内存中获取。
如果CPU发现A不在自己的缓存中,而需要去内存中读取A的值,实际上它需要等待100多个时钟周期才能从内存中得到A的值。在等待的这段时间里,CPU理论上可以进行几十次到上百次数学运算。
2)这也是上节课中提到过的数据一定要放在一起的原因,其实就是为了缓存去做这样的准备。
3)如果有些数据过大,那么也会导致缓存很难被利用好。
(2)纹理采样也要利用缓存
如果纹理采样等计算没有设计好,使得计算过程中经常发生缓存未命中事件,计算效率就会直线下降。
8.图形程序员的一些行话
bounds
(1)ALU Bounds 计算限制
程序的数学计算太多,阻塞了。其他的操作 (比如纹理采样等) 都能够及时完成,但需要等待数学运算的结果。
(2)Fill Rate Bounds 填充率限制
所有的数学运算都完成了,但是写入缓存的速度太慢,结果导致数据传输发生了堵塞;
9.硬件的演进
(1)大约十年前,从DirectX 11时代开始,GPU就可以完成更高级的曲面细分,
(2)实现更加灵活的Shader,包括更通用的计算Shader等功能。
(3)实时今日,GPU也可以支持更加容易处理的Mesh Shader。
基于硬件GPU的运行时细分,把原本三个shader的工作放到一个Mesh Shader中完成。
(4)而主机的架构又有所不同,因为主机使用的是一种叫做UMA的共享内存架构。因此,主机上使用的游戏引擎的架构又会不同于PC上所使用的架构。
(5)手机游戏平台
手机游戏运行在移动端,最关注的指标是功耗,因为移动端芯片的处理能力有限,而数据访问对于移动端来说是一个相当昂贵的操作,因此人们开发出了“Tile-Based Rendering” (分块渲染) 技术。大家所看到的手机上呈现的游戏画面 (比如1080P或者4K分辨率的画面) ,其实是分块渲染出来的。
三.GPU参数和性能瓶颈
1.硬件参数
-
核心频率(GPU Clock) :和 CPU 一样,决定 GPU 的运算性能
-
逻辑运算单元(ALU) :用于处理通用计算,比如 VS 和 FS 中的向量矩阵运算。
-
光删处理单元(ROPs - Raster Operations Units) :这个硬件单元主要负责将像素值写入到渲染目标中(深度测试、模板测试、透明混合)
-
纹理映射单元(TMUs - Texture Mapping Units) :这个硬件单元主要执行 texture 指令
-
显存频率(Memory Clock) :决定显存传递数据的响应速度,好比水管中的水流速度
-
显存位宽(Bus Width) :决定显存单次传递数据的大小,好比水管的粗细
2.GPU 评估参数
直接查看 GPU 硬件参数,并不太好理解一个显卡的能力,所以硬件参数经过计算,可以得出下面几个关键的用于评估 GPU 好坏的参数:
(1)像素填充率(Pixel Fillrate)
像素填充率是指 GPU 每秒能渲染的像素数量,单位是 GPixel/s(十亿像素/秒)
计算公式:像素填充率 = 核心频率(GPU Clock) x 光栅处理单元(ROPs)数量
(2)纹理填充率(Texture Fillrate)
纹理填充率是指显卡每秒能采样纹理贴图的次数,单位是 GTexel/s(十亿纹素/秒)
计算公式:纹理填充率 = 核心频率(GPU Clock)× 纹理单元(TMU)数量
(3)显存带宽(Memory Width)
显存带宽是指显存每秒所能传送数据的字节数,单位是 GB/s(十亿字节/秒)
显存带宽(Band width)= 显存频率(Memory Clock) x 显存位宽(Bus Width) / 8
(4)浮点运算(FLOPS/TFLOPS)
衡量 GPU 运算能力的核心标志是 FLOPS,即 Floating-point Operations Per Second,每秒所执行的浮点运算次数。
由于现代 GPU 的浮点运算能力很强,一般用 TFLOPS(万亿次浮点运算/秒)来表示。
这个值由 ALU 和 核心频率决定,以厂商实测数据为准。
二.高效渲染小型场景
一.可渲染物体
1.基本概念
(1)区分一个概念,即一个逻辑上所表达的游戏对象,和游戏中可以绘制的物体是不同的;比如很多行为的描述,是无法绘制的。
(2)Mesh Component,这个名词在不同的引擎中有很多的变化,有的引擎叫做“Mesh Component”,有的引擎叫做“Skinned Mesh Component”。而对于“Skinned Mesh Component”来说,引擎会假设这个网格是有骨骼的,可以进行变形。
(3)每一个gameObject(go)组件会挂一个“Renderable”的成员;获取到这个Renderable对象,就可以将其绘制出来,这就是绘制系统的核心数据对象。
2.生成一个基础的Renderable对象
角色会具有很多网格~角色的几何形体,比如角色的头盔、枪支。
每个网格上又有各种各样的材质,比如布料、金属、皮肤等等。
这些材质上还有很多的花纹,所以会呈现出各种纹理。
还有法线 (Normal) 等属性,这些属性更加细节,无法使用网格来表达。
这些就是可以绘制的属性,这就是Renderable对象最简单的构建块 (Building Block)。
Renderable对象在现代游戏引擎中比我们描述的更加复杂,这里描述的只是最基础的概念。
3.网格的表示
(1)最简单、笨拙的方式
定义一个网格图元 (Mesh Primitive) 。
在模型文件中保存了很多的顶点,每个顶点上有很多数据,比如顶点位置、顶点处的法线朝向、顶点的UV坐标,以及其他各种各样的属性。
每三个顶点就可以组成一个三角形,我们将这些三角形组合在一起,就形成了模型的外观。
(2)实战中更常用的方式
1)使用索引数据 (Index Data) 和顶点数据 (Vertex Data) 来定义三角形的信息,即将所有的顶点放在一个数组中,三角形不会再将顶点数据存储一遍,而只存储了三个顶点的索引位置信息。
2)模型文件很多顶点是被多个三角形共用的;
3)在大部分模型文件中,顶点的数量只有三角形数量的一半,而一个三角形又有三个顶点。因此,如果使用上述的索引方法,理论上的存储量可以节约六倍以上。
(3)索引数据都省了
1)将顶点按照一定的顺序存放,就可以不需要索引数据。
比如早期游戏引擎的三角形带(Triangle Strip),类似于一笔画问题。
假设有一个复杂的网络,需要一笔将网络的所有边全部勾勒出来。在勾勒过程中,画笔经过的所有顶点按照访问顺序形成了一个数组,数组中的每三个连续顶点都能够形成一个三角形,并且和模型三角形带所表示的形状相符。这样就不需要单独存储这个三角形带的索引信息,并且也能够表达一个网格。
2)还有一个附加的好处,顶点数据都按照三角形带所形成的顶点顺序进行访问,这种访问方式对于缓存是十分友好的。在早期的游戏引擎中,开发人员会尽可能地想办法将一些模型变为一系列的三角形带。随着计算机硬件的发展,现在已经不大使用这种方式。
(4)每个顶点都要存储一个法向量的原因
1)大部分情况下,计算出每个三角形的朝向,然后使用邻近的几个三角形的法向量进行平均,可以得到该顶点的法向量朝向;
2)但如果表面是一个硬表面 ( 比如立方体) ,即存在一条折线的时候,就会出现位于不同表面的两个顶点的位置重合的情况,两个顶点的法向完全不一样;所以顶点数据需要额外单独定义法向方向;
4.材质的表示
(1)注意跟物理材质(Physics Material)区分开
在绘制系统中定义的材质表达的是物体的视觉属性。而物理材质更多表达的是物体的物理属性,比如摩擦系数、反弹系数等。
(2)材质系统的发展
从最经典的“Phong模型”,到后面介绍的“基于物理的材质”即PBR材质,还有一些实现特殊效果的材质,比如半透明的“次表面散射材质~皮肤效果”等;
5.纹理的表示
人眼对于材质类型的感知,并不是由材质的参数决定的,很多时候是由它的纹理所决定的;
上图中大家看到这个生锈的铁球,对于光滑的金属表面和生锈的非金属表面的视觉表现的区分,实际上是通过粗糙度 (Roughness) 这类的纹理来区分的。所以纹理也是材质非常重要的一种表达方式。
6.着色器shader
(1)引擎一般严格区分数据和代码
1)数据,比如各种资产,或者各种模型。艺术家设计师处理数据。
2)代码,是程序员编写的代码。
3)Shader的神奇之处在于,Shader是一段代码,需要编写大量的代码来表达材质,但是在引擎中又会被当成数据来处理。
(2)shader绘制一个物体的流程大致如下:
首先告诉显卡需要绘制的具体物体,然后传入物体的纹理。
这时还需要传入一小段代码,一般称为一个Block (一个二进制的数据块) ,就是我们编译好的一段Shader代码。
显卡会使用这段Shader代码,将这些元素融合到一起,进行一些计算,绘制出我们想要的效果。
(3)分类
预置的着色器,自定义着色器
(4)Shader Graph
1)当艺术家想表达各种各样的材质时,会像搭积木一样,将各种元素按照自己的方法进行组合。
2)组合完之后,引擎就会生成一段Shader代码,而这段Shader代码又会被编译成一个Block,和网格存储在一起。
3)各种各样的网格和Shader代码组合在一起,就形成了多彩的游戏世界
7.子网格
(1)要对一个物体所拥有的各种不同的材质进行不同的处理;
对于每个游戏对象上的网格,根据所应用材质的不同,把其切分成很多子网格;然后对于每个子网格,分别应用各自的的材质、纹理和着色器代码。
(2)原理
将网格的顶点和三角形全部存放在一个大的缓冲区中,所以对于每个子网格,只需要存储偏移值 (Offset) 。换言之,只需要存储索引缓冲区中的起始位置和结束位置的偏移值即可,因为每个子网格只使用了大缓冲区中的一小段数据。
8.资源池Pool
很多网格、贴图和着色器都是一样的,为了节约空间,在现代游戏引擎中,通用的做法是建立一个池(Pool),共用的大家指向池中的同一份资源就好。
将所有的网格放到一起,形成一个网格池;
将所有的纹理放在一起,也形成一个纹理池。
9.Instancing(实例化)
(1) “Object Definition,Object Instance”概念
在游戏引擎的设计中是贯彻始终的,不仅在绘制部分,在游戏逻辑、游戏的场景物体的管理等模块,都有这个概念。
(2)将“Instance”这个词牢记于心,当你进行引擎开发时,一定要区分清楚,哪些数据是定义,哪些数据是实例。
定义了一个小兵,它的Renderable成员应该是什么。当我们在屏幕上绘制了几千个小兵的时候,每一个小兵只是这个数据定义的一个实例。
一般来说,在创建了实例之后,还可以再为每个实例增加一点变化。
(3)特别适合那些外观一致大量重复的渲染。比如小树组成的森林,一个发布会场景中的大量椅子等等。
(4)在 instance 渲染的时候,不需要传入大量的顶点数据(只需要传入每个 instance 的 matrix 数据),而是共享一份顶点数据,这样可以大大降低显存的使用率,降低显存带宽。但要注意下,instance 的使用所带来一些额外处理,比如单个物体的选择操作等问题。
(5)在 webgl 中我们使用的是ANGLE_instanced_arrays
扩展来实现 instance 渲染。
(6)threejs也有InstancedMesh InstancedBufferGeometry,来实现实例化。
参考这篇:events.jianshu.io/p/0c80ce973…
10.GPU批量渲染(batch rendering)的优化
1.改变参数特别影响GPU的高速运行
比如改变贴图、着色器代码等。对于前面介绍的流式多处理器来说,每次改变参数,所有32个小核都会停下来,等待参数修改完成,然后再继续运转。
2.合批
1)将整个场景的物体按照材质进行排序,将具有相同材质的网格分组到一起,然后设置一次材质,绘制这一组拥有相同材质的子网格。
2)现代的底层绘制API比如DirectX 12和Vulkan,会将对GPU的状态设置专门抽象成一个“Render State Object”,运行速度会快很多;
3.减少drawcall
1)很多物体其实是一模一样的,依次绘制这些物体,并依次设置顶点缓冲和索引缓冲,也是很浪费的。
2)在一个Drawcall中设置一次顶点缓冲和索引缓冲、以及所绘制的一堆位移数据。即将一列数据送入显卡之后,通过一次绘制调用 (Drawcall) ,就可以将成百上千个物体全部创建出来。这就是“GPU Based Batch Rendering”的思想。
4.绘制工作给GPU
1)尽可能的将绘制工作交给GPU来执行,而不是使用CPU来执行。
2)这种做法对于绘制大量相同的物体特别有用,比如绘制大量的树木、草丛等,这些物体看起来都差不多。如果需要一次性绘制几百米开外,甚至上千米这类物体的话,这种做法非常有用;
二.可见性剔除
1.可见性剔除(Visibility Culling)的基本原理
(1)每个物体都有一个包围盒。当我们给定一个四棱锥形的视锥体时,可以通过一些简单的数学运算,判断物体的包围盒是否位于视锥中。这就是可见性剔除的基础思想。
(2)是游戏绘制系统的一个最基础的底层系统。
2.包围盒
(1)最简单的包围盒~包围球 (Bounding Sphere)
使用一个最紧密的球体将物体包围;
(2)更常用~轴对齐包围盒 (AABB,Axis-Aligned-Bounding-Box)
计算效率除了包围球之外是最高的;
(3)定向包围盒 *(OBB,Oriented-Bounding-Box) *:
包围盒的XYZ边和所包围物体的局部坐标系的XYZ边平行;
(4)凸包 (Convex Hull) ,物理运算中经常使用;
(5)层次包围盒 (BVH,Bounding Volume Hierarchy)
1)将上面的包围盒一层一层地沿着树形结构向上合并,
2)树形结构,这样做的好处是,当进行剔除运算时,可以从上到下一层层进行计算和查询。比如上层大的包围盒不在视野,他的子元素肯定不用绘制了;
3)特点:不是最高效的算法,但是BVH构建树形结构的速度很快,所以被广泛使用;
2)现代游戏,特别是实时战略游戏,场景中会有很多小兵跑来跑去,需要频繁重建树形结构;
耗时=剔除耗时+重建树形结构耗时;
所以BVH虽然剔除耗时不是最短的,但是重建的优势很大,还是被广泛使用,特别是针对具有大量动态元素的场景时。
(6)web引擎,生成包围盒的方法
1)cocos引擎的geometry包含minPos,maxPos两个属性,通过最大点和最小点可以模拟出一个将整个模型包含在内的矩形来,也就是包围盒;
2)threejs
使用three-mesh-bvh库,生成层次包围盒
3.可见性剔除算法~潜在可见集 ( PVS ,Potential Visibility Set)
1.原理
(1)先使用BSP树(Binary Space Partioning Tree 二叉空间分区树),将空间划分成一个个的格子,每个格子之间通过一个入口 (Portal) 连接。计算在每个房间中,通过该房间的门窗所能看到的其他房间(射线检测),并且只渲染所能看到的房间。存下一个表,比如A房间能看到B和C,B房间能看到C,C房间能看到B。然后根据位置处于哪个房间,查表,渲染可见的房间即可。
2.优点
这个想法非常简单、直接,并且符合人类的直觉,而且执行效率非常高。
3.缺点
(1)虽然PVS原理简单,在实践中对于PVS的计算、包括对空间划分 (Partition) 的算法,仍然相当复杂
(2)现代游戏中真正使用PVS算法进行剔除的游戏已经越来越少。
(3)但是PVS算法的思想非常有用,很多3A大作也用了这个思想;比如开放世界中,但底层的区域划分仍然是线性的,玩家仍然行走在设计师预先设计好的分块 (Chunks) 中,类似上面的房间一样,每个区域中能够看到的其他区域也是不同的。
4.PVS用于资源加载
比如闯关游戏,当通关BOSS时,就会通过一道门来到后续场景,花费一定时间进行加载;帮助我们进行各种资源的调度。
4.GPU硬件做剔除计算
硬件性能突飞猛进的变化,越来越多剔除都不用CPU的算法,GPU自身就可以完成该工作;
(1)GPU提供的遮挡查询 (Occlusion Query)
将很多物体的数据传入显卡,显卡反馈回一个比特位数组,每个比特位依次记录各个物体可见性。
(2)GPU完成视锥体剔除
直接将包围盒数据传递给显卡,由显卡来完成计算。
(3)也可以在GPU上构建层次包围盒BVH;
(4)拥抱硬件的最新变化
千万不要用老的算法去限制你的想法,一定要拥抱硬件的最新变化。换言之,只要能够使用硬件功能完成的工作,一定使用硬件来完成。
5.Hi-z,也叫Early-z
(1)概念:
在逐个绘制像素时,有的像素会被别的像素遮挡,就不必绘制这个像素;
(2)简单方法:
1)先将场景绘制一遍,但不对像素进行着色,而只计算每个像素的深度;得到黑白色的图。
2)白色位置距离相机较近,黑色位置距离相机较远。注意:GPU中会将深度数据反转存储。
3)绘制一个像素时,发现该像素位于我们之前计算的像素的后方,就可以跳过该像素的绘制,甚至可以跳过整个物体的绘制。
(3)基于层次结构进行深度处理
整体思想是大同小异的。这类方法都是利用GPU高速的并行化能力,以尽可能廉价的成本,形成一组遮挡物的深度图,然后将可以剔除掉的物体尽量剔除掉。这种做法对于复杂的场景十分有用。
三.理解绘制系统,记住这四点
一.纹理压缩
1.Block Based 块压缩
在游戏引擎中,我们一般采用基于块 (Block Based) 的压缩方法。我们将图片切成一个个小方块,最经典的就是4×4的小方块,然后进行压缩。
2.DXT系列压缩算法
(1)概念:
对于DXT类型的纹理,在一个4×4的色块中,可以找到最亮的点和最暗的点,即颜色最鲜艳和颜色最暗的点,然后将该方块中的其他点都视为这两个点之间的插值。
(2)原理
因为对于很多图片来说,相邻的像素之间都有一定的关联度(Coherence)。所以我们可以存储一个最大值和一个最小值,然后为每个像素存储一个距离最大值和最小值的比例关系,这样就可以近似地表达整个色块中的每个像素的颜色值。在计算机图形学领域中,纹理压缩 (Texture Compression) 都是基于这个思想,称为块压缩 (Block Comppression)
(3)优点:
生成了一个纹理后,就可以在CPU上对纹理进行实时压缩。因为无论是压缩还是解压缩,这一系列算法的效率都非常高。
3.手机上使用的压缩算法,ASTC算法
(1)优点:
分块就不再是严格的4×4了,它可以使用任意的形状,而且ASTC的压缩效果是最好的,解压缩的效率也不低。
(2)缺点
ASTC算法压缩时的性能消耗较大,因此无法在运行中进行压缩。
二.建模工具
工具链模块会详细介绍,这里先做简单的介绍。
1.工具软件
(1)构建3D模型最经典的工具是3DS Max和Maya
(2)近几年来,Blender也变得越来越流行。非常看好Blender,因为它的功能越来越强大。
2.制作流程
(1)传统的制作流程
在这些软件中卡住一个个关键点,由粗到细地构建模型。
(2)sculpting,基于ZBrush通过雕刻生成素材的工具
ZBrush是一个雕刻性的工具。在真实的世界中,当雕塑家通过雕刻塑造一个形体时,会不断地对雕刻材料进行切削,从而形成想要的形状。而在计算机中,这种雕刻行为可以更加自由,因为我们不仅可以进行切削,还可以进行放大和拉伸,这给了艺术家更大的自由度。
(3)scanning,3D扫描。
围绕着拍照,然后建模。
得益于现在的深度学习、以及图像配准 (Registration) 算法的提升。
(4)procedural modeling,程序性生成算法,比如AIGC。
Houdini就是这样的一个工具。
三.渲染管线
1.传统的渲染管线
构建好模型、材质之后,通过顶点着色器和像素着色器进行渲染,预先将顶点缓冲区和索引缓冲区构建好,再将网格数据传入显卡。
2. Cluster-Based Pipeline
新的基于Cluster或者Meshlet的管线。
(1)概念:
对一个非常精细的模型,可将其分成一个个小的分块,称之为Meshlet或者Cluster。而每一个Meshlet都是固定的,比如32个或者64个三角形大小。
(2)核心思想
现代显卡可以基于数据,凭空计算出很多几何信息,而且如果输入一个三角形,现代显卡可以借此生成无数个三角形。
因此,当我们将每个Meshlet的大小固定之后,在显卡上的计算都是极其高效且一致的。
前文提到的GPU硬件架构和流式多处理器的概念,每个流式多处理器中都有很多小型的计算核心。
所以模型虽然复杂,但很多计算是完全一致的。
(3)缺点
基于Cluster或者Meshlet的管线对于程序员的要求要比以前高很多。因为要进行大量的处理和运算,而且具体实现代码也不易理解。
(4)优势
它可以产生无数的细节,并且可以让艺术家自由发挥。
(5)举例子
比如一个怪物模型,有很多面片。如果我们不将其分成Meshlet,会发现当相机在移动的时候,这个怪物会被整个剔除掉,因为我们只能够按照物体的粒度进行剔除。
但现在我们可以将其手部的一部分裁剪掉,因为每个Meshlet都有自己的包围区域,我们可以在GPU上实时计算出裁剪区域。即在当前的相机位置的情况下,哪些部位可以不需要绘制,这样做的效率也很高。
(6)虚幻引擎的Nanite,将Meshlet的思想又往前深入了一步。
四.理解渲染(绘制)系统,四点总结
1.了解硬件
游戏引擎的绘制系统是一个工程科学,并且深度依赖于你对现代图形硬件的理解。因此如果你想成为一个图形工程师,你必须要理解显卡的架构,知道显卡的性能卡点在哪里,了解各种性能限制。
2.渲染物体
核心问题就是网格模型、材质等数据之间的关系。最经典Mesh和Submesh就是一个非常好的解决方案。但是最前沿的技术会有所不同。
3.剔除,优化的最高境界~do nothing
4.GPU驱动 (GPU-Driven)
将很多在CPU上进行的一些复杂运算 (比如动画系统等) 全部转移到显卡