越 的个人资料三无之徒照片日志列表 工具 帮助

日志


12月31日

我的2008

“从今天的结果来看,引擎的材质系统和核心渲染流程的调整比我们预想的要快。”

“嗯,因为我们经过了详细的设计和思考,想的比较全面,所以实现起来也就比较快”

“对,其实二年多的引擎开发过程也积累了很多经验教训,为这次第二代引擎的开发打下基础,虽然之前走了不少弯路,但也并不白做。”

以上是我和引擎开发组的同事在20081231日晚上10点钟的对话,今天我们加班,明后天还要加班,加班的目的是尽快完成Dream Engine 2的材质系统和渲染核心,为全面开展第二代引擎的开发铺平道路。正是公司的新的游戏项目促进了第二代引擎的开发,次世代游戏的标准对引擎提出了更高的要求,由于第一代引擎兼容固定管线和低配置硬件,如果继续在其上开发次世代引擎,结构上会变得很复杂,实现上也很复杂,实际上从十月份开始的次世代渲染技术的尝试过程中已经显现出这一点,同时实现针对更高硬件平台特性的功能,比如新的图形硬件架构、多核架构等,原有的引擎更加难以适应。其实在几个月前我就已经有开发下一代引擎的想法,只是没有想到会这样快。实际上Dream Engine 2将是一款游戏引擎,图形引擎只是其中一部分,它将包含网络、物理、脚本等网络游戏开发相关的内容,它还会集成更多的可视化编辑工具,为游戏开发提供便利,成为次世代网络游戏的集成开发环境。程序同事们对开发次世代引擎是热情高涨,美术和策划同事们也希望能使用更强大的引擎编辑器来制作游戏,公司希望能制作出顶级的游戏,我也希望再一次挑战自己的极限,这些是我们的希望和期盼,是2009年的最重要的工作之一,这次的开发时间会很长,还有许多的技术需要研究和探索,我非常荣幸能有机会开发这样一款引擎,我相信我们能再一次超越自己,走向更加精彩的2009年!

5月5日

引擎的资源管理线程化

    五一假期无所事事,唯一值得欣慰的是作出了一个重要决定:将引擎的资源管理线程化。今天写了一个基于 Ring buffer 的双线程同步器,和在此基础上的资源处理器。写好后准备进行资源管理的改造,但是遇上的第一个难题就是如何处理资源加载的异步IO,其实这个问题在上一次的设计中也有提到,当时的方案时没有异步IO,因为将来引擎线程化时,资源的IO处理都是在主逻辑线程,所以对Rendering线程来说,也算是异步IO了,不过今天我推翻了之前的想法,觉得还是应该要异步处理,毕竟游戏逻辑也是不容阻塞的。但问题关键不在如何异步IO,因为线程同步器已经写好,而且引擎的抽象文件系统和线程模型无关,所以这不是问题,问题在于如何处理异步IO对引擎使用者的影响。比如:创建一个角色动画对象,游戏使用诸如 CreateActorFromFile 之类的函数,如果在创建时指定异步IO,很有可能出现对象没有创建好就被使用的情况,比如调用创建函数之后立即设置一个角色动画等。在这种情况下不作处理一定会产生问题。这个问题我之前也想了很久,一直没有想出很好的解决方法。其实我希望能做到外部创建资源而不知道资源加载的视线细节,和同步创建出来的资源相同的使用方式而不需要做任何特殊的处理。但实际上这是不可能做到的,像上面的例子就无法解决。下班后,我在咒骂公交公司害我等了1个小时公交车的过程中的想出了如下的解决方案:为引擎的资源对象增加一个AsyncWaitForLoading函数,无论是否是异步IO创建资源,都可以调用这个函数,如果是异步创建,这个函数内部用傻重试的机制来不断的测试资源是否加载完成,如果是同步创建,则立即返回。这样,当此函数返回时,资源对象就一定可以正常使用。这个在本质上此方案也是让外部知道了引擎资源加载的一些实现细节,不过仅是个函数调用而已,在接口使用上也不会太脏,最主要的是解决了异步加载和对象使用的复杂问题,总体来说是利大于弊。另外从实际的游戏开发中来看,一般静态的关卡资源可以使用异步机制,比如地形、室内、静态物体等,动态对象大多数是同步机制,比如角色(也有例外,比如《苍天》,进入后主城你户会看到走来走去的身体零件,虽然异步,但玩家感觉不好,比较好的是 wow,淡入淡出),即便是异步机制,对于游戏来说也可以是作为预加载技术来使用。此外,还需增加一个查询 Loading 是否完成的函数,为外部提供方便的查询机制。

    接下来就要细分资源的加载过程,我将资源的加载大体上分为4个阶段:磁盘IO读取-〉内存对象构建-〉设备资源创建-〉显存数据填充。这4个阶段是按照时间顺序执行的,理想的情况下每个阶段都是在不同的线程中被处理。磁盘IO读取可以是几个IO线程并行,设备资源的创建一定在渲染线程中,显存数据的填充可以另作一个线程,当然也可以直接放在渲染线程中。我希望各个阶段可以灵活配置,而不受于线程的限制。前面所说的资源创建完成的标志在第2个阶段设置完成时设置。这里有个细节问题还需要说明:引擎的资源之间存在着引用关系,所以当加载一个资源时,在构建其内存对象的过程中会解析出所引用的其他资源,比如角色资源中引用了骨骼资源,骨骼资源中又引用了动画资源等等,这种情况将出现在第2阶段中,由于我希望所有的IO处理都交给IO线程来完成,并且线程同步是基于Ring Buffer机制,所以当需要读取其他资源时,由内存对象构建线程向创建资源的线程投递IO请求,然后再创建资源线程投递IO读取请求,如此反复。

    线程化是个比较复杂的工作,调试起来也比较麻烦,但这是引擎线程化的第一步,没有资源管理的线程化,引擎线程化就是空谈。粗略估算了一下工作量,这要耗掉我80%的时间,不过我已经有近3个月没有对引擎做大的改进了,希望能在5月份完成这个工作。

5月1日

路漫漫

这阶段引擎的开发基本上是跟随着游戏开发的深入进行的,这种开发方式也是我认为比较合理的方式:根据游戏的实际需求来完善引擎.几乎很少有闲下来的时候,不过引擎的总体开发进度还是快于游戏的进度,因为在游戏项目计划的早期就及时订制了引擎的未来的功能需求.目前甚至可以确定了到今年底的开发计划:
1.优化,这部分包含的内容很多,比较大的有数学库的多媒体扩展指令集的优化,初步打算使用 mmx\sse 这两个国内市场普及的指令来优化。还包括动画系统的优化,重点是骨骼动画。还有粒子系统、场景图、文件系统等,大体定为2个月的优化时间。
2.兼容性测试和调整,在游戏正式 CB 之前要进行大范围的硬件兼容性测试,这对 3D 游戏很重要,所以这部分目前定为 2个月的测试和调整时间。
3.零碎地修修补补,整个引擎功能的细节修正和 bug 修改,这包括:引擎核心、渲染器、功能层部分、编辑器、导出插件,以及一些游戏的特殊需求,这是贯穿在今年的过程中了。实际上这1个多月来我一直都在忙着修修补补,一方面导出插件的功能有变化,另一方面随着引擎编辑器的开发和游戏的特殊需求,也暴露出引擎内部的一些结构和功能上的问题,还好问题都不大,都可以在现在的结构上进行改进。
上月末游戏发布 prototype 版本,策划提出了关卡模式的设计变化,目前还没有最后确认变更,不过这个变化需要引擎提供动态资源管理能力,这是我非常感兴趣的地方,之前的异步资源管理的设计都已经写好,但是由于更改比较大--如果做的话,是引擎开发以来最大的变化--而且还有特效和角色编辑器的需求,所以我一直没有动手,其实主要还是那时游戏并没有这样的需求,现在看来这种可能型还是比较大,因为关卡模式的变化的确会带来游戏更好地体验。
五月份的主要工作内容包含一个一引擎渲染系统的变化,问题还是由于游戏的需求引起的,这个需求让我决定改进渲染方式,其实在引擎开发的早期我已经发现问题,比如水面纹理的渲染、环境立方体贴图的生成,还有游戏中角色头像和物品在子窗口中的渲染等等。我跟同事讨论了一下,昨天定下来了设计方案,打算在 5 月中旬完成开发。由于游戏的人手紧张,我决定帮助游戏部门完成 avatar 系统和游戏的特效功能,这期间将正式使用4月份的开发成果,一定还会有许多问题。
现在我感觉引擎越来越成熟了,游戏开发已经有半年多的时间,这段时间引擎一直紧跟着游戏的实际需求,其中也作了几次大的重构和调整,这些都让引擎越来越稳定,功能越来越完善。如果没有游戏,是不可能做到这一点的。不过引擎的成熟和稳定都是相对的,和我心目中理想的标准比起来还要差得很远,但我知道这是一个漫长的过程,需要经过多个游戏项目才有可能达到这个目标,还有很多很多的事情需要做,我甚至已经把引擎的整体开发计划定到了 2010 年。
3月20日

关于引擎外存资源的管理问题

定义:外存资源指的是通过内容创建工具,比如 max\maya\photoshop\engine/game editor 等创建出来的存储在磁盘、光盘等外部设备的资源文件。

问题:通常外存资源之间存在引用关系,比如模型文件引用贴图、动画等,目前引擎的管理方式是直接在资源文件中包含被引用资源的相对路径文件名,但这两天对游戏资源目录的调整暴露了这个方法的严重问题:当资源目录改变时无法正确读取资源。实际上这种做法对于做数据库开发出身的我是非常不认可的,我们知道关系数据库在建立数据之间的关系时是通过对数据编号,然后指定关联的数据的编号即可,这是关系数据库的基础,通过编号来建立数据之间的关系。如果采用这种方式建立资源间的关系,用编号来标识资源,即使资源目录发生变化,甚至资源名称发生变化,也可以正常读取。在引擎开发的初始我也曾想这样做过,但如果真的这么做,却遇到了实际游戏开发中的问题。

    要做到对资源编号,就要求资源编号必定在统一的环境中生成,还是用关系数据库举例,数据库表中的数据的都是通过关系数据库系统的开发环境来创建的,创建的同时就指定了编号,所以可以做到通过编号引用数据。而游戏中的资源的创建却不是如此,如上面对外存资源的定义所说,资源是很多个不同的内容创建工具生成的,这种特殊的资源生成方式就决定了几乎无法做到统一的资源编号。当然,可以在每一个不同的工具中通过读取统一的资源编号列表文件来生成资源编号,但是资源往往是由多个人创建的,每个人都会修改这个统一的资源编号列表文件,这就会导致统一资源编号列表数据同步的问题,一旦产生同步错误,整个的游戏资源的管理就会陷入混乱。而且,在生成资源的同时,游戏开发团队中的其他人很可能会删除某些资源,为了保证资源编号的有效性,就必须要回收删除的资源编号,但这会更容易导致同步错误,所以这种方式不可取。那么主流的游戏引擎如何解决这个问题呢?

    Unreal:统一强大的游戏编辑器可以编辑游戏中所有资源,从这方面来说,它很容易做到资源统一编号,因为它有统一的数据生成环境。但是这种方式却不适合国内游戏的开发习惯,尤其不适合美术人员的使用习惯,通常 art 在 3dsmax \ photoshop 中做好 model \ texture 后通过导出插件导出到指定目录下就可以直接使用,但是 Unreal 却要求必须进行一次导入 Unreal 编辑器的操作,否则引擎无法直接使用导出资源。而实际的情况是 Art 希望导出资源后能立即看到渲染效果,然后对资源做相应的修改和调整。而 Unreal 本身的资源管理特性决定了无法做到这一点,要想看到最后效果,就必须导入 Editor。这种繁琐的操作对于 Art 来说是非常痛苦的。当然,如果经过一段时间的培训,不习惯也会习惯。

    Gamebryo \ Renderware:导出时可以即时看到资源的渲染效果,这对 Art来说很方便,而且可以将多个资源导出成一个包。但是和我们的引擎存在同样的问题:使用资源的文件路径\名称来定位引用的资源。Gamebryo \ Renderware内部的 API 提供了设置 Resource Path 的功能,甚至 Gamebryo在内部还做了局部的容错处理:当引用的资源无法加载时,尝试在当前资源所在的目录下加载引用资源。但即使这样,还是无法从根本上解决问题,在我看来,这种做法其实是纵容了资源管理的任意性,大大增加了资源目录结构混乱的可能性。

    从以上的分析似乎可以得出这样一个结论,要做到资源管理的灵活性,就要牺牲资源产生的便利性;要资源产生的便利性,就会失去资源管理的灵活性。也许这是无法调和的矛盾,或者,还有更好的解决办法?

1月16日

资源管理系统的初步想法及其它问题(续)

显存资源管理
    上次说到自己做显存资源管理,也就是说全部采用 D3DPOOL_DEFAULT 方式创建设备资源,当显存耗尽时,引擎使用 LRU 或者 MRU 算法来释放显存资源。这几天翻了翻国外网站,也看了一下主流的 3D 引擎的资源创建方式,大部分都倾向于采用托管的设备资源管理(D3DPOOL_MANAGED),理由有这么几点:
1.D3D 或者 Driver 对资源的管理更加高效,因为是在操作系统的内核模式下,所以当恢复资源时一定会比用户模式下更快。
2.由于 Managed 的含义是指托管给 D3D/Driver,则 D3D/Drvier 一定会根据自己的最优化方案来进行设备资源的管理。这一点要比自己做管理要灵活得多,自己管理的无论多么高效,总是不能放之四海而皆准,在不同的硬件平台下可能会产生完全不同的结果。
3.资源的管理结构简单、清晰,这是显而易见的。
    基于以上几点,调整引擎的设备资源管理方案,除了一些必要的 Default 资源外,其余全部以托管的方式来创建,这样引擎不必管理设备相关的资源,当显存不够时,Driver 会自动清除一定的 Managed 资源;当渲染不在 GPU Local memory 的资源时,Driver 会自动 Upload 到 GPU 中,整个过程对引擎完全透明,流程清晰简单。不过还是有必要跟踪显存使用情况,为调试性能提供参考数据。
File Mapping
    我之前关于 FM 的 MapViewOfFile 函数的理解是错误的。我以为在 MapViewOfFile 时操作系统会分配一段内存并将映射的文件数据拷贝到内存中,实际上并非如此,当 MapViewOfFile 时系统仅仅是做了内存地址的映射,并没有作任何文件数据的读取,但是当使用 MapViewOfFile 返回的内存指针访问/修改数据时,才会产生真正的文件读取操作。另外,使用 FM 不一定比读取普通文件更快,经过测试发现:当读取的数据在操作系统分配粒度(64k)以下时(包括64k),会有较明显的性能提升,但是当超过分配粒度大小时,性能会下降,读取的数据越大,性能下降的越厉害。对此种现象目前我没有找到任何解释答案,初步怀疑操作系统本身有缓存机制。最后测试 33k+ 的文件,总计 1.5G 以上,平均读取性能要快 30% 左右。
文件系统
    在原有方案中修改包数据存储时将包中所有数据都读入内存,再存入硬盘,这样效率太低,虽然可以多次修改一次保存来提高效率,但是如果包更新时(比如网络游戏更新游戏资源)对于大文件的数据包,存储的时间还是难以让人忍受的,更大的问题是在长时间的存储过程中发生异常情况的几率会变大,发生异常时会造成整个包无法读取,影响用户感受。所以要采取一定的策略提高包保存的性能。
    目前采取的策略:新增数据时,通过文件结构信息获取最适合的空间进行存储。在修改数据时,首先判断修改后的数据大小是否和原数据匹配,如果大于原数据,则将数据保存在文件尾处;删除数据时直接将文件结构信息中的文件信息删除,原有数据不作任何改动。
    实际上这是一种 Append 方式的数据更新机制,势必会造成包中存在碎片,如果不经整理,包会越来越大,碎片会越来越多,因此,需要增加碎片整理机制,整理包的算法基本如下:通过文件结构信息获取最适合的空间安排,从前到后的进行数据迁移,保证不会进行多余的数据拷贝。接口提供整理级别,整理大小,整理范围等参数。
引擎的线程模型及资源管理的线程同步策略
    初步打算将引擎的渲染作为单独线程,引擎的场景图更新放在另一线程,可以放在游戏逻辑主线程。渲染线程中使用 Command buffer 来处理所有请求,包括:设备资源创建请求、设备资源创建完成、渲染请求等等。设备资源数据的 Upload 由设备资源 Upload 线程完成。内存资源的创建由 IO 线程完成。这样引擎最多会同时存在 4 个线程:场景图线程、渲染线程、设备资源 Upload 线程,IO 线程。
    当画完线程处理流程图之后我才发现,需要同步的地方太多了,虽然锁中完成工作并不复杂,但是频繁的锁定会严重影响性能,因为在锁定的同时会使其它线程挂起(这一点在多核平台下后果会更严重,会造成其它核心挂起),严重降低 CPU 的使用率(因为其它线程在等待),不能完全并行。更为重要的是可能会造成死锁,虽然可以从设计上避免这个问题,但还会存在潜在的危险。一开始我首先想到的是 LockFree,这是一种乐观同步机制:当修改数据时,假定没有并发操作发生,在真正提交数据时如果和之前的期望数据不一致,则继续循环直到一致为止。实际上 LockFree 还是基于 CPU 的原子操作 CAS ,这个指令很多 CPU 都已提供,x86 还提供了 CAS2 语义的指令,但是 LockFree 最困难的地方在于内存管理,当数据被修改时也可能被读取,所以不能立刻销毁数据对应的内存,但何时销毁就是个比较麻烦的事情了。由于资源管理线程基本上是一个生产者和一个消费者,并且访问是顺序的,所以可以使用 RingBuffer 来完成同步,这也是一种 LockFree 同步机制,不同的是它无需采用 CAS,因为两个线程不会修改同一数据(但是会访问同一数据,所以还要做重试机制)。
1月9日

资源管理系统的初步想法及其它问题

    最近一直在思考动态资源加载的解决方案,上周末看了一下 d3dsdk november 2007 中的 content Streaming 的例子,对于动态多线程后台资源加载有了一些初步的认识和想法,结合最近已经开发了 80% 的虚拟文件系统,引擎的资源管理系统大体上有了一个设计。
    大概的内容是这样的,对于 3D 游戏来讲,按照存储位置可以将资源分为三种类型:
  • 外存资源,指的是存储在硬盘、光盘或者 usb 设备中的物理资源文件。
  • 内存资源,指的是存储在内存中的资源数据,这个数据可以是从外存中加载,也可是游戏运行中创建。
  • 显存资源,指的是存储在GPU控制的显示内存中的资源,这个资源只能通过内存来加载或者写入。

    三种类型的资源,采取不同的加载策略,同时设计为三个不同的层次,其中,外存资源的管理是最底层的,其加载和存储的管理由虚拟文件系统完成,内存资源和显存资源的管理则在引擎的资源管理系统层次上进行。先从外存资源也就是虚拟文件系统说起。

    虚拟文件系统的核心思想是存在一个抽象文件系统的管理接口,有两个接口的实现,一个是针对 windows 文件的,一个针对打包文件的。因为引擎的资源文件在生成时往往是分散的小文件(比如从 max 导出的模型或者动画资源),这些小文件又会引用其他的资源文件,比如模型引用某张纹理,骨骼引用了某个动画等等。如果没有抽象文件接口,而直接采用资源包的形式读取资源,则势必要求资源在生成时也要打入某个资源包中,就需要在资源生成时进行资源的包处理,这样对于美术来讲使用并不方便,并且国内的美术也不习惯这种方式,不利于快速开发。如果能做到文件系统在读取某个资源时不需要关心这个资源的存储方式(也就是说不管它是一个零散的文件还是在某个资源包中)的话,我们就可以在游戏开发期使用零散的资源,而在发布游戏版本时打成资源包,说白了讲就是把多个资源文件打包成一个资源包文件,然后所有的资源读取和存储都在这个资源包中进行操作。有了抽象文件系统的接口,用它读取资源时,就不用关心资源的存储方式了。
    对于 windows 的实现来说,就是普通的文件读写操作,用 api 或者 io stream 实现就可以。对于打包文件系统的实现有些麻烦,因为要做到接口抽象,所以在接口层次上不能暴露有关包的任何信息。在实现的过程中有两个需要注意的地方,一个是资源的快速查找,一个是资源数据的快速读取。查找资源的性能我还是比较在意的,因为性能是资源管理好坏的重要衡量指标之一,所以开始我没打算使用 stl::map,由于 stlext::hash_map 我早已用过,也在考虑之列,不过因为是使用资源的全路径来索引资源,所以总是避免不了要进行字符串的比较的,而且为了保存 string 的 key 占用的内存也不少,时空比上没有达到我比较满意的程度。以前曾经看过 blizzard 的 hash 算法,通过 3 次 hash 操作可以得到一个几乎不会重复的索引(当然还是有一定概率的,只是几百万亿分之一的可能),进行查找时几乎是 O(1) 的时间,更主要的是它无需保存 string 的 key,大大地减少了内存占用,我做了 hash_map 和 blizzard 的 hash 比较测试,30k 的不重复数据,随即查找 10k 数据,结果是在查找上 blizzard 稍有优势,但内存占用上却是 hash_map 的 1/4,这个时空比还是不错的,遂采用。
    对于资源包中资源的快速读取,原以为不会有问题,结果这两天在测试读取性能的时候,结果却有不同:在频繁读取超过某个大小的数据时(比如1M),读取就会比普通的读取零散文件要慢,而且慢的比率比较大,有时甚至能达到 2 倍,这个过程排除了包管理内部的开销造成的影响,甚至我单独写了个测试程序,从 1G 和 256M 的文件中分别读取 256M 大小的数据,1G 的读取就是慢,这让我有些不解,从理论上来说从一个大文件频繁随即读取数据,因为只有一次 fopen 的开销,应该比频繁的 fopen 再 fread 会更快,但实际的结果却不是这样,难道是大文件随机读取数据时硬盘磁头读取扇区数据的方式和读取零散文件不同?这个无从得知,如果知道的朋友请赐教。后来我改用 File Mapping 来加速资源读取操作,效果还是不错,不过由于 File Mapping 本身的一些特性,在随机读取数据时,还需要额外的一些处理,不过这些处理的开销都是可以忽略不计的,整体的性能依然可以达到相当不错的程度。但需要对 98 操作系统进行测试,目前这个工作正在进行。
    内存资源和显存资源的管理,目前引擎中也有基本的资源管理,这点在前一篇 blog 中也提到了,如果对内存和显存资源进行动态的管理,旧有的方式就完全不适合了。对于纯粹的内存资源来说,流程比较简单,当请求一个资源时,送入加载资源队列,在 IO 加载线程中由 virtual file system 进行资源读取,当读取完成后送入资源加载结束队列,游戏线程(可能和渲染线程不是同一个)使用这个内存资源。当物理内存或者指定的内存耗尽时,可以通过回收一些资源来释放内存空间,释放规则可以是最近最少使用等原则。对于显存资源,由于不能直接从外存资源加载,所以流程比较复杂,不过主要思想还是 content streaming 的设计思路,首先一定是渲染线程发起资源的请求,如果在资源管理器中没有找到资源,则视为未加载资源,并将请求加入到资源加载队列,在 IO 加载线程中由 virtual file system 进行资源读取,当读取完成后送入资源加载结束队列,渲染线程创建设备资源(比如 texture、vertex buffer、index buffer、shader 等),创建成功则进行映射数据操作,并送入资源处理队列,在资源处理线程中对设备资源的映射数据指针进行处理和拷贝,完成后送入资源处理完成队列,在渲染线程中就可以正式使用这个设备资源了。当显存或者指定的显存耗尽时,如果还有渲染资源需要上传,就必须要释放设备资源,释放规则可以是最近最少使用等原则。但释放时可以保留内存数据,以便下次请求时直接进行设备资源的处理和上传。关于这部分还有很多细节内容,这需要在实际开发中逐步解决,这里只是提出初步的设想和方案,下次完成之后再详细探讨。
1月1日

Dream3D 引擎开发状态(二)暨引擎的 2007 回顾与 2008 展望

昨晚 msn 上有人问我引擎开发的如何,是否有截图。说实话,我觉得截图只能说明引擎的的表象功能,比如渲染、工具的界面等,而这些还需要看渲染性能的好坏、工具的稳定与否、功能是否强大等等,只有通过使用引擎真正的开发一款游戏,才能了解一个引擎的真正能力。如果一个引擎能用几张截图就能说明其全部功能的话,我相信这是一个功能不完善的引擎。这一年的引擎开发让我也对引擎有了更进一步的了解,对引擎开发的认识也与以往有些不同。引擎到底是什么?如果放在5、6年前,这个问题还比较容易解释,但是近几年的 3D 引擎的发展,已经逐渐外延了引擎的概念。比如说 Unreal3 引擎,我个人认为它已经超越了传统的引擎的范畴,事实上它应该是 3D 次世代游戏的集成开发环境(IDE),从这一点来说,可以预见未来的 3D 引擎的发展将是越来越强大的开发工具和开发环境,使次时代游戏开发变得更容易、更快捷,缩短开发时间,减少开发成本。图形硬件技术的突飞猛进和 CPU\GPU 技术的不断发展,使得游戏本身变得越来越复杂,交互能力越来越强大,这直接导致了玩家希望能玩到越来越真实和更高交互能力的游戏的需求,从游戏开发商的角度来说,不断的超越竞争对手获得更大的市场回报使得更多更新的开发技术加入到游戏中,从而刺激玩家的的需求和硬件的发展,这是一个良性循环。言归正传,先说 2007 的回顾。起初我把 Dream 引擎定位在图形引擎,渲染方面达到本世代级别(farcry\unreal2),部分达到次世代级别(doom3\unreal3),因为是 3D 图形引擎不是游戏引擎,一开始这样定位也比较正确。不过目前 Dream 引擎已经不再是纯粹的 3D 引擎,(我做了层次结构,核心层还是图形,只是从整体功能来讲已经包含了游戏引擎的部分基础功能),实际上引擎的渲染功能在去年就开发完成了,到了现在也没有大改过。但是只有渲染是远远不够的,为了能达到灵活的图形表现,我用了前后近3个月的时间开发了材质系统(横跨 2006 ~ 2007),目的是使引擎具有自适应的渲染材质的能力,这样引擎本身不再限制渲染,而是通过编写不同的材质来设定渲染能力,这给后来的游戏开发带来了很大的方便,我也试过几种次世代渲染技术,有了这个系统,都比较容易的嵌入到引擎中。渲染是需要资源的,这又产生了对资源的管理,而我当初在开发资源管理时为了尽快地测试图形渲染功能,没有用太多的时间设计和开发,只是做了简单的加载、引用计数等功能,这部分一直是我的心病,我知道早晚要补上,2008 年上半年的工作目标之一就是开发全新的资源管理系统,稍后再细说。有了渲染、材质和简单的资源管理,静态的图形表现基本没有问题了,接下来就是动态表现,所以开发动画系统就很自然的成为引擎的 2007 年的又一个开发重点,在完成了渲染部分之后,引擎从 2 月份开始开发骨骼动画系统,后来逐渐加入了顶点动画、纹理动画、位移动画、粒子系统,而这些功能的开发,都对牵扯到资源管理系统,几乎每一次功能的开发都要对资源管理进行扩展和重构,这是当初轻视资源管理所带来的恶果,也反映出我们的引擎开发经验不足。相反,材质系统和渲染系统由于之前下足了功夫,所以没有太大变化。动画系统这部分另一个比较耗费时间的,当属 max 导出插件的开发,和上面一样,没有统一的设计和缺乏相关的开发经验也导致了 max 导出插件的多次重构,从功能上来说我自己也不满意,这部分也将作为 2008 年引擎重构一个重点。在这里详细说一下粒子系统,粒子系统进行过两次开发,第一次参考了 farcry 引擎的粒子编辑器,第二次参考了 unreal3 引擎的粒子编辑器,第二次的开发完全抛弃了原有的版本,因为看到了 unreal3 粒子编辑器的强大。从结构上新的粒子系统也与原有不同,尽管有些性能上的损失,但是带来的却是功能上的灵活和强大。实际上这之前引擎是有编辑器的,但是无论对于功能还是界面操作我都不满意,在第二次开发粒子编辑器的时候,我采用了 wxWidget 来开发界面,不过这也是无奈之举,MFC 或者 ATIL\WTIL 也可以做,这些我也用过,但是它们的结构复杂性太容易把代码搞乱,尤其对新人来说更是如此,MFC 是宏封装消息机制,ATL\WTL 是模板+宏,而 wxWidget 是完全面向对象的架构,虽然在实际开发上还有些麻烦,但是比较容易理解,门槛不高,但是其在架构上保证了一定的简易性,不象 MFC 使用那么麻烦和复杂,用好也不容易,如果让新人来写 MFC 程序恐怕难以维护。毕竟引擎不是开发完就可以了的,是需要不断维护的,否则人来人往,最后很容易就死掉。事实证明,一个毫无 GUI 经验的新手,用 wxWidget 很容易上手并且编写出很好的界面,至此,引擎的编辑器算是比较正规了。关于引擎的编辑器,也是 2008 年开发重点,目前我给引擎定位在这样几个编辑器:材质编辑器、粒子编辑器、地形编辑器、动画编辑器,其他的编辑器根据游戏需要再决定。我根据中国的上古十大神器来给引擎的编辑器命名,目前粒子编辑器被称为“神农鼎”,接下来还会有“轩辕剑、盘古斧、炼妖壶”等。2008 年另一个打算要做的扩展就是实现树形骨骼动画系统的功能,并且支持部分骨骼的 IK,最好能实现骨骼物理属性。引擎的性能优化也是 2008 年的开发内容,原计划在 2007 年下半年作性能优化,但目前游戏对引擎的功能要求比性能更加重要,所以可以放在游戏开发的后期再作性能优化。这部分主要包括数学库的计算性能,使用 CPU 扩展指令进行数学运算,多核技术的优化也在考虑之列,但如果要充分发挥多核的能力,需要对程序结构做一定的调整以适应多核,所以这部分还需要进行探索。前面提到了资源管理,实际上这部分内容还是不少的,从渲染层次上来说,资源管理是上层功能,资源管理需要更底层的文件系统来服务,所以这又引出文件系统。我打算实现抽象文件系统,屏蔽掉文件管理的细节差异,实现跨操作系统和包文件系统,12 月份就在作文件系统的设计和开发,期望在春节之前能全部开发完成。有了这一层次,在资源管理和文件系统之间就是引擎对象的序列化(或称之为持久化)层次,当然我不做那种面向对象的序列化,而且我也不认为序列化仅仅是指数据从内存到硬盘,从硬盘到内存也可以认为是序列化,只不过是方向不同,甚至可以是从网络到内存\硬盘,所以这个层次就是序列化框架,有个序列化接口,细节完全透明,目前已经实现了两种格式的序列化器:xml 和 Bin。对于需要序列化的引擎对象来说,直接使用这个接口来进行序列化。这个层次的存储的功能是通过抽象文件系统来完成的,每个对象要负责自己的数据序列。要达到这种程度,引擎还需要进行较大改动,因为这涉及到了引擎的很多个核心对象,同时还包括导出插件。有了这两个层次,资源管理的底层支持就完备了。动态的资源管理一直是我想要实现的功能,关于这部分功能以前也是没有什么思路,后来看了 云风 blog 上 的几篇关于资源管理的文章,再加上自己的思考和实际的需求,有了初步的设计,不过这需要在实际开发中去验证,同时后台资源加载也是我想要实现的功能,这对大型 mmo 来说也是一个必要的功能。
2008 年有许多工作要做,在这些工作中最重要的是目前我们在开发中的游戏,我们的目标是今年内能有一款 JCC 自己研发的游戏上市,,也是公司对上海研发方面的要求。2008 年公司对我的要求是负责管理整个游戏研发的技术,这就使我不仅要负责引擎的开发,还要负责游戏的开发,对我个人来讲是一个更大的挑战,我期望我能完成上面所说的引擎开发的内容,同时更希望能看到游戏的成功上市。道路曲折,而我喜欢富有挑战的生活,2008,我们来了!
11月22日

小试多核技术

昨日A项目经理跟谈起多核技术,说对引擎来说也许有一定效果,实际上引擎加入多核支持早在年初的一次引擎开发评审中韩国技术总监就提出过这样的意见,我原打算放在引擎开发后期再去实现。今天翻了一下 OpenMP 相关的知识,决定小试一下,测试结果还令人满意。我只是测试了引擎的例子系统更新部分,在 Core2 6750 CPU 上更新 4k 个粒子有接近 200 fps 的提升,由于其它开发人员的机器并不是物理双核,是逻辑双核(及超线程技术),我以为 OpenMP 对逻辑双核无用,结果运行发现还是有一定作用,看来我对超线程有认识上的误区。不过提升幅度并不大,4k 粒子更新只有 10 fps 左右的提升,并且两个逻辑核心都几乎是满负荷运行,而物理双核却只有 30% 左右的负荷。不论怎样,使用 OpenMP 多核技术确实对引擎运行效率有提升,而且现在大部分的计算机中即使不是多核也基本上是超线程,所以使用多核应该没有太大问题。不过在测试中我也发现 OpenMP 的缺陷,它对运算结果没有相互依赖的并行计算支持很好,但是如果有依赖关系,就要更改代码的实现方式,这会造成最终发布应用程序上的差别,所以多核技术还需要成熟,最好不要给程序员带来代码实现上的限制,vs2008 即将推出,希望在支持多核开发上有更大的改进,使多核技术在应用上更加成熟、方便。
11月16日

Dream3D 引擎开发状态(一)

好久没没有在 Blog 上更新关于 Dream3D 引擎的开发状态了,主要是一直在忙。目前引擎的核心部分已经完成了 3D 图形游戏的基本功能,包括场景图管理、渲染系统、动画系统、粒子系统、资源管理、材质系统,工具部分包括 max 导出插件、粒子特效编辑器、实体编辑器。以上各个系统具体的功能还有许多,比如动画系统支持骨骼动画、顶点动画、uv 动画、轨迹动画,其中骨骼动画还支持动画混合、动画过渡、子动画、多动画,uv 动画支持任意 texture 层等等。实际上上述功能在 7 月份就已纪基本完成,核心部分在 5 月份已经完成,其中骨骼动画部分在 5 月份进行了重构,导出插件部分在 6 月份进行了重构,7 月份又进行了资源管理的重构,目前正在进行的是粒子特效系统的重构,重构是为了能拥有更强的编辑和扩展能力。引擎的结构也进行了多次的大大小小的调整,目前调整部分比较少的就是场景图管理、渲染系统、材质系统。
从 8 月份开始,公司的游戏项目开始使用 Dream3D 引擎开发游戏 Demo,9月末 Demo 第一版开发完成。在图形表现上并没有特别复杂之处,加了 PostGlow 的全屏特效,Full Scene 的 ShadowMap,VS 实现的草被风动的效果,uv 动画,引擎的动画过渡技术使角色运动起来更平滑。游戏中动态物体比较多,有大量的碰撞会同时发生,不过从测试的效果上看,性能还是令人满意的,这归功于 Bullet 物理引擎的高效。另外,我们还为游戏 Demo 开发了关卡编辑器,在开发之前我希望在编辑器中实时切换到游戏状态,这会给策划提供很大的便利。为了这个目标,我搭建了一个编辑器和游戏应用的框架,最后做到了能在编辑器中编辑数据之后实时切换到游戏,效果还算令人满意。
在这期间我尝试了几种次时代 3D 技术,比如 Deferred Shadow Map,基本的功能目前已经完成,不过还需要一些完善才能达到实用,首先是软阴影的实现,目前我采用随机采样点实现软阴影的边缘,但是是基于屏幕空间,这样会出现无论物体远近,阴影的边缘都是相同粒度的现象,对于离 Camera 较远的阴影效果不好,所以我打算采用基于 View 空间来进行点采样,这也是 Crysis 和 UE3 采用的技术。其次是对于阴影物体的选择问题,因为是基于后期处理,所以对于不需要产生阴影和不需要被投影的物体进行过滤,我考虑可以使用 Stencil buffer,不过如果和其他渲染技术结合的话,有可能会出现争用 Stencil Buffer 的情况,所以最好是用单独的 RT 保存 Shadow Mask。
11月30日

停下来歇歇

    从9月中旬开始,我马不停蹄的开始引擎的代码编写工作到现在,有很多时候甚至晚上回家还再继续写,加上最近孩子刚刚出生,每晚睡的比较晚,有些疲惫。昨天照例回到家里打开电脑写程序,还没动手,发现引擎中的材质系统中渲染状态对象分类的一个问题,当然可以先按照目前的设计做下去,以后再改,不过以我的做事风格来说,发现了问题如果不解决,心里总是不舒服,经过仔细考虑,发现如果更改需要做较大的变化,可能需要几天的开发工作量,这样原来的引擎开发计划就受到了影响。
    其实这个问题在一开始的时候就曾经想过,是否要按照 D3D10 的标准来划分状态对象,但当时考虑只支持 DX9,所以这个念头也就放了下来,这段时间在实现 DX9 材质系统的时候,发现 DX9 的渲染状态实在是太乱,不过这并不是 MS 的开发人员的问题,本身 D3D 也在发展,这些混乱的标记也印证着 D3D 的发展历史。回头再翻 D3D10 文档,API 简洁明了,非常清晰,当然代价也是显而易见的,完全不向下兼容,从软件到硬件全部重新设计。想想也应该这样,否则 D3D 怎么能发展下去。由此,我决定引擎全面转向 D3D10,采用 D3D10 的一些先进理念设计引擎的内部结构,尤其是渲染和材质系统部分。对于 D3D9 的支持,可以通过映射来实现,毕竟这个引擎是面向两年以后的游戏,那个时候 D3D10 已经大行其道。
    再说说引擎的设计,一开始我曾想过把引擎抽象出来,做成插件形式,可以有不同的渲染 API 来实现,不过我发觉这实际上很难做到,毕竟不同渲染 API 差异很大,实现起来很困难,时间较长,而且要对各个 API 都非常了解,另外最重要的是效率问题,抽象独立的代价就是效率的降低,毕竟 3D 引擎性能很重要,前段时间看了一下 unrxxl3 的代码,也是和 D3D 混在一起,根本见不到 OGL 的影子。还有就是关于设计,也许早年受到 OO 的影响太深,总觉得抽象接口不应该依赖具体实现,但这几年写程序我发现这完全就是 OO 的谎言,OGRE 的代码我看过,为了抽象和 OO ,做了太多无用的工作,导致性能比同类引擎比要低很多,最近又听说 ATI 下一代 GPU 完全基于 D3D10 架构设计,以换取最佳的性能体验。硬件依赖于软件结构,这在以往是完全不合理的,但存在就是合理,事实上 ATI 早就放弃支持 OGL,为了提高性能,从硬件上按照 D3D 来设计,这样减少了许多无用的转换和判断开销,Driver 可以尽其优化,基于 OGL 的 DOOM3 一发售,ATI 的 OGL 问题立刻暴露出来,由此可见一斑。再说 Gamebryo 引擎,我也看过他的代码,是 For D3D9 的,这个引擎号称支持所有平台、所有渲染api,但实际上它每种平台和 3D API 都有不同的实现,抽象的部分并不多,比如 For D3D9 的,引擎代码中大量依赖了 D3DX 的库,甚至数学库干脆直接用 D3DX 的!想必也是为了性能考虑吧。
    不过尽管如此,我还是把引擎抽象了出来,目前只考虑实现两种 API:D3D9 和 D3D10,在性能方面我取了个巧:例如把 D3D9 的常量定义类型定义都原封不动的引擎的核心定义中,只是名称不同,这样就无须作类型转换和映射。在接口方面我原来是采用 D3D9 的接口方式,现在看来要转向 D3D10,这部分也要作相应更改,会更合理一些。
    说说材质系统,这段时间主要的开发量放在材质系统上了,由于引擎采用 D3D fx 格式来封装 Material ,同时为了自己管理渲染状态又不能直接使用 D3DXEffect ,所以自己从头写了 fx 脚本的解析器,重新组织渲染状态对象,这样才能自己管理渲染状态。虽然采用 FX 脚本格式,但引擎的抽象部分并没有任何依赖于 fx 细节的任何部分,我把解析全都放在了 D3D9、10 的实现中,但是这又带来了前面说的问题,为了抽象,我不得不付出性能上的代价,因为要借助中间数据结构,不能直接读入到引擎中。等做完看看性能如何,实在不行就只好直接放在引擎核心中吧。
    原来只作为最小化状态切换的 RenderTree 现在已经承担了更多的责任,什么 Full Screen PostEffect\Object PostEffect\MultiPass\Accumulate State\全都放在了这里,现在已经不是单纯的状态管理树,在保证正确绘制的前提下才会保证最小换切换,仔细想想也是这样,绘制都不正确速度再快又有何用?
    虽然创建非 pure device D3D 可以帮助开发人员 cache 渲染状态并能避免重复的状态设置,不过这毕竟是工作在 D3D Runtime 层,而且有可能还是工作在操作系统的核心层次,这样还要有在用户层到核心层上的传输开销,所以我自己实现了免除渲染状态切换的机制,并且这套机制也可以防止 fx 开发人员在写脚本时默认一些 pipeline 的渲染状态的情况,引擎能根据情况自动设置默认的渲染状态。这样带来的额外的好处是引擎就可以使用 pure device (尽管 MS 不建议这样做)。
    内存管理:自己写了一个 Memory Management,但为了记录分配情况,每次在 opertor new 中会增加记录链表节点,不过这又会调用 new,这样就形成了死循环,最终堆栈溢出。改为 malloc,好了一些,但是 stl 中的一些容器也重载了 new,总是崩溃,最后放弃。以后有时间再好好整理吧。结论:直接使用 BoundsChecker 最为方便。
    关于 D3D 文档中的错误,说实话,仔细看发现错误真的不少,甚至 SDK 代码中的注视都有错误。只能靠自己分辨了。
    STL:字符串处理使用的 STL,一些对性能不太重要的地方也用到了 STL,不过我还是自己实现了 LIST、MAP,倒不是说自己写得有多快,主要是发现 STL 在 iterate 时效率不高,每次都要生成临时对象,OGRE 中大量使用了 STL,这也是其效率低的原因之一。经过测试,自己写的优化后的 LIST MAP 在插入、删除、排序、遍历都优于 STL(Release 下),当然查找还不行,不过可以使用 Map,这是经过特殊预处理优化后的 Map,访问的时间是 O(1)。至少在这上面不会是性能杀手。
    今天给自己放假一天,仔细想想,引擎的结构、代码、功能、问题、性能...........
8月29日

Shader System 设计

职责
提供3D世界中几何体的着色服务,这包括:
Ø         着色脚本系统。
使用 FX 脚本描述着色信息,并且统一 FFP PFP管理。
Ø         着色信息的管理。
着色信息的载入、删除、合并等管理操作。
Ø         渲染管理。
系统使用ShaderQueue(着色队列)的数组来组织Geometry的着色数据,ShaderQueue是按照 Render Priority(渲染优先级别)来排序的。每个 ShaderQueue 内部,将Geometry分为 Solid Alpha,其中 Solid Shading ShaderTree 来组织,Alpha Shading ShaderList 来组织。
概念
Ø         The Definition of the Solid / Alpha Geometry
Geometry本身不能决定是否 Solid,而是由用于这个 Geometry 上的 Shader 来决定。如果 Shader 的任何一个 Pass 中含有 AlphaBlendEnable = ture,则称这个 Shader 所关联的 Geometry Alpha Geometry;反之称为 Solid Geometry。由于相同的 Geometry 可以在不同的时间或者触发事件设置不同的 Shader 效果,比如:event1设置为 Solid Shader1event2设置为Alpha Shader2,由于系统依据Shader来决定 Geometry的渲染管理属性,这样就能保证 Geometry 的正确绘制。
着色信息管理策略
Ø         Pass 中的 RenderStateRenderStateGroup
在载入 Shader 时,将 Pass 中的 RenderState 组合成 RenderStateGroup,如果 Pass 中存在多个 RenderStateGroup,则按照 RSCT 排序。
渲染管理策略
Geometry Shading分为3种基本情况:
Ø         Render Priority
Ø         每个几何体都有自己的Render Priority,比如:天空最先、UI最后等等。
Ø         Solid
非透明物体的渲染工作由一个 ShaderTree 来完成,ShaderTree的详细组织方式在下面描述。ShaderTree的最终目的是为了最小化渲染状态改变。
Ø         Alpha
半透明物体的绘制最重要的是保证绘制的正确性,所以要对半透明物体进行从远到近的排序,绘制时也是按照这个顺序。所以半透明物体用一个从远到近排序的ShaderList来组织的。
 
Geometry加入到ShaderQueue的流程:

 
根据GeometryRenderPriority来确定几何体的 所在的ShaderQueue 位置
Alpha Geometry
 
按照由远到近的顺序加入到 Alpha Shader List
 
加入到 Solid Shader Tree 中。
 
Y
N

类层次结构
参看 UML 设计文档。
数据结构
Shader
Ø       Shader描述了如何渲染物体的信息,包括Texture SetupMaterial PropertyRender StateBlend SetupPixel ShaderVertex ShaderRender Target Setup等等。Shader并不直接和Geometry相关联,因为对于同一个Geometry,有可能会用不同的Shader来渲染。
Ø       Shader Tree 是为了 Minimum Shader State Change,树中每一条 Branch都是一个 shader
 
Shader Tree
Data Structure
Ø         ShaderTreeST): node 的容器,它除了遍历树的功能之外还m包含创建 ShaderTreeBranch 的功能。
Ø         ShaderTreeBranchSTB):ShaderTree 的分支,相当于 Pass
Ø         ShaderTreeNodeSTN):用于构成 ST 的主要数据结构,ST中的非叶 STN 存储了一个RenderGroupST中的Leaf STN存储了Batched Geometry。多个Leaf节点可以共享一个Batched Geometry。当遍历ST时遇到Leaf  STN就开始绘制Batched Geometry
Ø         RenderStateRS):渲染状态的封装。
Ø         RenderStateGroupRSP):渲染状态组,将相同概念的渲染状态组合在一起,分组标准参考 D3D10 State Object 中的 RasterizerDepth StencilBlendSampler
Ø         RSCT状态改变开销表,预定义生成。
 
 
The Policy of Build the Solid Shader Tree
在遍历 SG 中,如果一个 geometry 可视,则将累积的 Shader 合并为一个 Shader,在合并的过程中对Pass中的 RenderStateGroup 按照 RSCT 排序,然后将合并后的 Shader 和这个 Geometry 送到 ShaderTree 中构建 STB。在构建 STB过程时,遍历 Shader 中的每个 Pass,按照已经经过 RSCT 排序的 RenderStateGroup 列表依次创建或者插入 STN,当遇到叶节点时,将关联的 Geometry 加入到这个叶节点中。由于 ST Root Leaf 是按照 cost 的大小排序的,这样就能做到最小状态切换。每个 Leaf 和使用这个 shader tree branch geometry 关联,这样每个 leaf 就会有一个 geometry list。这样就完成了一个 ST 的构建工作。
 
Build shader tree pseudo code
 
BuildBranch( const Shader& shader, Geometry* pGeometry )
{
       For each pass in shader
              RenderStateGroupList& rsgs = pass.getRenderStateGroups();
       // 获取 RSG 列表的最大项和 ST 的根节点
RenderStateGroup& rsgRoot = rsgs.first();
ShaderTreeNode* pSTRoot = GetRoot();
// 调用内部的构建 branch的函数
_BuildBranch(pSTRoot, rsgRoot );
       End for
}
 
_BuildBranch ( pSTNodestateGroup )
{
       Bool bInsert = true;
       // 遍历当前 STN 的每个子节点
       For each child in pSTNode
              // 判断是否与当前的 STN RSP相同
              If ( child.context == stateGroup)
                     // 相同则略过当前 STN,如果 RSG 列表还有下一项,则继续递归
                     bInsert = false;
                     if (state.hasNext )
_BuildBranch ( child, stateGroup.next )
                     Else
                            // 当前 RSG 没有下一项,则将当前的 Geometry 加入到当前的 STN 中。
                            AddGeometry( child, pGeometry );
End if
                    
              End if
       End for
// 如果不相同,则插入标志为真
       If ( bInsert )
              // 用当前的 RSG 创建一个 STN并加入到当前的 STN 为子节点
              Create newSTNode with this stateGroup
              pSTNode.AddChild(newSTNode);
              // 如果 RSG 列表还有下一项,则用新创建的 STN 继续递归
              if (stateGroup.hasNext )
                     _BuildBranch (newSTNode, stateGroup.next );
              Else
                     // 当前 RSG 没有下一项,则将当前的 Geometry 加入到当前的 STN 中。
                     AddGeometry(newSTNode, pGeometry );
              End if
       End if
}
8月24日

引擎中的 ShaderTree 概要设计

Shader
Ø       Shader描述了如何渲染物体的信息,包括Texture SetupMaterial PropertyRender StateBlend SetupPixel ShaderVertex ShaderRender Target Setup等等。Shader并不直接和Geometry相关联,因为对于同一个Geometry,有可能会用不同的Shader来渲染。
Ø       Shader Tree 是为了 Minimum Shader State Change,树中每一条 path 都是一个 shader
 
 Shader Tree
Data Structure
 
Ø         ShaderTreeST): node 的容器,它除了遍历树的功能之外还包含创建 ShaderPath 的功能。
Ø         ShaderTreeNodeSTN):用于构成 ST 的主要数据结构,ST中的非叶 STN 存储了一个RenderGroupST中的Leaf STN存储了Batched Geometry。多个Leaf节点可以共享一个Batched Geometry。当遍历ST时遇到Leaf  STN就开始绘制Batched Geometry
Ø         RenderStateRS):渲染状态的封装。
Ø         RenderStateGroupRSP):渲染状态组,将相同概念的渲染状态组合在一起,分组标准参考 D3D10 State Object 中的 RasterizerDepth StencilBlendSampler
Ø         CSCT状态改变开销表,预定义生成。
  
The Policy of Build the Shader Tree
 
在遍历 SG 中,如果一个 geometry 可视,则将累积的 Shader 和这个 Geometry 送到 ShaderTree 中构建 ST。在构建之前先对累积的 Shader 中的 state list 按照 Render State Grouping 规则组合成 RenderStateGroup list,按照 CSCT Cost 的大小对这个列表排序,然后将按照顺序构建 STP,每个 RenderStateGroup list cost 最小的 RenderStateGroup STN 作为 ST 的叶子节点。所以从 ST Root Leaf 是按照 cost 的大小排序的,这样就能做到最小状态切换。每个 Leaf 和使用这个 shader path geometry 关联,这样每个 leaf 就会有一个 geometry list,然后交给 RenderSystem 对这个 geometry list 排序,再batch。这样就完成了一个 ST 的构建工作。
 
Build shader tree pseudo code
 
BuildTreePath( ShaderStack& shaders, Geometry* pGeometry )
{
// 根据 Accumulate Shader 生成 RSG 列表
       RenderStateGroupList rsgs = GenerateRenderStateGroup( shaders );
       //按照 CSCT Cost 的大小对这个列表排序
SortRenderStateGroupByCSCT(rsgs );
// 获取 RSG 列表的最大项和 ST 的根节点
RenderStateGroup& rsgRoot = rsgs.first();
ShaderTreeNode* pSTRoot = GetRoot();
// 调用内部的构建 path 的函数
_BuildTreePath(pSTRoot, rsgRoot );
}
 
_BuildTreePath ( pSTNodestateGroup )
{
       Bool bInsert = true;
       // 遍历当前 STN 的每个子节点
       For each child in pSTNode
              // 判断是否与当前的 STN RSP相同
              If ( child.context == stateGroup)
                     // 相同则略过当前 STN,如果 RSG 列表还有下一项,则继续递归
                     bInsert = false;
                     if (state.hasNext )
                            _BuildTreePath ( child, stateGroup.next )
                     Else
                            // 当前 RSG 没有下一项,则将当前的 Geometry 加入到当前的 STN 中。
                            AddGeometry( child, pGeometry );
                    End if
                    
              End if
       End for
// 如果不相同,则插入标志为真
       If ( bInsert )
              // 用当前的 RSG 创建一个 STN并加入到当前的 STN 为子节点
              Create newSTNode with this stateGroup
              pSTNode.AddChild(newSTNode);
              // 如果 RSG 列表还有下一项,则用新创建的 STN 继续递归
              if (stateGroup.hasNext )
                     _BuildTreePath (newSTNode, stateGroup.next );
              Else
                     // 当前 RSG 没有下一项,则将当前的 Geometry 加入到当前的 STN 中。
                     AddGeometry(newSTNode, pGeometry );
              End if
       End if
}
 
TODO
Ø         Accumulate Shader 的算法。
Ø         CSCT的构建,以及构建标准。
Ø         加入Geometry渲染优先级。
Ø         半透明和实体物体的区分。
Ø         Multi-pass ShaderTree 中的表示。
8月14日

D3D10 的状态对象

这两天一直在思考 Shader Tree ,树中每个 Node 是 Shader State,问题是 Shader State 的粒度应该是怎样的?是针对每个 render state call 都对应一个 Node 吗?这样树的深度太大,在遍历和修改时效率会很差,所以开始考虑将 Shader state 分组,例如将设置 texture sampler 的分为一组,将 blend 的分为一组,将 alpha 操作的分为一组。今天在翻 D3D10 的文档时偶尔看到了 State Object,仔细看了一下说明,这正是我要解决的粒度问题。原来 D3D10 已经将这些 render state 分组,每一个分组对应一些操作,比如 Input Layout Object 就是将所有的关于绘制所需的顶点数据设置分为一组,这里使用一个描述结构来完成顶点结构的定义,跟 OGRE 的顶点语义结构定义类似,不过是由 D3D Runtime 来完成。这样 render state 的粒度就大了,配置和设置就很方便,将 state-setting 管线化,这样就使硬件缓存称为可能,从而达到最小的渲染状态改变。总体感觉,DX10 比以前版本更像一个图形引擎,比如这个 State Object,看来 D3D 已经将触角深入到了应用层次,也许以后的引擎构建会更加简单。对于我的引擎来说,可以参考 D3D10 的一些概念,融入到引擎中,针对不同的渲染器来实现。头痛,D3D10 已经包含了引擎的一部分概念了.....

浅谈 Scene Management(一)

也许我们都或多或少的听说过 SceneGraph,也听说过 Scene Management,还听说过 BSP\Portal\Quadtree\Octree 等空间管理数据结构,那么这些概念之间的关系是怎样的?他们之间的区别在哪里?分别在 3D 图形应用中起什么样的作用?针对这些疑问,我希望能用这个系列文章来解答这些问题。
以前我对 SceneManagement(以下简称 SM)的概念非常模糊,最直接的印象是 OGRE 的 SM 类(那个真的是一个包罗万象的管理类),后来又接触到了 SceneGraph(以下简称 SG),于是我到处寻找资料,国内的 SM、SG 的资料非常少,仅有的几篇也是含糊不清。这期间我主要看的是David H.Eberly 的《3D Game Engine Architecture》的关于 SceneGraph 的样章部分,有了一些概念。后来在 GameDev 上看关于 SG 的讨论时看到了一个帖子,这个帖子的内容基本上颠覆了我原来对 SG 的理解。后来我又去下了 OpenSG 的代码和技术文章,还有 Java3D 的 SG 接口说明,我逐渐认识到了 SG 的本质。
那么 SG 是什么?首先我给出自己对于 SG 的定义:SG 是一种更高层次的数据结构,它管理了 3D 场景中所有的有形和无形的对象,包括:transform\light\shader\geometry\animation 等等,它将有形的对象按照空间关系组织在一起,将无形的对象按照逻辑关系组织在一起。它提供了场景对象的统一的访问控制接口,使外部应用能够方便的访问并控制这些对象。SG 是由两种 Node 组成,一种是用于构建整个层次结构的 Node ,一种是保存场景对象的 Node,由于多个层次 Node 可以共享一个场景对象,所以这就构成了一个图的结构。由于这些逻辑空间关系是有向的,所以 SG 是一种有向非循环图 DAG。它是 SM 中不可缺少的一部分,也是最核心的部分。
为什么要使用 SG? 3D 应用中最简单的对象管理就是把所有对象放在一个列表结构中,访问时遍历,但是如果在运行时更改它们之间的逻辑关系,使用这种方式就非常困难,人们为了解决这个问题,自然而然的想到了层次的树形结构,但树形结构不能共享相同的资源,于是将树叶子共享,就形成了图的结构。使用 SG 能很容易的将骨骼动画这种层次结构包含在 SG 中,无须再另组织一套数据结构。类似的,对于层次运动变换,更是容易,比如:太阳系的运动。
SG 能做什么?SG 负责维护场景对象的逻辑关系,负责提供访问控制场景对象的统一接口,是整个 3D 应用运转的核心,如果没有好的 SG 来管理场景对象,维护复杂场景对象的关系将会变得异常困难,你将不得不 Hard code 一些功能在你的引擎中,这会使引擎变得越来越臃肿,没有扩展能力,很难适应应用需求的变化。由于包围体层次和 SG 层次类似,所以可以把 SG 作为 包围体层次结构,这样无需再建立另一套包围体层次结构,在一次遍历中就可以更新整个包围层次。
SG 不能做什么?SG 不能做渲染状态排序,不能做 minimize render state change,不能做静态场景的可视性剔除,不能做碰撞检测。那么这些事情由谁来做?Rendering state sort 和 minimize render state change 由 shader System 来完成;visible culling 由具体的空间管理算法完成,比如 BSP\Portal\Quadtree\Octree 等;碰撞检测由 Collision System 或者外部的 Physical Engine 来完成。
SG 包含什么?Transform \Shader\Geometry\Animation\Light 等等,所有有形无形的场景对象都可以放入到 SG 中管理,场景中的对象不是孤立存在的,它一定和其他对象有逻辑关系。比如:某一动态点光源会照亮某几个物体,在 SG 中就表示为 Light 节点下关联着几个被照亮的 Geometry 节点。
SG 还有哪些问题?有很多人试图将所有场景管理的操作都统一到 SG 中,但目前还没有一个很完美的办法。我的观点是分而制之,至少不在结构上搞得非常复杂,也许效率会受些影响,但硬件的发展速度能缓解这个问题。另外,对于动态物体和静态物体的管理,也是难以统一,所以这还要在实际的情况中去验证和测试,得出最适合自己情况的一套方案。
下一篇我会详细的讲解 SG 的内部结构和运行流程。
8月12日

为什么你要做3D引擎?

最近,总有人问我为什么去做引擎,为什么你们公司要自己做引擎,使用已有的商业的引擎不是很好吗?我很是奇怪他们为什么会有这样的问题,因为在我看来这是很正常的。其实,这些疑问背后的潜台词是:你能做好引擎吗?就算能做好能跟那些顶级的引擎一样好吗?我承认,这是我的第一个商业引擎,我没有百分百的把握做好,而且就算做出来也不可能跟那些世界顶级的引擎一样好,因为这是软件开发行业的客观规律,但我想谈的是另外一个问题,那就是软件开发中的勇气和自信。

在我10年的软件开发生涯里,曾听过无数次要自主研发中国的操作系统,可是现实的情况是没有。在10多年的中国游戏开发的发展过程里,也曾听过无数次要自主研发中国的3D引擎,可是现实的情况是只有一家(起点引擎,也许还有,请恕我孤陋寡闻)。这里我并不是吹嘘自己一定能做得好(更何况做出来也是韩国人的),我想说的是为什么会有这样的情况。其实能做操作系统,能做3D引擎的国内大有人在,那原因在哪呢?在韩国公司上班虽然没有多少时间,但我的感受是很深的。一天在同老总谈工作安排的时候,她说了一句话:在中国办公司两年里,她发现中国人总觉得自己能力不够,原因是他们觉得自己是中国人,其实中国人的能力是很不错的,但需要一定的帮助,就会达到自己认为不可能达到的程度。听了这句话之后,我不得不承认我们同韩国、日本等国家的差距,而且这种差距不是很小,是很大。也许有人会很反感我说的这些,但是我还要说,她一句话说到了我们中国人的痛处。谈过之后我想了很久:韩国人,曾经依附于中国的一个小民族;中国人,一个有着历史悠久对世界文化有深远影响的民族,现在为什么会有如此大的差距?我们都知道韩国人是很爱国的,他们的汽车工业、电子产业、文化娱乐产业(电影、电视、游戏)都是在自己的国民的支持下发展起来的。也许有人说那是他们的国民对民族产业的支持,但我要说那也是他们的民族企业很争气,真正的掌握了核心技术,缺少任何一个因素都无法发展。我们再看游戏行业,韩国的游戏行业其实并不比中国历史长,但现在中国在游戏产业上却逆差于韩国,可以说韩国的游戏产业就是中国人供出来的。尽管我们对所谓泡菜游戏嗤之以鼻,但不能再以老眼光来看韩国的游戏,我所在的韩国公司的网游产品《街头篮球》在中国大获成功,还有曾经在盛大创造了同时在线70万人的《泡泡堂》,九城目前代理的《奇迹世界》、《激战》,这已经说明韩国的游戏产业的发展已经做到了与时俱进,甚至是超前的。我记得在进入公司之前跟老总面试,当我提出自己带领一个团队做3D引擎开发时,她很惊讶,她告诉我说在中国的两年多时间里,她见过很多中国的游戏开发人员,她一直希望能在中国找到人来做3D引擎的开发,但是没有人主动提出要做引擎。《街头篮球》的就是基于公司自己研发的引擎上开发的,而且据我了解,很多韩国的大作都是自己研发的引擎,比如《一骑当千》。也许,这在国内来说是不可想象的,大家都知道一个游戏需要较长的时间才能完成,更不要说从头做引擎了。为什么韩国人要自己做引擎,那是因为他们有勇气这样做,有信心这样做。这样做的目的是为了掌握核心技术,而不是依赖于某一款图形引擎,更不是赚了一票就走人,就像他们的汽车、电子产业一样。有了核心技术,就可以创造出更有价值的产品,就可以节约成本,就可以不受制于人。从这点来说,韩国游戏业的开发者们,是有远见卓识的。反观国内,浮躁的心态比比皆是,用几个月做出来的打着民族旗号的游戏来与那些用几年时间投入上百万上千万做出来的精品游戏对抗,其结果可想而知。我们感叹于《魔兽世界》的细致和精妙,但有多少人想过这背后是有着十几年游戏研发经验的队伍和5年的研发历史支撑起来的,没有这些经验的积累和核心技术的掌握,是不可能做出这样一款成功的游戏。这是事物发展的客观规律,任何人任何事都无法摆脱这个规律。

每当跟圈里的人聊起国内游戏的开发,都不约而同地摇头。也许是各有苦衷吧。但是接下来要怎么办?还要继续这样下去吗?还是说最后成为全球最大的游戏市场(似乎也只能以此自居)?幸运的是我们还能看到国内游戏自主研发的希望,那就是一网易为首的自主研发游戏公司。网易投入几千万建立杭州研发中心,这是国内其他2个网游巨头没有做到的。而且网易的大话西游的成功,也证明了自主研发是可行的,我们国人也是有能力的。但是当我听说九城花3000万美元去代理《地狱门》时,我在想如果用这2个亿(哪怕1个亿)来投入到自主研发,会不会写出一个3D引擎?会不会打造出一款优秀的游戏?只要给足够的时间,良好的管理,优秀的人才,我想一定会的。为什么不投入到自主研发上,朱骏一定有他的理由,也许他不相信国内的研发实力,也许是为了不让竞争对手抢先代理,也许没有也许。鹬蚌相争,渔翁得利。大家还记得当年联想公司的走技术路线还是走销售路线之争吧,最后还是选择了品牌电脑销售,为了这个选择,联想失去了技术带头人倪光南。事实证明,联想的选择是错误的。至少在我的印象里,联想并没有给我们带来改变生活的技术,十几年前它是组装买电脑的,现在它依然是。还有前段时间的“汉芯事件”,“麒麟事件”让那些掌握着资源的老板们害了怕;而2004年闹得沸沸扬扬的联想裁员事件,和最近的百度裁员事件也让人对国内的公司寒了心(这种事情多了)。其结果就是,中国人不再相信中国人,最后就形成了老板们宁可花几个亿去买外国产品,打工者挤破了头也要去外企的局面。那我们何时能跳出这个怪圈?怎么能跳出这个怪圈?

也许,现在我所能做的,不是这样等下去,而是做自己想做的事情,一个人很难改变大环境,但是可以改变自己。少抱怨客观原因,多找自身问题,也许会能有所醒悟。对于核心技术,掌握一点是一点,总比没有好些,这不是空话,而是实话,希望中国的游戏行业,能有真正腾飞的那一天,也希望这个圈子里的人能随着这个行业一起腾飞。

5月9日

2006-4-22

本来想实现共享 Buffer 数据,以及实现渲染对象之间的树形结构,结果改变很大。重构了大部分的代码,而且在重构中不断的发现问题,共享的功能推翻了好多次,最终参照了 Ogre 的实现方式,其实也是我考虑到最后发现和他的做法一样。昨天在看到 HZJ 作的扩充 FX 的办法实现了自适应的 Shader 渲染流程的管理,虽然他做的还很不完善,不过却给了我一个启发。最近这两天我也在考虑自适应 Shader 渲染功能的实现,不过一直没有头绪。昨晚回家仔细看了了一下 D3DSDK 的文档,原来它已经有一个 FX 的标准的注释和语义模型,那就是 SAS,目前 SAS 比较完善,最新版本 1.0Nvidia FX Composer 1.8 支持 0.8,而且它还将作为 Direct10 Xbox360 的开发标准。不过它并不支持 Shader 中对 RenderTarget 的描述。但这时很重要的部分,所以我准备参照 FX Composer 的标准,在引擎中实现 RenderTarget 的解析。希望能在下一周完成这部分的功能,这样引擎的核心渲染流程也就完成一部分。
4月25日

DreamEngine 3D 图形引擎开发里程碑1

2006-4-7

版本:0.1

目前完成程度描述:

引擎部分:

数学库:

完成基本数学函数。

完成3D\2D向量 LyVector2\LyVector3的功能代码。

完成3D空间下多边形分割,点、线、面的位置关系处理辅助函数。

场景管理:

BSP代码基本完成,包括生成、绘制、持久化代码,没有测试。

完成简单的 LyEntity 功能代码。

完成简单的场景节点 LyNode 代码。

完成 LyEntityManager 功能代码。

图形:

完成简单材质的管理和构建功能(LyMaterial\LyMaterialManager\LyMaterialBuilder)。

完成简单着色器管理和构建功能(LyShader\LyShaderManager)。

完成简单纹理管理功能(LyTextrue\LyTextrueManager.

完成简单D3DAPI封装功能(CD3D9Device

基本完成硬件缓存管理功能(LyHardwareBuffer\LyHardwareBufferManager\LyHardwareVertexBuffer\LyHardwareIndexBuffer

完成简单渲染器(LyRenderer)。

工具:

       完成日志功能(LyLog)。

       完成简单文字绘制器(LyTextDrawer)。

       完成资源管理器模版基类,所有的各资源的管理起都从此类继承(TresourceManager)。

       完成单件(TSingleton)

       完成计时器(LyTimer)

几何:

       完成基本几何体基类(LyGeometry)及其管理(LyGeometryManager)功能。

       基本完成网格数据管理构建和持久化功能代码(LyMesh \ LyMeshManager \ LyMeshBuilder \ LyMeshSerializer)。

文件系统:

       完成封装IO功能(LyFile

       完成简单引擎文件目录管理器功能(LyFileSystem

       完成持久化机制功能(LySerializer \ LySerializerManager

动画:

       完成骨骼动画系统(cpu处理)功能(LyAnimation \ LySkeletonAnimation

完成骨骼动画管理、持久化和构建功能(LyAnimationManager \ LyAnimationSerialzer

完成骨骼系统及管理功能(LySkeleton \ LyKey \ LyLineKeyControler \ LySkeletonManager

导出插件部分:

       完成模型数据导出,包括材质信息(只支持单一纹理坐标)、空间位置信息、顶点信息、面信息、骨骼信息、蒙皮信息。顶点索引用 TriangleList 模式。

       基本完成CS Biped骨骼动画导出。

下一里程碑完成功能列表:

引擎部分:

数学库:

完成矩阵、四元组 LyMatrix*\LyQuaternion功能代码。使用 CPU扩展指令集优化数学库代码。暂时使用dx数学库代替。

场景管理:

测试并完成BSP场景管理功能,包括生成、绘制、持久化功能。

完成场景管理器功能代码(LySceneManager),并加入BSP管理算法。

修改并完善 LyEntity 功能代码,将骨骼动画功能移到 LySceneManager

修改完善的场景节点 LyNode 功能代码,加入Moveable LyTransform)功能并完成树状关系结构。

图形:

扩充材质功能(LyMaterial,支持fixed programmable pipeline,完成材质脚本功能。

完成复杂着色器功能,包括对 shader 的支持,多pass 渲染。

完成复杂纹理管理功能。

D3DAPI抽取出来,实现渲染API独立性。

工具:

完成内存管理功能,包括跟踪内存分配,内存碎片整理。

几何:

       增加几何体的LOD支持。

       完成对顶点数据的语义描述功能。

动画:

       完成硬件骨骼动画处理功能(Matrix Palette),vs 骨骼动画处理。

完成顶点动画功能。

完成纹理动画功能。

导出插件部分:

       完成顶点动画的导出。

       完成纹理动画的导出。

       支持多纹理坐标。

       导出顶点语义。

       支持导出设置。