doom3技术分析

前言

2011年11月doom3代码发布

2012年11月doom3 BFG代码发布

两个游戏内容虽然相差不多,但doom3 BFG版本在技术上有很大进化(多线程框架,SWF,立体渲染)。本文技术分析主要基于doom3BFG版本

架构

doom3技术分析_第1张图片

idCommon是总控制类

绿色的是子系统,可以在各处调用。

Entity是基于继承的结构

多线程渲染

整体分为两个线程,主线程(BackEnd)和Game线程(FrontEnd)。主线程负责获取输入,处理网络消息,调用渲染API,以及调度Game线程。Game线程主要运行游戏逻辑以及生成RenderCommandBuffer。据说这个架构在doom3的版本里就已经实现了,但当时两个线程是反过来的,主线程生成RenderCommandBuffer,子线程调用渲染API,(FrontEnd和BackEnd的名字可能就是从那时候来的),但实现后不知道为什么在某些机器上运行不稳定。因此doom3在在发布的最后时刻又改成了单线程实现。直到后来才发现OpenGL API非创建Device的线程上被调用时,在某些平台上会有问题,但所有文档上都没提过这事儿。(看来卡马克也是被各种坑啊)

主线程:

获取用户输入

接收和发送网络消息

交换RenderCommandBuffer

启动Game线程

渲染上一帧RenderCommandBuffer中的内容

 

Game线程:

运行一到多个逻辑帧

生成本帧的RenderCommandBuffer

说明:

1.为了保证游戏模拟结果与帧率无关(在多人网络环境中尤其要保证这一点),游戏逻辑是固定在每秒60帧的。并且时间是离散的,是毫秒的整数值,相同的输入总会得到相同的输出。(图中的RunFrame)

2.idFrameData是一个线程安全的DoubleBuffer,同一时刻一个buffer用于生成RenderCommand,另一个Buffer用于渲染。线程安全用原子操作实现。

3.整个架构只在线程对象的底层有锁的使用,上层实现中完全无锁。大量使用了只读对象和任务并行。

 

逻辑帧(RunFrame):

遍历entity更新

     物理模拟

     animation

     更新RenderWorld中对应的实例

统一处理event

简单提一句event系统,event系统实现了调用的封装,这样就可以:

1.       对象间的操作可以通过网络传输,或者保存

2.       延迟调用,比如3秒后调用一下对象A的B方法

3.       脚本对象的绑定,所有的event方法都可以被脚本调用

Draw

生成RenderCommandBuffer(Draw)主要做两件事:基于BSP的精确Culling,将Culling的结果生成Light和DrawSurf_t的链表,用于渲染。Culling的过程比较复杂,下面展开讲一下。

如图

整个场景被划分成多个area,area之间由portal连接。area引用其内部的Entity和Light对象。Culling过程如下:

1.首先通过bsp树快速计算camera位于哪个area内

2.根据这个area和当前portal状态,计算出所有联通的area集合(有些portal是可以打开关闭的

3.由camera构造一个culling volume。用这个volume剔除当前area中的所有entity和light。

4.遍历area中的portal,用portal构造一个更小的culling volume(portal stack),如果volume为空,继续计算下一个portal。

5.如果不为空,用新的culling volume剔除portal相连area中的entity和light,将可见的加入可见列表。并递归第4步。

6.所有protal遍历结束后,函数返回。

Culling完成后得到所有可见的entity和light列表。随后要针对这些entity和light生成RenderCommandBuffer。分为两步,AddLight和AddModel:

AddLights

     遍历链表中的Light

    EvalueateRegister

     复制渲染所需的数据

     遍历light作用到的area和area内所有的entity

         culling,确定一个light是否照到一个entity(只计算动态的entity,静态的是预计算好的,即interaction)

         shadow相关

AddModel

     遍历链表中的entity

     遍历链表中的light,找到照到entity的所有Light

     分配baseSurface,用于DepthPass

     对于找到的light,依次分配surface

     将DrawSurface链接到对应的light的链表中

需要指出的是,针对每一个Light的AddLight操作和每个Entity的AddModel操作都可以运行在单独的线程中。可以这样做是基于:

1.       线程安全的FrameDataBuffer,新的计算结果记录到分配出的对象中,并用链表链接到当前对象中

2.       引用到的其他对象都是只读的

idInteraction

大部分场景和光源都是静态的,如何加速静态场景的culling和渲染?

每个PortalArea本身也是一个Entity,并对应一个mesh

每个(Light,Entity)对生成一个对象,即Interaction

蒙皮模型和粒子不会计算interaction

场景加载后会预计算所有的静态的interaction,精确地计算被光源照到的表面的index

预计算的index会用于渲染(AddModel的过程中加入链表)

一个light移动后对应所有的interaction会被释放

GameThread生成的一帧的数据

橘黄色的都是本帧从FrameData中分配出的临时对象,这些对象保存在RenderCommandBuffer中,在下一个Frame中,主线程会使用这些数据进行渲染,渲染完后这些内存会被自动释放。蓝色的对象是从游戏逻辑模块中复制过来的数据。

渲染

多Pass,no lightmap

depth pass

每个light一个Pass

    global stencil

    no selfshadow

    local stencil

    has selfshadow

材质

idTech系列引擎的材质系统是一个表达式求值系统。大部分的材质只是简单保存一些贴图,shader,颜色,渲染状态等。但另一些材质需要复杂一些的功能。想象一个不停地闪烁的灯,它的颜色实际上是时间t的函数。而材质系统正是提供了这样的功能。例如这样一个材质

models/weapons/soulcube/soulcube3fx
{
   noSelfShadow
   translucent
   noShadows
   {
        if ( parm7 > 3 )
        blend add
        map models/weapons/soulcube/soulcube3fx
        rgb scTable[ time * .5 ]
   }
}

中括号内是这个材质的一个stage,parm7是使用这个材质的Entity的一个参数。这个材质的描述是当这个参数大于3时才执行这个stage,这个stage本身的操作是混合一张贴图和一个颜色,这个颜色是根据时间查表得来的。当一个Entity确定会被渲染后,在AddModel过程中会对其使用的材质做一次求值,if判断和查表操作都在这时计算。

                          //evaluate the reference shader to find our shader parms
                          floatrefRegs[MAX_EXPRESSION_REGISTERS];
                          renderEntity->referenceShader->EvaluateRegisters(refRegs,
renderEntity->shaderParms,   
tr.viewDef->renderView.shaderParms,
       tr.viewDef->renderView.time[renderEntity->timeGroup]* 0.001f, renderEntity->referenceSound );

虽然这些功能在现在的引擎中都可以在VertexShader和PixelShader中完成,但这些Entity粒度的计算确实没有必要在每个顶点,甚至每个pixel中都计算一次,更何况那还会shader组合爆炸的问题。

网络同步

FPS是对实时性要求极高的游戏类型。特别是在多人网络游戏环境中,网络的带宽总是有限的,网络延迟也不可能无限小。那么如何实现一个一致的,高响应,低延迟的网络游戏架构就成为了一个极具挑战性的工作。

 doom3技术分析_第2张图片

整理架构

各客户端每隔40毫秒将用户输入发送给服务器,服务器运行整个游戏逻辑,物理模拟等。然后每隔100毫秒左右将整个游戏的状态存成一个快照,将快照发送给个客户端,客户端接收到快照后,将自己的状态跟快照同步。

由于网络延迟的存在,客户端和服务器不可能完全同步。实际上,客户端总是领先于服务器,理想情况下,客户端的输入发送到服务器时,服务器的时间正好和客户端获取用户输入的时间相同。当然这只是理想情况,实际情况是客户端总是要领先的更多一些。由于客户端领先于精确的游戏世界(在服务器端),因此客户端需要根据已知的游戏世界的状态做一些预测,直到服务器发来下一个游戏世界的快照。然而新收到的快照的时间也早于客户端当前的游戏时间,客户端需要退回到快照中的游戏世界,然后重新预测直到当前的游戏时间。

快照中会有其他玩家最新的输入,这样有助于更准确的预测。由于用户输入的频率远小于网络同步的频率(10-20Hz),因此玩家几乎察觉不到自己身处的游戏世界是预测来的。

网络数据压缩

doom3使用了多种网络数据压缩方法,其中最有效的的是delta compression,因为游戏世界中的大部分对象的大部分属性是不变的,改变的只是小部分,因此跟上一个快照相比,我们只需要发送这个快照不同的部分。


参考

http://fabiensanglard.net/doom3_bfg/index.php

http://fabiensanglard.net/doom3_documentation/The-DOOM-III-Network-Architecture.pdf

你可能感兴趣的:(doom3技术分析)