1. 程式人生 > >cocos2dx渲染架構

cocos2dx渲染架構

2dx的時代UI樹便利和渲染是沒有分開的,遍歷UI樹的時候就渲染.3dx版本為了分離了ui樹的遍歷和渲染,先遍歷生成渲染命令發到渲染佇列,之後遍歷渲染命令佇列開始渲染.這樣做的好處是渲染命令可以重用,單獨的渲染可以做優化例如自動批繪製.本篇首先介紹cocos2D-X 3.x版本的渲染結構,之後會深入opengl es.

mainLoop

void DisplayLinkDirector::mainLoop()
{
    if (_purgeDirectorInNextLoop)
    {
        //只有一種情況會呼叫到這裡來,就是導演類呼叫end函式
        _purgeDirectorInNextLoop = false;
        //清除導演類
        purgeDirector();
    }
    else if (! _invalid)
    {
        //繪製
        drawScene();
        //清除記憶體
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

分析的起點是mainLoop函式,這是在主執行緒裡面會呼叫的迴圈,其中drawScene函式進行繪製。那麼就進一步來看drawScene函式。mainLoop實在opengl的ondrawframe呼叫過來的即平臺每幀渲染會呼叫.

drawScene

void Director::drawScene()
{
    //計算間隔時間
    calculateDeltaTime();
    //如果間隔時間過小會被忽略
    if(_deltaTime < FLT_EPSILON){ return;}
    //空函式,也許之後會有作用
    if (_openGLView)
    {
        _openGLView->pollInputEvents();
    }

    //非暫停狀態
    if (! _paused)
    {
        //scheduler更新 會使actionmanager更新和相關的schedule更新 引擎物理模擬都是在繪製之前做的
        _scheduler->update(_deltaTime);
        _eventDispatcher->dispatchEvent(_eventAfterUpdate);
    }

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //切換下一場景,必須放在邏輯後繪製前,否則會出bug
    if (_nextScene)
    {
        setNextScene();
    }

    kmGLPushMatrix();
    //建立單位矩陣
    kmMat4 identity;
    kmMat4Identity(&identity);

    //繪製場景
    if (_runningScene)
    {
        //遞迴的遍歷scene中的每個node的visit生成渲染命令放入渲染佇列
        _runningScene->visit(_renderer, identity, false);
        _eventDispatcher->dispatchEvent(_eventAfterVisit);
    }

    //繪製觀察節點,如果你需要在場景中設立觀察節點,請呼叫攝像機的setNotificationNode函式 
    if (_notificationNode)
    {
        _notificationNode->visit(_renderer, identity, false);//這是一個常駐節點
    }
    //繪製螢幕左下角的狀態
    if (_displayStats)
    {
        showStats();
    }
    //渲染
    _renderer->render();
    //渲染後
    _eventDispatcher->dispatchEvent(_eventAfterDraw);

    kmGLPopMatrix();

    _totalFrames++;

    if (_openGLView)
    {
        _openGLView->swapBuffers(); //交換緩衝區
    }
    //計算繪製時間
    if (_displayStats)
    {
        calculateMPF();
    }
}

其中和繪製相關的是visit的呼叫和render的呼叫,其中visit函式會呼叫節點的draw函式,在3.x之前的版本中draw函式就會直接呼叫繪製程式碼,3.x版本是在draw函式中生成將繪製命令放入到renderer佇列中,然後renderer函式去進行真正的繪製,首先來看sprite的draw函式.

渲染命令

void Sprite::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated)
{
    //檢查是否超出邊界,自動裁剪
    _insideBounds = transformUpdated ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;

    if(_insideBounds)
    {
        //初始化
        _quadCommand.init(_globalZOrder, _texture->getName(), _shaderProgram, _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
    }
}

這裡面用了兩種不同的繪製命令quadCommand初始化後就可以加入到繪製命令中,customDebugDrawCommand傳入了一個回撥函式,具體的命令種類會在後面介紹。其中自定義的customDebugDrawCommand命令在初始化的時候只傳入了全域性z軸座標,因為它的繪製函式全部都在傳入的回撥函式裡面,_quadCommand則需要傳入全域性z軸座標,貼圖名稱,shader,混合,座標點集合,座標點集個數,變換

Render

void Renderer::render()
{
    _isRendering = true;
    if (_glViewAssigned)
    {
        //清除
        _drawnBatches = _drawnVertices = 0;
     
        //排序
        for (auto &renderqueue : _renderGroups)
        {
            renderqueue.sort();
        }
        //繪製
        visitRenderQueue(_renderGroups[0]);
        flush();
    }
    clean();
    _isRendering = false;

}

Render類中的render函式進行真正的繪製,首先排序,再進行繪製,從列表中的第一個組開始繪製。在visitRenderQueue函式中可以看到五種不同型別的繪製命令型別,分別對應五個類,這五個類都繼承自RenderCommand。

繪製命令
  1. QUAD_COMMAND:

    QuadCommand類繪製精靈等。所有繪製圖片的命令都會呼叫到這裡,處理這個型別命令的程式碼就是繪製貼圖的openGL程式碼,

  2. CUSTOM_COMMAND:

    自定義繪製,自己定義繪製函式,在呼叫繪製時只需呼叫已經傳進來的回撥函式就可以,裁剪節點,繪製圖形節點都採用這個繪製,把繪製函式定義在自己的類裡。這種型別的繪製命令不會在處理命令的時候呼叫任何一句openGL程式碼,而是呼叫你寫好並設定給func的繪製函式,並自己實現一個自定義的繪製。

  3. BATCH_COMMAND:

    批處理繪製,批處理精靈和粒子,其實它類似於自定義繪製,也不會再render函式中出現任何一句openGL函式,它呼叫一個固定的函式。

  4. GROUP_COMMAND:

    繪製組,一個節點包括兩個以上繪製命令的時候,把這個繪製命令儲存到另外一個renderGroups中的元素中,並把這個元素的指標作為一個節點儲存到renderGroups[0]中。

render流程
void Renderer::addCommand(RenderCommand* command)
{
    //獲得棧頂的索引
    int renderQueue =_commandGroupStack.top();
    //呼叫真正的addCommand
    addCommand(command, renderQueue);
}

void Renderer::addCommand(RenderCommand* command, int renderQueue)
{
    //將命令加入到陣列中
    _renderGroups[renderQueue].push_back(command);
}

addCommand它是獲得需要把命令加入到renderGroups位置中的索引,這個索引是從commandGroupStack獲得的,commandGroupStack是個棧,當我們建立一個GROUP_COMMAND時,需要呼叫pushGroup函式,它是把當前這個命令在_renderGroups的索引位置壓到棧頂,當addCommand時,呼叫top,獲得這個位置

groupCommand.init(globalZOrder);
renderer->addCommand(&_groupCommand);
renderer->pushGroup(_groupCommand.getRenderQueueID());

GROUP_COMMAND一般用於繪製的節點有一個以上的繪製命 令,把這些命令組織在一起,無需排定它們之間的順序,他們作為一個整體被呼叫,所以一定要記住,棧是push,pop對應的,關於這個節點的所有的繪製命令被新增完成後,請呼叫pop,將這個值從棧頂彈出,否則後面的命令也會被新增到這裡。

為什麼呼叫的起始只需呼叫為什麼只是0,其他的呢?

visitRenderQueue(_renderGroups[0]);

它們會在處理GROUP_COMMAND被呼叫

else if(RenderCommand::Type::GROUP_COMMAND == commandType) {
    flush();
    int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
    visitRenderQueue(_renderGroups[renderQueueID]);
}