在Cocos2d-x 3.0的版本之前,Cocos2d-x的每个元素的绘制逻辑均分布于每个元素内部的draw()方法里,紧密依赖UI树的遍历;3.0开始,对绘制部分进行了重构,新的代码将绘制部分从UI树的遍历中分离出来,使得绘制系统设计更优雅、更灵活和易于扩展。
UI树的遍历
这是渲染系统比较重要的一个职责,遍历UI树中每一个元素,遍历的有两个重要的目的,一是遍历的顺序基本决定了元素被绘制的顺序,二是在遍历过程中实现元素的模型视图变换矩阵的计算,计算结果供OpenGL ES渲染管线计算顶点位置。
在3D渲染系统中,元素可以用任何顺序被绘制,最终图形惯性能够根据元素的Z轴,使用深度测试进行正确的绘制;在2D图形绘制中,各个元素在渲染管线中具有相同的Z深度,这些元素之间的层级以及绘制关系必须依赖同一个逻辑的深度,Cocos2d-x使用localZOrder来表示元素的逻辑深度,UI树的遍历采用中序的深度优先算法进行遍历。
遍历顺序及特点为:
遍历左边的子节点;
遍历根节点;
遍历右边子节点。
Cocos2d-x按元素的层级关系组织了一颗“”二叉树“”,左边的子节点表示逻辑深度小于0的子元素,右边的“”子节点“”表示逻辑深度大于0的子元素,这样,就能通过逻辑深度的顺序来表示元素被绘制的顺序,参见Node::visit()方法:
void Node::visit(Renderer* renderer,const Mat4 &parentTransform,uint32_t parentFlags) { // quick return if not visible. children won't be drawn. if (!_visible) { return; } uint32_t flags = processParentFlags(parentTransform,parentFlags); // IMPORTANT: // To ease the migration to v3.0,we still support the Mat4 stack,// but it is deprecated and your code should not rely on it _director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW); _director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW,_modelViewTransform); bool visibleByCamera = isVisitableByVisitingCamera(); int i = 0; if(!_children.empty()) { sortAllChildren(); // draw children zOrder < 0 for( ; i < _children.size(); i++ ) { auto node = _children.at(i); if (node && node->_localZOrder < 0) node->visit(renderer,_modelViewTransform,flags); else break; } // self draw if (visibleByCamera) this->draw(renderer,flags); for(auto it=_children.cbegin()+i; it != _children.cend(); ++it) (*it)->visit(renderer,flags); } else if (visibleByCamera) { this->draw(renderer,flags); } _director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW); // FIX ME: Why need to set _orderOfArrival to 0?? // Please refer to https://github.com/cocos2d/cocos2d-x/pull/6920 // reset for next frame // _orderOfArrival = 0; }
可见,Node先会将所有子节点排序,如果两个子节点的localZOrder值相同,则按照它们出现的顺序来表示绘制的顺序:
bool nodeComparisonLess(Node* n1,Node* n2) { return( n1->getLocalZOrder() < n2->getLocalZOrder() || ( n1->getLocalZOrder() == n2->getLocalZOrder() && n1->getOrderOfArrival() < n2->getOrderOfArrival() ) ); } void Node::sortAllChildren() { if (_reorderChildDirty) { std::sort(std::begin(_children),std::end(_children),nodeComparisonLess); _reorderChildDirty = false; } }
排序之后,Node就会按照小于0,根节点,然后是大于0的节点这样的顺序来一次绘制每个节点。
渲染命令和渲染队列
概述
新的绘制流程大致分为三步:生成绘制命令、对绘制命令进行排序、执行绘制命令。
生成绘制命令
在UI树的遍历的时候,对每一个元素生成一个绘制命令,RenderCommand表示一个绘制类型,它定义了如何去绘制一个元素,
class CC_DLL RenderCommand { public: enum class Type { UNKNOWN_COMMAND,QUAD_COMMAND,CUSTOM_COMMAND,BATCH_COMMAND,GROUP_COMMAND,MESH_COMMAND,PRIMITIVE_COMMAND,TRIANGLES_COMMAND }; /** * init function,will be called by all the render commands */ void init(float globalZOrder,const Mat4& modelViewTransform,uint32_t flags); /** Get Render Command Id */ inline float getGlobalOrder() const { return _globalOrder; } //...其他定义省略 };
Type这个enum class里定义了几种绘制的类型,一般情况下,每个UI元素会关联0个或1个RenderCommand,并重写基类Node::draw()方法,在draw方法中将绘制命令发送给render。
void Sprite::draw(Renderer *renderer,const Mat4 &transform,uint32_t flags) { #if CC_USE_CULLING // Don't do calculate the culling if the transform was not updated _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform,_contentSize) : _insideBounds; if(_insideBounds) #endif { _quadCommand.init(_globalZOrder,_texture->getName(),getGLProgramState(),_blendFunc,&_quad,1,transform,flags); renderer->addCommand(&_quadCommand); #if CC_SPRITE_DEBUG_DRAW _debugDrawNode->clear(); Vec2 vertices[4] = { Vec2( _quad.bl.vertices.x,_quad.bl.vertices.y ),Vec2( _quad.br.vertices.x,_quad.br.vertices.y ),Vec2( _quad.tr.vertices.x,_quad.tr.vertices.y ),Vec2( _quad.tl.vertices.x,_quad.tl.vertices.y ),}; _debugDrawNode->drawPoly(vertices,4,true,Color4F(1.0,1.0,1.0)); #endif //CC_SPRITE_DEBUG_DRAW } }
Sprite::draw()方法示意了这样绘制分离的方式,它只负责将绘制命令发送给render,并不会执行任何的GL命令,render会将RenderCommand放入一个栈中,等所有的UI元素遍历结束,render才开始执行所有的RenderCommand。
绘制命令的排序
绘制命令被执行的顺序不一定是UI元素被遍历的顺序,Cocos2d-x使用一个新的globalZOrder直接设置元素的绘制顺序,因此,UI元素绘制的顺序首先由globalZOrder决定,
然后再由遍历的顺序决定。
绘制命令执行
最后,render对经过排序的绘制命令执行绘制。 对于一般的RenderCommand,按顺序执行;对于Sprite使用的QuadCommand,如果两个QuadCommand相邻且使用相同的纹理、着色器等,render会将它们组合合成一个QuadCommand,这种情况称为
自动批绘制。自动批绘制减少了绘制次数,提升了绘制性能。
绘制命令、绘制队列和绘制类
RenderCommand
class CC_DLL RenderCommand { public: enum class Type { UNKNOWN_COMMAND,uint32_t flags); /** Get Render Command Id */ inline float getGlobalOrder() const { return _globalOrder; } };
每一个R enderCommand实例中,都包含一个globalZOrder属性,它是决定绘制顺序的重要属性。还有一个属性是Type,引擎内置了多个RenderCommand类型,其中QUAD_COMMAND用来绘制1个或多个矩形区域(比如说Sprite和ParticalSystem),相邻的QuadCommand如果使用相同的纹理,则可以实现自动批绘制。
BATCH_COMMAND用来绘制一个TextAtlas,如Label、TileMap等。 GROUP_COMMAND可以包装多个RenderCommand的集合,而且GroupCommand中的每一个RenderCommand都不会参与全局的排序。
RenderQueue
class RenderQueue { public: enum QUEUE_GROUP { GLOBALZ_NEG = 0,OPAQUE_3D = 1,TRANSPARENT_3D = 2,GLOBALZ_ZERO = 3,GLOBALZ_POS = 4,QUEUE_COUNT = 5,}; public: RenderQueue() { clear(); } void push_back(RenderCommand* command); ssize_t size() const; void sort(); RenderCommand* operator[](ssize_t index) const; void clear(); inline std::vector<RenderCommand*>& getSubQueue(QUEUE_GROUP group) { return _commands[group]; } inline ssize_t getSubQueueSize(QUEUE_GROUP group) const { return _commands[group].size();} void saveRenderState(); void restoreRenderState(); protected: std::vector<std::vector<RenderCommand*>> _commands; //Render State related bool _isCullEnabled; bool _isDepthEnabled; GLboolean _isDepthWrite; };
场景中每一个UI元素的绘制命令被发送到一个RenderQueue的绘制栈上,由类的定义可知,RenderQueue中存储着一组RenderCommand。而在QUEUE_GROUP中定义了每个RenderCommand应该添加到vector的哪个对应索引下:
static bool compareRenderCommand(RenderCommand* a,RenderCommand* b) { return a->getGlobalOrder() < b->getGlobalOrder(); } static bool compare3DCommand(RenderCommand* a,RenderCommand* b) { return a->getDepth() > b->getDepth(); } void RenderQueue::sort() { // Don't sort _queue0,it already comes sorted std::sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]),std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]),compare3DCommand); std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_NEG]),std::end(_commands[QUEUE_GROUP::GLOBALZ_NEG]),compareRenderCommand); std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_POS]),std::end(_commands[QUEUE_GROUP::GLOBALZ_POS]),compareRenderCommand); }
这是RenderQueue的排序方式,有sort()函数体的代码可以看到,QUEUE_GROUP::TRANSPARENT_3D表示的是3D的物体的绘制命令,将这些绘制命令排序时用到compare3DCommand,比较他们的Depth;而QUEUE_GROUP::GLOBALZ_NEG和QUEUE_GROUP::GLBALZ_POS分别表示globalZOrder小于0和大于0的绘制命令。
Render类实际上维护着一个RenderQueue的数组,每一个RenderQueue对应一组RenderCommand或者一个GroupCommand。
GroupCommand
class CC_DLL GroupCommand : public RenderCommand { public: GroupCommand(); ~GroupCommand(); void init(float depth); inline int getRenderQueueID() const {return _renderQueueID;} protected: int _renderQueueID; };
每个GroupCommand都对应着一个单独的RenderQueue,由_renderQueueID标识。
Renderer
class CC_DLL Renderer { public: static const int VBO_SIZE = 65536; static const int INDEX_VBO_SIZE = VBO_SIZE * 6 / 4; static const int BATCH_QUADCOMMAND_RESEVER_SIZE = 64; static const int MATERIAL_ID_DO_NOT_BATCH = 0; /** Adds a `RenderComamnd` into the renderer */ void addCommand(RenderCommand* command); /** Adds a `RenderComamnd` into the renderer specifying a particular render queue ID */ void addCommand(RenderCommand* command,int renderQueue); /** Pushes a group into the render queue */ void pushGroup(int renderQueueID); /** Pops a group from the render queue */ void popGroup(); /** returns whether or not a rectangle is visible or not */ bool checkVisibility(const Mat4& transform,const Size& size); protected: void processRenderCommand(RenderCommand* command); void visitRenderQueue(RenderQueue& queue); std::stack<int> _commandGroupStack; std::vector<RenderQueue> _renderGroups; MeshCommand* _lastBatchedMeshCommand; std::vector<TrianglesCommand*> _batchedCommands; std::vector<QuadCommand*> _batchQuadCommands; GroupCommandManager* _groupCommandManager; };
Renderer类主要的部分如上,_commandGroupStack保留了一个RenderQueue的栈,开始一个GroupCommand时,会对应新建一个新的RenderQueue的Id入栈,默认情况下,addCommand会将RenderCommand添加到_commandGroupStack栈的最后一个元素所对应的RenderQueue中,这样就能将所有子元素的RenderCommand添加到单独一个RenderQueue中,当分组结束,GroupCommand从_commandGroupStack上移除自己,后续的RederCommand将继续加入之前的RederQueue中:
int GroupCommandManager::getGroupID() { //Reuse old id for(auto it = _groupMapping.begin(); it != _groupMapping.end(); ++it) { if(!it->second) { _groupMapping[it->first] = true; return it->first; } } //Create new ID // int newID = _groupMapping.size(); int newID = Director::getInstance()->getRenderer()->createRenderQueue(); _groupMapping[newID] = true; return newID; }
这是GroupCommand获取groupID的方法,在GroupCommandManager的init()方法中,说明了GroupCommand所在的_commandGroupStack索引不能是0 :
bool GroupCommandManager::init() { //0 is the default render group _groupMapping[0] = true; return true; }
而在Renderer::Renderer()中,说明了_commandGroupStack[0]表示默认的绘制队列:
_commandGroupStack.push(DEFAULT_RENDER_QUEUE); // DEFAULT_RENDER_QUEUE == 0
Renderer还有两个函数,当创建一个GroupCommand并将其作为一个普通的RenderCommand发送到当前的RenderQueue上,GroupCommand会在Renderer上创建新的RenderQueue,并调用pushGroup()方法将其renderQueueId添加到_commandGroupStack栈中,结束时,调用popGroup():
void Renderer::pushGroup(int renderQueueID) { CCASSERT(!_isRendering,"Cannot change render queue while rendering"); _commandGroupStack.push(renderQueueID); } void Renderer::popGroup() { CCASSERT(!_isRendering,"Cannot change render queue while rendering"); _commandGroupStack.pop(); }
个人的一些源码阅读理解,欢迎路过的各位大大指出错误~ 后续还会有更新~