本文基于CocosCreator2.2.0,主要是通过修改粒子系统定制引擎实现粒子特效播放序列帧动画的效果。
对于粒子特效播放序列帧动画目前来将引擎是不支持的,不过引擎已经完成了基础的粒子系统,剩下的我们在粒子系统的基础上稍做修改即可实现。
主要的思路就是:将序列帧资源合并到一张大图内,在每个粒子进行更新的时候,根据粒子的frameIndex去计算出正确的UV
首先将引擎的js代码复制出来,然后在项目设置中,设置自定义引擎的javascript代码的位置
关于这一部分的设置,请参考官方文档:引擎定制流程
接下来将我们复制出来的javascript引擎文件夹在vscode中打开
找到CCParticleSystem.js文件,打开并编辑
CCParticleSystem是RenderCompoent的子类,它的主要用途就是去储存粒子的renderData,以及选用什么样的assembler去组装我们的renderData,同时还有粒子使用什么样的Material。
关于这块可以参考官方文档:自定义渲染
当你搞清楚了RenderComponent和Assembler,再来做这些定制的工作就会很快速,写起来得心应手。
在ParticleSystem的properies下添加这些属性:
/**
* !#zh 是否启用粒子动画
* @property {boolean} enableAnimation
* @default false
*/
enableAnimation: false,
/**
* !#zh 帧动画贴图的列数
* @property {Number} sizeX
* @default 0
*/
sizeX: 0,
/**
* !#zh 帧动画贴图的行数
* @property {Number} sizeY
* @default 0
*/
sizeY: 0,
/**
* !#zh 帧动画贴图,每一张序列帧图的宽度(需要等宽)
* @property {Number} uv_deltaX
* @default 0
*/
uv_deltaX: 0,
/**
* !#zh 帧动画贴图,每一张序列帧图的高度(需要等高)
* @property {Number} uv_deltaY
* @default 0
*/
uv_deltaY: 0,
/**
* !#zh 帧动画贴图,序列帧动画的帧率(默认60帧)
* @property {Number} uv_deltaY
* @default 0
*/
animationRate: 60
enableAnimation
属性主要用来控制这里粒子是否启用序列帧动画模式。
sizeX
和sizeY
是贴图资源,也就是我们的序列帧大图中,小图的行列数
例如我这里使用的资源的行列数就是2行4列
uv_deltaX
和uv_deltaY
是小图的宽高大小,这里需要每一张小图都是同样的尺寸。方便我们进行计算
ps:其实这里的行列数可以优化掉,当我们的小图尺寸相同的情况下,通过大图的宽高尺寸和小图的宽高尺寸,我们可以计算出行列数,不过我这边不想再去计算了,所以干脆拿出来进行配置
animationRate
是我们这个序列帧动画播放的帧率,默认我们设置为60
CCParticledSystem的修改到这里就完了。我们这里主要是需要通过CCParticleSystem来收集序列帧动画的配置信息。
再进一步,你可以修改CCParticleSystem中的_initWithDictionary
方法,将这几个需要配置的属性的读取也进行改造,这样我们就可以在粒子的plist文件中增加对应字段,方便我们进行配置和管理。这里就不细说了。
particle-simulator,顾名思义,就是粒子的模拟器,它负责了每个粒子的发射,位置更新,颜色更新,旋转度计算,粒子的回收,等等。
文件的一开头,我们就可以看到引擎对于单个粒子的定义:
let Particle = function () {
this.pos = cc.v2(0, 0);
this.startPos = cc.v2(0, 0);
this.color = cc.color(0, 0, 0, 255);
this.deltaColor = {r: 0, g: 0, b: 0, a: 255};
this.size = 0;
this.deltaSize = 0;
this.rotation = 0;
this.deltaRotation = 0;
this.timeToLive = 0;
this.drawPos = cc.v2(0, 0);
// Mode A
this.dir = cc.v2(0, 0);
this.radialAccel = 0;
this.tangentialAccel = 0;
// Mode B
this.angle = 0;
this.degreesPerSecond = 0;
this.radius = 0;
this.deltaRadius = 0;
}
这里定义了单个粒子的属性,像他的初始大小,初始位置,当前位置,等等这些信息。
在这里我们增加下面的额这些属性
//animation mode
this.enableAnimation = false;
this.sizeX = 0;//列
this.sizeY = 0;//行
this.frameindex = 0//当前帧的帧号
this.lastFrameTime = 0;//上一帧的时间
this.uv_deltaX = 0
this.uv_deltaY = 0;
this.animationRate = 0;
frameindex
和lastFrameTime
属性是我们用来进行切换帧的计算使用的。
其他属性对应刚刚我们在CCParticleSystem中设置的那些配置属性
接下来我们看到粒子对象池的定义,这个Pool就是用来回收粒子和取出粒子使用的地方,通过这个Pool,引擎在发射粒子的过程中就可以实现对象的复用,同时避免特效播放的过程中还要去进行alloc的操作。
同时我们看到它定义了put方法的回调,这个回调中将粒子的动态属性都做了还原,这样就保证了我们需要使用粒子的时候,拿出来的粒子都是经过初始化的粒子。相当于对粒子做reset的操作。
let pool = new js.Pool(function (par) {
par.pos.set(ZERO_VEC2);
par.startPos.set(ZERO_VEC2);
par.color._val = 0xFF000000;
par.deltaColor.r = par.deltaColor.g = par.deltaColor.b = 0;
par.deltaColor.a = 255;
par.size = 0;
par.deltaSize = 0;
par.rotation = 0;
par.deltaRotation = 0;
par.timeToLive = 0;
par.drawPos.set(ZERO_VEC2);
// Mode A
par.dir.set(ZERO_VEC2);
par.radialAccel = 0;
par.tangentialAccel = 0;
// Mode B
par.angle = 0;
par.degreesPerSecond = 0;
par.radius = 0;
par.deltaRadius = 0;
}, 1024);
同样的,我们在这个方法中增加我们frameindex
和lastFrameTime
属性的reset代码。
//animation mode
par.frameindex = 0//当前帧
par.lastFrameTime = 0;
再往下,我们看下emitParticle方法。
这个方法用来发射一个个的粒子,每一个粒子的发射都会调用这个方法
这个方法里面就是通过我们在CCParticleSystem的配置,对单个粒子的初始状态进行计算和赋值,如果有一些随机范围,也是在这里进行计算的。比如通过我们配置好的startSize以及startSizeVal计算出单个粒子的初始size:
Simulator.prototype.emitParticle = function (pos) {
let psys = this.sys;
let clampf = misc.clampf;
let particle = pool.get();
this.particles.push(particle);
// Init particle
// timeToLive
// no negative life. prevent division by 0
......
// size
let startS = psys.startSize + psys.startSizeVar * (Math.random() - 0.5) * 2;
startS = Math.max(0, startS); // No negative value
particle.size = startS;
......
};
在这个方法的最后,我们添加下面的代码
//animation mode
particle.enableAnimation = psys.enableAnimation;
if (particle.enableAnimation) {
particle.sizeX = psys.sizeX;
particle.sizeY = psys.sizeY;
particle.frameindex = 0;
particle.lastFrameTime = particle.timeToLive;
particle.uv_deltaX = psys.uv_deltaX;
particle.uv_deltaY = psys.uv_deltaY;
particle.animationRate = psys.animationRate;
}
如果ParticleSystem的enableAnimation
属性为true,也就意味着我们开启了帧动画模式,这时将我们在ParticleSystem里面配置的相关数据,赋值给单个的粒子,同时初始化它的frameindex
和lastFrameTime
属性
注意粒子本身的timeToLive
属性是粒子的剩余存活时间,不是粒子发射后的存活时间,是在这个方法前面计算出来的。在每一步step中,会对timeToLive
做减法,扣除一定的时间,直到这个值为0
时,simulator会回收这个粒子。
接着往下,我们来看updateParticleBuffer
方法。
这个方法每一个step的时候都会调用
它是用来更新单个粒子的buffer,这个buffer就保存了GL需要使用的顶点信息,包含了粒子的位置信息,UV信息,color信息
左下(BL),右下(BR),左上(TL),右上(TR)四个顶点
它的排列方法大概就是
[
左下的XY, 左下的UV, 左下的Color,
右下的XY, 右下的UV, 右下的Color,
左上的XY, 左上的UV, 左上的Color,
右上的XY, 右上的UV, 右上的Color,
]
Simulator.prototype.updateParticleBuffer = function (particle, pos, buffer, offset) {
let vbuf = buffer._vData;
let uintbuf = buffer._uintVData;
let x = pos.x, y = pos.y;
let size_2 = particle.size / 2;
// pos
if (particle.rotation) {
let x1 = -size_2, y1 = -size_2;
let x2 = size_2, y2 = size_2;
let rad = -misc.degreesToRadians(particle.rotation);
let cr = Math.cos(rad), sr = Math.sin(rad);
// bl
vbuf[offset] = x1 * cr - y1 * sr + x;
vbuf[offset+1] = x1 * sr + y1 * cr + y;
// br
vbuf[offset+5] = x2 * cr - y1 * sr + x;
vbuf[offset+6] = x2 * sr + y1 * cr + y;
// tl
vbuf[offset+10] = x1 * cr - y2 * sr + x;
vbuf[offset+11] = x1 * sr + y2 * cr + y;
// tr
vbuf[offset+15] = x2 * cr - y2 * sr + x;
vbuf[offset+16] = x2 * sr + y2 * cr + y;
}
else {
// bl
vbuf[offset] = x - size_2;
vbuf[offset+1] = y - size_2;
// br
vbuf[offset+5] = x + size_2;
vbuf[offset+6] = y - size_2;
// tl
vbuf[offset+10] = x - size_2;
vbuf[offset+11] = y + size_2;
// tr
vbuf[offset+15] = x + size_2;
vbuf[offset+16] = y + size_2;
}
// color
uintbuf[offset+4] = particle.color._val;
uintbuf[offset+9] = particle.color._val;
uintbuf[offset+14] = particle.color._val;
uintbuf[offset+19] = particle.color._val;
};
要注意的是,由于GL的坐标原点是在左上方,而不是左下角。因此这里看引擎给的值也是做了转换的,通过下面的图,应该可以理解这个转换
所以我们在这个代码中添加的UV计算的代码如下:
//animation mode
let spriteframeSize = this.sys._renderSpriteFrame.getOriginalSize();
let maxFrameIndex = particle.sizeX * particle.sizeY;
let nextFramePosIndex = (particle.frameindex + 1) % maxFrameIndex;
let duration = 1 / particle.animationRate;
//判断是否需要切换帧
if (particle.lastFrameTime - particle.timeToLive > duration) {
//计算需要第几列第几行的小图进行显示渲染
let sizeY = Math.floor(nextFramePosIndex / particle.sizeX);
let sizeX = nextFramePosIndex % particle.sizeX
let uv_x = sizeX * particle.uv_deltaX / spriteframeSize.width;
let uv_y = sizeY * particle.uv_deltaY / spriteframeSize.height;
let uv_w = particle.uv_deltaX / spriteframeSize.width;
let uv_h = particle.uv_deltaY / spriteframeSize.height;
//uv bl
vbuf[offset + 2] = uv_x;
vbuf[offset + 3] = uv_y + uv_h;
//uv br
vbuf[offset + 7] = uv_x + uv_w;
vbuf[offset + 8] = uv_y + uv_h;
//uv tl
vbuf[offset + 12] = uv_x;
vbuf[offset + 13] = uv_y;
//uv tr
vbuf[offset + 17] = uv_x + uv_w;
vbuf[offset + 18] = uv_y;
//记录时间,提供给下一帧进行计算。帧号+1;
particle.frameindex++;
particle.lastFrameTime = particle.timeToLive;
}
通过这段代码,我们计算出粒子当前的正确UV,从而达到显示小图,并且按照一定频率进行切换的效果。
由于ParticleSystem的属性面板是通过packages://inspector/inspectors/comps/particle-system.js
去实现的,我们无法更改界面的现实,这样就导致了我们之前新增的属性没办法在属性检查器中进行配置。
var ParticleSystem = cc.Class({
name: 'cc.ParticleSystem',
extends: RenderComponent,
mixins: [BlendFunc],
editor: CC_EDITOR && {
menu: 'i18n:MAIN_MENU.component.renderers/ParticleSystem',
inspector: 'packages://inspector/inspectors/comps/particle-system.js',
playOnFocus: true,
executeInEditMode: true
},
因此我们在项目中新建一个名为AnimationParticle的脚本组件,该组件我们让它继承自cc.ParticleSystem
这样我们就可以通过使用AnimationParticle组件来配置我们的帧动画粒子。
当然,你可以参照官方关于扩展inspector的文档,定制一个新的inspector给它,这样会更好一些。
最后保存我们的修改,并且编译引擎。
然后将我们前面的的AnimationParticle在引擎中,用上我们的测试资源,配置一下粒子的属性看下效果:
由于资源是随手找的,效果大家凑合着看。
这里大部分的计算都还是在particle-simulator中去做的,进一步优化的话就是将index这些信息传给GPU。在GPU里面,也就是利用材质Shader去做UV的处理计算,同样也可以将粒子的位置计算,color计算这些都放到GPU里面去。这样使用粒子时,CPU的占用就会大大降低,提升性能。
https://github.com/ayarami/AnimationParticle