好久没写BLOG了,也是这一年都在项目上混,没什么时间去学习其他的东西,刚好最近闲下来了,就开始研究cocos2d了。最近学习了下它的渲染模式,和大家分享一下,其实具体的流程网上教程一抓一大把,我只是用几个例子来分析一下。
例子一:
for(int i=0;i<100;i++){ Sprite* sprite1 = Sprite::create("CloseNormal.png"); sprite1->setPosition(origin.x+sprite1->getContentSize().width/2 * (i%10),origin.y+sprite1->getContentSize().height/2*(i%10)); this->addChild(sprite1); }
结果:
渲染次数竟然仅仅为1(怎么说也应该是2来着,也许并没有把那几个标签的渲染算进来,不过这并不是今天的重点),我们绘制了100个精灵,竟然只调用了一次渲染命令。这是3.0以后版本的一个优化特性。
从Cocos2d-x3.0开始,Cocos2d-x引入了新的渲染流程,它不像2.x版本直接在每一个node中的draw函数中直接调用OpenGL代码进行图形渲染,而是通过各种RenderCommand封装起来,然后添加到一个CommandQueue队列里面去,而现在draw函数的作用就是在此函数中设置好相对应的RenderCommand参数,然后把此RenderCommand添加到CommandQueue中。最后在每一帧结束时调用renderer函数进行渲染,在renderer函数中会根据ID对RenderCommand进行排序,然后才进行渲染。(尽情的copy!!!)。大概就是先访问每一个节点,节点决定自己如何渲染,加什么命令自己决定,放到渲染队列中,当访问完了所有节点,再开始进行真正的渲染--执行各个节点加入进来的命令。
渲染命令RenderCommand分成以下几种:TrianglesCommand,QuadCommand,MeshCommand,GroupCommand,BatchCommand,PrimitiveCommand。真正对精灵绘制的只是QuadCommand这种渲染命令。
当访问到的命令是QuadCommand时,是如何处理的呢?看源码吧。
//Draw if we have batched other commands which are not quad command flush掉之前的所有绘图命令 flush3D(); flushTriangles(); //Process quad command auto cmd = static_cast<QuadCommand*>(command); //Draw batched quads if necessary 如果顶点数量超过了VBO所限制的顶点数量,马上进行绘制。 if(cmd->isSkipBatching()|| (_numberQuads + cmd->getQuadCount()) * 4 > VBO_SIZE ) { CCASSERT(cmd->getQuadCount()>= 0 && cmd->getQuadCount() * 4 < VBO_SIZE,"VBO for vertex is not big enough,please break the data down or use customized render command"); //Draw batched quads if VBO is full drawBatchedQuads(); } //Batch Quads _batchQuadCommands.push_back(cmd); fillQuads(cmd); if(cmd->isSkipBatching()) { drawBatchedQuads();//真正执行绘制精灵的操纵 }
那么接下来看看drawBatchedQuads是如何执行的。
void Renderer::drawBatchedQuads() { //..........省略部分代码......这里忽略的 一些opengl的操作,大概就是进行一些顶点数据和顶点索引数据的绑定,要不要采用VAO方式 //Start drawing verties in batch for(const auto& cmd : _batchQuadCommands) { auto newMaterialID = cmd->getMaterialID(); if(_lastMaterialID != newMaterialID || newMaterialID == MATERIAL_ID_DO_NOT_BATCH) { //Draw quads if(indexToDraw > 0) { glDrawElements(GL_TRIANGLES,(GLsizei) indexToDraw,GL_UNSIGNED_SHORT,(GLvoid*) (startIndex*sizeof(_indices[0])) ); _drawnBatches++; _drawnVertices += indexToDraw; startIndex += indexToDraw; indexToDraw = 0; } //Use new material cmd->useMaterial(); _lastMaterialID = newMaterialID; } indexToDraw += cmd->getQuadCount() * 6; } //..........省略部分代码...... }这里我们很高兴看到了_lastMaterialID 和newMaterialID;这里就是看如果多条命令的“渲染纹理”是一样的那么就把它们放在一起渲染,只调用一次glDrawElements命令,
那么命令的"渲染纹理"是否是一样的取决于什么呢?我们看QuadCommand是如何生成materialId的。
int glProgram = (int)_glProgramState->getGLProgram()->getProgram(); int intArray[4] = { glProgram,(int)_textureID,(int)_blendType.src,(int)_blendType.dst}; _materialID = XXH32((const void*)intArray,sizeof(intArray),0);原来是着色器 ,纹理ID,混合模式的一个HASH,所以两条命令的materialId要一样,必须保证这些都是一样的。
所以现在就很容易理解为什么例子一为什么是1了吧。
例子二
for(int i=0;i<100;i++){ Sprite* sprite1 = Sprite::create("CloseNormal.png"); Sprite* sprite2 = Sprite::create("CloseSelected.png"); sprite1->setPosition(origin.x+sprite1->getContentSize().width/2 * (i%10),origin.y+sprite1->getContentSize().height/2*(i%10)); spite2->setPosition(origin.x+sprite2->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-sprite2->getContentSize().height/2*(i%10)); this->addChild(sprite1); this->addChild(sprite2); }
结果:
what????感觉不会再爱了。。。
怎么又变成了200了,难道不是2么。之前的结论是:连续几条命令的“渲染纹理”是一样的,那么就会放在一起进行渲染,那么命令队列的命令是如何一次加进来的呢?现在要从Scene的渲染开始说起,Scene的render是在程序循环中一直在调用的,所有节点的访问都从这里开始。
void Scene::render(Renderer* renderer) { auto director = Director::getInstance(); Camera* defaultCamera = nullptr; const auto& transform = getNodeToParentTransform(); if (_cameraOrderDirty) { stable_sort(_cameras.begin(),_cameras.end(),camera_cmp); _cameraOrderDirty = false; } for (const auto& camera : _cameras) { if (!camera->isVisible()) continue; Camera::_visitingCamera = camera; if (Camera::_visitingCamera->getCameraFlag() == CameraFlag::DEFAULT) { defaultCamera = Camera::_visitingCamera; } director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION); director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION,Camera::_visitingCamera->getViewProjectionMatrix()); //visit the scene visit(renderer,transform,0); renderer->render(); director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION); } Camera::_visitingCamera = nullptr; }
visit(renderer,0);就是访问所有节点及其子节点,加入rendercommand。renderer->render()就是去执行这些rendercommand队列。在visit中有这么一句:
sortAllChildren()在访问该节点和子节点之前会对所有节点进行排序,排序的规则是:
bool nodeComparisonLess(Node* n1,Node* n2) { return( n1->getLocalZOrder() < n2->getLocalZOrder() || ( n1->getLocalZOrder() == n2->getLocalZOrder() && n1->getOrderOfArrival() < n2->getOrderOfArrival() ) ); }在该节点中的深度(优先)和到达的时间。
知道了这点就可以解释例子二了。在相邻两次用同一张图片去创建精灵之间插入了另外一张图片去创建精灵,由于我们没有指定localZorder,所以所有精灵都是默认localZorder,而到达时间就被分割开了,所以不能用优化的方案去绘制精灵。我们稍微改一下代码就可以马上让绘制次数变成2:(指定同一张图片的精灵的localZorder一样)。
for(int i=0;i<100;i++){ Sprite* sprite1 = Sprite::create("CloseNormal.png"); Sprite* sprite2 = Sprite::create("CloseSelected.png"); sprite1->setPosition(origin.x+sprite1->getContentSize().width/2 * (i%10),origin.y+sprite1->getContentSize().height/2*(i%10)); sprite2->setPosition(origin.x+sprite2->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-sprite2->getContentSize().height/2*(i%10)); this->addChild(sprite1,0); this->addChild(sprite2,1); }
例子3:
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("close.plist"); for(int i=0;i<100;i++){ <span style="white-space:pre"> </span>Sprite* sprite1 = Sprite::createWithSpriteFrameName("CloseNormal.png"); <span style="white-space:pre"> </span>Sprite* sprite2 = Sprite::createWithSpriteFrameName("CloseSelected.png"); <span style="white-space:pre"> </span>sprite1->setPosition(origin.x+sprite1->getContentSize().width/2 * (i%10),origin.y+sprite1->getContentSize().height/2*(i%10)); <span style="white-space:pre"> </span>sprite2->setPosition(origin.x+sprite2->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-sprite2->getContentSize().height/2*(i%10)); <span style="white-space:pre"> </span>this->addChild(sprite2); <span style="white-space:pre"> </span>this->addChild(sprite1); }结果:
for(int i=0;i<100;i++){ Sprite* sprite1 = Sprite::create("CloseNormal.png"); Sprite* sprite2 = Sprite::create("CloseSelected.png"); sprite1->setPosition(origin.x+sprite1->getContentSize().width/2 * (i%10),origin.y+visibleSize.height-sprite2->getContentSize().height/2*(i%10)); this->addChild(sprite1); this->addChild(sprite2); sprite1->setGlobalZOrder(1); }
效果:
enum QUEUE_GROUP { /**Objects with globalZ smaller than 0.*/ GLOBALZ_NEG = 0,/**Opaque 3D objects with 0 globalZ.*/ OPAQUE_3D = 1,/**Transparent 3D objects with 0 globalZ.*/ TRANSPARENT_3D = 2,/**2D objects with 0 globalZ.*/ GLOBALZ_ZERO = 3,/**Objects with globalZ bigger than 0.*/ GLOBALZ_POS = 4,QUEUE_COUNT = 5,};
static bool compareRenderCommand(RenderCommand* a,RenderCommand* b) { return a->getGlobalOrder() < b->getGlobalOrder(); }
所以globalZorder也会影响两个命令之间的位置关系。