今天早晨學習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的誤解,這兩組精靈應該各自都能知足,都能分別做爲一組批處理進行渲染。然而,運行結果以下:測試
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》:
我英語可好了,因此我是開着有道詞典看的(這是給有道打廣告的意思麼?),看了很久,總算弄明白這個問題了。
簡單地說,要繪製的精靈(應該說是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條哪去了啊?被你吃了嗎)
這麼一說,太含糊了,咱們再來一次,用代碼來羅列。
首先是開始,簡單點,看代碼:
void DisplayLinkDirector::mainLoop(){ if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector(); } else if (! _invalid) { drawScene(); // release the objects PoolManager::getInstance()->getCurrentPool()->clear(); }}
調用drawScene函數,開始繪製場景
接下來,drawScene函數裏有一小段代碼(我就不貼所有了,多嚇人):
if (_runningScene) { _runningScene->visit(_renderer, identity, false); _eventDispatcher->dispatchEvent(_eventAfterVisit); }
沒錯,調用visit函數遍歷場景的全部子節點(包括子節點的子節點,一直遞歸),而後作一些操做。
固然,咱們最終關心的是,調用這些子節點的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); }}
我刪掉了一些嚇人的代碼。
上面的代碼就是重點了,初始化_quadCommand對象,這就是QuadCommand,渲染命令。
其實渲染命令不只僅只有QuadCommand,還有其餘的,好比CustomCommand,自定義渲染命令,顧名思義,就是咱們用戶本身定製的命令,因爲我沒有使用過,就不介紹了。
而後,接着就調用addCommand函數將渲染命令加入隊列。
這裏有一點,也很重要,因爲渲染命令有好幾種,因此addCommand的時候,實際上是會根據不一樣的命令類型把渲染命令添加到不一樣的隊列。本文只想針對QuadCommand,因此就忽略這一點,假設咱們的全部命令都是QuadCommand。
draw函數執行完,就輪到渲染邏輯幹活了。
輪到渲染邏輯幹活了,以前介紹了,渲染命令有好幾種,若是我沒有理解錯誤的話,只有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的數字了~
要知足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團隊的人吧(邪惡)。
不連續的渲染命令,即便材質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的精靈也會在一塊兒。
運行結果,來一個很壯觀的截圖:
渲染批次是5,等等!爲何是5?爲何不是2?
繼續回答剛剛的問題,圖中的渲染批次是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