轉載自笨木頭的Cocos2d-x Auto-batching分析

今天早晨學習cpp-tests物理引擎實例,順便學習了Cocos2d-x 3.0新引入的Auto-batching技術。期間,在結合秦春林著做有關論述的同時學習了笨木頭同窗的文章,完整引用以下:php


近兩天都在折騰Auto-batching這東西,比較曲折,總結一句話就是:愛折(騰)纔會贏。node

 

看了好久的文檔,以及跟蹤了好久的源碼,對於Auto-batching這實現的流程總算是有點眉目了。git

 

=========== 如下是回憶,是我對Auto-batching產生疑惑的過程,能夠忽略不看=========github

這得從昨天提及(小若:咱們不是來聽故事的!),我在更改以前SpriteBatchNode的教程,因爲Cocos2d-x3.0新增了Auto-batching,因而就不得不把它也加進去。ide

這一加,不對勁,越寫愈加現本身對Auto-batching的理解有誤,在個人腦海中,只要精靈是使用同一個紋理、沒有更改blendFunc、沒有更改shader,那麼就知足Auto-batching,會自動將這些精靈加入到同一個渲染批次裏,優化渲染速度。函數

 

可我纔剛準備寫一個例子,卻發現,不對!沒有自動批處理。我當時作了這樣一個實驗,代碼以下:oop

/* 建立不少不少個精靈 */    for(inti = 0; i < 14100; i++)    {        Sprite* xiaoruo = Sprite::create("sprite0.png");        xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));        this->addChild(xiaoruo);          xiaoruo = Sprite::create("sprite1.png");        xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));        this->addChild(xiaoruo);    }

我建立了兩組精靈,分別使用sprite0.png和sprite1.png圖片,每組14100個(小若:爲何非得是14100,爲何不能是14000?你讓咱們這些強迫症的人怎麼辦?!)學習

按照我對Auto-batching的誤解,這兩組精靈應該各自都能知足,都能分別做爲一組批處理進行渲染。然而,運行結果以下:測試

41_173442_fe2008ed26661ba

 

 

GL calls(渲染批次)居然是16425次?這和想象中的徹底不同,不是應該是個位數麼?優化

這顛覆了我對Auto-batching的理解,因而,我又作了一些實驗,發現了一些謬論,但結果是好的,由於我知道,我對Auto-batching的理解一直都是錯的。

關於我作的那幾個實現,你們能夠看看這個帖子:Cocos2d-x3.0 Auto-batching 三個小實驗

 

因爲是使用Windows平臺作測試的,而後個人電腦配置比較高(小若:這是在炫耀的意思麼?敢亮出你的配置嗎?),因此幀率不能做爲參考。

 

還所以勞師動衆地到論壇發了這個帖子,真是有點對不起你們,是我不夠嚴謹,對我而言,這但是大忌T_T..

 

總之,那個帖子得出的疑問是:爲何不連續建立的精靈(相同紋理、相同混合函數、沒有對shader作什麼處理)不能知足Auto-batching的要求?

 

必定是我對Auto-batching產生了誤解,它應該還有一些我不知道的限制。

好,既然知道我對Auto-batching產生了誤解了,我固然就要再一次去看官方文檔了,首先是中文文檔:

https://github.com/chukong/cocos-docs/blob/master/manual/framework/native/v3/auto-batching/zh.md

反覆看了好幾回,不行,徹底找不到能對這個問題有幫助的內容,可是我找不到英文文檔。

終於仍是找到了,要×××才能看到(好可憐,我們國內的引擎,要×××看文檔T_T),標題是《Cocos2d (v.3.0) rendering pipeline roadmap》:

https://docs.google.com/document/d/17zjC55vbP_PYTftTZEuvqXuMb9PbYNxRFu0EGTULPK8/edit#heading=h.dii2kgdfqgcp

 

我英語可好了,因此我是開着有道詞典看的(這是給有道打廣告的意思麼?),看了很久,總算弄明白這個問題了。

簡單地說,要繪製的精靈(應該說是Node)先存放到隊列裏,而後由專門的渲染邏輯來渲染。對於隊列中的精靈,一個個取出來(其實存取的不是精靈,這裏先簡單這麼理解),發現材質同樣的話(相同紋理、相同混合函數、相同shader),就放到一個批次裏,若是發現不一樣的材質,則開始繪製以前連續的那些精靈(都在一個批次裏)。而後繼續取,繼續判斷材質。

若是相同材質的精靈,中間間隔了不一樣材質的精靈,那也無法在同一個批次裏渲染。

這就是那個問題的答案:爲何不連續建立的精靈(相同紋理、相同混合函數、相同shader)不能知足Auto-batching的要求,由於只要中間有不一樣材質的渲染對象,就會中斷,會先把以前連續的相同材質的對象進行批渲染。

======================== 以上是回憶,回憶結束========================

 

好了,上面是回憶的過程,而且已經有了大體的結論,如今正式來用代碼解釋。

 

笨木頭花心貢獻,啥?花心?不呢,是用心~

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

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

 

渲染流程

如今,一個渲染流程是這樣的:

(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開始繪製場景

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

void DisplayLinkDirector::mainLoop(){    if (_purgeDirectorInNextLoop)    {        _purgeDirectorInNextLoop = false;        purgeDirector();    }    else if (! _invalid)    {        drawScene();             // release the objects        PoolManager::getInstance()->getCurrentPool()->clear();    }}

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

 

2.遍歷場景的子節點

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

if (_runningScene)    {        _runningScene->visit(_renderer, identity, false);        _eventDispatcher->dispatchEvent(_eventAfterVisit);    }

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

 

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

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

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

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

 

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

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

 

其實渲染命令不只僅只有QuadCommand,還有其餘的,好比CustomCommand,自定義渲染命令,顧名思義,就是咱們用戶本身定製的命令,因爲我沒有使用過,就不介紹了。

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

 

這裏有一點,也很重要,因爲渲染命令有好幾種,因此addCommand的時候,實際上是會根據不一樣的命令類型把渲染命令添加到不一樣的隊列。本文只想針對QuadCommand,因此就忽略這一點,假設咱們的全部命令都是QuadCommand。

 

5.丟完QuadCommand就完事了

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

 

6.開始渲染

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

if(commandType == RenderCommand::Type::QUAD_COMMAND)                {                    auto cmd = static_cast<QuadCommand*>(command);                    _batchedQuadCommands.push_back(cmd);                                    }                else if(commandType == RenderCommand::Type::CUSTOM_COMMAND)                {}                else if(commandType == RenderCommand::Type::BATCH_COMMAND)                {}                else if(commandType == RenderCommand::Type::GROUP_COMMAND)                {}                else                {}

爲了不你們睡着了,我把不少重要的代碼刪了,咱們只要關注_batchedQuadCommands.push_back(cmd);。_batchedQuadCommands就是QuadCommand命令隊列了。

 

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

for(const auto& cmd : _batchedQuadCommands)    {        if(_lastMaterialID != cmd->getMaterialID())        {            //Draw quads            if(quadsToDraw > 0)            {                glDrawElements(GL_TRIANGLES, (GLsizei) quadsToDraw*6, GL_UNSIGNED_SHORT, (GLvoid*) (startQuad*6*sizeof(_indices[0])) );                _drawnBatches++;                _drawnVertices += quadsToDraw*6;                startQuad += quadsToDraw;                quadsToDraw = 0;            }            //Use new material            cmd->useMaterial();            _lastMaterialID = cmd->getMaterialID();        }        quadsToDraw += cmd->getQuadCount();    }

又爲了不你們睡着了,我刪了不少重要的代碼。(小若:我說,重要的代碼隨便刪除真的好嗎?)

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

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

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

 

7. 爲何必需要相同紋理、相同混合函數、相同shader?

要知足Auto-batching,就必須有這三個條件,這是爲何呢?

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

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

這個init函數就是關鍵:

void QuadCommand::init(float globalOrder, GLuint textureID, GLProgram* shader, BlendFunc blendType, V3F_C4B_T2F_Quad* quad, ssize_t quadCount, const kmMat4 &mv){    _globalOrder = globalOrder;    _textureID = textureID;    _blendType = blendType;    _shader = shader;    _quadsCount = quadCount;    _quads = quad;    _mv = mv;        _dirty = true;    generateMaterialID();}

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

void QuadCommand::generateMaterialID(){    if (_dirty)    {        //Generate Material ID                //TODO fix blend id generation        int blendID = 0;        if(_blendType == BlendFunc::DISABLE)        {            blendID = 0;        }        else if(_blendType == BlendFunc::ALPHA_PREMULTIPLIED)        {            blendID = 1;        }        else if(_blendType == BlendFunc::ALPHA_NON_PREMULTIPLIED)        {            blendID = 2;        }        else if(_blendType == BlendFunc::ADDITIVE)        {            blendID = 3;        }        else        {            blendID = 4;        }                // convert program id, texture id and blend id into byte array        char byteArray[12];        convertIntToByteArray(_shader->getProgram(), byteArray);        convertIntToByteArray(blendID, byteArray + 4);        convertIntToByteArray(_textureID, byteArray + 8);                _materialID = XXH32(byteArray, 12, 0);                _dirty = false;    }}

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

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

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

 

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

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

 

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

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

bool nodeComparisonLess(Node* n1, Node* n2){    return( n1->getLocalZOrder() < n2->getLocalZOrder() ||           ( n1->getLocalZOrder() == n2->getLocalZOrder() && n1->getOrderOfArrival() < n2->getOrderOfArrival() ));

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

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

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

 

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

void Renderer::render(){    if (_glViewAssigned)    {        //1. Sort render commands based on ID        for (auto &renderqueue : _renderGroups)        {            renderqueue.sort();        }    }

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

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

 

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

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

/* 建立不少不少個精靈 */    for(inti = 0; i < 14100; i++)    {        Sprite* xiaoruo = Sprite::create("sprite0.png");        xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));        this->addChild(xiaoruo);          xiaoruo = Sprite::create("sprite1.png");        xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));        this->addChild(xiaoruo);    }

這樣建立的精靈確定就無法連續了,由於sprite0.png的精靈和sprite1.png的精靈是不斷間隔着建立的,沒有連續。並且它們默認的localZOrder都是0,因此排序不起效。

 

那麼,稍微改改就行了,以下:

/* 建立不少不少個精靈 */    for(inti = 0; i < 14100; i++)    {        Sprite* xiaoruo = Sprite::create("sprite0.png");        xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));        this->addChild(xiaoruo, 1);          xiaoruo = Sprite::create("sprite1.png");        xiaoruo->setPosition(Point(CCRANDOM_0_1() * 480, 120 + CCRANDOM_0_1() * 300));        this->addChild(xiaoruo, 2);    }

只是給精靈分別指定了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的探索之旅總算是結束了。

我對OpenGL的東西還真不太懂,因此,有可能在研究代碼的時候有一些東西被我忽略了,或者誤解了,若是文章有錯誤的地方,那…你來打我啊(別,開玩笑的)。

 

PS:好了,由於今天上午還要出門,就刻意提早了5分鐘起牀整理這篇文章了,足足整理了1個多小時了。(小若:那你早起5分鐘的意義是什麼啊!)

 

 

PS(2014.06.18):

今天偶然發現我這篇文章的部份內容被放到官方文檔裏了,有種受寵若驚的感受~

但很奇怪的是,文檔里居然沒有註明出處,這個…就不要緊了。

爲了不之後你們反過來,覺得我這篇文章是摘錄了官方文檔,特此說明。

文檔地址:

https://github.com/chukong/cocos-docs/blob/master/manual/framework/native/v3/auto-batching/zh.md#rd

http://cn.cocos2d-x.org/tutorial/show?id=784

相關文章
相關標籤/搜索