CocosCreator游戲性能優(yōu)化(2):合批渲染之RenderToTarget

本文從降低drawcall角度來分析如何提升性能。

本文鏈接?CocosCreator游戲性能優(yōu)化(2):合批渲染

相關鏈接?CocosCreator游戲性能優(yōu)化(1):性能分析工具

? ? ? ? ? ? ? ?CocosCreator游戲性能優(yōu)化(3):GPU優(yōu)化之降低計算分辨率

一、分析Drawcall性能瓶頸

首先,每次CPU向GPU提交渲染指令,都會消耗一系列性能。例如,CPU計算、數(shù)據(jù)傳輸IO、GPU申請創(chuàng)建、綁定VBO等對象、GPU程序編譯鏈接損耗。如果多次執(zhí)行,而GPU每次都處理不飽和,那么會造成性能熱點和浪費。

我們看下CPU向GPU提交渲染會有哪些操作:

? ? 1、CPU遍歷場景節(jié)點樹,計算渲染指令。此過程中還會檢測合批(如果開啟了動態(tài)合批)或其他操作。

? ? 2、CPU提交渲染指令到渲染隊列。CPU創(chuàng)建或查詢需要使用的紋理數(shù)據(jù)和格式Texture;創(chuàng)建并綁定VertexBufferObject,調用gl接口解析特定格式的頂點數(shù)據(jù)VertexData;同時創(chuàng)建GPU程序,設定shader uniform值,編譯鏈接shader代碼。

/** 解析頂點數(shù)據(jù) */

GLuint vbo;? // 聲明vbo變量

glGenBuffers(1, &vbo);?// 生成1號vbo對象

glBindBuffer(GL_ARRAY_BUFFER, vbo);??// 綁定并使用1號vbo對象

glBufferData(GL_ARRAY_BUFFER, sizeof(float) * M * N, nullptr, GL_STATIC_DRAW);?// 向vbo對象中填充頂點數(shù)據(jù)

glBindBuffer(0, &vbo);??// 解綁?

/** 編譯鏈接GPU程序 */

...

...

二、合批規(guī)則和原理

????????因此,為了減少上述的計算頻率和冗余,我們需要進行渲染合批。單并不是所有紋理都能合批,目前了解至少需要滿足以下條件。

? ? ? ? 1、使用同一張紋理

? ? ? ? 2、紋理顏色、透明度相同。(cocos高版本已經支持,不需要滿足此條件)

? ? ? ? 3、紋理使用的材質相同

????????4、紋理GPU程序Shader相同。包括uniform參數(shù)一致

? ? ? ? 5、降低打斷合批的情況。

? ? ????原理是cocos通過上述多種對象和數(shù)據(jù)來計算最終的哈希值,根據(jù)這個哈希值來決定是否能夠進行合批操作。

? ? ? ? 為了滿足以上條件,我們要做的合圖或者盡可能多地創(chuàng)造可以進行合圖地條件(比如避免合批被打斷)。

? ? ? ? 打斷合批的原因主要有:

? ? ? ? 1、渲染順序。CocosCreator訪問節(jié)點樹是深度優(yōu)先的,也就是說不同父節(jié)點的紋理即使相同,也會被打斷。

? ? ? ? 2、文字渲染。紋理中間插入了文字,那么也會被打斷渲染批次。

? ? ? ? 3、材質參數(shù)不一致。

/** cc.Material 材質設置參數(shù)會重新計算hash值 */

getHash(){

????if(!this._dirty)returnthis._hash;

????this._dirty=false;

????leteffect=this._effect;

????lethashStr='';

????if(effect){

????????hashStr+=utils.serializeDefines(effect._defines);

????????hashStr+=utils.serializeTechniques(effect._techniques);

????????hashStr+=utils.serializeUniforms(effect._properties);

????}

????returnthis._hash=murmurhash2(hashStr,666);

}

二、合批方式

? ? ? ? 目前CocosCreator支持多種合批方式。合成大圖以后,程序用到的N張小圖,如果都在這張大圖中,那么提交的紋理地址都指向這張大圖,屬于同一個,也就利于合批。

? ? ? ? 1、靜態(tài)合批? ??

????????????靜態(tài)合批是指在游戲運行之前,就將圖片打包成大圖。

????????????CocosCreator對大圖操作比較友好。

????????????1、提供了是否打大圖的可選操作,并且提供預覽。缺點是參數(shù)支持有限,比如小圖間距不可調,不能保留透明邊框等。

? ? ? ? ? ? 2、大圖在編輯器中能夠直接讀取和操作小圖,和合圖之前操作差別不大,非常方便。

? ? ? ? ? ? 靜態(tài)合圖方式:

? ? ? ? ? ? 1、使用cocosCreator編輯器自帶AutoAatlas合圖。操作如下。

????????????在需要合圖的文件夾下新建一個自動圖集配置即可。打包時會將該文件夾下的所有圖片合成一張或多張大圖。


創(chuàng)建自動圖集配置

? ? ? ? ? ? ? 2、使用TexturePacker打包大圖。優(yōu)點是功能很豐富,可以設定很多自定義參數(shù)。

? ? ? ? ? ? ? ? ? ? 下載地址:?TexturePacker Download


????????2、動態(tài)合批

? ? ? ? ? ? 動態(tài)合批行為發(fā)生在程序運行中,程序會根據(jù)設定的規(guī)則,將小圖寫入大圖目標紋理。在cocosCreator中主要有以下幾種動態(tài)合批方式。

? ? ? ? ? ? 1、圖片動態(tài)合批

????????????????(1)編輯器中的圖片的屬性檢測面板中,勾選Packable選項,表明此圖可以應用動態(tài)合批規(guī)則。

? ? ? ? ? ? ? ? (2)程序中加入代碼cc.dynamicAtlasManager.enabled?=?true;表明該程序允許進行動態(tài)合批操作。

? ? ? ? ? ? 執(zhí)行以上操作后,程序就會在運行時將允許合圖的小圖按優(yōu)化算法寫入大圖中。進而達到合批的目的。

? ? ? ? ? ? 但是要注意,動態(tài)合批有一定的規(guī)則,比如寬高不能超過一定值等。

? ? ? ? ? ?2、自定義合批

? ? ? ? ? ? 除了cocosCreator自定義的合批方式外,我們還可以自己實現(xiàn)對任意節(jié)點樹或關心的節(jié)點集進行自定義合圖。

? ? ? ? ? ? 優(yōu)點是可以突破不同層級、不同父節(jié)點的限制,缺點是會消耗一部分內存。

? ? ? ? ? ? 介紹兩種比較簡單的方式:

? ? ? ? ? ? (1)渲染到紋理。利用OpenGL的FBO,存儲到RenderTexture,然后使用RenderTexture代理原來節(jié)點的顯示。在CocosCreator中,F(xiàn)BO的使用可以通過Camera的Render來實現(xiàn)。

? ? ? ? ? ? (2)動態(tài)修改父節(jié)點和節(jié)點的順序。理想的情況主要是將能夠合批(見合批規(guī)則與原理)的節(jié)點放在一起,并將文字節(jié)點和圖片節(jié)點分開放。滿足引擎渲染隊列廣度遍歷、以及避免文字會打斷合批的情況。

在相對靜態(tài)或對更改父節(jié)點不頻繁、不影響游戲流程的情況下,可以采用。此過程可用Spector.js插件來查看實際渲染的內容是否符合預期。

? ? ? ? ? ? (3)關于文字,CocosCreator已經提供了三種可選的動態(tài)合批模式,分別是None、BitMap、Char模式。其中None標識不合批,BitMap表示將使用到的每個Label轉換成texture后,合到一張?zhí)囟ǖ拇髨D中。Char表示使用到的單個文字轉換成texture后,和到一張?zhí)囟ǖ拇髨D中。由此可見,每個方式各有應用場景。

但是要注意的是,當文本數(shù)量或用到的單個字數(shù)量超過一定上限(即引擎內部大圖不足以填充)的時候,就會出問題。這個可以通過修改該引擎內部的邏輯來優(yōu)化這個問題。比如新文本texture替換未使用的文本位置的替換算法,復用大圖空間。這個以后可以單獨開專題講。

下面重點看下渲染到紋理這種做法。

我們先實現(xiàn)基礎截圖組件FBOComponent類。

export?class?FBOComponent?extends?cc.Component?{

????/**?目標源節(jié)點?*/

????public?_inTarget:?cc.Node?=?null; // 攝像機會將該節(jié)點渲染到RenderTexture中

????/**?緩存紋理對象?*/

????public?_renderTexture:?cc.RenderTexture?=?null; //?RenderTexture對象

????/**?FBO攝像機?*/

????public?_fboCamera:?cc.Camera?=?null; // cocosFBO的API實現(xiàn)在相機中

????/**?輸出sprite?*/

????public?_outSprite:?cc.Sprite?=?null; // 需要使用RenderTexture的精靈組件

????/**?輸出是否翻轉Y?*/

????public?_isFlipY:?boolean?=?true;

????/**?輸出目標的原始scaleY?*/

????public?_scaleY:?number?=?1;

????/**?是否使用外部renderTexture?*/

????public?_useNewTexture:?boolean?=?false;

????/**?是否每幀繪制?*/

????public?_frameDraw:?boolean?=?true;

????/**?是否采用late繪制?*/

????public?_useLate:?boolean?=?true; // 為了數(shù)據(jù)同步,需要在lateUpdate中進行Camera的Render操作

????/**?僅繪制一幀時,是否可以繪制?*/

????public?_canOnceDraw:?boolean?=?false; // 某些情形只需要Render一次,提升性能(例如靜態(tài)或更新不頻繁的頁面)

默認在lateUpdate中進行RenderTexture繪制。

lateUpdate():?void?{

????????if?(this._useLate)?{

????????????if?(this._frameDraw)?{

????????????????this.updateFBO();

????????????}else?if?(this._canOnceDraw)?{

????????????????this._canOnceDraw?=?false;

????????????????this.updateFBO();

????????????}

????????}

????}


updateFBO()?{

????????this.onFBOUpdateBegin();

????????if?(this._fboCamera)?{

????????????if?(!this._renderTexture?&&?!this._useNewTexture)?{

????????????????this._renderTexture?=?new?cc.RenderTexture();

????????????????this._renderTexture.initWithSize(this._inTarget.width,?this._inTarget.height,?cc["gfx"].RB_FMT_D24S8);

????????????????this._fboCamera.targetTexture?=?this._renderTexture;

????????????}

????????????if?(!this._renderTexture)?{

????????????????return;

????????????}else?{

????????????????if?(Math.floor(this._renderTexture.width)?!=?Math.floor(this._inTarget.width)?||?Math.floor(this._renderTexture.height)?!=?Math.floor(this._inTarget.height))?{

????????????????????this._renderTexture.updateSize(this._inTarget.width,?this._inTarget.height);

????????????????}

????????????}

????????????this._fboCamera.enabled?=?true;

????????????this._fboCamera.render(this._inTarget);

????????????this._fboCamera.enabled?=?false;

????????????if?(this._outSprite)?{

????????????????this._outSprite.spriteFrame?=?this._outSprite.spriteFrame?||?new?cc.SpriteFrame();

????????????????this._outSprite.spriteFrame.setTexture(this._renderTexture);

????????????????this._outSprite.node.scaleY?=?this._isFlipY???-this._scaleY?:?this._scaleY;

????????????}

????????}

????????this.onFBOUpdateEnd();

????}


以上關鍵代碼實現(xiàn)了將任意節(jié)點樹轉換成RenderTexture并更新至目標顯示Sprite組件的功能??梢杂糜谛枰l繁或每幀進行render的情況。

例如2d游戲中,在存在多障礙物的情況向,移動時,每幀繪制陰影區(qū)域的的黑色部分到一張texture中,就可以利用該FBOComponent組件來實現(xiàn)。

需要注意的是,這里沒有同步目標節(jié)點位置、寬高等,默認是一樣的不變的。

那么,我們現(xiàn)在注意到_canOnceDraw這個成員變量,這個成員變量是用來控制單次render功能的。也是用于一些僅需合批一次或按需render的情況。

例如很多行文字的任務列表。只有在收到新任務或完成任務等狀態(tài)發(fā)生變化的時候,才需要重新render一次。

這個時候有可能位置寬高都發(fā)生了變化。我們來實現(xiàn)少量繪制和自同步的功能。

以下實現(xiàn)BitmapCacheComponent組件類。它繼承于FBOComponent,且實現(xiàn)了drawOnece()方法和自動同步真正用于目標顯示的大小、位置等屬性。

還可以按自己shader需求可以實現(xiàn)自定義材質屬性。

export?class?BitmapCacheComponent?extends?FBOComponent?{

????/**?bitmap父節(jié)點?可選參數(shù)?*/

????public?_bitmapParent:?cc.Node?=?null; // 要將真正顯示的Sprite放在哪個父節(jié)點

????/**?是否啟用修復?可選參數(shù)?*/

????public?_enableFix:?boolean?=?false;

????/**?材質數(shù)組?可選參數(shù)?*/

????public?_materials:?{? // 真正顯示的Sprite 需要綁定的自定義材質

????????index:?number,

????????url:?string,

????????script?:?typeof?ShaderScript,

????????matParams:?any,

????}[]?=?[];

...

}

開始和結束render時的回調函數(shù)。在這里進行屬性同步。

protected?onFBOUpdateBegin()?{

????????super.onFBOUpdateBegin();

????????this._outSprite?=?this._outSprite?||?this.initBitMapSprite(this._bitmapParent?||?this.node.parent);

????????this._outSprite.node.width?=?this.node.width;

????????this._outSprite.node.height?=?this.node.height;

????????this._outSprite.node.anchorX?=?this.node.anchorX;

????????this._outSprite.node.anchorY?=?this.node.anchorY;

????????this._outSprite.node.x?=?this.node.x;

????????this._outSprite.node.y?=?this.node.y;

????????this._outSprite.node.opacity?=?0;

????????this.node.opacity?=?255;

????}

????protected?onFBOUpdateEnd()?{

????????if?(this._outSprite)?{

????????????let?parent?=?this._outSprite.node.parent;

????????????let?wpos?=?this.node.convertToWorldSpaceAR(cc.Vec2.ZERO);

????????????let?lpos?=?parent.convertToNodeSpaceAR(wpos);

????????????this._outSprite.node.x?=?lpos.x;

????????????this._outSprite.node.y?=?lpos.y;

????????????this._outSprite.node.width?=?this.node.width;

????????????this._outSprite.node.height?=?this.node.height;

????????????this._outSprite.node.anchorX?=?this.node.anchorX;

????????????this._outSprite.node.anchorY?=?this.node.anchorY;

????????????this.node.opacity?=?0;

????????????this._outSprite.node.opacity?=?255;

????????}

????????if?(this._enableFix)?{

????????????this.tryFixBitMap();

????????}

????????if?(this._cacheCallback)?{

????????????this._cacheCallback();

????????}

????????super.onFBOUpdateEnd();

????}

在外部,我們如下使用我們的BitMap組件。

// this.missionContent? 任務文字列表容器cc.Layeout

let bitmapCacheComp?=?this.missionContent.node.addComponent(BitmapCacheComponent); // 創(chuàng)建Bitmap組件

this.bitmapCacheComp._bitmapParent?=?this.bitmapLayer; // 指定父節(jié)點

然后在合適的時機(例如初始化或任務狀態(tài)變化時)進行render。

this.bitmapCacheComp?.drawOnce();

以上就實現(xiàn)了N行文本合成一張texture來顯示的功能,達到了合批的目的。draw也從N變成了1個。

當然,不僅是文本,可以render任意可渲染節(jié)點。

關聯(lián)文章鏈接:

CocosCreator游戲性能優(yōu)化(1):性能分析工具

CocosCreator游戲性能優(yōu)化(3):GPU優(yōu)化之降低計算分辨率

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容