cocos2dx的渲染树

cocos2dx的渲染顺序由渲染树决定,它是一个树形结构,由Director控制场景Scene,Scene包含Layer,Layer包含精灵等。节点通过addChild()组织成Scene Graph,ZOrder决定渲染顺序。cocos2dx使用中根遍历算法,先渲染ZOrder小的节点。渲染过程从main.cpp开始,通过Director的run方法,逐层调用visit和draw方法,最终在Renderer::render()中执行OpenGL绘制命令。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

cocos2dx的渲染顺序是由渲染树来决定的。渲染树是由各种游戏元素按照层次关系构成的树结构,表示Cocos2d-x游戏的绘制层次。
由导演类Director控制渲染树的根节点场景Scene,场景中包含层Layer,层Layer中又包含精灵等。
层次关系如图:
这里写图片描述
按照官网开发者指南中的说法,所有节点都是存储在一个场景图_scene graph_中,_scene graph_是一个用来存储场景图形的数据结构,其实是一个树形结构。
可以通过API中的addChild()方法来创建_scene graph_场景。
在对节点之间添加调用addchild(node,Zorder,tag)的时候,就把该子节点添加到该父节点的子树中,ZOrder决定了子节点是在父节点左边的子树还是右边的子树。没有设置ZOrder的会按照添加的前后顺序决定渲染次序。
对_scene graph_进行遍历的时候,cocos2dx采用了_in-order walk_算法(按顺序)。类似于二叉树的中根遍历,先遍历左子树,然后访问根结点,最后遍历右子树。所以ZOrder值小的先渲染,ZOrder值大的后渲染,最右边的子树会最后显示到场景中,在所有节点的最上面。

接着从代码中看一下cocos2dx的渲染结构。
游戏程序从win32目录下的main.cpp开始运行

int APIENTRY _tWinMain(HINSTANCE hInstance,
                       HINSTANCE hPrevInstance,
                       LPTSTR    lpCmdLine,
                       int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // create the application instance
    AppDelegate app;
    return Application::getInstance()->run();
}

运行Application单例的run方法。继续追踪run方法

int Application::run()
{
   ......

    while(!glview->windowShouldClose())
    {
        QueryPerformanceCounter(&nNow);
        if (nNow.QuadPart - nLast.QuadPart > _animationInterval.QuadPart)
        {
            nLast.QuadPart = nNow.QuadPart;

            director->mainLoop();
            glview->pollEvents();
        }
        else
        {
            Sleep(0);
        }
    }
......

}

通过导演类来运行mainLoop方法。

void DisplayLinkDirector::mainLoop()
{
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector();
    }
    else if (! _invalid)
    {
        drawScene();

        // release the objects
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

drawScene方法

void Director::drawScene()
{
    ......

    // draw the scene
    if (_runningScene)
    {
        _runningScene->visit(_renderer, Mat4::IDENTITY, false);
        _eventDispatcher->dispatchEvent(_eventAfterVisit);
    }

    // draw the notifications node
    if (_notificationNode)
    {
        _notificationNode->visit(_renderer, Mat4::IDENTITY, false);
    }

    if (_displayStats)
    {
        showStats();
    }

    _renderer->render();
    _eventDispatcher->dispatchEvent(_eventAfterDraw);
    ......
}

场景的visit方法

void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
    ......
    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
        this->draw(renderer, _modelViewTransform, flags);

        for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
            (*it)->visit(renderer, _modelViewTransform, flags);
    }
    else
    {
        this->draw(renderer, _modelViewTransform, flags);
    }
    ......

}

先获取子节点,然后递归调用子节点的visit()函数,到了没有子节点的节点,开始调用draw()函数。
在节点Node的visit方法可以看到,要完成渲染树的绘制,就要遍历根节点,然后绘制自己的左根节点,再绘制根节点,最后绘制右根节点。继续追踪节点的draw方法

void Node::draw(Renderer* renderer, const Mat4 &transform, uint32_t flags)
{
}

居然什么都没做?原来draw是个虚函数,上面this指针调用的是其子类的draw方法,那就看Sprite的draw方法。

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    // Don't do calculate the culling if the transform was not updated
    _insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;

    if(_insideBounds)
    {
        _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform);
        renderer->addCommand(&_quadCommand);
#if CC_SPRITE_DEBUG_DRAW
        _customDebugDrawCommand.init(_globalZOrder);
        _customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);
        renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
    }
}

看到draw函数在向renderer添加了两个绘制命令,其中quadCommand初始化后就可以加入到绘制命令中,customDebugDrawCommand传入了一个回调函数。再来看addCommand函数。

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

void Renderer::addCommand(RenderCommand* command, int renderQueue)
{
    CCASSERT(!_isRendering, "Cannot add command while rendering");
    CCASSERT(renderQueue >=0, "Invalid render queue");
    CCASSERT(command->getType() != RenderCommand::Type::UNKNOWN_COMMAND, "Invalid Command Type");
    _renderGroups[renderQueue].push_back(command);
}

最终把绘制命令添加到_renderGroups数组中,然后在Renderer::render()中实现。

void Renderer::render()
{
    //Uncomment this once everything is rendered by new renderer
    //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //TODO setup camera or MVP
    _isRendering = true;

    if (_glViewAssigned)
    {
        // cleanup
        _drawnBatches = _drawnVertices = 0;

        //Process render commands
        //1. Sort render commands based on ID
        for (auto &renderqueue : _renderGroups)
        {
            renderqueue.sort();
        }
        visitRenderQueue(_renderGroups[0]);
        flush();
    }
    clean();
    _isRendering = false;
}

Renderer::render()中实现了真正的绘制,首先清除,然后排序,最后通过visitRenderQueue方法执行_renderGroups数组中的绘制命令

void Renderer::visitRenderQueue(const RenderQueue& queue)
{
    ssize_t size = queue.size();

    for (ssize_t index = 0; index < size; ++index)
    {
        auto command = queue[index];
        auto commandType = command->getType();
        if(RenderCommand::Type::QUAD_COMMAND == commandType)
        {
            flush3D();
            auto cmd = static_cast<QuadCommand*>(command);
            //Batch quads
            if(_numQuads + cmd->getQuadCount() > VBO_SIZE)
            {
                CCASSERT(cmd->getQuadCount()>= 0 && cmd->getQuadCount() < VBO_SIZE, "VBO is not big enough for quad data, please break the quad data down or use customized render command");

                //Draw batched quads if VBO is full
                drawBatchedQuads();
            }

            _batchedQuadCommands.push_back(cmd);

            memcpy(_quads + _numQuads, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount());
            convertToWorldCoordinates(_quads + _numQuads, cmd->getQuadCount(), cmd->getModelView());

            _numQuads += cmd->getQuadCount();

        }
        else if(RenderCommand::Type::GROUP_COMMAND == commandType)
        {
            flush();
            int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
            visitRenderQueue(_renderGroups[renderQueueID]);
        }
        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::MESH_COMMAND == commandType)
        {
            flush2D();
            auto cmd = static_cast<MeshCommand*>(command);
            if (_lastBatchedMeshCommand == nullptr || _lastBatchedMeshCommand->getMaterialID() != cmd->getMaterialID())
            {
                flush3D();
                cmd->preBatchDraw();
                cmd->batchDraw();
                _lastBatchedMeshCommand = cmd;
            }
            else
            {
                cmd->batchDraw();
            }
        }
        else
        {
            CCLOGERROR("Unknown commands in renderQueue");
        }
    }
}

drawBatchedQuads()中调用OpenGL的API来进行渲染.

流程图如下:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值