cocos2d-x auto-batching

轉載請註明,原文地址:  http://www.benmutou.com/blog/archives/1006

文章來源:笨木頭與遊戲開發

CSDN原文地址:https://blog.csdn.net/musicvs/article/details/28226299


渲染流程

現在,一個渲染流程是這樣的:

(1)drawScene開始繪製場景

(2)遍歷場景的子節點,調用visit函數,遞歸遍歷子節點的子節點,以及子節點的子節點的子節點,以及…
(小若:夠了!給我停!)

(3)對每一個子節點調用draw函數

(4)初始化QuadCommand對象,這就是渲染命令,會丟到渲染隊列裏

(5)丟完QuadCommand就完事了,接着就交給渲染邏輯處理了。

(7)是時候輪到渲染邏輯幹活幹活,遍歷渲染命令隊列,這時候會有一個變量,用來保存渲染命令裏的材質ID,遍歷過程中就拿當前渲染命令的材質ID和上一個的材質ID對比,如果發現是一樣的,那就不進行渲染,保存一下所需的信息,繼續下一個遍歷。好,如果這時候發現當前材質ID和上一個材質ID不一樣,那就開始渲染,這就算是一個渲染批次了。

看官方的一張圖就完全明白了:


(8) 因此,如果我們創建了10個材質相同的對象,但是中間夾雜了一個不同材質的對象,假設它們的渲染命令在隊列裏的順序是這樣的:2個A,3個A,1個B,1個A,2個A,2個A。那麼前面5個相同材質的對象A會進行一次渲染,中間的一個不同材質對象B進行一次渲染,後面的5個相同材質的對象A又進行一次渲染。一共會進行三次批渲染。

(小若:突然發現,第6條哪去了啊?被你吃了嗎)

 

這麼一說,太含糊了,我們再來一次,用代碼來羅列。

 

1. drawScene開始繪製場景

首先是開始,簡單點,看代碼:

 

  1. void DisplayLinkDirector::mainLoop()
  2. {
  3.     if (_purgeDirectorInNextLoop)
  4.     {
  5.         _purgeDirectorInNextLoop = false;
  6.         purgeDirector();
  7.     }
  8.     else if (! _invalid)
  9.     {
  10.         drawScene();
  11.      
  12.         // release the objects
  13.         PoolManager::getInstance()->getCurrentPool()->clear();
  14.     }
  15. }

調用drawScene函數,開始繪製場景

 

2.遍歷場景的子節點

接下來,drawScene函數裏有一小段代碼(我就不貼全部了,多嚇人):

 

  1. if (_runningScene)
  2.     {
  3.         _runningScene->visit(_renderer, identity, false);
  4.         _eventDispatcher->dispatchEvent(_eventAfterVisit);
  5.     }

沒錯,調用visit函數遍歷場景的所有子節點(包括子節點的子節點,一直遞歸),然後做一些操作。

 

3.對每一個子節點調用draw函數

當然,我們最終關心的是,調用這些子節點的draw函數。

 

  1. void Sprite::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated)
  2. {
  3.     // Don't do calculate the culling if the transform was not updated
  4.     _insideBounds = transformUpdated ? isInsideBounds() : _insideBounds;
  5.     if(_insideBounds)
  6.     {
  7.         _quadCommand.init(_globalZOrder, _texture->getName(), _shaderProgram, _blendFunc, &_quad, 1, transform);
  8.         renderer->addCommand(&_quadCommand);
  9.     }
  10. }

我刪掉了一些嚇人的代碼。

 

4.初始化QuadCommand對象,這就是渲染命令

上面的代碼就是重點了,初始化_quadCommand對象,這就是QuadCommand,渲染命令。

 

其實渲染命令不僅僅只有QuadCommand,還有其他的,比如CustomCommand,自定義渲染命令,顧名思義,就是我們用戶自己定製的命令,由於我沒有使用過,就不介紹了。

然後,接着就調用addCommand函數將渲染命令加入隊列。

 

這裏有一點,也很重要,由於渲染命令有好幾種,所以addCommand的時候,其實是會根據不同的命令類型把渲染命令添加到不同的隊列。本文只想針對QuadCommand,所以就忽略這一點,假設我們的所有命令都是QuadCommand。

 

5.丟完QuadCommand就完事了

draw函數執行完,就輪到渲染邏輯幹活了。

 

6.開始渲染

輪到渲染邏輯幹活了,之前介紹了,渲染命令有好幾種,如果我沒有理解錯誤的話,只有QuadCommand才能參與自動批處理,因此,這裏會對渲染命令進行篩選,發現是QuadCommand類型的命令就保存到一個隊列裏。如代碼:

 

  1. if(commandType == RenderCommand::Type::QUAD_COMMAND)
  2.                 {
  3.                     auto cmd = static_cast<QuadCommand*>(command);
  4.                     _batchedQuadCommands.push_back(cmd);
  5.                                     }
  6.                 else if(commandType == RenderCommand::Type::CUSTOM_COMMAND)
  7.                 {}
  8.                 else if(commandType == RenderCommand::Type::BATCH_COMMAND)
  9.                 {}
  10.                 else if(commandType == RenderCommand::Type::GROUP_COMMAND)
  11.                 {}
  12.                 else
  13.                 {}

爲了避免大家睡着了,我把很多重要的代碼刪了,我們只要關注_batchedQuadCommands.push_back(cmd);。_batchedQuadCommands就是QuadCommand命令隊列了。

 

接着,調用drawBatchedQuads函數遍歷QuadCommand命令隊列:

 

  1. for(const auto& cmd : _batchedQuadCommands)
  2.     {
  3.         if(_lastMaterialID != cmd->getMaterialID())
  4.         {
  5.             //Draw quads
  6.             if(quadsToDraw > 0)
  7.             {
  8.                 glDrawElements(GL_TRIANGLES, (GLsizei) quadsToDraw*6, GL_UNSIGNED_SHORT, (GLvoid*) (startQuad*6*sizeof(_indices[0])) );
  9.                 _drawnBatches++;
  10.                 _drawnVertices += quadsToDraw*6;
  11.                 startQuad += quadsToDraw;
  12.                 quadsToDraw = 0;
  13.             }
  14.             //Use new material
  15.             cmd->useMaterial();
  16.             _lastMaterialID = cmd->getMaterialID();
  17.         }
  18.         quadsToDraw += cmd->getQuadCount();
  19.     }

又爲了避免大家睡着了,我刪了很多重要的代碼。(小若:我說,重要的代碼隨便刪除真的好嗎?)

大家睜大耳朵鼻子什麼的看看,_lastMaterialID是重點,當發現當前遍歷的渲染命令的材質ID和_lastMaterialID不一樣時,就會開始進行渲染,然後記錄新的材質ID,繼續遍歷。

這就是我們所說的,只有連續的相同材質ID的對象纔會被放到同一個批次裏進行渲染,如果不連續,那麼材質ID再怎麼相同也沒有辦法了。

對了,_drawnBatches變量就是我們左下角經常看到的GL calls的數字了~

 

7. 爲什麼必須要相同紋理、相同混合函數、相同shader?

要滿足Auto-batching,就必須有這三個條件,這是爲什麼呢?

我們回到之前的代碼,在調用節點的draw函數時,調用了QuadCommand的init函數:

 

  1. _quadCommand.init(_globalZOrder, _texture->getName(), _shaderProgram, _blendFunc, &_quad, 1, transform);

這個init函數就是關鍵:

 

  1. void QuadCommand::init(float globalOrder, GLuint textureID, GLProgram* shader, BlendFunc blendType, V3F_C4B_T2F_Quad* quad, ssize_t quadCount, const kmMat4 &mv)
  2. {
  3.     _globalOrder = globalOrder;
  4.     _textureID = textureID;
  5.     _blendType = blendType;
  6.     _shader = shader;
  7.     _quadsCount = quadCount;
  8.     _quads = quad;
  9.     _mv = mv;
  10.    
  11.     _dirty = true;
  12.     generateMaterialID();
  13. }

init函數裏最後調用了generateMaterialID函數,這個函數就是關鍵。(小若:夠了你,什麼都是關鍵,關鍵個毛線啊)

 

  1. void QuadCommand::generateMaterialID()
  2. {
  3.     if (_dirty)
  4.     {
  5.         //Generate Material ID
  6.        
  7.         //TODO fix blend id generation
  8.         int blendID = 0;
  9.         if(_blendType == BlendFunc::DISABLE)
  10.         {
  11.             blendID = 0;
  12.         }
  13.         else if(_blendType == BlendFunc::ALPHA_PREMULTIPLIED)
  14.         {
  15.             blendID = 1;
  16.         }
  17.         else if(_blendType == BlendFunc::ALPHA_NON_PREMULTIPLIED)
  18.         {
  19.             blendID = 2;
  20.         }
  21.         else if(_blendType == BlendFunc::ADDITIVE)
  22.         {
  23.             blendID = 3;
  24.         }
  25.         else
  26.         {
  27.             blendID = 4;
  28.         }
  29.        
  30.         // convert program id, texture id and blend id into byte array
  31.         char byteArray[12];
  32.         convertIntToByteArray(_shader->getProgram(), byteArray);
  33.         convertIntToByteArray(blendID, byteArray + 4);
  34.         convertIntToByteArray(_textureID, byteArray + 8);
  35.        
  36.         _materialID = XXH32(byteArray, 12, 0);
  37.        
  38.         _dirty = false;
  39.     }
  40. }

看到沒?~我們的材質ID(_materialID)最終是要由shader(_shader->getProgram())、混合函數ID(blendID)、紋理ID(_textureID)組成的啊喂!所以這三樣東西如果有誰不一樣的話,那就無法生成相同的材質ID,也就無法在同一個批次裏進行渲染了。

 

_blendType就是我們的BlendFunc混合函數,注意一下,這裏所說的相同的混合函數,並不是指要完全相同的值,
其實只是相同類型,看看if else的那幾個判斷就知道了,最後需要的只是blendID這個值。

當然,至於爲什麼要這樣生成材質ID,我就沒有去深究了,我只是個寫遊戲的,引擎底層,還是交給Cocos2d-x團隊的人吧(邪惡)。

 

8. 怎樣才能讓相同材質的對象的渲染命令連續排列?

不連續的渲染命令,即使材質ID相同也沒有用,那,我們應該怎麼讓這些傢伙連續起來呢?

 

這個問題好辦,還記得場景繪製的時候會遍歷所有子節點吧?

在遍歷子節點之前,其實還偷偷做了一件事情,那就是,調用sortAllChildren();函數對子節點進行排序,對比的規則是:

 

  1. bool nodeComparisonLess(Node* n1, Node* n2)
  2. {
  3.     return( n1->getLocalZOrder() < n2->getLocalZOrder() ||
  4.            ( n1->getLocalZOrder() == n2->getLocalZOrder() && n1->getOrderOfArrival() < n2->getOrderOfArrival() )
  5. );

好吧,我們不要管代碼了(小若:那你還貼個毛線啊,很嚇人的好不好)

總之,排序的規則是按照子節點的localZOrder和orderOfArrival進行的,orderOfArrival是用於localZOrder相同的情況下,進一步區分渲染順序的(就是誰在上面誰在下面,額,請不要想歪)。

那麼,我們只要調整節點的zOrder就能改變節點的遍歷順序,於是,節點的QuadCommand添加順序也就被改變了。

 

但是,注意,但是來了,除了場景子節點會進行排序之外,在渲染邏輯裏,渲染命令隊列也會進行一次排序:

 

  1. void Renderer::render()
  2. {
  3.     if (_glViewAssigned)
  4.     {
  5.         //1. Sort render commands based on ID
  6.         for (auto &renderqueue : _renderGroups)
  7.         {
  8.             renderqueue.sort();
  9.         }
  10.     }

當然,我刪了很多重要的代碼renderqueue是RenderQueue對象,就是用於保存渲染命令的隊列,它的sort函數是這樣的:

 

  1. void RenderQueue::sort()
  2. {
  3.     // Don't sort _queue0, it already comes sorted
  4.     std::sort(std::begin(_queueNegZ), std::end(_queueNegZ), compareRenderCommand);
  5.     std::sort(std::begin(_queuePosZ), std::end(_queuePosZ), compareRenderCommand);
  6. }
  7. bool compareRenderCommand(RenderCommand* a, RenderCommand* b)
  8. {
  9.     return a->getGlobalOrder() < b->getGlobalOrder();
  10. }
沒錯,渲染隊列會根據節點的globalOrder再一次進行排序,默認的globalOrder當然是0了,也就是排不排序結果都一樣。這涉及到localZOrder和globalOrder的概念,這就幫star特做個廣告吧,看看他的帖子:Cocos2dx 3.0 過渡篇(二十九)globalZOrder()與localZOrder() 

 

總之,結論就是,如果沒有對節點的globalOrder進行設置,那就只需要調整節點的localZOrder,便可以實現對渲染命令的排序順序進行控制。

來看下面的代碼,一開始貼過的:

 

  1. /* 創建很多很多個精靈 */
  2.     for(inti = 0; i < 14100; i++)
  3.     {
  4.         Sprite* xiaoruo = Sprite::create("sprite0.png");
  5.         xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));
  6.         this->addChild(xiaoruo);
  7.  
  8.         xiaoruo = Sprite::create("sprite1.png");
  9.         xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));
  10.         this->addChild(xiaoruo);
  11.     }

這樣創建的精靈肯定就沒法連續了,因爲sprite0.png的精靈和sprite1.png的精靈是不斷間隔着創建的,沒有連續。而且它們默認的localZOrder都是0,所以排序不起效。

 

那麼,稍微改改就好了,如下:

 

  1. /* 創建很多很多個精靈 */
  2.     for(inti = 0; i < 14100; i++)
  3.     {
  4.         Sprite* xiaoruo = Sprite::create("sprite0.png");
  5.         xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));
  6.         this->addChild(xiaoruo, 1);
  7.  
  8.         xiaoruo = Sprite::create("sprite1.png");
  9.         xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));
  10.         this->addChild(xiaoruo, 2);
  11.     }

只是給精靈分別指定了localZOrder值,這樣在排序的時候sprite0.png的精靈就會在一起,同樣,sprite1.png的精靈也會在一起。

運行結果,來一個很壯觀的截圖:

圖片1

 

渲染批次是5,等等!爲什麼是5?爲什麼不是2?

 

9. 渲染隊列存儲上限

繼續回答剛剛的問題,圖中的渲染批次是5,爲什麼是5?爲什麼不是2?

首先,即使我一個精靈也不創建,渲染批次也至少是1。

那麼,我創建了兩組材質ID相同的精靈,理論上GL calls應該是3,爲什麼是5?

 

這個也很簡單,因爲渲染隊列最大隻存放10922個渲染命令,注意,是“只存放”而不是“只能存放”,這個只是在代碼裏做的限制。

當渲染隊列(指的是Render類的成員變量:std::vector<QuadCommand*> _batchedQuadCommands; ,之前有講到)存放的渲染命令大於10922時,就會自動進行一次渲染操作,

把隊列裏的渲染命令處理掉。

 

因此,我創建了2組精靈,每組14100個,已經超過了10922的範圍,所以,即使這2組精靈各自都是相同的材質,但也不得不被分成2次進行渲染,於是,這2組精靈共進行了4次渲染操作。

再加上GL calls默認就有1(爲什麼默認會有一次,我就沒有去研究了),那麼,就是5次了。

 

話又說回來了,誰家的遊戲那麼誇張,要創建28200個精靈啊!這樣那些跑分8000左右的手機怎麼辦啊,我在自己手機裏試過了,幀率是60!沒錯,是60,已經太慢了無法正確計算了。因爲每一幀的渲染消耗的時間是2秒多!

一幀就消耗2秒多,太刺激了。

嗯,跑題了。

 

結束語

好了,關於Auto-batching的探索之旅總算是結束了。


        PS:看了這篇文章,我對Auto-batching的理解就是多個節點如果材質是一樣並且是連續的,就可以直接批渲染。如果不是連續的怎麼辦呢,就可以給他設置一個層級(setLocalZOrdersetGlobalZOrder),括號裏面倆種函數都可以,但最重要的是,這個同一個層級裏面不能有其他不同的材質,才能做批渲染。

        同時也可以去看看這篇文章的前半段COCOS2DX 3.0 優化提升渲染速度 Auto-batching

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章