本文從降低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合圖。操作如下。
????????????在需要合圖的文件夾下新建一個自動圖集配置即可。打包時會將該文件夾下的所有圖片合成一張或多張大圖。

? ? ? ? ? ? ? 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)文章鏈接: