Particle Use Case
创建粒子并加入场景的demo示例:
auto particle = ParticleSystemQuad::create("dc/atom.plist"); particle->setPosition(size.width / 2, size.height / 2); this->addChild(particle);
Particle源码解析
顺着上边的代码,我们最直接的就会找到
bool ParticleSystem::initWithFile(const std::string& plistFile){ // 从文件中读取字典数据,并进行初始化 ValueMap dict = FileUtils::getInstance()->getValueMapFromFile(_plistFile); ret = this->initWithDictionary(dict, listFilePath); } bool ParticleSystem::initWithDictionary(ValueMap& dictionary, const std::string& dirname){ int maxParticles = dictionary["maxParticles"].asInt(); if(this->initWithTotalParticles(maxParticles)){ } } bool ParticleSystem::initWithTotalParticles(int numberOfParticles) { // 这里初始化_particleData if( !_particleData.init(_totalParticles) ) if (_batchNode)// 批处理节点,类似SpriteBatchNode { for (int i = 0; i < _totalParticles; i++) { _particleData.atlasIndex[i] = i; } } } bool ParticleData::init(int count) { maxCount = count; // 申请了最大粒子数量的内存空间,用来存放每个粒子的数据,通过offset来确定粒子的具体数据 posx= (float*)malloc(count * sizeof(float)); posy= (float*)malloc(count * sizeof(float)); // ... }
particle变量 | plist key | 解释 |
_totalParticles | maxParticles | 粒子的最大数量 |
_life | particleLifespan | 粒子的生命周期时间 |
剩下的逻辑就是靠update驱动
void ParticleSystem::update(float dt) { if (_isActive && _emissionRate && _AllActiveState) { // _life = dictionary["particleLifespan"].asFloat(); // _emissionRate = _totalParticles / _life; float rate = 1.0f / _emissionRate; // 排放速率 = 粒子声明周期 / 粒子数量 addParticles(emitCount); // 每一个粒子的生命递减, // 将末尾最后一个生命周期还存在的数据,copy到开头第一个生命周期结束的位置 // 就是将死去的粒子的位置,用末尾还存在的粒子代替 // 保证所有存在的粒子都是排在数组的前边,方便后续遍历 // 接着就是计算各种属性,size,pos,angle,color等 // 更新渲染数据 updateParticleQuads() // only update gl buffer when visible if (_visible && ! _batchNode) { postStep(); } } } // 当添加粒子的时候,因为_particleData已经提前申请好了内存,此时通过下标填充数据即可 void ParticleSystem::addParticles(int count) { //life for (int i = start; i < _particleCount ; ++i) { float theLife = _life + _lifeVar * RANDOM_M11(&RANDSEED); _particleData.timeToLive[i] = MAX(0, theLife); } // positon // color } void ParticleSystemQuad::updateParticleQuads(){ // 这里面是纯粹在操作_quads数据,将_particleData正确的填充到_quads里面 } V3F_C4B_T2F_Quad *_quads; // quads to be rendered struct CC_DLL V3F_C4B_T2F_Quad { /// top left V3F_C4B_T2F tl; /// bottom left V3F_C4B_T2F bl; /// top right V3F_C4B_T2F tr; /// bottom right V3F_C4B_T2F br; }; void ParticleSystemQuad::postStep() { // ↓ 需要关注下这个buffer的来源:ParticleSystemQuad::setupVBOandVAO里面有具体的数据绑定操作等行为 glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO[0]); // ↓之前操作的粒子数据 glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(_quads[0])*_totalParticles, _quads); glBindBuffer(GL_ARRAY_BUFFER, 0); CHECK_GL_ERROR_DEBUG(); }
void ParticleSystemQuad::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags) { //quad command if(_particleCount > 0) { _quadCommand.init(_globalZOrder, _texture, // _texture:对应粒子使用的纹理 getGLProgramState(), _blendFunc, _quads, // 粒子的数据 _particleCount, // 粒子的个数 transform, flags ); renderer->addCommand(&_quadCommand); } }
- 使用到的shader: ShaderPositionTextureColor_noMVP
void main() { gl_FragColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord); } void main() { gl_Position = CC_PMatrix * a_position; v_fragmentColor = a_color; v_texCoord = a_texCoord; }
因为QuadCommand也是TrianglesCommand,所以也会参与合批
CCParticleBatchNode
void ParticleSystem::setBatchNode(ParticleBatchNode* batchNode) { if( _batchNode != batchNode ) { _batchNode = batchNode; // weak reference if( batchNode ) { //each particle needs a unique index for (int i = 0; i < _totalParticles; i++) { _particleData.atlasIndex[i] = i; } } } } // 在场景中添加一个ParticleBatchNode,这个ParticleBatchNode接管了渲染, // ParticleBatchNode.addChild(particle) 将顶点数据复制到BatchNode // ParticleNode需要和ParticleBatchNode使用同样的纹理 void ParticleBatchNode::draw(Renderer* renderer, const Mat4 & /*transform*/, uint32_t flags) { _batchCommand.init(_globalZOrder, getGLProgram(), _blendFunc, _textureAtlas, // 这个图集是需要由Texture2D进行初始化 _modelViewTransform, flags ); renderer->addCommand(&_batchCommand); CC_PROFILER_STOP("CCParticleBatchNode - draw"); } bool ParticleBatchNode::initWithTexture(Texture2D *tex, int capacity) { // 这个纹理必须是粒子纹理,只有多个相同的粒子 _textureAtlas = new (std::nothrow) TextureAtlas(); _textureAtlas->initWithTexture(tex, capacity); } // batchCommand的渲染 void BatchCommand::execute() { // Set material _shader->use(); _shader->setUniformsForBuiltins(_mv); GL::bindTexture2D(_textureID); GL::blendFunc(_blendType.src, _blendType.dst); // Draw _textureAtlas->drawQuads(); }
优化
粒子的数学运算会消耗一定的性能,这个无法避免。
理论上,只要扩展下Particle.texture使其支持从TextureAtlas里面获取纹理,也能完美实现Particle和Sprite合批,因为底层都是TrianglesCommand,只需要注意下blend混合模式即可。
实现方式,可以考虑追加一个textureAtlas
解析,现在是支持textureImageData
、textureFileName
的解析,因为studio不支持,所以只能单独开发工具做支撑了。
ParticleBatchNode对性能优化意义不大,因为项目中很少有粒子是连着在一起,不连续就会断批。
ParticleBatchNode会将add进去的Particle的顶点数据放在一个大的buffer里面,这样在填充到renderbuffer里面的时候,就避免了碎片化拷贝内存,会带来一定的性能提升,这和拷贝压缩包比碎文件快,都是一样的道理