2d粒子支持多纹理

背景

Cocos 2d粒子相比于3d粒子来说,可供定制化的空间要小很多,如部分参数只能设置一个固定值和一个变化范围,不能使用3d粒子参数的Curve、TwoCurves等模式;部分参数(如Gravity)只能设置一个固定值,不同的粒子无法使用不同的参数,更不能在生命周期过程中发生变化;纹理只能使用一张,不能使用多张,使每个粒子使用不同的纹理,3d粒子可以通过TextureAnimationModule做到。因此很多粒子效果没法使用粒子系统来实现,只能转而使用序列帧动画。基于项目要求,本文对2d粒子系统进行了部分修改,使得支持多纹理,在js侧和c++测均有实现。

实现方案

2d粒子使用的材质和Sprite是一样的,都是builtin-2d-sprite,该材质有一个纹理参数。如果要支持多纹理,在不改变材质的情况下,就要通过图集的方式,将多张纹理放到一个texture中,通过不同的uv坐标来引用texture的不同部分。为了方便控制和兼容已有项目代码,给CParticalSystem增加以下参数:
textureType: 使用单纹理/多纹理,默认单纹理
atlas: 图集资源,每个粒子以一定规则从图集中取一份SpriteFrame
textureMode: 粒子以什么方式从图集中取SpriteFrame,支持顺序取和随机取

js侧实现详情

由于C++侧没有图集的概念,js侧无法把图集传递给C++,所以下面的实现中将图集中各个纹理的纹理坐标提取出来设置给_simulator.texsInfo属性,C++侧会注册texsInfo属性读写器,拿到这里设置的信息

class ParticleSystem {
    // 设置atlas属性后触发的函数
    _applyAtlas () {
        if (this._atlas && this._assembler) {
            this._onTextureLoaded();
            if (CC_JSB && CC_NATIVERENDERER) {
                // 图集信息传递给Native
                const spFrames = this._atlas.getSpriteFrames();
                const texsInfo = [];
                spFrames.forEach(spFrame => {
                    texsInfo.push({
                        uv: spFrame.uv,
                    })
                })
                this._simulator.texsInfo = texsInfo;
            }
        }
    },
}

Simulator.prototype.emitParticle = function (pos) {
    ……
    // 下面部分为原有的emitParticle里新增的代码
    const TextureType = cc.ParticleSystem.TextureType;
    // 多纹理模式时,给每个粒子赋予一个spriteFrame
    if (psys.textureType === TextureType.MULTI && psys.atlas) {
        const spFrames = psys.atlas.getSpriteFrames();
        if (spFrames.length > 0) {
            const TextureMode = cc.ParticleSystem.TextureMode;
            if (psys.textureMode === TextureMode.SEQUENCE) {
                this._atlasIndex = (this._atlasIndex + 1) % spFrames.length;
                particle.spriteFrame = spFrames[this._atlasIndex];
            } else {
                particle.spriteFrame = spFrames[Math.floor(Math.random() * spFrames.length)];
            }
        }
    }
}

Simulator.prototype.updateUVs = function (force) {
    let assembler = this.sys._assembler;
    if (!assembler) {
        return;
    }
    let buffer = assembler.getBuffer();
    if (buffer) {
        const TextureType = cc.ParticleSystem.TextureType;
        const FLOAT_PER_PARTICLE = 4 * assembler._vfmt._bytes / 4;
        let vbuf = buffer._vData;

        let start = force ? 0 : this._uvFilled;
        let particleCount = this.particles.length;

        if (this.sys.textureType === TextureType.MULTI && this.sys.atlas) {
            for (let i = start; i < particleCount; i++) {
		// 使用每个粒子附带的spriteFrame的uv数据来填充vData
                if (this.particles[i].spriteFrame) {
                    let uv = this.particles[i].spriteFrame.uv;
                    let offset = i * FLOAT_PER_PARTICLE;
                    vbuf[offset+2] = uv[0];
                    vbuf[offset+3] = uv[1];
                    vbuf[offset+7] = uv[2];
                    vbuf[offset+8] = uv[3];
                    vbuf[offset+12] = uv[4];
                    vbuf[offset+13] = uv[5];
                    vbuf[offset+17] = uv[6];
                    vbuf[offset+18] = uv[7];
                }
            }
        } else if (this.sys.textureType === TextureType.SINGLE && this.sys._renderSpriteFrame) {
	    // 使用粒子系统的spriteFrame的uv数据来填充vData
            let uv = this.sys._renderSpriteFrame.uv;
            for (let i = start; i < particleCount; i++) {
                let offset = i * FLOAT_PER_PARTICLE;
                vbuf[offset+2] = uv[0];
                vbuf[offset+3] = uv[1];
                vbuf[offset+7] = uv[2];
                vbuf[offset+8] = uv[3];
                vbuf[offset+12] = uv[4];
                vbuf[offset+13] = uv[5];
                vbuf[offset+17] = uv[6];
                vbuf[offset+18] = uv[7];
            }
        }
        this._uvFilled = particleCount;
    }
}

c++测实现详情

c++侧的实现整体逻辑上来说流程是类似的,jsb中增加几个属性的读写器,用于保存js侧设置的属性值。创建新的粒子结点时,从texsInfo中分配一个纹理坐标信息,填充buffer的时候uv坐标使用分配给粒子结点的。

// jsb_cocos2dx_particle_auto.cpp
bool js_register_cocos2dx_particle_ParticleSimulator(se::Object* obj) {
    auto cls = se::Class::create("ParticleSimulator", obj, nullptr, 
    _SE(js_cocos2dx_particle_ParticleSimulator_constructor));
    ……
    // jsb中增加3个字段的定义
    cls->defineProperty("texsInfo", _SE(js_cocos2dx_particle_ParticleSimulator_get_texsInfo), _SE(js_cocos2dx_particle_ParticleSimulator_set_texsInfo));
    cls->defineProperty("textureType", _SE(js_cocos2dx_particle_ParticleSimulator_get_textureType), _SE(js_cocos2dx_particle_ParticleSimulator_set_textureType));
    cls->defineProperty("textureMode", _SE(js_cocos2dx_particle_ParticleSimulator_get_textureMode), _SE(js_cocos2dx_particle_ParticleSimulator_set_textureMode));
    ……
}

void ParticleSimulator::emitParticle(cocos2d::Vec3 &pos) {
    ……
    // 下面部分为原有的emitParticle里新增的代码
    if (textureType == TextureType::MULTI && !texsInfo.empty()) {
        if (textureMode == TextureMode::SEQUENCE) {
            particle.uv = texsInfo[atlasIndex].uv;
            atlasIndex = (atlasIndex + 1) % texsInfo.size();
        } else {
            auto texIndex = rand() % texsInfo.size();
            particle.uv = texsInfo[texIndex].uv;
        }
    } else {
        particle.uv = _uv;
    }
}

void ParticleSimulator::render(float dt) {
    ……
	std::vector<float>& uv = textureType == TextureType::MULTI ? particle.uv : _uv;
	// bl
    vb.writeFloat32(x1 * cr - y1 * sr + x);
    vb.writeFloat32(x1 * sr + y1 * cr + y);
    vb.writeFloat32(uv[0]);
    vb.writeFloat32(uv[1]);
    vb.writeUint32(tempColor);

    // br
    vb.writeFloat32(x2 * cr - y1 * sr + x);
    vb.writeFloat32(x2 * sr + y1 * cr + y);
    vb.writeFloat32(uv[2]);
    vb.writeFloat32(uv[3]);
    vb.writeUint32(tempColor);
	……
}

对于本文的实现有错误的地方,欢迎各位看官指正

多纹理的演示视频

3赞

想和官方讨论下,2d粒子系统的参数为什么不能设计的和3d粒子系统的参数一样,更加灵活可控一点?粒子参数在生命周期中不可变或者变化模式只能线性,能做出来的效果就少了很多可能性。
@dumganhar

1赞

cocos 2dx里面还有SpriteFrame类来管理图集,cocos creator的Native侧就去掉了这个类?

1赞

@minggo @boyue 可以帮忙看下这里是否有问题吗?

1赞