先看效果,按下空格键添加粒子特效:
一般而言粒子特效的实现都是比较复杂的,且不说实现粒子特效的编码和设计,光是编写一个粒子编辑器就不是简单的一件事,但是作者使用了很取巧的方式来完成,我们接下来深入代码看看作者是怎么处理的。
在我还没有看这本书的这章之前我认为一个Particle应该是一个单一的粒子(或许是一个面片或者是一个简单的模型),而最终的粒子效果则是有成千上万个Particle组成渲染得出的,所以应该存在一个JSON或XML描述文件来指定每个粒子的运动轨迹和生命周期,不过作者可不是这样想的。
作者的一个Particle类即表示一个在3D软件中渲染好的粒子群,如下图:
是的,一个粒子类其实就是加载了上面的模型,而所有的粒子其实都在模型中创建好了,按照作者的说法,这样可以实现批处理,即一次DrawCall可以绘制模型中的所有粒子(废话);
那么我们接着看,粒子类继承自Entity类,主要的改动是添加了Mesh2这个属性,表示该类可以解析两个模型,那么为啥要解析两个模型呢?
关键帧插值:
如果我们的粒子对象就是一个包含了多个粒子的Mesh模型,该如何控制具体的面片或小粒子进行缩放位移或旋转呢?答案就在关键帧插值这里,引入的Mesh2和Mesh的数据是一一对应的,但是具体的顶点数据的位置缩放或旋转不同(其实就是复制第一个模型文件在3DMax中微调以后输出的新文件);
如果存在Mesh2的数据我们会使用下面的这段顶点着色器代码:
1 vertexShader.assemble 2 ( 3 Context3DProgramType.VERTEX, 4 //换算起始坐标 5 "mul vt0, va0, vc4.xxxx " + 6 //换算终止坐标 7 "mul vt1, va2, vc4.yyyy " + 8 //在两个坐标里进行插值 9 "add vt2, vt0, vt1 " + 10 //和 mvp 矩阵进行 4x4 运算 11 "m44 op, vt2, vc0 " + 12 //将 uv 数据传递到片段着色器 13 "mov v1, va1" 14 );
我们把Mesh的顶点数据作为源xyz,Mesh2的顶点数据作为终点xyz,传递一个时间常量vc4进行插值计算,就可以得到平滑的关键帧过渡的效果了。
如果只有一个Mesh,则仅仅是输出原有坐标这么简单。
同时我们的片段着色器也进行了插值:
1 fragmentShader.assemble 2 ( 3 Context3DProgramType.FRAGMENT, 4 //对纹理进行取样 5 "tex ft0, v1, fs0 <2d,linear,repeat,miplinear> " + 6 //乘以淡入淡出矩阵 7 "mul ft0, ft0, fc0 " + 8 //输出颜色 9 "mov oc, ft0 " 10 );
我们传入的fc0的alpha会根据时间进行0到1到0的变化,来实现淡入淡出的效果;
不同角度观察的处理:
想要使一个粒子对象无论从任意的角度看都保持一致一般来说大部分的粒子都是使用Billboard系统,作者没有使用这个方法,而是将一个面片复制成三个,像xyz三个平面一样交叉起来,具体可以看上面的截图,这样无论从那个方向看都可以看见我们的粒子,但是效果没有Billboard的好。
渲染模式:
使用了ADD模式去掉纹理的黑边,同时去掉了深度检测,由于是最后才进行渲染所以特效会显示在所有3D内容的前方;
确切的说是一个粒子对象池,避免大量创建新的粒子对象,同时使用粒子对象的clone方法创建新的粒子来达到共用顶点数据和贴图的目的。
添加了粒子特效的创建代码和更新代码,同时输出粒子特效的三角形数量,具体的大家可以看代码。