轉載請註明,原文地址: 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開始繪製場景
首先是開始,簡單點,看代碼:
- 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進行設置,那就只需要調整節點的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的精靈也會在一起。
運行結果,來一個很壯觀的截圖:
渲染批次是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的理解就是多個節點如果材質是一樣並且是連續的,就可以直接批渲染。如果不是連續的怎麼辦呢,就可以給他設置一個層級(setLocalZOrder,setGlobalZOrder),括號裏面倆種函數都可以,但最重要的是,這個同一個層級裏面不能有其他不同的材質,才能做批渲染。
同時也可以去看看這篇文章的前半段COCOS2DX 3.0 優化提升渲染速度 Auto-batching