• cocos2dx 2.x
    • 遍历UI树时,在节点的draw方法中执行绘制指令
  • cocos2dx 3.x
    • 遍历UI树时,在节点的draw方法中生成绘制指令,比加入到Renderer管理的绘制队列中。

改进的好处:

  • 1、绘制逻辑从UI树中剥离
  • 2、应用程序级的视口裁剪
    • OpenGL ES会在图元装配阶段进行图元丢弃或裁剪,但依然执行了绘制命令。而改进之后的逻辑则从应用程序就进行了裁剪工作,减少了对GPU的占用。
  • 3、自动批绘制auto batching
    • 如果“相邻”元素使用相同的纹理,相同的着色器等,我们可以只调用一次绘制命令。减少绘制次数(draw calls)提高性能。
  • 4、更好的自定义绘制(扩展性)

整个绘制模块的结构图如下:
点击查看【processon】

整个绘制流程可以总结为3个阶段:

  • 生成绘制命令
    • 在UI树遍历时在每个元素中生成其绘制命令,并插入到当前的RendeQueue中。
  • 绘制命令排序
    • 对绘制栈中RenderQueue的元素按globalZOrder的升序排序。
    • 首先,排序GLOBALZ_NEG(globalZOrder<0)的RenderQueue。
    • 然后,排序GLOBALZ_ZERO(globalZOrder<0)的RenderQueue。
    • 最后,排序GLOBALZ_POS(globalZOrder>0)的RenderQueue。
  • 执行绘制命令
    • 由Renderer统一解释执行RenderCommand
    • 对满足条件的,自动批绘制auto batching
    • 执行顺序
      • 从index=0的RenderQueue开始执行,如果里面有GroupCommand,则递归执行执行的RenderQueue。
      • 如何遍历执行RenderQueue中的命令
        • 首先,执行globalZOrder<0的指令
        • 然后,执行globalZOrder=0的指令
        • 最后,执行globalZOrder>0的指令

          Renderer

          绘制系统的“负责人”。
  • 管理绘制栈
    • 由RenderQueue组成,新加入的RenderCommand总是插入到栈顶的RenderQueue中。index=0的RenderQueue为主Queue,在Renderer构造时便创建,常驻于内存,每次渲染都是从这里开始。
  • 执行绘制命令
    • 每条RenderCommand的最终执行都在这里完成。
    • auto batching也是在这里完成的。

      数据结构

      点击查看【processon】

      遍历指令

      ```cpp

//——————————————Renderer void Renderer::render(){ _isRendering = true; if (_glViewAssigned){

  1. for (auto &renderqueue : _renderGroups)
  2. renderqueue.sort(); // 见下面RenderQueue解释
  3. // 从index=0的主RenderQueue开始遍历,如果遇到GroupCommand,则递归遍历
  4. // GroupCommand执行的RenderQueue。
  5. visitRenderQueue(_renderGroups[0]);
  6. }
  7. ......

}

void Renderer::visitRenderQueue(RenderQueue& queue) { // * // 首先绘制globalZOrder < 0的RenderCommand // ** const auto& zNegQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_NEG); if (zNegQueue.size() > 0){ for (const auto& zNegNext : zNegQueue){ processRenderCommand(zNegNext); //这里就是真正开始执行OpenGL指令了。 } }

  1. // *********************************************
  2. // *** 绘制globalZOrder = 0的RenderCommand
  3. // *********************************************
  4. const auto& zZeroQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_ZERO);
  5. if (zZeroQueue.size() > 0){
  6. for (const auto& zZeroNext : zZeroQueue){
  7. processRenderCommand(zZeroNext);
  8. }
  9. }
  10. // *********************************************
  11. // *** 绘制globalZOrder > 0的RenderCommand
  12. // *********************************************
  13. const auto& zPosQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_POS);
  14. if (zPosQueue.size() > 0){
  15. for (const auto& zPosNext : zPosQueue){
  16. processRenderCommand(zPosNext);
  17. }
  18. }

}

  1. <a name="X43Q1"></a>
  2. ## 执行指令
  3. ```cpp
  4. void Renderer::processRenderCommand(RenderCommand* command)
  5. {
  6. auto commandType = command->getType();
  7. if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
  8. {
  9. // flush other queues
  10. flush3D();
  11. auto cmd = static_cast<TrianglesCommand*>(command);
  12. // flush own queue when buffer is full
  13. if(_filledVertex + cmd->getVertexCount() > VBO_SIZE || _filledIndex + cmd->getIndexCount() > INDEX_VBO_SIZE)
  14. {
  15. CCASSERT(cmd->getVertexCount()>= 0 && cmd->getVertexCount() < VBO_SIZE, "VBO for vertex is not big enough, please break the data down or use customized render command");
  16. CCASSERT(cmd->getIndexCount()>= 0 && cmd->getIndexCount() < INDEX_VBO_SIZE, "VBO for index is not big enough, please break the data down or use customized render command");
  17. drawBatchedTriangles();
  18. }
  19. // queue it
  20. _queuedTriangleCommands.push_back(cmd);
  21. _filledIndex += cmd->getIndexCount();
  22. _filledVertex += cmd->getVertexCount();
  23. }
  24. else if (RenderCommand::Type::MESH_COMMAND == commandType)
  25. {
  26. flush2D();
  27. auto cmd = static_cast<MeshCommand*>(command);
  28. if (cmd->isSkipBatching() || _lastBatchedMeshCommand == nullptr || _lastBatchedMeshCommand->getMaterialID() != cmd->getMaterialID())
  29. {
  30. flush3D();
  31. CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_MESH_COMMAND");
  32. if(cmd->isSkipBatching())
  33. {
  34. // XXX: execute() will call bind() and unbind()
  35. // but unbind() shouldn't be call if the next command is a MESH_COMMAND with Material.
  36. // Once most of cocos2d-x moves to Pass/StateBlock, only bind() should be used.
  37. cmd->execute();
  38. }
  39. else
  40. {
  41. cmd->preBatchDraw();
  42. cmd->batchDraw();
  43. _lastBatchedMeshCommand = cmd;
  44. }
  45. }
  46. else
  47. {
  48. CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_MESH_COMMAND");
  49. cmd->batchDraw();
  50. }
  51. }
  52. else if(RenderCommand::Type::GROUP_COMMAND == commandType)
  53. {
  54. flush();
  55. int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
  56. CCGL_DEBUG_PUSH_GROUP_MARKER("RENDERER_GROUP_COMMAND");
  57. visitRenderQueue(_renderGroups[renderQueueID]);
  58. CCGL_DEBUG_POP_GROUP_MARKER();
  59. }
  60. else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)
  61. {
  62. flush();
  63. auto cmd = static_cast<CustomCommand*>(command);
  64. CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_CUSTOM_COMMAND");
  65. cmd->execute();
  66. }
  67. else if(RenderCommand::Type::BATCH_COMMAND == commandType)
  68. {
  69. flush();
  70. auto cmd = static_cast<BatchCommand*>(command);
  71. CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_BATCH_COMMAND");
  72. cmd->execute();
  73. }
  74. else if(RenderCommand::Type::PRIMITIVE_COMMAND == commandType)
  75. {
  76. flush();
  77. auto cmd = static_cast<PrimitiveCommand*>(command);
  78. CCGL_DEBUG_INSERT_EVENT_MARKER("RENDERER_PRIMITIVE_COMMAND");
  79. cmd->execute();
  80. }
  81. else
  82. {
  83. CCLOGERROR("Unknown commands in renderQueue");
  84. }
  85. }

RenderQueue重复利用

当renderer绘制栈完成一次绘制,注意,不会清空绘制栈,而是清空绘制栈中所有RenderQueue的元素,这样可以减少内存分配操作。

  1. // ************************************************
  2. // ********** Renderer.cpp
  3. // ************************************************
  4. void Renderer::render(){
  5. _isRendering = true;
  6. if (_glViewAssigned){
  7. for (auto &renderqueue : _renderGroups){
  8. renderqueue.sort(); //排序
  9. }
  10. visitRenderQueue(_renderGroups[0]);
  11. }
  12. clean(); // 绘制完成,清理绘制栈
  13. _isRendering = false;
  14. }
  15. void Renderer::clean(){
  16. // Clear render group
  17. for (size_t j = 0, size = _renderGroups.size() ; j < size; j++){
  18. // std::vector<RenderQueue> _renderGroups;
  19. _renderGroups[j].clear(); // 清空RenderQueue
  20. }
  21. }
  22. //--------------------------------GroupCommand
  23. GroupCommand::GroupCommand(){
  24. //由于GroupCommandManager分配一个空的闲置的RenderQueue
  25. _renderQueueID = Director::getInstance()->getRenderer()
  26. ->getGroupCommandManager()->getGroupID();
  27. }
  28. GroupCommand::~GroupCommand(){
  29. //回收RenderQueue,因为是在构造析构中完成分配和回收,所以这是自动化的。
  30. Director::getInstance()->getRenderer()->getGroupCommandManager()
  31. ->releaseGroupID(_renderQueueID);
  32. }
  33. //--------------------------------GroupCommandManager
  34. int GroupCommandManager::getGroupID(){
  35. //std::unordered_map<int, bool> _groupMapping; 每个RenderQueue的是否正在被使用
  36. //std::vector<int> _unusedIDs; 当前可以使用的RenderQueue
  37. //重复使用已有的RenderQueue
  38. if (!_unusedIDs.empty()){
  39. int groupID = *_unusedIDs.rbegin();
  40. _unusedIDs.pop_back();
  41. _groupMapping[groupID] = true;
  42. return groupID;
  43. }
  44. //当前没有可用的RenderQueue则创建Queue并压入栈顶。
  45. int newID = Director::getInstance()->getRenderer()->createRenderQueue();
  46. _groupMapping[newID] = true;
  47. return newID;
  48. }
  49. void GroupCommandManager::releaseGroupID(int groupID){
  50. //回收RenderQueue,groupID指的是Queue。
  51. _groupMapping[groupID] = false;
  52. _unusedIDs.push_back(groupID);
  53. }

RenderQueue

RenderQueue的数据结构见上图,按GroupCommand来理解RenderQueue,因为GroupCommand就是执行RenderQueue。

  1. class RenderQueue {
  2. public:
  3. // 队列中的指令按如下进行分组:
  4. enum QUEUE_GROUP{
  5. GLOBALZ_NEG = 0, // globalZ < 0
  6. OPAQUE_3D = 1, // globalZ = 0的不透明3D object
  7. TRANSPARENT_3D = 2, // globalZ = 0的透明3D object
  8. GLOBALZ_ZERO = 3, // globalZ = 0的2D object
  9. GLOBALZ_POS = 4, // globalZ > 0的2D object
  10. QUEUE_COUNT = 5,
  11. };
  12. public:
  13. // 根据command的globalZoder是>0,<0,==0来将command插入到对应的queue,
  14. void push_back(RenderCommand* command);
  15. ssize_t size() const; //全部的命令数
  16. void sort() {
  17. // 排序绘制顺序,只排序globalZ!=0的Command
  18. // globalZoder=0分组就按插入时的顺序,也即是UI树遍历顺序。
  19. std::stable_sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]), std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]), compare3DCommand);
  20. std::stable_sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_NEG]), std::end(_commands[QUEUE_GROUP::GLOBALZ_NEG]), compareRenderCommand);
  21. std::stable_sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_POS]), std::end(_commands[QUEUE_GROUP::GLOBALZ_POS]), compareRenderCommand);
  22. }
  23. protected:
  24. //RenderQueue的数据结构
  25. std::vector<RenderCommand*> _commands[QUEUE_COUNT];
  26. };

自动批绘制:Auto Batching

用OpenGL的一段伪代码解释:

  1. // 根据顶点数据vetices、顶点索引数据indcies绘制数个三角形
  2. void drawTriangles(vetices, indcies){
  3. setupVAO_VBO(vetices, indcies); // 构建VAO、VBO
  4. // *******************
  5. // **** 我们把以下状态数据称为materialID
  6. // *******************
  7. glBindTexture(textureID);
  8. shader.use();
  9. shader.setUniforms(...);
  10. glEnable(GL_BLEND);
  11. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  12. // draw call
  13. glDrawElements(GL_TRIANGLES, vertexCount, GL_UNISGNED_INT, (GLvoid*)(0*sizeof(GLuint)) )
  14. }
  15. void main()
  16. {
  17. // *********************************************
  18. // ******* 需求:要绘制10批顶点数据的三角形,他们的materialID相同
  19. // *********************************************
  20. // *********************************************
  21. // ******** 普通方法
  22. // *********************************************
  23. while(i<10){
  24. drawTriangles(......); // 执行10次glDrawElements,也即是10次draw calls
  25. }
  26. // *********************************************
  27. // ******** auto batching
  28. // *********************************************
  29. verticesAll, indicesAll = gatherVertexData(...); // 将这10批顶点数据集中起来
  30. drawTriangles(verticesAll, indicesAll); // 1次draw call,完成全部绘制
  31. }

对于有OpenGL基础的同学理解起来非常简单。

要满足自动批绘制有几个条件:

  • 必须都是绘制GL_TRIANGLES
  • materialID相同
  • 必须相邻连续执行,不然materialID会被破坏。

    materialID

    cocos对于materialID的设计如下: ```cpp

// 每条TrianglesCommand对应一个materialID void TrianglesCommand::generateMaterialID(){ struct { void* glProgramState; GLuint textureId; GLenum blendSrc; GLenum blendDst; } hashMe;

  1. memset(&hashMe, 0, sizeof(hashMe));
  2. //我们可以看到,_materialID是一个hash值,因子包括:
  3. hashMe.textureId = _textureID; // 纹理ID
  4. hashMe.blendSrc = _blendType.src; // 混合
  5. hashMe.blendDst = _blendType.dst;
  6. hashMe.glProgramState = _glProgramState; // shader程序状态(glProgram、uniforms/attributes)
  7. _materialID = XXH32((const void*)&hashMe, sizeof(hashMe), 0);

}

  1. Renderer中执行指令的逻辑:
  2. ```cpp
  3. void Renderer::processRenderCommand(RenderCommand* command){
  4. auto commandType = command->getType();
  5. if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
  6. {
  7. ......
  8. auto cmd = static_cast<TrianglesCommand*>(command);
  9. // VBO(Vertex Buffer Object)满了,立即执行绘制
  10. if(_filledVertex + cmd->getVertexCount() > VBO_SIZE || _filledIndex + cmd->getIndexCount() > INDEX_VBO_SIZE){
  11. ......
  12. drawBatchedTriangles(); //立即执行队列中的TriangleCommand,
  13. }
  14. // VBO还没有满,则放入TriangleCommand的队列中,直接执行下一个command,
  15. // 如果下一个command不是TriangleCommand,则立即flush,执行队列中的TriangleCommand
  16. // 如果下一个还是TriangleCommand,则依然加入TriangleCommand队列
  17. _queuedTriangleCommands.push_back(cmd);
  18. ......
  19. }
  20. else if (RenderCommand::Type::MESH_COMMAND == commandType){
  21. flush2D();
  22. ......
  23. }
  24. else if(RenderCommand::Type::GROUP_COMMAND == commandType)
  25. {
  26. flush();
  27. ......
  28. }
  29. else if(RenderCommand::Type::CUSTOM_COMMAND == commandType){
  30. flush();
  31. ......
  32. }
  33. else if(RenderCommand::Type::BATCH_COMMAND == commandType){
  34. flush();
  35. ......
  36. }
  37. else if(RenderCommand::Type::PRIMITIVE_COMMAND == commandType){
  38. flush();
  39. ......
  40. }
  41. }
  42. void Renderer::flush(){
  43. flush2D();
  44. flush3D();
  45. }
  46. void Renderer::flush2D(){
  47. flushTriangles();
  48. }
  49. void Renderer::flushTriangles(){
  50. drawBatchedTriangles();
  51. }
  52. void Renderer::drawBatchedTriangles()
  53. {
  54. //_queuedTriangleCommands保存的是TriangleCommand
  55. //这些Command是在此之前连续相邻的TriangleCommand
  56. if(_queuedTriangleCommands.empty())
  57. return;
  58. // 假设_queuedTriangleCommands = [
  59. // command1, //matrialID = 1
  60. // command2, //matrialID = 2
  61. // command3, //matrialID = 2
  62. // command4, //matrialID = 3
  63. // command5, //matrialID = 3
  64. // command6, //matrialID = 4
  65. // ]
  66. // 最终绘制指令是
  67. // [
  68. // command1, //matrialID = 1
  69. // command3, //matrialID = 2
  70. // command5, //matrialID = 3
  71. // command6, //matrialID = 4
  72. // ]
  73. ......
  74. }

自动裁剪:Auto Culling

在UI树遍历阶段,cocos就会对一些元素进行位置计算,如果发现在屏幕外,就不会发送绘制指令到绘制栈中。

  1. void Node::visit(......){
  2. // quick return if not visible. children won't be drawn.
  3. if (!_visible) //元素不可见,直接跳过遍历,不会发送任何绘制指令,子元素也不会遍历
  4. {
  5. return;
  6. }
  7. draw(......);
  8. ......
  9. }
  10. void Sprite::draw(......){
  11. #if CC_USE_CULLING //是否开启自动裁剪,自动裁剪是需要每帧计算位置的,可能会降低性能
  12. //如果你确定不会有出现在屏幕外的情况,可关闭自动裁剪,提高性能。
  13. // Don't calculate the culling if the transform was not updated
  14. ......
  15. else if (visitingCamera == defaultCamera) {
  16. _insideBounds = ((flags & FLAGS_TRANSFORM_DIRTY) || visitingCamera->isViewProjectionUpdated()) ?
  17. renderer->checkVisibility(transform, _contentSize) : _insideBounds;
  18. //_insideBounds:是否在屏幕内
  19. //FLAGS_TRANSFORM_DIRTY:模型视图变换矩阵如果改变了即元素位置发生改变才会进行位置计算。
  20. //renderer->checkVisibility,计算元素可见性。
  21. }
  22. ......
  23. if(_insideBounds)
  24. #endif
  25. {
  26. _trianglesCommand.init(......);
  27. renderer->addCommand(&_trianglesCommand); //发送绘制指令
  28. ......
  29. }
  30. }