GAMES104复习总结,课堂提炼(一)

主要是给自己看的,Games104课程很全面,王希老师学识广播,课程组也很认真努力。但是在文字版里面有大量的示例,比如可能会为了引入话题讲个故事等等。。。对于上课来说引人入胜,对于要找工作的我来说,每次看一遍都要看这些有点浪费时间。。。所以来一个精华版的。

Games104总结

1. 游戏引擎导论(上,中,下)

数字人虚拟人,鹿鸣等等背后都是游戏引擎。好莱坞,军事模拟。数字孪生也都是游戏引擎在后面。

各个游戏引擎,第一个游戏引擎是卡马克的doom,之后是他的quake,现在的引擎包括虚幻引擎,unity引擎,比较厉害的Crytek引擎。
游戏大厂来说,育碧的Anvil(铁砧(zhen)引擎),EA寒霜引擎,RAGE
一些中间件公司,physx,speedtree这种的引擎。

游戏引擎的挑战:算力有限,实时。
引擎同时需要一个很强大的工具链,有着二次开发能力,而不是一大堆代码,一个SDK。

基础的构建:MVVM设计模式,其实是就是在说游戏到底分为几层。

ps:打开一个游戏引擎,从update函数开始看,主要是里面的tick函数。
系统需要有,渲染系统,动画系统,物理系统,gameplay(事件系统,脚本系统),特效系统,除了这些之外还有一些杂项系统,比如说寻路系统,最后还要有一个相机系统。

游戏引擎最核心的是构建一个工具体系。
这里面最重要的概念是反射体系。你做了一套工具,后面不停地升级它的数据格式,但过去与未来的数据需要兼容,使得你要做100,1000个工具,这些工具之间还要能通信,这就需要反射系统。

OnlineGaming网络
其实就是网络同步这方面的知识,包括帧同步,状态同步这一些。但是光服务器这一些就比较复杂,及其复杂,难以招架。

2.引擎架构分层(上,中,下)

引擎架构分层(上)

工具层,是首先映入我们眼帘的一层,里面有各种各样的编辑器,这也就是引擎最上面的那一层----工具层。

功能层,渲染,动画,物理模拟,gameplay,脚本这些,根游戏相关的这些功能,这些基础服务,叫做功能层。
功能层核心做的事情非常简单:就是我们能够让这个世界可以被看见,可以动的起来,而且还可以玩起来。

资源层 就是把那些来自photoshop,3D MAX,或者maya等这些软件里面出来的模型,图形,几何,声音,视频等各种各样复杂的数据,在游戏引擎中统一处理,进行处理的这一层就是资源层,负责加载管理这些资源。 在功能层下面就是资源层,资源层可以理解成给功能层提供各种各样的弹药,让他去处理,去生成。

核心层
不管是在动画系统还是物理系统,或者其他各种各样的系统,都会频繁的调用一些底层的代码。

核心层主要任务比如说容积创建,内存分配,线程管理,数学计算。

平台层:也叫做平台无关层,有的人是用pc,有的人是使用mac,有的人手机,有的人xbox或者playstation。
无论什么样的输入设备到游戏引擎里面,都要翻译成统一的语言,这种平台的差异不仅仅是在对于硬件设备上的,还包括软件的发布平台。

第三方插件
第三方代码在引擎里面是一个非常特别的存在,它有些代码是通过SDK的形式直接集成到我们的引擎里面去,就是说引擎里面编译的时候,就要把第三方代码编译进去。但是,还有一些第三方库,它实际上是变成了一个独立的工具。它和引擎之间的数据交换,只是通过文件格式进行交换。

所以说现代游戏引擎一般都是5+1层次

引擎架构分层(中)

回顾游戏引擎5层架构:平台层、核心层、资源层、功能层和工具层。

假设小明想做一个能动起来的角色进行时间,下面就是小明需要在每个阶段做的事情。

资源层:
美术朋友做的模型,贴图,动画,叫做resource,资源,格式都不一样。工具做的十分复杂,吐过直接在引擎中加载,效率会很低,所以会做一步转换,把这些数据全部转换成引擎的高效数据,这个转换发生后,就叫做Asset(资产)。
从resource到asset,有什么不同呢?
一个例子:一张贴图,不管是jpg还是png的,都有很多压缩算法。如果在GPU中以数据格式存储的话,绘制效率很低。
就类似于一篇文章,用word写,文件会很大。保存成txt,文件会超级小。所以说小明同学第一步就是要把这些数据引擎化,变成资产。
同时添加上GUID,也就是识别号。游戏资产身份识别号。

这个资产叫做composed asset,用来描述这些资源之间到底是什么关系。
相当于一个关系脚本,定义了一个资产,mesh是什么,纹理是什么,该用什么动画,当引擎读取这个composed asset的时候,就知道我们要加载这些资源,本身也是资产之一。

游戏引擎中,现在游戏中,核心的功能是数据之间的关联,或者叫做reference。各个游戏资源项无数的网关联在一起,里面有一个很有意思的概念,叫做GUID。给资产打个唯一标识号,也是全局唯一编号,实际上是游戏资产的身份识别号。

Runtime asset manager
把游戏原始散乱的文件变成资产,进到原始资源系统的时候,需要一个实时的资源管理器。

这些资产在资产管理器中,一般叫做runtime asset manage(这里的runtime指运行时或实时)。
游戏跑起来是一个一个文件,掐头去尾就只是内容,但是会相互指向对方。游戏引擎中有一个handle系统。handle系统简单解释就像邮箱一样,可以搬来搬去,我也不知道你在不在,但是我始终有你的邮箱的要是,我知道你是105号邮箱,邮箱也知道你是105号邮箱的主人,这样,邮箱的主人在不在,我们只需要问这个邮箱就知道了。

简单的来说,游戏中最核心的是管理所有这些资产的生命周期,所以资源层是游戏引擎非常核心的一个层次。

同时资源生命周期的管理,需要不断地加载卸载资源,这些都是handle系统和GUID系统在解决问题,同时涉及到GC(垃圾回收)。 GC做不好会使得系统的效率变得及其低下。
另外一种叫做延迟加载,走到哪,加载到哪,所有的东西都是从模糊开始慢慢清晰。

功能层:
首先是tick,分为逻辑tick和渲染tick。
其实就是利用现代计算机高的计算速度,在1/30秒把整个世界的逻辑和绘制全部跑了一遍,这就是tick的魔力。

一般会先逻辑tick,之后再渲染tick。先逻辑再渲染。
未来的引擎架构一定是一个多核架构。大家开始进行底层架构的时候,推荐从多核开始去设计和思考整个底层代码。

引擎架构分层(下)part1

核心层:
数学库和数据结构
为了提高运算性能,很多公司都自己写数学库,不止是数学库,ea自己还写了一个estl标准库。。。。
同时现在有一个SIMD,单指令多数据,相当于一个alu把4个运算全部做掉。

同时内存管理要遵循三大底层逻辑:
在游戏引擎中,预先申请一块内存,在核心层做内存管理的时候,会用一个pool先把所有的内存资源管起来,然后管各种各样的数据,比如向量,列表等数据资源的分配。

  1. 把数据放在一起2.访问数据的时候尽可能顺序访问3.读写的时候,尽可能批量读写

引擎架构分层(下)part2

平台层
不同平台上的很多细节是不同的,比如说文件路径这些。平台层的本质是在上面写核心代码/功能/逻辑,可以无视平台的差别。平台层又叫做平台无关层,就是把平台的差异性全部掩盖掉。
但是不同的渲染API怎么办呢,有的用metal,有的用vulkan,有的用opengles、

所以说有一层十分重要的叫做RHI——Render Hardware Interface
重新定义了一层Graphics API将这些硬件的SDK差别封装起来。

一小段PHI的代码,这些函数又被重新定义了一遍,而且全部都是虚函数。
平台层超级难写,最丧心病狂的是,不同的CPU,有的连架构都不一样。

工具层实际上就是允许别人以lever editor(地图编辑器)为中心形成的一系列编辑器,比如我们很熟悉的蓝图编辑器。
游戏引擎的材质编辑器保证你在预览里看到的效果和在真实游戏里看到的效果是一模一样的。

架构中的基本原则,越底层的越不要去动。写需求的时候,先想好要做的这件事属于哪一层,而不是着急把算法写出来。
各个层次之间的调用,一般只允许上面的层次调用下面层次的功能,绝对不允许下面反向调用上面一层的功能,这就是分层的一个核心的体系结构。

.如何构建游戏世界(上)

面向对象的游戏世界

知道了5层,工具层,功能层,资源层,核心层,平台层,怎么来搭建游戏呢?
放眼游戏,有各种系统,包括可交互动态物,静态物等,还有地形系统,天空系统,日夜变换系统。植被(属于动静结合态系统),因为固定在一个地方,不能动,但是又会随着风雨等做各种交互,做出各种各样的变化。还有空气墙等。
把所有这些动静态物体统称为游戏对象GameObject(GO)
有了游戏对象,先分析这个游戏对象的属性和行为,然后抽象成类。属性就比如它的血量,它的几何外形,空间位置,速度,油量等。行为就比如它可以四处移动,可以攻击,可以加油。
所有在游戏世界里描述的物体,可以归类为两类,一类属性,一类行为。
还可以使用继承多态,无人机的子类,侦察攻击化无人机,只需要多加一个属性表示弹药量,一个行为表示攻击就可以了。

组件化

面向对象的逻辑去构建世界,有缺陷,有些东西并没有那么清晰的父子关系。
比如水陆两栖坦克,它的父亲是车辆还是船呢?这是个非常经典的面向对象的问题,这个问题的经典解法是组件化。我们把对象的行为拆分为无数的组件,就像一辆挖土机,看起来是挖土机,但是把铲子换成不同组件的话,可以把挖土机变成压路机,推土机,举重机,挖路机。。。
再回到无人机,组件化,则可以有motor的组件表示各种运动属性,飞行速度,加速度等。Health组件,告诉无人机到底有多少血。包括AI行为,物理行为,动画行为等等。

UE所有的东西都派生自一个Uobject,可以管理任何对象的生命周期,创建完后内存释放,就是垃圾回收。
总结起来,两件事:
第一:游戏世界里面几乎把所有的东西抽象成了游戏对象GameObject(GO)这样一个东西。
第二:每个GO用各种各样多功能的组件把它组合起来,所以各种组件又是游戏对象的原子。

如何让世界动起来

把游戏物体的每一个组件都tick一下,只要把游戏世界里面的所有物体都tick一下,这个世界就动起来了。
在游戏引擎中,并不是按照每个对象进行tick的,我们会把一个个系统里面的全部组件进行tick,比如说我们先把弹壳系统里各个组件全部tick一遍,再去天空的飞机或者拿枪的老兵。
因为这样的话,会流水线化,在这种的架构下处理效率是最高的。

实体-组件-系统(ESC)架构。

现代游戏引擎,为了追求效率,会转向每个系统或者每一种组件进行tick,就是我要造个流水线进行批处理,这样效率高很多。

9.如何构建游戏世界(下)

比如我跳上了坦克,然后向远方的敌人开了一炮,把敌人击倒了。如果每个游戏对象都是自己独立的tick,我这开一炮,怎么样让地方被我打伤呢?这里面其实就需要游戏对象之间是相互关联的。
硬编码的情况下,开炮发生,会生成一个炮弹,炮弹每个tick往前跑,突然在一个tick的时候落地,马上要爆炸,所以说要检测查询周边的对象,如果对象是人就降低血量,如果是飞机就坠机,如果是石头就飞起来。。。

早期真的是这样写的,现在使用事件与事件机制。

事件与事件机制

举个例子,我们不能粗暴的去敲别人家的门,直接告诉别人你被我打了一下。而是应该给每个人家放个邮箱,只需要给方圆20米内的所有对象写邮件,告诉每个人不好意思,你要扣100点血,然后把这个游戏右键放在这个tick,等到下一个tick的时候。小兵或者其他对象打开邮件,接受事件,处理自己的逻辑。

所以一个非常复杂的问题,通过一个简单的时间机制变得非常清晰明确,这叫解耦合。把游戏对象间的通讯,统一变成了事件机制。

所以说,举个例子,比如unity里面,可以简单的注册一个事件,然后发送事件,最后销毁事件,如果物体核心组件收到了这个事件后,有一个回调函数会激活,然后作相应的处理,这一切只需要一个字符串符合就可以了。
它的本质就是说让接口的消息不断扩展定义,其实大家做游戏引擎核心的就是要做一个可扩展的消息系统,让游戏开发者可以在我们的引擎之上不断的定制玩法和相关的各种各样的消息类型,然后他们可以定制各种各样自己想要的组件去对这些消息,让事件进行按照自己想要的逻辑的处理,这就是现在游戏引擎的核心的一个工作。

但是游戏里那么多对象,每一件事情怎么通知到每一个对象呢?游戏里面有那么多的大兵、飞机、坦克、大炮。要怎么去管理呢?
使用全球唯一标识符(Global Unique ID)去管理,如果每个都去问一遍的话就是游戏引擎的N平方挑战。

所以说要引入场景管理

场景管理----分而治之

用一个层级结构去管理整个场景,也就是空间加速算法。
思想很简单,就像地图一样,北京海淀区发生的事情,只需要去北京海淀区里面找就可以了,不必去上海找。
在实际中,可以以以四叉树不断的划分,如果每个地方的人数足够少了,只有一个到两个了,那就不用再划分了。如果人数很多,就不停的划分,就形成了一个树状结构。

现在比较流行的是BVH,也就是比较常用的bounding box。比如视椎体裁剪等工作,都是使用的BVH进行场景管理。

总结下就是,在游戏中,所有物体都是用组件的形式去描述,用一个个组件整合出不同的行为,所有的组件用tick的方法不断得无tick它的内在的逻辑去往下一个时刻前去。

时序一致性问题

在游戏中很常见的,比如说物体间的绑定,比如大家特别喜欢的神海里面的飙车,当车是对象,人是对象,怎么进行联动?
实际上这时候进行tick就会有一个先后关系的问题,比如说我们一般会要求父节点先tick,然后人被挂在车上所以后tick。
问题很多时候tick组件的时候是要分散到多CPU上去执行的,很多的tick是并行执行的,所以说并行执行的时序是非常重要的。

还有一个难题,通讯是一个个时间传递的,但是当相互发送时,很容易出现逻辑混乱性。
精彩回放一般是怎么做的呢,它并不是把刚才整个游戏过程全部录下来。而是记录了每一个小伙伴的输入,然后把游戏重新跑了一遍。所以要求是确定的。

因此这时候我们要引入一个”邮局”,关键的第三方。就能确保时序是严格一致的。

10.绘制系统真正面临的挑战是什么?

挑战一:复杂性问题
一幅场景里,有不同的对象,不用的对象需要用到不同的算法,比如说水体的算法,植被的算法,毛发的算法,皮肤的算法,还有地面的算法等,这些算法都完全不同。
同时还要加上大量后处理,光照运算等操作。

挑战二:硬件的深度适配问题
要对经典计算机架构有所了解,比如南桥北桥概念,显卡和CPU通信方式等。游戏绘制系统所实现的所有算法都要在现代的PC或者主机上高效运行。

挑战三:性能预算问题
游戏引擎需要保证在1/30秒内计算完毕,游戏画幅越来越大,主流分辨率是1080P,现在接近4K了,即将到来的是8K时代。

挑战四:时间预算分配问题

在一台电脑上跑游戏,可以使用100%的显卡性能。但肯定无法让绘制系统用掉100%的CPU时间,因为还有gameplay,网络等模块也需要使用cpu。

真实游戏开发中有一个重要过程叫做”profileing”,性能分析。现在的性能分析过程是全自动的,每天都会自动运行,如果某个系统运行时间超出了预算,就会提醒开发者。

11.设计渲染系统,为什么要特别关注显卡?

渲染系统对象

游戏世界是通过顶点及其相关信息来表示的,在空间中存在很多的顶点,顶点连成三角形。三角形形成面,面经过投影矩阵投影到屏幕上。然后通过光栅化的过程,将三角形光栅化为一个个像素点。然后在每个小像素点上,我们去寻找这个像素点对应的材质和纹理,将这个像素点渲染成各种颜色。

绘制的最核心工作是计算。

要将绘制与GPU结合在一块考虑,软硬件都要懂,比如GPU有uniform缓存,有tex采样核心,比如下图那些模块,都要知道是干什么的。

在绘制过程中,着色器代码需要通过常量获取很多数值,然后进行大量的加减乘除运算。
计算一个Phong模型,通过常数访问,变量访问,加上纹理访问,我们就可以得到想要的结果。
在绘制过程中,纹理采样的性能消耗十分大。因为需要进行大量的采样和插值工作。
进行一次纹理采样,需要采样八个像素点,进行七次插值。因此,纹理采样是绘制过程中的一个很重要的运算。

了解GPU

游戏绘制是实践性问题,需要运行在各种性能的现代硬件上。

两个概念:
SIMD:单指令多数据,对于一个四维向量,每进行一次加法操作,它的XYZW坐标会同时进行运算。所以一条指令就能够完成四个加法或者四个减法。
如果在阅读C++代码的时候,看到SSE扩展宏,下面的代码就是在调用SIMD指令。
SIMT:单指令多线程,在多个线程上执行同一条指令,这样便可以共用一个指令解码器,加快效率。
GPU是高并行的,多利用GPU高并行的优势。

图中的结构是重复的
图形处理集群中,有很多流多处理器,在每个流多处理器中国,很多小核心。计算机术语中称为ALU(算数逻辑单元),在NVIDIA中称为CUDA,如果向流多处理器发送一条指令,这些CUDA核心可以同时执行同一条指令。
有专门的硬件进行纹理采样工作,比较复杂的运算,比如正弦,余弦,指数,对数等超越函数运算,因为超越函数的运算速度比较慢,所以显卡中有一些专门的SFU(特殊功能单元)负责处理这些运算。还有一个tensor core,就是用于人工智能处理核心。

同时还有一个RTcore,用来加速光线追踪BVH算法的硬件逻辑电路,这就是现代GPU架构。在GPU上的运算都被分配到每个流多处理器上处理,而流式多处理器中的几十个核心不仅可以进行并行处理,相互之间还可以交换数据,从而进行协作。因此费米架构中的流式多处理器相对于之前的架构增加了共享内存(Shared Memory)。如果同学们有过并行化编程的经验,就会知道,如果CPU之间还可以交换数据,那么就可以实现一些非常酷炫的运算。以上大致说明了SIMT的概念。

尽量减少在CPU和GPU之间传送数据,超级慢。
如果CPU已经准备好数据交给显卡去计算,但需要等显卡计算完毕之后,再将结果从显卡回读到CPU,CPU再基于显卡的计算结果进行一些判断,然后再告诉GPU如何进行绘制。这称之为数据的“Back Force”。这简直是灾难。。。。。。
而且,绘制和逻辑通常不同步,如果绘制需要等待back force,则可能会导致半帧到一帧的延迟。出现逻辑和画面不同步的问题。。。
所以一个原则是,尽可能将数据单向传输。

缓存对于现代计算的性能影响很大,在CPU上,先去cache去取数,当cache miss时,需要去内存中读取A的值。利用好数据的局部性。
在流水线中,哪一块没有处理好,是性能的瓶颈,哪一块就是bounds,比如ALU Bounds(数学运算速度太慢)。
Fill Rate Bounds(写入缓存速度太慢)。
移动端最关注的是功耗,因为移动端的芯片处理能力有限,数据访问对移动端来说十分昂贵,因此开发出了TBR。分块渲染技术。

12.如何高效渲染小型游戏场景

可渲染物体

游戏中大部分物体GO,所有的GO构成了世界。
要区分一个概念,即一个逻辑上所表达的游戏对象,和游戏中可以绘制的物体是不同的。因此,我们在介绍组件时,提到过一个组件,mesh component。有的引擎会叫做skinned mesh component,引擎会假设这个网格是有骨骼的,可以进行变形。
我们需要在mesh component当中放一个renderable的可渲染对象。会有很多花纹,很多纹理,还有法线等属性,这些可绘制的属性,就是renderable最简单的构建块(building block)。

使用EBO使得三角形顶点存储数变少,减少存储压力。但是很久之前会使用三角形带的方式进行一笔画那种的存储三角形顶点,这样甚至连索引都省了,而且还对缓存比较友好。随着硬件的发展,已经不太使用这种方法了。

为什么每个顶点都要存一个法向量。一般来说,计算出每个三角形的朝向,然后使用邻近的几个三角形法向量平均,就可以计算出来顶点的法向量的朝向。但如果是硬表面的折线,就会出现不同表面的两个顶点的位置重合的情况。这两个顶点的法向完全不一样,很次在游戏引擎绘制的时候,在定义你的顶点数据时,一定要为每个顶点单独定义它的法向方向。

材质系统,从phong模型,到基于物理的材质,到次表面散射材质等,变化很大。
有了这些材质模型之后,接下来我们需要的是纹理,在表达一种材质的时候,纹理扮演了非常重要的角色。
Shader Graph。当艺术家想表达各种各样的材质时,会像搭积木一样,将各种元素按照自己的方法进行组合。组合完之后,引擎就会生成一段Shader代码,而这段Shader代码又会被编译成一个Block,和网格存储在一起。各种各样的网格和Shader代码组合在一起,就形成了多彩的游戏世界。因此,着色器代码也是一种关键的可渲染数据。

子网格:对每个游戏对向上的网格,根据应用材质不同,切分成很多子网格,对于每个子网格,分别应用各自的材质、纹理和着色器代码。一般情况下,我们会将网格的顶点和三角形全部存放在一个大的缓冲区中,所以对于每个子网格,只需要存储偏移值(Offset)。换言之,只需要存储索引缓冲区中的起始位置和结束位置的偏移值即可,因为每个子网格只使用了大缓冲区中的一小段数据。这样就可以对每个子网格,亦即缓冲区中某个起始位置到结束为止所形成的所有三角形,单独应用材质、着色器和纹理进行绘制。子网格是现代游戏引擎中经常用到的一个概念,如果大家打开虚幻引擎,或者其他引擎,都会看到类似的结构。有些引擎中可能不将其称之为子网格,但基本原理是一样的。
(其实这个子网格就相当于我在Blender当中进行抠模型的时候那样)。。。。。。

为了节约空间,在现代游戏引擎中,通用的做法是建立一个池(pool),将所有的网格都放到一起,形成一个网格池;将所有纹理放到一起,形成一个纹理池。当绘制一个场景时中的各种角色、以及各种小兵时,我们会发现这些角色和小兵只是通过了一个引用指向了各自需要的数据,比如网格,材质等。

用池的概念去管理网格是最省的。材质池,纹理池。

这就引入了游戏引擎架构中一个很经典的概念,叫做实例化(instance)。刚刚介绍的数据都是实例定义,即我们定义了一个小兵,它的renderable成员应该是什么。当你进行引擎开发时,一定要区分清楚,哪些数据是定义,哪些数据是实例。一般来说,在创建了实例之后,还可以再为每个实例增加一点变化。

GPU有个特点,就是改变参数特别影响GPU的高速运行,比如改变贴图,着色器代码等。每次改变参数,所有32个小核都会停下来,等待参数修改完成,然后再运转。

对上述过程优化,对一个场景来说,有很多的物体使用的都是同一个材质,具有相同的参数,相同的纹理。于是,我们可以将整个场景的物体按照材质进行排序,将具有相同材质的网格分组到一起,然后设置一次材质,绘制这一组拥有相同材质的子网格。直观上看,计算量是一样的,但是这样的运行速度确实会变快。对于底层API来说,会将GPU的状态设置专门抽象成一个Render State Object。具体API不同,基础逻辑相似,其实就是说,预先设置好显卡的状态,尽量不要动,然后进行一大堆运算。所以在绘制的时候,可以用材质进行排序,将同样的子网格归集在一起。

并且在绘制的时候,依次绘制这些物体,并依次设置顶点缓冲和索引缓冲,也是很浪费的。如果我们使用的是现代的绘制API,可以在一个DrawCall中设置一次顶点缓冲和索引缓冲,以及所绘制的一堆位移数据。
即将一系列数据送入显卡后,通过一次绘制调用(Drawcall),就可以让成百上千个物体全部创建出来,这就是GPU Based Batch Rendering的思想。

尽可能把工作给GPU来做。

可见性裁剪

裁剪掉看不见的物体,渲染这些物体会造成渲染浪费。因此,可见性裁剪是游戏绘制系统的一个最基础的底层系统。

每个物体都有一个包围盒,对于包围盒来说,当我们给定一个四棱锥形的视椎体时,我们可以通过一些简单的数学运算,判断物体的包围盒是否位于视锥之中,这就是可见性裁剪的基础思想。

包围盒不仅用于绘制系统,还用于ai,逻辑,物理等模块。包围盒有很多种,最简单的是球形包围盒,最常用的是轴对齐包围盒(aabb),我们只需要存储两个顶点,就可以将轴对齐包围盒构建出来。而且,除了包围球之外,轴对齐包围盒的计算效率也是最高的,如果让包围盒的xyz边和物体的xyz坐标系平行,这种包围盒就叫做定向包围盒(OBB),还有一种包围盒叫凸包,常用于物理运算中。
有了包围盒,我们就可以进行相交计算,就可以进行裁剪。显然,这样的效率不高,因为很多无效判定。联系到BVH层次包围盒,BVH是将包围盒一层一层地沿着树形结构向上合并,当进行裁剪计算时,可以从上到下一层层地进行计算和查询。在BVH中沿该节点一次向下迭代计算和查询,直到叶节点。这时,我们就可以具体得知,哪些物体可见,哪些不可见。

BVH使用相当广泛,原因是BVH在构建树形结构的时候,速度上比较有优势。但是对于小兵这种动态的来说,建立完BVH后,BVH中节点发生移动时,需要进行重建,而BVH算法的重建成本比较低,所以说我们广泛使用BVH。特别是对于拥有大量动态元素的场景时。

潜在可见集是卡马克大神发明的一种算法,方法很简单,使用BSP树,将空间划分成一个个格子,每个格子之间通过一个入口Portal连接。

在建筑物中,房间都是通过门和窗连接在一起的,当玩家处于一个房间当中时,通过每个入口所看到的其他房间是不一样的。PVS的想法十分淳朴,即计算在每个房间中,通过该房间的门窗所能看到的其他房间,并且只渲染所能看到的房间。

PVS除了用于可见性裁剪之外,还可以用于资源加载。

GPU的发展使得很多裁剪方法不再使用上述的方法完成,GPU本身就可以完成这项工作,比如GPU提供的遮挡查询(Occlusion Query)功能。即将很多物体的数据传入显卡在,显卡会反馈回一个比特位数组,每个比特位一次记录了各个物体的可见性。显卡的并行计算能力十分强大,所以计算起来非常迅速。包括视椎体裁剪,我们也可以直接将包围盒数据传递给显卡,由显卡来完成计算。

Hi-Z,就是early z,即在逐个绘制像素时,有的像素会被别的像素遮挡,这时,就不必绘制这个像素。最简单方法是先将场景绘制一遍,但不对像素进行着色,而只计算每个像素的深度。
现在有一种更复杂的方法,比如基于层次结构Hierarchy的方法来进行深度的处理。但是整体思想还是大同小异的。方法都是利用GPU高速的并行化能力,以尽可能廉价的成本,形成遮挡物深度图,然后将可以裁剪掉的物体尽量的裁剪掉,这种做法对于复杂场景十分有用。

13.理解绘制系统,记住四点就够了

纹理压缩

在游戏引擎中,我们一般都会将纹理压缩存储。一般压缩和没压缩之前对比一下,相差会有十倍以上。而在游戏引擎中,我们没法使用一些流行的、非常优秀的算法对图片进行压缩,因为这些压缩后的图片无法进行随机访问。举例来说,对于Jpeg格式的文件,如果给定一个UV,系统无法快速从jpeg中获取到相应坐标的信息,而且这个计算成本非常高。
在游戏引擎中,我们一般采用基于块(block based)的压缩方法。我们将图片切成一个个小方块,最经典的就是4x4的小方块,然后进行压缩。

对于DXT类型的纹理,在一个4×4的色块中,可以找到最亮的点和最暗的点,即颜色最鲜艳和颜色最暗的点,然后将该方块中的其他点都视为这两个点之间的插值。因为对于很多图片来说,相邻的像素之间都有一定的关联度(Coherence)。所以我们可以存储一个最大值和一个最小值,然后为每个像素存储一个距离最大值和最小值的比例关系,这样就可以近似地表达整个色块中的每个像素的颜色值。在计算机图形学领域中,纹理压缩(Texture Compression)都是基于这个思想,称为块压缩(Block Comppression)。在DirectX中,最经典的就是DXT系列的压缩算法。块压缩系列压缩算法的最新版本已经演进到了BC7。DXT系列压缩算法的优势在于,当我们生成了一个纹理后,就可以在CPU上对纹理进行实时压缩。因为无论是压缩还是解压缩,这一系列算法的效率都非常高。
另外一类压缩算法就是手机上使用的压缩算法,使用较多的就是ASTC算法。ASTC压缩分块不再是严格的4x4了,可以是任意形状,而且ASTC压缩效果很好,解压缩的效率也不低。然而,ASTC算法压缩时的性能消耗较大,因此无法在运行中进行压缩。

Houdini就是一个这样的工具,使用程序化生成算法,能够生成漂亮的地形网格。

渲染管线

游戏引擎的绘制系统来说,并不是一个静态的系统,技术一直在进步。其中一个很重要的发展方向,叫做”Cluster-Based Pipeline”。这是一条新的模型表达主管线,基本思想来自2015年育碧软件的<刺客信条大革命>
我们可以介绍一些最基础的概念。当我们面对一个非常精细的模型时,我们可以将其分成一个个小的分块,可以称之为Meshlet或者Cluster。而每一个Meshlet都是固定的,比如32个或者64个三角形大小。因为对于现代计算机来说,显卡已经能够基于数据高效地创建很多几何细节,而并不需要像传统的管线那样,需要预先将顶点缓冲区和索引缓冲区构建好,再将网格数据传入显卡。现代显卡可以凭空计算出很多几何信息,而且如果我们输入一个三角形,现代显卡可以借此生成无数个三角形。因为很多计算是完全一致的,所以可以很好地利用上GPU的并行性能。

在新的着色器hull shader壳着色器,domain shader域着色器还有曲面细分着色器。这些shader的核心思想是,我们可以使用一个算法,基于数据凭空生成很多的几何细节,而且可以根据距离相机的远近,选择所生成的几何细节的精度。对于显卡来说,最高效的数据组织方式就是大小一致的一个个小分块。

Nanite是将meshlet的思想往前深入了一步,做的更加工业化,成熟。
一个非常重要的趋势,越来越多的绘制系统,包括一些复杂的处理,都已经从CPU转移到GPU,以利用现代GPU的高速处理能力。这就是GPU驱动(GPU-Driven)的思想,即将很多在CPU上进行的一些复杂计算全部转移到显卡。利用GPU帮助CPU分担负载。

14.所有渲染,一个方程就可以表示

渲染方程,L是辐射率,E辐照度。
渲染方程的几个难题:
第一个挑战是和光源相关的问题,这其中分为两个问题:第一个问题对光源的可见性问题,即“Visibility to Lights”。即对于某个点,我们能够看到,而这个点是否能被光线照射到。更熟悉的解释就是阴影。
第二个问题是大家不太注意到的,这就是光源自身的复杂性。根据光源的形状,分为方向光(平行光线),聚光灯,点光源。面光源是最烦的,面光源意味着当光源自身发生转动的时候,阴影的形状也会发生变化,这就是光源自身的复杂性。
这个问题的难度在于,对于入射的irradiance来说,我们实际上很难得到它。
第二个挑战是在硬件上如何高效积分,也就是怎么解这个方程,进行着色。
没办法使用数值方法,通过采样求解,效率太低了。只能用于离线渲染。
第三个挑战是在光线的弹射(bounding)问题,这导致所有的物体都可能成为光源。这个比较经典的案例就是康奈尔盒。
这个问题在于,无限递归。比如whitted-style raytracing,最后的计算量爆炸。

15.渲染中的光的数学魔法

基础光照解决方案

对于环境光来说,人们使用环境贴图,由六片图片拼接而成,类似于将一个立方体的六个面展开之后所形成的图像。根据观察者所在的位置,以及表面的法线方向,进行反射,找到立方体所在位置的图像。这样就可以表达光场在物体表面所形成的强烈反射效果。通过这样简单的组合,我们就基本上实现了渲染方程。我们将半球形的光场分布,模拟成一个均匀的环境光,来表示Irradiance的低频信息,再加上一个突出的主光源,然后通过环境贴图来表达Irradiance中的一些高频信息,这样就可以表示BRDF中的Specular项。

其实就是把灯光进行特例化后,得到方程的解析解。

Blinn-Phong模型,十分简单,分为三项,分别是环境光项(Ambient)、漫反射项(Diffuse)和高光项(Specular)。

漫反射(Diffuse)项的计算是使用入射角的余弦项乘以入射光,再乘以一个反射率,因为有些能量会在这个过程中被吸收。高光(Specular)项则用来模拟一些特别亮的效果,看上去很像金属,很像光滑表面感觉得效果。将漫反射项和高光项相加(还有Ambient项,不过一般只是Diffuse项的一个百分比乘积,就得到了Blinn-Phong材质模型)。

两个问题:
1是能量不守恒,
2是对艺术家们来说一点也不友好,无法表现一些复杂材质,做啥都有塑料感。

对于阴影来说,阴影贴图是一切阴影算法的基础。
阴影贴图方法的思想非常简单。因为我们需要知道的是某个点到光源的可见性。首先,我们从光源的视角渲染一次场景,渲染过程中并不需要进行着色,只需要计算出每个点的深度信息即可。这个深度就表示了每个点到光源的距离。然后从真正的相机位置渲染场景,并将每个点反向投影到光源视角的场景中,以得到该点在阴影贴图中的深度。将该点的深度与从阴影贴图中对应位置采样得到的深度值相比较,如果该点的深度更大,则说明该点位于阴影中。否则,该点就不在阴影中。该方法十分简单。

阴影贴图的问题是:走样()和自遮挡(加一个bias)。

16.上个时代的3A游戏,使用的是什么光照技术

对于游戏场景来说,百分之90的物体都不会动,而太阳的角度也是固定的,因此我们可以通过空间换时间的思路,预先计算出光照的信息。

全局光照,我们将其分解成直接光照和间接光照。
全局光照十分重要,如果只有直接光照,没有间接光照的话,画面会不那么真实。

这次我们的挑战什么?我们所得到的全局光照信息,其实是在一个球面空间进行采样,我们可以将球面像地图一样展开成一个贴图。然而,这种贴图数据量十分大,即使我们可以保存整个场景的光照信息,当我们为材质计算光照信息的时候,仍然需要进行积分计算。

使用蒙特卡洛,采样后进行累加,实际上是一个卷积运算,计算量十分巨大。因此,我们需要一种方法,能够实现快速积分运算。

把全局光照信息看成是空间上的一个连续信号,我们可以通过傅里叶变换将其变换成频域中的多个不同频率信号的累加。然后截取前几项,完成对空间中的信号的近似表达。

傅里叶变换两个优点,第一个优点是可以高效的压缩数据。第二个优点是可以降低卷积运算的复杂度。
因此我们使用球面调和函数,也称球谐函数来表示全局光照的diffuse分量(低频信息)

对于场景中任意一点来说,每一点都有一个探针(Probe),记录了该点的irradiance,我们将每个点的irradiance信息展开,然后使用球谐函数对其进行压缩。随后在程序中对irradiance进行重建,如果我们想知道任意一个方向上的光强,只需要一次线性的向量运算即可。
寒霜引擎的报告称,使用了2阶球面调和函数的信息就已经足够了。因为使用球面调和函数是为了表达环境光。
球面调和函数方法的主要思想是,在球面上的两个函数的卷积可以简化成将两个函数分别投影到球面调和函数上,然后对投影得到的结果进行卷积。

光照贴图(lightmap)

我们可以通过球面谐波函数,将整个场景的光照信息烘焙(light farm)到一张图上。
通过上述方式烘焙出来的贴图称为Atlas,翻译成中文叫做航海图或者图集,即将很多几何体平铺到一张图上。首先,需要对世界的几何信息进行简化,不能使用非常精细的几何体。因为过程中有一步称为参数化(Paramitization),会将三维空间中复杂的几何体投影到二维空间。对于简单的几何体来说(比如立方体或球体),参数化是比较简单的。而对于任意形状的物体来说,参数化是比较复杂的,因此需要对几何体进行简化,并在参数空间中进行分配。这里有个细节需要注意,即我们希望对于同样的面积或体积,所分配的纹理的精度基本相同。

图中将每个纹素都标记成了不同的颜色,如果将该图反向投影回场景,每个格子的大小都比较均匀。这是光照贴图的一个很重要的要求。这个要求实现起来并不简单。DirectX在十几年前专门开发了一个API来帮助开发人员进行参数化展开,当时还存在不少问题。

下面,我们就可以进行光照计算了。这是通过很多台高配置的电脑所组成的集群来进行计算的,也叫做“Light Farm”。美术制作完场景之后,将光源等参数设置好,然后就可以开始烘焙。烘培过程一般需要半个小时到一个小时左右。这项技术会让3A游戏的开发效率急剧下降,后来我们自己开发引擎时,由于这个原因,没有应用光照贴图技术。当然,这样的效果会比应用了光照贴图技术的效果差,因为光照贴图烘焙出来的效果确实很棒。它可以烘焙出非常漂亮的全局光照效果,再叠加上直接光照,基本上可以实现光线的反弹,以及相邻建筑物之间的软阴影。这时再加上主光源,整个的光照环境就非常饱满。再配合上材质的效果,你会发现这种感觉非常真实。

光照贴图的优点:一:光照贴图虽然烘焙的速度及其之慢,但是运行时的效率非常高,因为运行时的成本很低。
二:光照贴图是离线烘焙的,在将整个空间进行分解之后,可以产生很多细节而微妙的效果。

光照贴图的缺点:一:烘焙的时间很长,超级无敌长。二:他只能处理处理静态物体和静态光。三:光照贴图的存储和GPU占用的消耗也很大。
光照贴图本身也是一个空间换时间的策略。

光照探针

光照探针(light probe)。在光照贴图算法中,需要对场景进行参数化,将其”拍平”到二维空间上。我们可以在空间中撒一堆采样点,叫做光照探针。每个点上的光照信息用一个探针来表示,我们可以在场景中布置一堆光照探针。对于每个光照探针,采样它的整个光场,当物体或角色在场景中移动时,就可以利用物体周围的光照探针中存储的光照信息,进行插值,计算出物体上的光照信息。图中的小球再移动时,颜色一直在变化,是因为随着小球的移动,周围光照探针中存储的光照信息也在变化。这就是一个简单的空间体素化的算法。先将光照探针变成了一个个的点,然后将这些点互相连接,形成了很多四面体,随后就可以进行插值了。
光照探针算法不是很难,但是需要写一个光照探针自动化生成的算法。

在对光照探针采样时,采样密度会很大。为了避免占用过大的空间,我们会使用一些压缩算法来对光照信息进行压缩。因为我们只需要处理光照信息,而且大部分是漫反射信息。漫反射信息非常低频,我们可以使用球面调和函数对其进行压缩。
游戏场景中,还有一些发光物体,产生高频信息,当角色走在这种环境时,反射信息经常变化。因此又是制作一种专门记录反射信息的探针,叫做反射探针(Reflection Probe)。反射探针数量不多,但采样精度很高。因为反射对于高频信息非常敏感,这样才能反射出周围的环境。反射探针一般会和光照探针分开采样。反射探针加上光照探针,就能够实现不错的全局光照效果。

反射探针和光照探针运行效率非常高,并且能够同时处理静态物体和动态物体。而且还可以在运行时对反射探针和光照探针进行更新。当场景中发生变化时,包括角色位置发生变化时,现代计算机上的光照探针是可以实时更新的。这里介绍一个光照探针的实现细节,在实现时,我们会在探针位置放置一个相机,然后分别向周围的六个位置(上下前后左右)观察,拍摄6张照片。将这6张图片拼成一个图,然后使用GPU的着色器对其进行处理。一般来说,在对光照探针进行更新时,我们不会每帧都对其进行更新,而会每隔几帧进行更新,或者当场景中的位置信息发生巨大变化时,才会进行更新。即使在这时,引擎也不会立即更新,而是会寻找一帧相对较空的时间进行更新。这也是现代引擎架构中所存在的一个理念,即一些可以延迟进行的更新可以延后进行,因为立即进行处理可能会导致帧率不稳定。

和光照贴图相比,光照探针可以给出同样的光照感,包括环境的效果。然而,对于光照贴图技术所能够实现的软阴影以及物体之间的交叠感来说,光照探针就无能为力了。同时,对光的渗透效果(color bleeding)的实现,光照探针也不擅长。归根接地是因为光照探针的采样率相比光照贴图来说,少太多了。。。。

17.什么是迪士尼信条?

基础光照解决方案

PBR,先介绍了微表面理论。
微平面理论使用一个假设来解释光学现象,即光线会在一个表面发生无数次反射。而对于一个金属表面来说,看起来粗糙还是光滑,实际上和该表面上法线的聚集度有关。如果表面的所有微表面的法线方向比较集中,表面看起来就会闪闪发光。如果法线都朝向一个方向,就会表现出类似于镜面的效果。而如果法线分散地比较开,表面看上去就会比较粗糙。这符合人类观察的直觉。
所以对于PBR材质来说,实际上包括两部分效果,一个就是漫反射项,一般用 表示。 实际上是对球面上的所有漫反射能量的积分,这部分可以简单表示为 。其中c是进入物体的能量。如下图所示:

D使用GGX模型。F使用施利克近似。G使用史密斯公式
PBR两种模型,SG模型,高光光泽度模型。没有什么参数,全部属性都是用图来表示。

上图中间图片的第一张图片是漫反射纹理,有三个通道,分别表示RGB颜色分量。然后是Specular纹理,控制Fresnel参数。大家需要注意,Fresnel参数的RGB系数是不一样的。对于黄金、铜这种带颜色的金属来说,它们对于不同色彩、不同角度的反应是不一样的。最后一张图是Glossiness纹理,控制着粗糙度和光滑度。SG模型是一个全模型,它非常完整,非常经典,而且符合迪斯尼信条,即每个参数都在0到1之间。
我们通过纹理采样,就可以得到diffuse、specular、normal和glossiness参数。然后,就可以编写Shader代码。如下所示:

SG模型的f0就是specular。因为specualr不好控制,太灵活了,所以说又出现了MR模型。

MR材质模型中有一个Lerp(线性插值)操作,根据metallic的值来控制金属和非金属效果。
灵活性下降了,但是不容易出问题。。。所以说一般都倾向于MR模型,而不是SG模型。

18.经典3A游戏光照解决方法

基于图像的光照

IBL(image-based lighting)。一个点的光照来自于四面八方的,我们一般说来自于一个球面。在计算机中,我们可以用cube map来表达球面上的光照信息。
但是使用蒙特卡洛采样解方程实在太慢了。。

对于ibl来说,分成diffuse部分和specular部分。
漫反射部分是漫反射辐照度贴图
对于漫反射项来说,它本质上就是一个余弦函数在球面上的分布。对于任何一个法向的朝向点,在给定一个光照时,我们在计算出这个面和球面上所有点的余弦波瓣积分之后,就可以提前计算出漫反射结果。这样计算出的图叫做“Diffuse Irradiance Map”。

镜面光部分是使用split sum的思想,最核心的点在于对粗糙度(roughness)的处理处理预滤波环境贴图和LUT查找图。该方法利用了硬件Cubemap中的mipmap功能,将粗糙度不同的结果存储在mipmap的不同层级中,就像大家现在看到的这张图。

对镜面光照的lighting项进行求解,先查询mipmap等级,之后确定mipmap后,在mipmap中进行查找,预滤波环境光贴图。

BRDF项使用LUT来搞定,里面的值分别是菲涅尔的比例和偏差。横坐标是视线和法线夹角的余弦值,纵坐标是粗糙度。

经典阴影方法

对阴影主流的解决方法————Cascade Shadow,级联阴影映射。
其实类似于一种mipmap的思想,根据距离玩家的距离,生成不同精度的阴影。符合近大远小的原则。在远处人眼采样率和阴影贴图的采样率同时下降形成了完美的匹配。

挑战:cascade shadow需要在不同层级之间需要进行不断的插值,否则可能会出现一条很硬的边界。如果我们不做任何处理,当推动相机的时候,会发现有一个固定的地方,在这个地方阴影会破掉。这是因为不同层级分辨率不同导致的。
Cascade shadow需要多次进行绘制和裁剪,比较昂贵,但是阴影绘制都是比较昂贵的。

软阴影的生成

  1. PCF:利用滤波的方法来处理阴影的软硬
  2. PCSS:自适应滤波核的PCF方法
  3. VSSM:使用数学方法计算出深度的均值和方差,然后利用切比雪夫不等式进行估算,得到阴影的软硬比例。

总结:上个时代,光照一般使用光照贴图和光照探针来解决。一般来说,很多引擎这两个方法都会用,因为这两个方法分别解决了不同的问题。而对于材质来说,主要就是PBR方法。而对于背光面即环境光照的表达,基本以IBL为主。PBR只需掌握两个模型,一个是SG模型,一个是MR模型。掌握了以上知识之后,基本上就可以处理光照效果了。对于阴影,则可以使用cascade shadow方法,然后在使用VSSM或者PCSS,给阴影加上软阴影效果。

前沿:实时全局光照

屏幕空间GI,基于SDF的GI,基于体素的GI,VXGI,RSM。这些GI都在相互竞争。
Lumen是多种算法的组合,是一个超级复杂的工程问题,现在实时光照的革命正在进行,要拥抱变化,不要停滞不前。

3S的皮肤材质,毛发渲染,次表面散射模型等复杂材质的渲染,都推到了一个非常高的维度。毛发主要是借助于Geometry shader技术的发达。

Virtual Shadow Map技术,卡马克提出过一个概念,叫做虚拟纹理,virtual texture。将游戏环境中所有要用到的纹理全部打包到一张巨大的纹理上,这个巨大的纹理就叫做虚拟纹理。需要使用的时候将纹理调出来使用,不用的时候就将纹理卸载掉。
虚拟阴影贴图想法和虚拟纹理想法很相似,在对一个很大的区域生成Cascade Shadow时,该方法会计算哪些地方真正需要生成shadow map,所生成的shadow map密度是多少?然后在一块完整的虚拟的shadow map分配所需要的空间,分块来生成shadow map。相机在进行渲染时,每次命中一个物体,就可以知道应该取哪个shadow map的值,然后再去获取shadow map的值。这个方法能解决cascade shadow 在有些场合下空间利用率不高、占用空间过大的问题。

Shader 的管理,Uber Shader,进行shader 的组合编写,修改任意出问题的起始位置,编译器会自动编译出各种分支的组合。

19.如何用0和1诠释我们生活的大地

地形的几何

表达地形的方法,高程图(也称为高度场),高程图是现代游戏中地形渲染的主要方法。

LOD叫做level of detail,即根据物体在屏幕上占据的面积大小或距离的远近,或者观察对物体的信号的敏感度,设置物体的细节精度。
但是在地形上的LOD和游戏中的物体不同。对于游戏中的物体来说,当其向相机远处移动时,我们可以将其切换成一个更低精度的模型。但地形在空间上是连续的,不会像游戏中物体那样一个个独立于其他物体,所以地形的LOD需要进行特殊处理。
一个简单思想是对网格进行连续细分。
地形渲染有一个优化点是,我们真正关心的内容是在FOV中的内容,所以可以对FOV中的三角形进行精细的细化,呈现很多细节,对于FOV外和远处的地形,可以稀疏地分布三角形。当fov越小时,三角形可以越来越细密,形成望远镜功能。
所以说在绘制地形和游戏中的物体时,需要决定绘制精度时,我们不仅要考虑它的远近,还要考虑到它的FOV。
对网格简化有两个基础原则。第一个原则,我们采取近处密一点,远处稀疏一点的方式进行简化。第二个原则很麻烦,叫做”Error Bound”,即我们需要在数学上进行保证,当对这些网格记性简化合并时,因为采样点导致的地形高低差之间误差不能超过一个给定的阈值。

我们可以使用三角形剖分,一开始地形是一个个方格,我们在方格中间切一刀,它就变成两个等腰直角三角形。如果我们觉得网格的密度不够,还可以继续切下去,即在等腰直角三角形的斜边中间再切一刀,形成两个更小的等腰直角三角形。
可能会出现T-连接问题。

对于给定的大型地形,比如128x128km大小,我们对其不断进行切分,很多引擎最终会将其切分到512x’512m大小,称其为block。存储核心依然是四叉树,它的存储方式和纹理的存储方式类似,都是一个方块一个方块地进行存储,而不会存储三角形。因为存储三角型纹理会浪费存储空间。

从事引擎开发的话,非常推荐基于四叉树的帝乡表示方法,而不要使用基于三角形的方法。因为基于三角形的方法只是一个渲染方法,而基于quad的方法中暗含了资源管理的逻辑。比如虚拟纹理的方法,就是基于quad的方法来实现的。

基于Quad的方法和基于三角形的方法一样,也会产生T型连接,主要是由于两侧的切分次数不同导致的。和基于三角形方法不同,我们不需要切分次数少的一次,而是采用缝合的方法。即将切分更细一侧多出来的点吸附到切分更稀疏的一侧上去。

使用高程图生成地形,这样生成的地形密度非常高。我们可以使用网格简化的方法,将一些不必要的顶点全部简化掉,但会对一些顶点对齐到一些特征上。

十多年前,网格需要程序员来进行构建,然后再使用一些技巧将顶点在Shader中一个个拼接起来。现在我们可以使用GPU硬件自动完成。比如DirectX 11提供的Hull Shader、Domain Shader和Geometry Shader,这些Shader就实现了细分表面的功能。比如Hull Shader的功能就是生成一个细分所使用的的面片(Patch),我们可以将面片理解成一小块的几何区域,由若干个三角形组成,同时受一些控制点控制。Hull Shader的代码有两个阶段,一个阶段是“control point phase”,用于告知硬件控制点数据;一个阶段是“patch constant phase”,用于告知细分器(Tessellator)一些常数信息,比如细分次数。这时硬件管线中有一个固定的硬件阶段叫做细分器(Tessellator),会将面片细分成很多细密的三角形。下图中的中右图(Tessellated Mesh)就是细分后的结果。

这时的数据会由Domain Shader进行处理,Domain Shader会将新插入的顶点,根据采样的高度图来移动顶点的位置,这样会形成地形的高低起伏感。然后就是Geometry Shader,Geometry Shader会将移动过位置的顶点的顶点数据再计算一遍,比如纹理的UV坐标、以及需要传递到像素着色器的数据等等。
动态地形,比如玩家围绕自己的周围,有一个地方,生成了一个地形变形的纹理,玩家在此之上发生的所有打斗,所踩的脚印都会记录在这张纹理上。然后玩家移动的时候,纹理会跟随玩家移动,以保证数据的一致性。这时,只要整个地形都是实时由GPU细分出来的,我们就可以在运行时给地形加上各种各样的变化,再辅之以一定的细节,比如再加上一层offset,增强材质的表现,添加粒子效果等,就可以实现非常酷炫的地形变形效果。

20.手把手教你在游戏引擎中实现地形系统

地形的材质

有了几何表达之后,就要给地形上色,我们使用MR模型,需要保存base color,法线方向,roughness,metallic(作为alpha通道存入到base color中),还有高度图需要进行保存。

如果需要将几种材质混合在一起,还需要一个solatting贴图,即混合贴图,这个贴图的每个通道对应了一种材质的权重。

这种混合将每个材质渲染的结果按照alpha混合方法混合在一起,这种方法其实是有问题的。因为真实世界中,各种材质是按照一定的逻辑混合在一起的,不是简单的混合。比如石头和草。
所以我们利用之前存的高度场纹理,在两种材质过渡区域中,如果发现需要混合两种材质,就使用物体的高度对权重进行调整。比如,物体高,权重就下降得慢一点。如果物体的高度较低,那么权重就很快降低。这样就能够实现沙子好像入侵到石头中,但又不会长在石头上的效果。
如果两种颜色的切换是01切换,直接切换可能会变成高频信息,产生很多抖动。我们可以为高度值添加一个偏移。在数学上,当两者材质加权高度差小于0.2时,我们就不使用01切换,而使用各自的权重进行插值。这样在过渡区域会有一点点颜色混合。

两个概念:纹理数组,和3D纹理
在现代GPU数组中,我们使用纹理数组来存储这些纹理,将所有的颜色纹理打包成一个数组。
对于3D texture来说,当我们在对其进行采样的时候,在当前的mipmap精度下,我们需要对上下左右前后八个点进行采样。我们可以理解成采样需要落在一个xy平面上,而3D纹理还有一个Z坐标,当我们对任意点进行采样时,Z坐标可能不会精确地落在每一层的纹理上,而是需要对Z坐标的上一层四个点进行采样,然后进行双线性插值,而对于下一层的四个点,也得进行采样,并进行双线性插值,然后对这两个插值的结果再进行一次插值,这样才能得到我们需要的纹理值。所以3D纹理的采样是一个三线性插值,因此采样的消耗很大。

而对于纹理数组来说,虽然也是很多层纹理叠在一起,但是每层纹理之间是没有关系的,因此在对其进行采样时,第三个坐标就不是Z坐标,而是索引(index).索引值只是我们对底基层纹理进行采样,而不会对中间层(比如1.5层)进行采样。这样也符合我们表达地表材质的方式。

当进行材质混合时,会在splatting map中存储两个值,一个是用于混合的权重,还有一个是所使用的材质的索引。我们使用索引来获取纹理数组中的相应材质的对应采样点信息,然后乘以权重,再依次获取每个材质的信息和权重,讲这些颜色值混合到一起。

在绘制小石子路的凹凸感的时候,会使用贴图的方式进行渲染。
当我们使用法线贴图时,会形成明暗相间的凹凸感。这就是视差贴图,视差贴图的方法比较简单,假设地表几何体有凹凸感。当人眼看过去的时候,因为空间高度不一样,所以会产生视差。当人眼看过去时,本该看到的是A点,但因为存在高度差,A点被B点所遮挡,所以人眼看到的是B点。
这种偏移是使用raymarching方法实现的,即通过一条光线,一步步往前走,直到和某处相交,这时会产生更强烈的立体感。和凹凸贴图相比,视差贴图立体感更强,但是更昂贵。

还有一个更彻底的方法叫位移贴图,它将近处的网格进行更多次细分,然后根据高度图信息对地形进行真实的变形。但是对于现在的很多游戏来说,大部分游戏还是各种凹凸贴图技术的结合应用。

之前说了,纹理采样的性能消耗非常大,采样一个最简单的2D纹理,需要采样8个点,进行7次插值。在实际应用中,如果一个点上有4种材质,那么采样次数也要相应的乘以材质数量,计算量会急剧增加,十分昂贵。
纹理数据储存在显存当中,获取数据时需要经常来回寻址,效率非常低。因此,基于splatting的材质混合的性能消耗很大。

在任何一个游戏中,我们在任何时刻所看到的地形只是游戏场景中的一部分,因为太远的地方会被裁剪掉。即使太远处的地形仍然可以看到,我们也可以使用很粗糙的几何体和纹理进行表达,所以我们引入虚拟纹理。

上面的几句翻译为下面的:
构建虚拟索引纹理以表示整个场景的所有混合地形材质
仅根据视图的LOD数据加载材质数据的tile块。
把混合的材质预烘焙到tiles当中,并且把它们存到物理纹理中。

虚拟纹理的核心思想就是只将需要用到的数据装载在内存中,而其他不用的数据仍然在硬盘上。该方法依然是分块的思想
虚拟纹理的相关介绍和实现
知乎上关于虚拟纹理的文章,写的很好。

虚拟纹理的优点:

  1. 第一好处是地形数据在显存中占用的空间极大地变小了。我们只需要用到的地方,纹理永远是以上次的1/4精度向下递减,上确界是1/3
  2. 可以减少纹理的混合操作次数,
    对于屏幕上任意像素来说,该像素所用到的所有纹理在每次渲染时都需要进行混合操作(blending),实际上,这个混合操作,可以在这个分块被加载时,执行一次,在后续渲染中,只要这个分块依然位于此处,没有变化,我们就不需要对其进行混合操作。这个过程叫做烘焙。

现在虚拟纹理的方法已经一统天下了了,因为好处实在太大了。

游戏运行时,我们需要在硬盘,内存和显存三处来回调度数据,中间有一个内存管理算法,不断发布调度指令。但是数据是通过总线来传递的,当从硬盘读取数据时,CPU会发出呼指令将数据从硬盘读到内存,然后再从内存读取到显存。实际上,显存才是数据的消费者,和内存还有CPU没有关系。

第一个技术是”Direct Storage”。在现在的一些新型显卡上,DS可以让数据只在内存中经过,但是不进行解压缩等处理。直接到显存中解压,这样传输过程中的数据都是压缩过的,也就是几乎是最小的那些数据。
最高效的技术是DMA技术。在Direct Storage中,数据还需要在内存中中转一次,而DMA方法就是我们可以直接将数据从磁盘读取到显卡中。把CPU和内存给绕过去了。可能会改变PCIE架构。

在绘制地形的时候,可能存在的问题还有浮点数精度溢出,且这个问题当地形越做越大时,就会遇到这个问题。
计算机中是IEEE754的float标准,计算机用了32个比特位来存储浮点数的所有整数和小数信息,所以当我们表达一个特别大的距离,又要求距离精度精确时,就会出现精度不足的问题。当浮点数越来越小或者越来越大的时候,特别是越来越大的时候,数字的小数部分的精度会变得很低,甚至会超过1.

针对浮点数精度溢出的情况,可以考虑将所有的float型都换成double型。但数据量会膨胀。有一个很简单的方式,现代引擎中常用的一个方法,叫做”Camera Relative Rendering”。即相机位置是相对的。渲染方法很简单,将物体坐标从世界坐标系转换到相对相机的位置。
量级的数据就可以用浮点数轻松表示。
在UE5中,实现了subLevel,即将一个关卡切分成很多小的子关卡。在每一个子关卡中,会将世界坐标系重置一次,这样就可以解决浮点数的精度问题。

植被,道路和贴花

渲染树来说,在近处,看到的树是真正的网格,而到了远处,会逐渐过渡并使用插片来表树木。这些插片不会让观察者感觉到任何错误,而且会越来越稀疏,到更远处就会变成billboard,而且一次会绘制一大批的billboard。
一个中间件,speedtree,渲染数目很成功。

Decorator(装饰物)是另一个在环境中经常遇到的。Decorator主要是指地面上的草丛、灌木丛、小碎石等,这些内容一般都会使用最简单的网格来表达。
Decorator的实现方法是插片方法。

道路系统最常见的实现方法是样条曲线,即使用各种控制点,对曲线进行拖拽和控制。
但是没有那么简单,在设置道路时,程序不仅需要将整个道路上的所有贴图全部生成,还需要对高度场进行切割和腐蚀,对高度场进行处理。

贴花(Decal)实际上是小贴片,一开始出现在射击游戏中,枪击墙壁,墙壁上会有很多弹孔,这些弹孔就是贴花。再加上一些法线相关效果。

无论是道路贴图还是贴花,在现代游戏pipeline中,都会被烘焙到虚拟纹理中。在虚拟纹理的每个分块中,混合了原始的地形和材质,在有道路的地方,在混合上道路的纹理,对于贴花的地方,也把贴花混合上去。在进行渲染时,可以一次性将所有这些效果全部渲染出来,这也是虚拟纹理的一个优势,因为虚拟纹理将所有的复杂性都放在了烘焙过程中,而在运行时进行渲染时,成本十分低。

21.刻画游戏氛围的”大气”,该如何实现

大气散射理论

两个元素,天空、云层,不能混为一谈。还有一个效果是雾效,我们会在后处理效果中介绍雾效,因为雾效不只是天空中的大气现象的一种表现。

为什么天空是蓝色的?
当太阳直射天空时,大量的蓝色光会在大气场景中被散射开,然后经过多次的散射进入人眼。这时候就会发生瑞利散射现象。而傍晚时的天空是红色的,因为光线斜射到人眼所处的位置,很多蓝色光在大气层边缘直接向大气层外进行散射,因此天空中蓝色的光线越来越少。红色成分越来越高,因此天空呈现出红色。

22.超真实的云朵效果是如何渲染的?(稍微阅读下就可以,因为大量数学公式推导,大气辐射模型那些。。。)

实时大气渲染

算法Ray Marching沿着一条视线进行步进,并将沿途的结果一步步积分起来。

云的渲染

一些比较重要的点,柏林噪声,沃利噪声。
云的渲染主要是使用了RayMarching方法。

你可能感兴趣的:(笔记)