美术资源文件可以看成是代码的源文件,其中可能包含有不够优化的组织结构,冗余或者有轻微错误的数据,平时制作资源写入的临时数据,比如maya使用插件后容易在maya文件中留下插件自定义的信息。所以游戏引擎通常需要有一套资源编译工具来生成干净,专门为引擎需求服务的资源数据文件。

工具设计

传统工具的工作流是:

  • 引擎工具层有XXXImporter来导入某个格式的文件,然后生成引擎自定义文件,该工具由引擎编辑器提供UI操作
  • 引擎运行时读取该自定义文件,简单处理后得到渲染要用的数据,显示

缺点:

  • 没有考虑过把引擎运行时读取文件的代码跟引擎解耦。以unreal engine为例,很难在其他引擎里使用uasset格式的文件,或者重新导出成某个格式的模型文件。
  • 缺少引擎到引擎数据沟通的考虑。假如我有一个移动端引擎和一个云端桌面引擎,当它们需要同时协作得到更好的体验时,需要有一份统一的数据格式,然后两个引擎各自写一遍导入,很难把共同的解析部分统一起来。
  • 没有考虑到从引擎反向把场景数据导出成某种开源格式的场景文件,回到DCC软件或者其他引擎里编辑或浏览。Ominiverse的demo表现出这个功能的支持是有一定价值的。

为了达成以上这些目的,我们首先是跟传统工作流一样,要设计引擎自己的asset格式文件,或者叫场景格式文件。重点在于我们会将解析该格式到内存中的模块称为是生产者,读取内存生成该格式的模块称为是消费者。理解方式是,生产者会把资源数据写进内存,写之前需要先解析,做的工作是为当前进程服务;消费者呢,只需要读取内存中的资源数据,然后做任意的事情,可以是把内存的数据整理成vertex buffer,index buffer等提交到GPU渲染,也可以是本地CPU软渲染,也可以是什么都不画,只是把这块内存的数据导出到磁盘的某个文件里。

对于unreal engine导入fbx生成uasset,然后渲染举例来说:fbx -> ue资源编译工具 -> uasset -> 引擎内存 -> 简单加工后提交到gpu渲染。ue资源编译工具是生产者,引擎是消费者。

但我们拆得更细来说,其实是分为两个流程:
1.fbx -> 内存 -> uasset,负责fbx解析的代码是生产者,负责生成uasset的代码是消费者
2.uasset -> 内存 -> 渲染,负责uasset解析的代码是生产者,负责生成渲染数据的代码是消费者

所以对于新设计的资源编译工具,我希望把它的粒度拆得更细,可以将单个资源编译工具拆成是两部分,也可以把引擎运行时拆成是两部分,自由组装。

架构

生产者们:glTFProducer, SceneFormatProducer
消费者们:SceneFormatConsumer, EngineRenderDataConsumer

对应传统工具的工作流:

  • 以导入glTF为例,引擎工具层集成glTFProducer + SceneFormatConsumer两个模块,完成从glTF文件导入到生成场景格式文件
  • 引擎运行时集成的是SceneFormatProducer + EngineRenderDataConsumer两个模块,完成从导入场景格式文件到准备渲染数据提交

这样子作为拆分,我们能让Producer依赖的SDK跟Consumer想做的事情解耦,两者天然没有依赖,然后将Producer和Consumer都作为dll之后,我们可以任意地进行Producer + Consumer的组合来完成不同的工作。
举两个例子:
1.SceneFormatProducer + SceneFormatConsumer : 可以校验一来一去之后数据文件是否发生了变化,变化的内容就是当前导入导出不够稳定的地方
2.glTFProducer + EngineRenderDataConsumer :跳过了SceneFormat直接在运行时导入模型文件然后渲染,这个功能在游戏快速迭代开发时会省很多力气,不需要转换SceneFormat来节约机器处理的时间,以及编辑器或者命令行操作的时间

除了Producer和Consumer以外,有一个叫Processor的中间人,负责对接Producer和Consumer,它除了持有Producer和Consumer的实例进行调用以外,它可以在Producer完成工作后,对内存里的数据进行清洗,比如剔除掉退化三角形等。当然这个操作由Producer来做也可以,但如果Processor能够完美地做好这件事情,那么所有的Producer就不需要再写剔除退化三角形的代码了。所以Processor潜在的好处是可以写一些通用的数据处理算法,作为options供给Producer或者Consumer来打开,方便使用。同时也很适合开放给所有Producer和Consumer代码权限,让他们都能对同一块代码进行修改来达成数据互通的目的。

想的更远一点的就是除了把Producer和Consumer分成两个模块,两个dll,还可以把他们部署成客户端 + 服务器,使用网络通信;或者说把单个Producer + 单个Consumer的关系变化成一对多,多对一,多对多等。业务场景的自由度会比较高。

编码实现

  • SceneDatabase :场景内存数据库
    • Meshes
    • Materials
  • IProducer
    • 派生glTFProducer
      • 持有SceneDatabase指针,可读可写
  • IConsumer
    • 派生SceneFormatConsumer
      • 持有const SceneDatabase指针,只读
  • Processor
    • 连接Producer和Consumer开始工作前,申请一个新的SceneDatabase或者重置
    • 持有IProducer指针
    • 持有IConsumer指针

对于工具,它需要部署glTFProducer以及其他所以要支持模型格式的Producer,和自己引擎的SceneFormatConsumer。
对于运行时,它需要部署SceneFormatProducer,和自己的RenderDataConsumer。
可以通过makefile来使用同一份代码。