几周前,从同事那里拿过来Stefan Zerbst与Oliver Duvel合著的《3D Game Engine Programming》来看。 书是用电子版打印的,全英文。看起来还是有点吃力的。这本书的内容是从零开始,讲述如何实现一个3D游戏引擎。开发平台为Windows,图形接口使用的是DirectX。

游戏引擎的作用

关于游戏引擎的作用,实际上没有严格的定义,其主要目的是为了代码重用 本书里面里面给出了几个定义,我觉得有点难于理解和翻译,所以把原文也摘抄下来:

  1. Manage all data in its responsibility area 管理其责任范围内的所有数据。游戏引擎的一个功能就是游戏资产管理,游戏资产是指游戏中要使用到的资源如图片、音频、字体、配置等。
  2. Compute data 计算数据。指的是除开游戏逻辑部分,属于游戏引擎负责的任务的计算。
  3. Pass data to its following instance 将数据传给其子实例。个人认为子实例或许指的是游戏引擎中用到的具体实例,如DirectX中创建的实例等。
  4. Accept data 接受数据。接受数据应该是指游戏引擎可以接受外部传进来的数据,如通过输入设备,或是游戏逻辑部分计算的数据。

一般来讲,游戏引擎应该独立于具体的游戏,表面上看就是游戏引擎代码要能独立编译,不能有对具体游戏逻辑代码的依赖。从内部来分析,就是游戏引擎在设计的时候应该是能兼容某一类游戏,提供弹性的接口。

3D游戏引擎 vs 3D 引擎

首先,必须得区分3D游戏引擎和3D引擎的区别。一个游戏引擎包含很多子系统,如图形引擎,音效引擎,物理引擎等。 3D引擎一般主要指图形引擎,负责3D图形的计算和渲染。也就是说3D引擎主要是负责游戏中图形图像的相关处理。 一般来说一个最简单的游戏引擎应该包含如下几个子系统:

  1. 3D图形引擎(本书主要讲述的是3D游戏引擎的设计,所以对应的是3D图形引擎)
  2. 音效引擎
  3. 输入系统:键盘、鼠标、操作杆等
  4. 网络
  5. 物理引擎
  6. AI

以上只是一个游戏引擎的大体结构,具体的运行时游戏引擎架构,我觉得可以参考Jeff Lander和Matt Whiting合著的《游戏引擎架构》中第26页的图1.11。该图给出了从底层硬件,到顶层游戏逻辑的详细结构。有兴趣的可以看看。这本书虽然看起来很厚,但是内容不是很深,广度是还可以,比较适合作为游戏行业的入门书籍,帮助了解一些基本术语和知识。后面,我也会把这本书的笔记整理出来。

游戏引擎一般是面向接口设计的,大部分子系统都使用了单例模式

本书引擎结构

在本书中,作者实现了一个简单的3D游戏引擎,包括以下几个模块:

  1. RenderSystem:渲染系统。
  2. Audio:音效系统
  3. Input:输入模块
  4. Network:网络

除此之外,还有一个 3D 模型系统,以及一些工具类。

RenderSystem

渲染系统,即3D图形引擎,主要负责渲染设备的初始化和管理,提供一组用于渲染3D图形的接口。 在具体编程实现上体现为,首先设计一个接口,提供了该引擎上层模块需要用到的基本图形接口。然后在选取一个具体的图形接口如DirectX或者OpenGL来实现这个接口。当然,在设计这个接口的时候也应该使接口与具体实现无关。在游戏代码中,最好不要直接使用具体的图形接口提供的函数。

OpenGL是一个跨平台的图形接口,但是针对OpenGL的扩展函数却是依赖于平台的,而我们在使用OpenGL的时候,一般情况下都会使用这些扩展函数,所以在实现渲染系统的时候必须要考虑扩展函数的处理。

同样的情况也发生在关于窗口的操作上,Mac OS X、Linux、Win32的窗口系统都不一样,因此有必要建立一个中间层来对窗口进行封装。

3D 模型系统

该系统实现了对基础3D模型的建模,同时提供了相关的计算方法,这个系统主要涉及到计算机图形学中算法的应用。 这些3D模型包括:

  • Vector:用来表示向量或三维坐标。为了便于与4*4矩阵进行计算,通常使用四维Vector,第四个值大部分时候没有意义,置为1。
  • Matrix:44矩阵,常用于坐标变换。33矩阵用于法向量的变换。
  • Ray:射线(或线段),其属性包括一个起始坐标,和一个方向向量。可以用于模拟子弹的轨迹。
  • Plane:平面
  • AABB:Axis-Aligned Bounding Box,坐标轴对齐的有界盒子
  • OBB:Oriented Bounding Box
  • Polygon:多边形

对于这些几何模型,都有一些相关的计算:

  • 基本运算,如Vector的+、-、/、点乘、叉乘等
  • 相交计算:判断两个物体是否相交
  • 几何变换:平移、旋转等
  • 裁剪

单纯的图象渲染,基本上只需要涉及到顶点(可以用Vector表示)和矩阵(用于变换),像射线、平面、有界盒子等模型多用于物理系统中。将一个复杂的物体(如人体)划分成若干个简单的几何模型,通过这些简单模型的物理属性(如体积、质量等),来模拟复杂物体的物理属性。 在最简化的情况下,可以将人体模拟为一个圆柱体(设想一个透明的圆柱体将整个人体罩住),通过这个圆柱体来代理碰撞等物理事件。 复杂的情况下,可以细化到人体躯干、四肢,甚至小到手指、睫毛都可以绑定一个基本物理模型。

材质、纹理、光照

以上的3D模型只是构成了3维世界中,物体的几何特性。真正展现在显示器上的,是各种各样的图片。 比如,一个长方体,长宽高,位置等都是它的几何信息。可以用一个AABB来表示。但是这些信息并不能构成它到底看起来是什么样子。

对于现实世界中的任何一个物体,基本上都有一个材质(Material)属性。比如一个用木头做的长方体。

(实际上,这可以从物理学上分析,一个没有材质的物体,是不可能反射光的,而人眼看到物体,都是因为物体能反射光到人的眼睛里) 不同的材质,看起来的样子不同,主要是因为光线的反射系数不同(个人理解,具体请参考光学书籍) 作为对现实世界材质的抽象模型,在计算机图形学(CG, Computer Graphic)中,材质主要由几个光照反射因子组成。 在CG中,将光线分为三种类型:环境光(Ambient)、漫反射光(Diffuse)、镜面反射光(Specular),这三种光都可以用RGB值来表示,对应的材质具有对这三种光的反射因子:Ka、Kd、Ks,对于镜面反射还有一个镜面反射强度参数(Shininess)。 大部分的物体都是因为反射光线进入人眼,人们才能看到该物体,但是有一种物体不需要反射光线,人们也能看到,那就是光源。 为了表示自发光物体,引入了自发射因子(Emission)。 除了上面提到的这些元素外,还有其他一些元素用来模拟更复杂的材质模型,如透明度、折射率等。 综上,可以用下面的结构来表示一个基本的材质模型。

struct Material
{
	// 与光源交互的因子
	Color kAmbient;
	Color kDiffuse;
	Color kSpecular;
	// 自发射颜色,表明物体即可反射光线,其自身也可发射光线
	Color Emission;
	// 表征镜面光的锐利程度
	float Shininess;
};

材质能反应出物体表面与光线的交互情况,要想表现出丰富的视觉效果,就需要使用纹理。 纹理实际上是2D图形,比如木头上的一个年轮可以认为是一个纹理,一个坦克模型上绘制的迷彩等,也可以认为是纹理,纹理可以用来表现物体的细节。 通过建模工具,可以为一个模型的所有顶点生成纹理坐标(常见的2D纹理坐标如(u, v),其中u, v都在0到1的区间内),假设给定一个2D纹理贴图,其左下角的像素位置为(0, 0),右上角的像素位置为(1, 1),通过插值计算,就可以将纹理图片映射到三维物体表面。

材质和纹理都能影响物体看起来的样子,除此之外,还有另外一个因素能影响物体看起来的样子,那就是光照。光照不同,物体反应在显示器上的样子也不同。 从光照类型来区分的话,如前面所说的,可以分为三类:环境光(Ambient)、漫反射光(Diffuse)、镜面反射光(Specular)。

  1. Ambient Light:环境光用于模拟光线在物体间反射所造成的光照效果。因为在CG中一般无法直接计算这种光线的间接反射,所以直接用一个环境光来代替所有这些间接光照的效果。其特点是没有位置,没有方向,等量作用于当前环境内的所有物体表面。

  2. Diffuse Light:漫反射表示直接光源所发出的光线在粗糙的物体表面随机反射的现象。是一种主要光照类型。其特点是与入射光和物体朝向有关,与观察者位置无关,能反映出粗糙物体表面光线的明暗过渡。

  3. Specular Light:镜面光用来模拟对于像金属、塑料等材质进行光照时,会形成特别明亮的区域的现象。其特点是反射光分布具有方向性,可见区域为一个圆锥体,与观察者位置有关。

光源类型来区分的话,也可以分为三类:平行光(Directional Light),点光源(Point Light),聚光灯(Spot Light)。

  1. Directional Light:也可称为Parallel Light,平行光,如太阳光。这类光源的属性有方向,RGBA颜色值,强度
  2. Point Light:点光源,类似于没有没有灯罩的电灯泡,其特点是向所有方向发射光线。这类光源的属性有位置,RGBA颜色值,强度。其光线的方向是需要计算的。
  3. Spot Light:聚光灯,类似于手电筒,其光线向某一个区域发射,通常是圆形区域。这类光源的属性有位置方向,RGBA颜色值,强度,内圆锥夹角外圆锥夹角。Spot Light照在平面上看起来是一个圆环,内圆锥夹角对应的半径为R1的圆,外圆锥夹角对应的是半径为R2的夹角,R2>R1,在半径0 - R1的区域,亮度较高,在R1 - R2的区域亮度逐渐降低,大于R2的区域,基本上为环境光状态。

平台独立层

前面提到了RenderSystem不可以避免的会使用到与平台相关的一些功能(如窗口创建与管理),因此对于一个大型的跨平台游戏引擎,一般都会通过平台独立层,来将上层引擎模块与底层隔离。(此处强调跨平台,但是即使对于不需要考虑跨平台的游戏引擎来说,也有必要对平台进行一些封装,以考虑以后的引擎升级)。 平台独立包含的内容实际上是相当丰富的。基本上游戏引擎会使用到的,各个平台具有差异性的内容,都需要在平台独立层实现封装隔离。 按照《游戏引擎架构》的描述,有以下方面需要涉及到。

  1. 平台检测:作用显而易见,就是检测当前使用的平台。
  2. 原子数据类型:同样是int,不同的平台对应的字节数可能不同,因此有必要使用像typedef这样的类型定义语句,结合前面的平台检测,来保证程序使用的原子数据类型的一致性。
  3. 文件系统:游戏引擎必然会涉及到游戏资源的管理,大部分资源都是动态加载的,所以针对不同的文件系统进行封装,也是有必要的。

其它的还有网络传输层、线程库、图形包裹类等。

未完待续……