Cocos2dx Render绘制系统

1 绘制系统概览

cocos-2dx 3.0之前,元素的绘制逻辑分布在元素内部的draw()方法内,因此紧密地依赖UI树的遍历,这种捆绑式的架构通常难易优化。为什么呢?因为UI树的遍历顺序直接决定了元素的绘制顺序,我们便无法通过优化绘制序列来提升绘制效率。实际上,绘制序列应当与UI树的遍历序列分离。
3.0之后的版本对此进行了重构。新的绘制系统架构完成了以下优化目标:

  • 将绘制系统的具体实现从UI树中分离:UI元素的类型一般是依据它在应用程序中的特征进行划分,不同类型的UI元素很有可能拥有相同的绘制方式,将两者分离,能使得彼此的职责更加明确;
  • 采用应用程序级别的视窗裁剪:将位于视窗区域外的UI元素丢弃,丢弃的方式就是禁止其向绘制桟发送任何绘制命令;
  • 采用自动批绘制:减少OpenGL ES的绘制次数可增强绘制性能(一次绘制产生一个图元,每个图元在装配时都要经理视锥体裁剪、视口变换等操作,因此图元越少性能越高),如果一个场景上很多元素都是用了同一张纹理、同一个着色器程序,理论上我们只需要调用一次绘制命令。
  • 更简单的实现绘制命令的自定义

2 绘制系统详解

新的绘制流程包括三阶段:生成绘制命令、绘制命令排序以及执行绘制命令。

2.1 生成绘制命令 RenderCommand

在UI元素内部,将不再进行具体的绘制工作,而是生成一个绘制命令,该绘制命令会携带所有绘制需要用到的属性信息,该命令会被压入绘制桟。以CCSprite为例:

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    // Don't do calculate the culling if the transform was not updated
    auto visitingCamera = Camera::getVisitingCamera();
    auto defaultCamera = Camera::getDefaultCamera();
    if (visitingCamera == defaultCamera) {
        _insideBounds = ((flags & FLAGS_TRANSFORM_DIRTY)|| visitingCamera->isViewProjectionUpdated()) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
    }
    else
    {
        _insideBounds = renderer->checkVisibility(transform, _contentSize);
    }

    if(_insideBounds)
    {
        _trianglesCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, _polyInfo.triangles, transform, flags);
        renderer->addCommand(&_trianglesCommand);
    }
}

在生成绘制命令之前,首先检查当前sprite是否在视窗范围内,其中flags & FLAGS_TRANSFORM_DIRTY位操作用于快速判断当前transform是否发生变化。
如果当前sprite在视窗范围内,则初始化一个trianglesCommand类型的renderCommand,并将该command添加至绘制桟中。
渲染系统管理下的每一次绘制都是一个RenderCommand(后面简称cmd),一个cmd是一种特定绘制方式的封装。cocos内置多种cmd类型,quad用以绘制多个矩形区域,batch_cmd用以绘制Texture_Atlas,如label、TileMap,Group_cmd可用以包装多个cmd,它可以用来实现子元素裁剪(ClippingNode)、绘制子元素至纹理(RenderTexture)等。

2.1.1 GroupCommand

一个GroupCommand对应一个RenderQueue,默认情况下所有RenderCommand被加至主绘制桟(_renderGroup中索引为0的renderQueue)中。最终RenderCommand构成的绘制桟组成的树形结构如下:
这里写图片描述
想要实现上述结构,最直观的做法是:在GroupCommand内部维护一个RenderQueue,其所有子RenderCommand都添加至这个RenderQueue中,但是这种设计是十分脆弱的,因为这种做法总需要让RenderCommand知道自己需要添加至哪个GroupCommand中,而实际上使用者更加期望无论这个cmd是加入普通绘制桟或是加入GroupCommand,都应该有一个一致的做法,即简单调用render->addCommand(cmd)即可。

cocos采用逻辑上建立树形结构,数据层面数组存储的方式实现上述功能。不想限制RenderCommand的加入方式,则GroupCommand中不应该独立维护一个RenderQueue,那么RenderQueue放哪里维护呢?在cocos的CCRender中本来已经维护了一个数组_renderGroups,数组存放了RenderQueue,当创建一个新的GroupCommand时,向_renderGroups中压入一个新的RenderQueue,该GroupCommand将保存新的RenderQueue在_renderGroups中的位置索引ID。

void GroupCommand::init(float globalOrder)
{
    _globalOrder = globalOrder;
    auto manager = Director::getInstance()->getRenderer()->getGroupCommandManager();
    manager->releaseGroupID(_renderQueueID);
    _renderQueueID = manager->getGroupID(); // 保存RenderQueue在_renderGroups中的索引,完成索引绑定
}
int GroupCommandManager::getGroupID()
{
    //Reuse old id
    if (!_unusedIDs.empty())
    {
        int groupID = *_unusedIDs.rbegin();
        _unusedIDs.pop_back();
        _groupMapping[groupID] = true;
        return groupID;
    }

    //Create new ID
    int newID = Director::getInstance()->getRenderer()->createRenderQueue(); // 初始化时向_renderGroups压入一个新的RenderQueue
    _groupMapping[newID] = true;

    return newID;
}
void GroupCommandManager::releaseGroupID(int groupID)
{
    _groupMapping[groupID] = false;
    _unusedIDs.push_back(groupID);
}

如何保证GroupCommand关联的RenderQueue与CCRender的addCommand操作对应的RenderQueue保持一致呢?这里用了一个_commandGroupStack桟结构实时记录了当前正在操作的RenderQueue,具体做法是:在将GroupCommand的子cmd加入绘制桟之前,先执行pushGroup将当前正在操作的RenderQueue的ID压入,等到GroupCommand所有子cmd均压入后,再执行popGroup将正在操作的ID弹出。这里为什么想到用桟结构呢?因为通过上图我们发现,RenderQueue的创建次序其实就是一个树的深度遍历,而记录深度遍历操作的最高效的结构就是桟。

void ClippingNode::visit(Renderer *renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
    if (!_visible || !hasContent())
        return;

    //Add group command
    _groupCommand.init(_globalZOrder);
    renderer->addCommand(&_groupCommand); // 首先将_groupCommand添加至现有绘制桟

    renderer->pushGroup(_groupCommand.getRenderQueueID()); // _commandGroupStack将保存该ID

    _beforeVisitCmd.init(_globalZOrder);
    _beforeVisitCmd.func = CC_CALLBACK_0(StencilStateManager::onBeforeVisit, _stencilStateManager);
    renderer->addCommand(&_beforeVisitCmd); // 加入至当前pushGroup进去的id对应的renderQueue中

    _stencil->visit(renderer, _modelViewTransform, flags);

    _afterVisitCmd.init(_globalZOrder);
    _afterVisitCmd.func = CC_CALLBACK_0(StencilStateManager::onAfterVisit, _stencilStateManager);
    renderer->addCommand(&_afterVisitCmd);

    renderer->popGroup();
}
void Renderer::addCommand(RenderCommand* command)
{
    int renderQueue =_commandGroupStack.top();
    _renderGroups[renderQueue].push_back(command);
}
void Renderer::pushGroup(int renderQueueID)
{
    _commandGroupStack.push(renderQueueID);
}
void Renderer::popGroup()
{
    _commandGroupStack.pop();
}

2.2 绘制命令排序

void RenderQueue::push_back(RenderCommand* command)
{
    float z = command->getGlobalOrder();
    if(z < 0)
    {
        _commands[QUEUE_GROUP::GLOBALZ_NEG].push_back(command);
    }
    else if(z > 0)
    {
        _commands[QUEUE_GROUP::GLOBALZ_POS].push_back(command);
    }
    // ...
}
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);
}

绘制命令cmd将被添加至绘制命令桟RenderQueue。向RenderQueue压入数据时,会基于UI的GlobalOrder进行分组存储,这么做的目的是在进行排序时,做到只对GlobalOrder<0或>0的UI元素绘制cmd进行排序以提升排序效率,因为我们知道,大部分的cmd对应的GlobalOrder=0,这些cmd无需排序,将按照visit序列执行。

2.3 执行绘制命令

cocos3.0之后采用批绘制的方式以加速绘制效率。对于相邻的cmd,如果它们使用相同的纹理、着色器等绘制特征,这只调用一次OpenGL ES绘制命令。

void Renderer::render()
{
    _isRendering = true; // 标注为正在绘制
    if (_glViewAssigned)
    {
        for (auto &renderqueue : _renderGroups)
        {
            renderqueue.sort(); // 基于globezorder排序所有renderqueue
        }
        visitRenderQueue(_renderGroups[0]); // 0对应了主绘制桟 其余绘制桟将在遍历到GroupRender时递归visit
    }
    clean(); // 清空绘制桟
    _isRendering = false;
}
void Renderer::visitRenderQueue(RenderQueue& queue)
{
    queue.saveRenderState();
    const auto& zNegQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_NEG);
    if (zNegQueue.size() > 0) { // ...}
    //Process Global-Z = 0 Queue
    const auto& zZeroQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_ZERO);
    if (zZeroQueue.size() > 0)
    {
        // setRenderState ...
        for (auto it = zZeroQueue.cbegin(); it != zZeroQueue.cend(); ++it)
        {
            processRenderCommand(*it);
        }
        flush();
    }
    const auto& zPosQueue = queue.getSubQueue(RenderQueue::QUEUE_GROUP::GLOBALZ_POS);
    if (zPosQueue.size() > 0) {//...}
    queue.restoreRenderState();
}
void Renderer::processRenderCommand(RenderCommand* command)
{
    auto commandType = command->getType();
    if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
    {
        auto cmd = static_cast<TrianglesCommand*>(command);//Process triangle command
        //Draw batched Triangles if necessary
        if(cmd->isSkipBatching() || _filledVertex + cmd->getVertexCount() > VBO_SIZE || _filledIndex + cmd->getIndexCount() > INDEX_VBO_SIZE)
        {
            // _filledVertex记录了一次批绘制所需要绘制的顶点数,总顶点数不能超过一个图元的VBO_SIZE
            drawBatchedTriangles();//如果cmd需要独立绘制或者VBO(vertix Buffer Object)满了,则完成已缓存图元的绘制 
        }
        _batchedCommands.push_back(cmd); // 添加至批绘制缓存中
        fillVerticesAndIndices(cmd); // 装在_filledVertex与_filledIndex:vertex表示全部顶点数组,index表示每个图元使用的顶点索引
        if(cmd->isSkipBatching()) // 如果cmd需要SkipBatch 上面的条件保证了cmd之前的命令先绘制完 此处进行cmd的独立绘制
        {
            drawBatchedTriangles();
        }
    }
    else if(RenderCommand::Type::GROUP_COMMAND == commandType)
    {
        //....
        int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
        visitRenderQueue(_renderGroups[renderQueueID]); // 对于GroupRender 深度递归遍历
    }
}
void Renderer::drawBatchedTriangles() // 以绘制Triangles为例
{
    int indexToDraw = 0; // 一次绘制的点数
    int startIndex = 0;
    //... 这一部分 将vertex 与 index装载至glBufferData中
    for(const auto& cmd : _batchedCommands)//Start drawing vertices in batch
    {
        auto newMaterialID = cmd->getMaterialID(); // 使用的ID通过xmd定义的generateMaterialID生成
        if(_lastMaterialID != newMaterialID || newMaterialID == MATERIAL_ID_DO_NOT_BATCH)
        { 
            // 材质发生变化或当前材质不允许批绘制
            // 这里的材质不仅包含纹理和着色器,还包含混合模式及其他GL状态
            if(indexToDraw > 0)//完成一直之前缓存vectix的绘制
            {
                glDrawElements(GL_TRIANGLES, (GLsizei) indexToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (startIndex*sizeof(_indices[0])) );
                _drawnBatches++;
                _drawnVertices += indexToDraw;
                startIndex += indexToDraw;
                indexToDraw = 0;
            }
            cmd->useMaterial();//使用新材质 即设置一些列gl参数
            _lastMaterialID = newMaterialID;
        }

        indexToDraw += cmd->getIndexCount(); // 记录本轮需绘制的点数
    }
    if(indexToDraw > 0) // 绘制剩余节点
    {
        glDrawElements(GL_TRIANGLES, (GLsizei) indexToDraw, GL_UNSIGNED_SHORT, (GLvoid*) (startIndex*sizeof(_indices[0])) );
        _drawnBatches++;
        _drawnVertices += indexToDraw;
    }
    //...
    _batchedCommands.clear(); // 清空批绘制缓存数组
    _filledVertex = 0; // 绘制完成清空
    _filledIndex = 0;
}
void TrianglesCommand::generateMaterialID()
{
    if(_glProgramState->getUniformCount() > 0) // 检查是否有自定义的着色器变量 如果包含 表明当前纹理的绘制开发者必须提供自定义的着色器程序
    {
        _materialID = Renderer::MATERIAL_ID_DO_NOT_BATCH;
    }
    else
    {
        // 使用着色器 | 纹理ID | 混合方程 生成Hash值 该值将作为是否将多个临近cmd进行批处理绘制的基准 
        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);
    }
}

在执行一次绘制drawBatchedTriangles()之前,首先将renderqueue中所有的cmd加至缓存数组中,这里要处理两种特殊情形,一是如果当前cmd设定了禁止批处理,那么应当先完成前面缓存的绘制命令,然后在完成本次cmd的绘制;二是如果发现缓存中的节点总数超出了一次可绘制的VBO最大上限,应当立即完成前面缓存的绘制命令。

在进行一次绘制时,遍历所有缓存的cmd,如果发现材质ID(特殊生成的Hash值)发生变化,则完成一次gl命令的调用,这里可以看到,批处理的本质就是讲同ID的需要绘制的节点全部积蓄起来,这样只需要调用一次gl绘制命令就可以完成多个cmd的绘制。

猜你喜欢

转载自blog.csdn.net/XIANG__jiangsu/article/details/80402236
今日推荐