Filament 使用了可扩展渲染管线(FrameGraph)来组织渲染通道和管理渲染资源,网上也搜了下可扩展渲染管线的相关的文章,一般认为可扩展渲染管线是次时代渲染引擎应该具备的比较先进的管线组织架构方式。所以就硬着头皮读了读 Frostbite 对 FrameGraph 的宣讲PPT,看完后觉得似懂非懂(应该是脑袋榆木,英语不是很好的原因)。网上的文章也有点“含混晦涩”,就是看懂了,实现起来估计也费尽,很多细节不明朗。回过头来想,这不是有现成的代码,分析下现成的教材(filament)岂不是更好?!
看了一段时间filament 对FrameGraph 的实现,基本上是了解了其原理。其实先不管代码驱动(code-driven) 还是数据驱动(data-driven)这些比较前卫的编程理念(网上搜了个遍,也没有什么资料把code-driven的概念讲清楚,倒是有个网站编程提及了一些code-driven 的概念)。可扩展渲染管线虽然说的"高大上",其实简单的理解起来还是比较容易,就是将渲染管线的组织变得具有一定的扩展性,渲染管线的组织尽量不走固化代码的那套,可以动态的配置。对渲染资源的管理也要更具灵活性。渲染管线与渲染资源的定义,用户尽可能使用起来简单和抽象,方式也比较统一。资源的使用是在真正执行渲染之前才实例化,没有用到的渲染资源和渲染通道从渲染管线中动态剔除掉。因为没有实例化资源,预编译渲染通道及资源的成本比较低廉 。
设计渲染管线,摆在我们面前的是面对复杂多样的业务需求,如何定义及组织渲染通道来实施各种技法,如何管理渲染资源并最优的使用它们。
下面看看 filament 的FrameGraph 的实现方式:
(1)首先,大体上FrameGraph 中的渲染通道(PassNode)以一维数组的形式组织起来,渲染时遍历这个一维数组来执行渲染。没有Frostbite介绍的实现方法那样复杂,虽然是构建的是有向无环图,但没有看到filament多路径并发渲染的实现,应该可以看做是framegraph 的简化版。
(2)渲染资源及渲染通道的有效性测试及剔除是在FrameGraph 的编译阶段进行的,究竟是如何根据资源的使用情况来动态剔除没有使用的资源和渲染通道,在后面的文章里会介绍。
(3)每个渲染通道都是一个可执行实例,调用其方法实例化资源并完成渲染。
这个章节我们主要是看看虚拟资源在filament 的FrameGraph中是如何定义及创建的。先搞清虚拟资源,后面的内容也好理解了。
既然FrameGraph 中的资源在没有使用它们之前是没有被实例化的虚拟资源,应该有一种机制能够持有创建具体资源的所有信息,能够在用到这些资源的时候实例化虚拟资源,并对上层FrameGraph的构建者提供必要的构建信息,使其了解资源的在各渲染通道中的使用情况以支持上层对资源的剔除操作, 那么主要是由两个类来支持,虚拟资源入口类ResourceEntry和构建FrameGraph的资源状态类ResourceNode 。
说是入口类,其实它就是虚拟资源类,在整个filament 分析中要有这个概念,不要被它的Entry迷惑。
虚拟资源入口的接口类定义:
虚拟资源入口的基类定义:
虚拟资源入口实现类:
ResourceEntry 是类模板,可以实例化具体的模板类, 如果是纹理资源入口类,可直接使用 FrameGraphTexture 实例化该类模板, 例如 ResourceEntry
渲染目标资源类:
为什么要重新定义一个新的RenderTargetResourceEntry类 ? 很明显,渲染目标资源需要扩展一些方法来支持更复杂的渲染目标的操作。
最后来张类图:
总结:
(1)ResourceEntry持有创建具体资源的所有信息 {descriptor 属性}, 这个descriptor 属性的定义来源于具体资源的定 义,通过类模版实例化完成具体资源的定义。
(2)T resource{} 是具体资源,它包括具体资源的描述和具体资源实例。
(3)ResourceEntry提供虚拟资源的管理功能,比如实例化具体资源,删除具体资源实例等 。
虚拟资源是资源的信息的集合和实例化后的渲染资源持有者,它在渲染管线需要实例化时,根据其资源的描述信息创建具体的资源实例。作为渲染过程,不管用什么形式最终主要是与纹理和渲染目标打交道。
filament 的framegraph 的具体资源分为两类:
可以看到下图纹理资源的Descriptor描述了具体资源创建属性(除了没有向GPU上传数据), 纹理图片宽width, 高height, 深depth, 在纹理细节等级的哪一级采样 levels,多重采样纹理采样数samplers, 纹理类型,格式, 用途。
纹理资源的硬件句柄 texture 代表了实例化后的GPU端的纹理对象资源。
渲染目标资源是对帧缓存对象的抽象描述,每个帧缓存对象都会有N个缓存附件,以实现RTT 或MRT。从下图可以看出每个帧缓冲对象有6个缓存附件,分别为4个颜色附件, 一个深度附件和模板附件。(如果你读过framegraph去年的代码,你会发现还没有FrameGraphRenderTarget这个类,也就是虚拟资源还没有统一起来,设计的不合理,新版修正了)。
仔细看上面两个类的定义,都定义了资源描述descriptor 和创建具体资源的接口 create 和 destroy。这就简单多了,只要用这两个类去实例化ResourceEntry类模版, 虚拟资源入口类的定义就完成。
综合起来,filament 的 FrameGraph 的虚拟资源有两类: ResourceEntry
和 ResourceEntry 。 有时具体资源(例如FrameGraphTexture)需要被导入到渲染通道中使用,那么 ResourceEntry 与 FrameGraphTexture就不是一一对应的关系了,而是多对一的关系(多个虚拟资源使用同一个具体资源)。当虚拟资源被初始化时,ResourceEntry 实例通过imported 标记来判断使用到的具体资源是否要被实例化。
ResourceNode 资源节点类主要是在FrameGraph编译阶段为资源的剔除操作提供支持,它记录渲染通道(PassNode)对资源的使用情况,渲染通道对该资源读取一次,资源的相应引用计数就会加1次。如果没有渲染通道使用资源节点关联的虚拟资源,资源将会被剔除。
虚拟资源的创建由FrameGraph构建器Builder类提供相应的接口createTexture 和 createRenderTarget 负责,底层统一调用FrameGraph接口创建具体资源。
当渲染通道需要使用资源时,由FrameGraph构建器Builder 创建相应的虚拟资源,下面列一下相关代码:
Builder创建纹理资源
Builder 创建渲染目标资源
Builder 创建纹理资源和渲染目标资源的公共接口,最终还是通过FrameGraph 实例去创建具体资源:
FrameGraph 创建资源接口
注意: FrameGraph 使用的虚拟资源句柄在这里现了原形~~~ 原来是资源节点实例在资源节点数组中的索引。
上面的代码可以看出创建一个虚拟资源对象(最终返回的是ResourceNode 在 ResourceNodes数组中的索引),FrameGraph会创建虚拟资源入口实例并加入到虚拟资源入口数组中,随后创建虚拟资源节点并将其添加到虚拟资源节点数组(mResourceNodes)和虚拟资源节点入口数组(mResourceNodeEntries)中。
有了mResourceNodes ,为什么还要用 mResourceNodeEntries ?先看一下mResourceNodes 和 mResourceNodeEntries在FrameGraph中的定义,mResourceNodeEntries 只是个不能共享对象的指针的数组,超出作用域或资源生命周期让其自动调用析构函数释放内存,防止内存泄露。
既然两者等价,那么只用mResourceNodeEntries 不就行了?@@@ 也不要太计较这些,filament有些模块的早期代码看是去逻辑设计的确实不是那么好(尤其是早期FrameGraph 的设计,作者貌似也是在似懂非懂的在尝试,有些逻辑很不合理,我就上了它的“当”,研究它的老代码,有些地方看了许久没整明白它想干啥,比较蹩脚。看了现在这版代码设计上已经好了很多,把纹理虚拟资源,渲染目标虚拟资源给统一起来了, 尤其那个moveResource操作也干净利落了)。
ResourceNode、ResourceEntry 、FrameGraphTexture 和 FrameGraphRenderTarget 关系:
(1)FrameGraphTexture 和 FrameGraphRenderTarget 是具体资源,具体资源描述了资源的属性和状态。它对外提供创建和销毁具体资源实例的接口。具体资源在没有被实例化时,只是资源描述的信息集合。
(2)ResourceEntry 是 FrameGraph的虚拟资源。ResourceEntry 和 具体资源之间有可能是多对一的关系。ResourceEntry 保存了虚拟资源被引用到的最终计数。
(3)ResourceNode 是构建FrameGraph时资源的状态节点,它记录渲染通道对虚拟资源的使用情况。FrameGraph上层对资源的操作都是针对ResourceNode 而言。
到现在为止,我们见识了什么是FrameGraph中的虚拟资源,以及系统如何创建它们,下一节我们再剖析下filament 代码,看看它是如何创建 FrameGraph 中的渲染通道、如何编译和剔除无用的渲染资源和其关联的渲染通道,执行渲染通道进行渲染的。