cocos2d-x的渲染流程及原理

时间:2024-03-16 07:33:55

 

cocos2d-x的渲染流程及原理

环境: cocos3.10 Xcode

 

UI元素的渲染流程图示:

cocos2d-x的渲染流程及原理

 1. 从main进入到Application:run中,该方法下有个while循环,用于处理设定的每帧(FPS)刷新相关

cocos2d-x的渲染流程及原理

{// ...
    glview->retain();
    
    while (!glview->windowShouldClose()) {       
        director->mainLoop(); 
    }  
    return 0;
}

cocos2d-x的渲染流程及原理

2. mainLoop做的事情如下:

cocos2d-x的渲染流程及原理

void Director::mainLoop()
{
    if (! _invalid)
    {
        // 绘制场景
        drawScene(); 
        // 自动内存释放相关,每帧结束后,检测释放掉内存池中引用计数为1的节点
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

cocos2d-x的渲染流程及原理

3.绘制场景drawScene的主要代码:

cocos2d-x的渲染流程及原理

void Director::drawScene()
{
    // 渲染当前运行场景
    if (_runningScene)
    {
        //clear draw stats
        _renderer->clearDrawStats();
        
        //render the scene
        _openGLView->renderScene(_runningScene, _renderer);
        
        _eventDispatcher->dispatchEvent(_eventAfterVisit);
    }
}

cocos2d-x的渲染流程及原理

4. 通过renderScene会进入到Scene::render()中,它是处理渲染相关的主要接口:

cocos2d-x的渲染流程及原理

void Scene::render(Renderer* renderer, const Mat4* eyeTransforms, const Mat4* eyeProjections, unsigned int multiViewCount)
{
    auto director = Director::getInstance();
    Camera* defaultCamera = nullptr;
    // 转换坐标系,将ui元素相对坐标转换为世界坐标,原因在于OpenGL ES坐标系与cocos世界坐标系一致
    const auto& transform = getNodeToParentTransform();

    for (const auto& camera : getCameras())
    {
        if (!camera->isVisible())
            continue;

        Camera::_visitingCamera = camera;
        if (Camera::_visitingCamera->getCameraFlag() == CameraFlag::DEFAULT)
        {
            defaultCamera = Camera::_visitingCamera;
        }
        // 遍历场景中UI树,会进入到Node::visit中,进行UI树遍历
        visit(renderer, transform, 0);
        // 绘制,会进入到Renderer::render中,执行绘制
        renderer->render();
        // 
        camera->restore();
        //
        for (unsigned int i = 0; i < multiViewCount; ++i)
            director->popProjectionMatrix(i);
    }
    Camera::_visitingCamera = nullptr;
}

cocos2d-x的渲染流程及原理

5. visit主要用于UI树的遍历,遍历的规则使用的是中序(in-order)深度优先算法。即:

1. 遍历左边的子节点(小于0) 

2. 遍历根节点(等于0)

3. 遍历右边的子节点(大于0)

cocos2d-x的渲染流程及原理

void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
    // 如果不可见,则不再绘制
    if (!_visible)
    {
        return;
    }
   bool visibleByCamera = isVisitableByVisitingCamera();
    int i = 0;
    if(!_children.empty())
    {
     // 按照localZOrder排序,若相同则按照UI树顺序排序
        sortAllChildren();
        // 遍历 localZOrder < 0
        for(auto size = _children.size(); i < size; ++i)
        {
            auto node = _children.at(i);
            if (node && node->_localZOrder < 0)
                node->visit(renderer, _modelViewTransform, flags);
            else
                break;
        }
        // 遍历自身
        if (visibleByCamera)
            this->draw(renderer, _modelViewTransform, flags);
        // 遍历localZOrder > 0
        for(auto it=_children.cbegin()+i, itCend = _children.cend(); it != itCend; ++it)
            (*it)->visit(renderer, _modelViewTransform, flags);
    }
    else if (visibleByCamera)
    {
        this->draw(renderer, _modelViewTransform, flags);
    }
    _director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
}

cocos2d-x的渲染流程及原理

而遍历的方法主要通过sortAllChildren方法实现,比较的是localZOrder。

cocos2d-x的渲染流程及原理

// sortAllChildren
void Node::sortAllChildren()
{
    if (_reorderChildDirty)
    {
        sortNodes(_children);
        _reorderChildDirty = false;
    }
}

// sortNodes
static void sortNodes(cocos2d::Vector<_T*>& nodes)
    {
        static_assert(std::is_base_of<Node, _T>::value, "Node::sortNodes: Only accept derived of Node!");
       // 按照localZOrder排序
        std::stable_sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
            return n1->_localZOrder < n2->_localZOrder;
        });
    }

cocos2d-x的渲染流程及原理

6. 在visit排序遍历中,会依次根据localZOrder依次执行draw, 我们以Sprite为例:

cocos2d-x的渲染流程及原理

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    // 判定纹理是否有效
    if (_texture == nullptr)
    {
        return;
    }

#if CC_USE_CULLING
    // Don't 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
    {
        // XXX: this always return true since
        _insideBounds = renderer->checkVisibility(transform, _contentSize);
    }

    // 判定渲染的纹理是否在可见区域内
    if(_insideBounds)
#endif
    {    
        _trianglesCommand.init(_globalZOrder,
                               _texture,
                               getGLProgramState(),
                               _blendFunc,
                               _polyInfo.triangles,
                               transform,
                               flags);
        // 将绘制命令添加到renderer绘制栈RenderQueue中
        renderer->addCommand(&_trianglesCommand);
    }
}

cocos2d-x的渲染流程及原理

7. draw并未执行绘制,而是生成了RenderCommand绘制命令,放置到了RenderQueue中。

void Renderer::addCommand(RenderCommand* command)
{
    int renderQueue =_commandGroupStack.top();
    addCommand(command, renderQueue);
}

8. UI树遍历结束后,会生成一系列的绘制命令,此时Renderer::render()开始对绘制命令进行排序,绘制。

这样做的目地主要使得渲染系统可以对绘制做一些优化,比如:使用相同纹理的Command可执行自动批绘制。

cocos2d-x的渲染流程及原理

void Renderer::render()
{
    _isRendering = true;
    
    if (_glViewAssigned)
    {
        for (auto &renderqueue : _renderGroups)
        {
            // 执行遍历,遍历的是globalZOrder相关
            renderqueue.sort();
        }
        visitRenderQueue(_renderGroups[0]);
    }
    clean();
    _isRendering = false;
}

cocos2d-x的渲染流程及原理

关于其遍历sort的实现看下:

cocos2d-x的渲染流程及原理

void RenderQueue::sort()
{
    // 3D相关
    std::sort(std::begin(_commands[QUEUE_GROUP::TRANSPARENT_3D]), std::end(_commands[QUEUE_GROUP::TRANSPARENT_3D]), compare3DCommand);
    // GLOBALZ_NEG 表示globalZOrder < 0
    std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_NEG]), std::end(_commands[QUEUE_GROUP::GLOBALZ_NEG]), compareRenderCommand);
    // GLOBALZ_POS 表示globalZOrder > 0
    std::sort(std::begin(_commands[QUEUE_GROUP::GLOBALZ_POS]), std::end(_commands[QUEUE_GROUP::GLOBALZ_POS]), compareRenderCommand);
}

static bool compareRenderCommand(RenderCommand* a, RenderCommand* b)
{
    return a->getGlobalOrder() < b->getGlobalOrder();
}

cocos2d-x的渲染流程及原理

在这里,我们可以明白,RenderQueue对RenderCommand下globalZOrder不为0的,又执行了排序。我们可以总结到:

1. 过多的使用globalZOrder会影响UI元素的绘制性能

2. UI元素的绘制顺序并非仅受localZOrder的影响,也会受到globalZOrder的影响

 

9. 通过 visitRenderQueue 这一步会进入到渲染流程,而最终执行到的代码为:processRenderCommand

cocos2d-x的渲染流程及原理

void Renderer::processRenderCommand(RenderCommand* command)
{
    auto commandType = command->getType();
    if( RenderCommand::Type::TRIANGLES_COMMAND == commandType)
    {
        // ...
    }
    else if (RenderCommand::Type::MESH_COMMAND == commandType)
    {
        flush2D();
        // ...
    }
    else if(RenderCommand::Type::GROUP_COMMAND == commandType)
    {
        flush();
        int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
        CCGL_DEBUG_POP_GROUP_MARKER();
    }
    else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)
    {
        flush();
        auto cmd = static_cast<CustomCommand*>(command);
        cmd->execute();
    }
    else if(RenderCommand::Type::BATCH_COMMAND == commandType)
    {
        flush();
        auto cmd = static_cast<BatchCommand*>(command);
        cmd->execute();
    }
    else if(RenderCommand::Type::PRIMITIVE_COMMAND == commandType)
    {
        flush();
        auto cmd = static_cast<PrimitiveCommand*>(command);
        cmd->execute();
    }
}

cocos2d-x的渲染流程及原理

可以看到,更具渲染类型不同,会调用不同的渲染方法等。其相关命令主要有:

cocos2d-x的渲染流程及原理

enum class Type
{
    /** Reserved type.*/
    UNKNOWN_COMMAND,
    // 用于绘制1个或多个矩形区域,比如Sprite, ParticleSystem
    QUAD_COMMAND,
    /**Custom command, used for calling callback for rendering.*/
    CUSTOM_COMMAND,
    /*
    用来绘制一个TextureAtlas, 比如Label, TileMap等
    而对于TextureAtlas来说,它主要用于对同一纹理下多个精灵的封装
    */
    BATCH_COMMAND,
    // 用来包装多个RenderCommand,而其中的RenderCommand不会参与全局排序,多用于ClippingNode,RenderTexture等
    GROUP_COMMAND,
    /**Mesh command, used to draw 3D meshes.*/
    MESH_COMMAND,
    /**Primitive command, used to draw primitives such as lines, points and triangles.*/
    PRIMITIVE_COMMAND,
    /**Triangles command, used to draw triangles.*/
    TRIANGLES_COMMAND
};

cocos2d-x的渲染流程及原理