基于Flash3D的粒子系统实现

因为项目需要,自己动手实现了一个粒子系统,同时为其编写了配套的粒子编辑器。由于自己对这一块并不是很熟悉,于是前前后后推翻重做了好多版,花费了大量的时间。所谓折腾使人进步,随着对粒子系统的编写、重构、优化,我自己对3D渲染的各方面也有了更深入的理解。随着时间的推进,粒子系统的设计也已经慢慢稳定下来,并经受了实际项目的考验。我想是时候记录一下自己在粒子系统这块探索的过程。

f

 

从无到有,纯GPU运算的粒子系统

第一版粒子系统借鉴了Away3D的粒子系统。嘛,也可以说是直接照抄,把Away3D的粒子系统移植进我们的引擎。粒子编辑器部分,Liao Cheng 大大为Away3D实现了一个粒子编辑器,名字为Sparticle,可惜是闭源的。我当时还厚着脸皮向人家拿源码,不过他没理我……

Away3D的粒子系统设计上有许多优秀的地方,比如把粒子的各个行为按照组件的方式分开,通过拼装简单行为的方式实现复杂行为。同时,由于Away3D的粒子系统是预计算粒子行为、纯GPU运算,因此单个粒子组的渲染效率极高。在实际测试中,同屏十万个粒子依旧是60帧满。

然而Away3D粒子系统最大的问题在于它把所有行为都放入Agal中实现,导致一个简单的操作都需要大量的Agal代码,使得后期添加新需求非常复杂。同时,受到Flash寄存器数量限制(主要是属性寄存器),同一个粒子的行为不能过于复杂。在实际项目使用中,经常出现当美术做出来一个绚丽而复杂的粒子特效时,引擎报错说寄存器使用超标。面对种种问题,我想到的是如何改进Away3D粒子系统。

对Away3D粒子系统的改进

Away3D的粒子系统是按照以下规则使用寄存器的:
1. 对于常量型粒子特性,使用常量寄存器传递数据。比如为所有粒子赋予红色,那么只需要在常量寄存器中传入红色。
2. 对于变量型粒子特性,将数据预计算后使用属性寄存器引用。比如如果每个粒子的颜色各不相同,那么就会预先计算出所有粒子的颜色,然后生成一个顶点Buffer,通过属性寄存器引用。
3. 对于拖尾型粒子(相对于世界坐标发射的粒子),会动态生成一个记录粒子发射时的矩阵的顶点Buffer,并通过属性寄存器引用。

由于Flash提供的属性寄存器只有8个,当粒子本身带有大量的随机行为时,就会导致属性寄存器不够用。如果粒子同时还是拖尾的,那么就更加不够用了。然而在美术看来,随机性是粒子特效中很重要的一个特性,同时拖尾型粒子也是一个很常见的需求。因此,美术做出来的粒子特效经常导致属性寄存器使用量超标。

我的改进方案是这样的:
1. 对于简单型粒子,维持Away3D的原有方式。此时一个批次可以合并16383个粒子。
2. 对于拖尾型粒子,退化为位置使用常量寄存器传递,此时一个批次可以合并28个粒子。
3. 如果常量寄存器不够用了,那么退化为所有数据使用常量寄存器传递,同时不合并粒子渲染,即每个批次只渲染1个粒子。

这样改进过后,即使是复杂粒子,引擎也不会报错,而是退化到能运行的状态。然而,我低估了复杂型粒子在项目中使用的数量。复杂型粒子由于不合并渲染,因此效率十分低下,这也导致了我们游戏十分的卡。迫不得已,我只能推翻Away3D粒子系统全GPU运算的架构。

CPU和GPU混合运算的粒子系统

在这个时候我想的还是把尽可能多的行为让GPU运算。经过考虑后我是这样分的:

1. 与粒子运动相关的行为放在GPU内计算。包括位置、速度、旋转、缩放、重力等。
2. 其他行为放在CPU内计算。包括颜色、透明度、UV动画、序列帧、布告板等。

在这样的设计中,所有和运动相关的信息都会在CPU中计算为一个矩阵,通过常量寄存器传递给粒子。由于至少有一半的行为现在是在CPU中计算,因此属性寄存器肯定不会超标,这点是计算过的。同时,这种架构下同一个批次可以合并28个粒子,这主要是受到了常量寄存器数量的限制。

这样的设计带来了一定的复杂性。由于一部分的功能是在CPU中实现,另一部分功能是在GPU中实现,那么一些公用的功能就必须两边都实现一边。比如时间轴控制,比如曲线。

在项目初期的运行结果来看,这样的粒子系统效率还是不错的。我们主要运用粒子系统是在人物的技能上,还有就是场景中点缀的一些特效。然而到后期,项目经理发现粒子特效让游戏增色了不少,于是开始大量运用。美术把粒子特效挂在了每一个人身上,每把武器上,每个坐骑的每条腿上。一瞬间场景中的DrawCall飙升到800+,游戏帧率掉到了十几帧,新的粒子系统遇到了性能瓶颈。

纯CPU运算+跨组合并的粒子系统

问题出现在新粒子系统并没有实现粒子组之间的合并。以坐骑为例,坐骑的四只脚上会挂接四团粒子火,他们是一摸一样的,但他们之间的绘制却是独立的,并不会合并。当场景中有20匹马的话,那就是80团火了。如果每团火5个DrawCall,那就是400个DrawCall了。

在考虑实现跨组合并时,我发现使用GPU计算的行为导致合并的难度呈指数增长。为了实现上的简单,我把所有的计算都放在了CPU中。在这个架构下,我会在每次遍历场景树时把所有粒子分组放入一个容器中合并,并且一次性把合并后的内容提交给显卡渲染。现在,如果是80团一摸一样的火,也只需要5个DrawCall就可以完成渲染。

在一开始,我考虑的是尽可能合并更多的粒子。比如,如果两个粒子形状一样、纹理一样,那我就考虑合并。但后来,我发现这样的合并会遇到透明度排序问题。为了可以比较正确地为透明度进行排序,只能牺牲一些合并数量。

由于把所有工作都放在CPU中计算,CPU的开销就变大了。为了解决这个问题,我引入了ShareUpdate。如果粒子被标记为ShareUpdate,那么同种的粒子同一时刻只有一个会进行更新,其他粒子只是单纯地取它的结果。还是以80团火为例,如果它们都被标记为ShareUpdate,那么只有一个会运算更新。这样会导致80团火以一摸一样的频率跳动,但节省了大量的CPU。

总结

现在想起来,标准的粒子系统实现应该就是CPU计算,然后合并提交。我一开始也是受到了Away3D的“误导”,尝试把计算交给GPU。实际上基于GPU计算的粒子系统也是有的,但也要求一些额外的渲染API,比如可以在显卡中生成多边形,比如浮点型纹理用来储存粒子数据,比如可以在VertexShader中访问纹理资源,这些在Flash中都没有~!因此对于Flash平台来说,考虑到可拓展性和项目实际情况,还是把计算放在CPU中比较好。

比如,我遇到的最变态的一个需求是这样的:
每个粒子是个模型,带有骨骼动画,同时骨骼上面可以挂接其他的粒子特效。
我无语的问为什么会有这样的需求?
答曰:要实现火牛阵冲锋。

附图是粒子特效编辑器,结构上是不是很像Sparticle?

QQ图片20150213145458

基于Flash3D的粒子系统实现》上有1条评论

  1. Pingback引用通告: 基于Flash3D的粒子系统实现 » 潮汕IT男

发表评论

电子邮件地址不会被公开。 必填项已用*标注