Cocos Creator 性能優化:DrawCall

前言

在遊戲開發中,DrawCall 作爲一個非常重要的性能指標,直接影響遊戲的整體性能表現。

無論是 Cocos Creator、Unity、Unreal 還是其他遊戲引擎,只要說到遊戲性能優化,DrawCall 都是絕對少不了的一項。

本文將會介紹什麼是 DrawCall,爲什麼要減少 DrawCall 以及在 Cocos Creator 項目中如何減少 DrawCall 來提升遊戲性能。


正文

什麼是 DrawCall?

DrawCall 中文譯爲“繪製調用”或“繪圖指令”。

DrawCall 是一種行爲(指令),即 CPU 調用圖形 API,命令 GPU 進行圖形繪製。

DrawCall 一般可以簡稱爲“DC”,當然此“DC”非彼“DC”…


爲什麼要減少 DrawCall?

發生了什麼

當我們在討論減少 DrawCall 時我們在討論什麼?

其實我們真正需要減少的並不是 DrawCall 這個行爲本身,而是減少每個 DrawCall 前置的一些消耗性能和時間的行爲。

看不懂?其實我也不知道我在說些什麼,還是接着看下面的內容吧 : p

舉個栗子

問:嘗試在兩個硬盤之間傳輸文件,傳輸 1 個 1MB 的文件和傳輸 1024 個 1KB 的文件,同樣是傳輸了共 1MB 的文件,哪個更快?

答:傳輸 1 個 1MB 的文件要比傳輸 1024 個 1KB 的文件要快得多得多。因爲在每一個文件傳輸前,CPU 都需要做許多額外的工作來保證文件能夠正確地被傳輸,而這些額外工作造成了大量額外的性能和時間開銷,導致傳輸速度下降。

回到渲染

圖形渲染管線的大致流程如下:

上圖只是對渲染管線的部分概括,方便大家理解,實際的圖形渲染管線比較複雜,不在本文討論範圍內。

從圖中可以看到在渲染管線中,在每一次 DrawCall 前,CPU 都需要做一系列準備工作,才能讓 GPU 正確渲染出圖像。

而 CPU 的每一次內存顯存讀寫、數據處理和渲染狀態切換都會帶來一定的性能和時間消耗。


到底是誰的鍋?

一般來說 GPU 渲染圖像的速度其實是非常快的,繪製 100 個三角形和繪製 1000 個三角形所消耗的時間沒差多少。

但是 CPU 的內存顯存讀寫、數據處理和渲染狀態切換相對於 GPU 渲染來說是非常非常慢的。

實際的瓶頸在於 CPU 這邊,大量的 DrawCall 會讓 CPU 忙到焦頭爛額暈頭轉向不可開交,而 GPU 大部分時間都在摸魚,是導致遊戲性能下降的主要原因。

所以 DrawCall 這玩意越少越好~


如何減少 DrawCall?

在遊戲運行時引擎是按照節點層級順序從上往下由淺到深進行渲染的,理論上每渲染一張圖像(文本最終也是圖像)都需要一次 DrawCall。

既然如此,只要我們想辦法將盡可能多的圖像在一次 DrawCall 中渲染出來(也就是“渲染合批”),就可以儘量少去調用 CPU,從而減少 DrawCall。

簡單點,就是減少讓 CPU 工作的次數,但是每次都多給點活,不就可以省去一些“CPU 準備工具然後工作”和“工作結束叫 GPU 加工”的步驟了嘛,代價就是每次工作的時間會變長~

明白了這個原理之後,下面讓我們看看在實際遊戲開發中應該如何操作吧。


靜態合圖

靜態合圖就是在開發時將一系列碎圖整合成一張大圖

圖集對於 DrawCall 優化來說非常重要,但是並不是說我們把所有圖片統統打成圖集就萬事大吉了,這裏面也有它的門道,胡亂打圖集的話說不定還會變成負優化。

最重要的是儘量將處於同一界面(UI)下的相鄰且渲染狀態相同的碎圖打包成圖集,才能達到減少 DrawCall 的目的。

還記得遊戲渲染時是按順序渲染的嗎,所以“相鄰”很關鍵!要考,做筆記!

改變渲染狀態會打斷渲染合批,例如改變紋理狀態(預乘、循環模式和過濾模式)或改變 Material(材質)、Blend(混合模式)等等,所以使用自定義 Shader 也會打斷合批。

舉個栗子,我這裏有一個由 10 張碎圖和 1 個文本所組成的彈窗(假設都使用同樣的渲染方式):

  1. 在不做任何優化且未開啓動態合圖的情況下,渲染這個彈窗需要 11 個 DrawCall。
  2. 將所有碎圖打成一個圖集,文本節點夾在精靈節點之間的情況下需要 3 個 DrawCall,在頂部最外層或者底部最外層的情況下需要 2 個 DrawCall。
  3. 文本使用 BMFont,將所有碎圖和 BMFont 打成一個圖集的話只需要 1 個 DrawCall,如果碎圖不和 BMFont 打成一個圖集的情況則參考第 2 項。
  4. 碎圖不打包圖集,開啓動態合圖,在理想情況下,文本使用 BMFont 最少只需要 1 個 DrawCall,不使用 BMFont 的情況同樣參考第 2 項。

如果上面的例子你不太能理解的話,那請接着看下面的內容,相信你閱讀完本篇文章的全部內容後再來看這個例子將會茅塞頓開哈哈哈~

動態合圖和 BMFont 會在後面說到。

當然上面這個例子算是比較理想的情況,實際上的情況可能會比例子更爲複雜,精靈和文本可能會更多,也不一定能將所有圖像資源都打包進一個圖集。所以我們只能是儘量合理地去優化,避免出現“撿了芝麻,丟了西瓜”的情況。

不建議任何圖像資源的尺寸超過 2048 * 2048,否則在小遊戲和原生平臺可能會出現問題;

而且圖像尺寸越大,加載的時間也越長,而且是非線性的那種增長,例如加載一張圖像比加載兩張圖像所消耗的時間還長,得不償失。


下面介紹兩種打包靜態圖集的方式:

自動圖集資源(Auto Atlas)

利用 Cocos Creator 內置的自動圖集資源來將碎圖打包成圖集。

在項目構建時,編輯器會將所有自動圖集資源所在文件夾下的所有符合要求的圖像分別根據配置打包成一個或多個圖集。

自動圖集資源使用起來很靈活,編輯器在打包圖集時會自動遞歸子目錄,若子目錄下也有自動圖集資源(即 .pac 文件)則會跳過該目錄,所以我們可以對同一目錄下的不同部分的碎圖配置不同的參數。

創建自動圖集配置

資源管理器中右鍵,點擊 [ 新建 -> 自動圖集配置 ] 就會新建一個名爲 AutoAtlas.pac 的資源。

配置屬性

資源管理器中點擊自動圖集資源文件就可以在屬性檢查器面板中看到自動圖集資源可配置的屬性,點擊 Preview 按鈕即可預覽圖集。

關於自動圖集的幾點建議
  1. 合理控制圖集最大尺寸,避免單個圖像加載時間過長。
  2. 尺寸太大的圖像沒有必要打進圖集(如背景圖)。
  3. 善用九宮格(Sliced)可以節省很多空間(這一點需要美術大佬配合)。
  4. 間距保持默認的 2 並保持勾選擴邊選項,避免圖像裁剪錯誤和出現黑邊的情況。
  5. 勾選不包含未被引用資源選項,自動排除沒有用到的圖像以節省空間(該選項預覽時無效)。
  6. 開發時預覽圖集,根據結果進行調整,以達到最好的優化效果。

關於每個屬性具體的作用請參考官方文檔。

自動圖集資源官方文檔:http://docs.cocos.com/creator/manual/zh/asset-workflow/auto-atlas.html#配置自動圖集資源


TexturePacker

我們也可以使用第三方軟件 TexturePacker 來預先將圖像打包成圖集再導入項目中。

TexturePacker 是收費軟件,但是一般情況下免費功能就已經夠用了。

另外使用 TexturePacker 打包圖集時需要注意配置形狀填充(Shape Padding,對應 Auto Atlas 中的間距),避免某張圖像出現相鄰圖像的像素的情況。

TexturePacker 官網地址:https://www.codeandweb.com/texturepacker


Auto Atlas 和 TexturePacker 的對比

Auto Atlas
  • Cocos Creator 內置,方便到家了
  • 功能不多但是該有的都有,免費
  • 項目構建時才生成圖集,開發時任意修改無壓力
  • 圖集尺寸在生成時自適應,節省空間
  • 支持自動紋理壓縮
TexturePacker
  • 第三方軟件需自行安裝,不夠方便
  • 收費功能很多很專業但是用不着,免費功能也夠用
  • 先生成圖集再使用,更換圖像又要重新生成圖集
  • 尺寸固定需要自己設置
  • 自己壓縮去

總結:Auto Atlas 真香!


動態合圖(Dynamic Atlas)

這裏引用官方文檔中對於動態合圖的介紹:

Cocos Creator 提供了在項目構建時的靜態合圖方法 —— 自動合圖(Auto Atlas)。但是當項目日益壯大的時候貼圖會變得非常多,很難將貼圖打包到一張大貼圖中,這時靜態合圖就比較難以滿足降低 DrawCall 的需求。所以 Cocos Creator 在 v2.0 中加入了 動態合圖(Dynamic Atlas)的功能,它能在項目運行時動態的將貼圖合併到一張大貼圖中。當渲染一張貼圖的時候,動態合圖系統會自動檢測這張貼圖是否已經被合併到了圖集(圖片集合)中,如果沒有,並且此貼圖又符合動態合圖的條件,就會將此貼圖合併到圖集中。

動態合圖官方文檔:https://docs.cocos.com/creator/manual/zh/advanced-topics/dynamic-atlas.html

簡單來說,開啓動態合圖之後,引擎會在運行時幫我們對符合條件(即尺寸小於碎圖限制的最大尺寸)的精靈進行合圖,達到和提前打包圖集一樣的效果。

引擎的動態圖集尺寸最大是 2048 * 2048,可合併的碎圖限制的最大尺寸是 512,用戶可以通過下面的 API 進行修改:

cc.dynamicAtlasManager.maxFrameSize = 512;

啓用動態合圖會佔用額外的內存,不同平臺佔用的內存大小不一樣。小遊戲和原生平臺上默認會禁用動態合圖,但如果你的項目內存空間仍有富餘的話建議強制開啓:

cc.macro.CLEANUP_IMAGE_CACHE = false;
cc.dynamicAtlasManager.enabled = true;

另外還需要保證紋理的 Premulyiply Alpha(預乘)、Wrap Mode(循環模式) 和 Filter Mode(過濾模式) 等信息與動態圖集一致才能夠動態合批。

靜態圖集也可以參與動態合圖

在動態合圖的官方文檔中有提到:

當渲染一張貼圖的時候,動態合圖系統會自動檢測這張貼圖是否已經被合併到了圖集(圖片集合)中,如果沒有,並且此貼圖又符合動態合圖的條件,就會將此貼圖合併到圖集中。

但其實只要靜態圖集滿足動態合圖的要求(即尺寸小於碎圖限制的最大尺寸),也是可以參與動態合圖的

注意:自動圖集資源(Auto Atlas)需要在其屬性檢查器面板中開啓 Texture 欄下的 Packable 選項,該選項默認是禁用的。

額外補充

只有紋理開啓了 Packable 選項的精靈才能夠參與動態合圖,該選項默認開啓。

紋理參與動態合圖後會修改原始貼圖的 UV 座標,所以在 Shader 中的無法正確計算 UV 座標,導致 Shader 無效。

如果需要對精靈使用自定義 Shader,需要禁用其紋理的 Packable 選項。

也可以在代碼中禁用該選項:

let sprite = this.node.getComponent(cc.Sprite);
let texture = sprite.spriteFrame.getTexture();
texture.packable = false;

Packable 官方文檔:https://docs.cocos.com/creator/manual/zh/asset-workflow/sprite.html?h=packable


位圖字體(BMFont)

在場景中使用系統字體或 TTF 字體的 Label 會打斷渲染合批,特別是 Label 和 Sprite 層疊交錯的情況,每一個 Label 都會打斷合批增加一個 DrawCall,文本多的場景下輕輕鬆鬆 100+。

對於遊戲中的文本,特別是數字、字母和符號,都建議使用 BMFont 來代替 TTF 或系統字體,並且將 BMFont 與 UI 碎圖打包到同一圖集中(或開啓動態合圖),可以免除大部分文本導致的 DrawCall。

舉個栗子

例如一個場景中有 80 張精靈和 80 個文本(系統字體)相互交錯,節點層級如下圖:

運行起來之後可以看到左下角的 Profile 顯示 DrawCall 已經高達 161 個,也就是說每一個精靈和文本都增加一個 DrawCall,這種情況即使精靈打了圖集也一樣無濟於事。

不要問明明只有 80 張精靈和 80 個文本不應該是 160 個 DrawCall 嗎爲什麼是 161 個…

因爲左下角的 Profile 也要佔一個 : (

對比栗子

還是上面的場景,嘗試將 Label 的系統字體換成 BMFont 並且與精靈打包到同一個圖集之後,同樣是 80 個精靈和 80 個文本。

但是 DrawCall 只有 2 個,同時幀時間降低到了 1ms,幀率提升了 10 FPS,渲染耗時降低到了 0.6ms。

實際上場景只佔了 1 個 DrawCall,另一個 DrawCall 是左下角的 Profile 佔的…

另外,對於漢字可以嘗試使用 Label 組件的 Cache Mode 來優化。


文本緩存模式(Cache Mode)

Cocos Creator 2.0.9 版本在 Label 組件上增加了 Cache Mode 選項,來解決系統字體和 TTF 字體帶來的性能問題。

Cache Mode 官方文檔:https://docs.cocos.com/creator/manual/zh/components/label.html#文本緩存類型(cache-mode)

Cache Mode 有以下3 種選擇:

NONE(默認)

每一個 Label 都會生成爲一張單獨的位圖,且不會參與動態合圖,所以每一個 Label 都會打斷渲染合批。


BITMAP

當 Label 組件開啓 BITMAP 模式後,文本同樣會生成爲一張位圖,但是只要符合動態合圖要求就可以參與動態合圖,和周圍的精靈合併 DrawCall

一定要注意 BITMAP 模式只適用於不頻繁更改的文本,否則內存爆炸了後果自負!

舉個栗子

同樣是上文提到的精靈和文本相互交錯的例子,文本使用 BITMAP 模式,精靈不打包成圖集,開啓動態合圖

結果是所有精靈(包括背景)和文本都成功動態合圖,實際 DrawCall 降至 1 個。

如果精靈打包成了圖集則會變成 160 個,因爲圖集默認不參與動態合圖。

所以當前這種情況(少精靈多文本)不打圖集反而是比較好的選擇。


CHAR

當 Label 組件開啓 CHAR 模式後,引擎會將該 Label 中出現的所有字符緩存到一張全局共享的位圖中,相當於是生成了一個 BMFont。

適用於文本頻繁更改的情況,對性能和內存最友好。

注意:該模式只能用於字體樣式和字號固定,並且不會頻繁出現巨量未使用過的字符的 Label。因爲共享位圖的最大尺寸爲 2048*2048,佔滿了之後就沒辦法再渲染新的字符,需要切換場景纔會清除共享位圖。

開啓了 CHAR 模式的 Label 無法參與動態合圖,但是可以和相鄰的同樣是 CHAR 模式的 Label 合併 DrawCall(相當於是一張未打包進圖集的 BMFont)。

舉個栗子

還是是上文提到的精靈和文本相互交錯的例子,爲了更好體現 CHAR 模式的優勢,我更改了場景節點的結構,將精靈和文本進行分離(關於這點可以看下面的 UI層級調整)。

所有 Label 開啓 CHAR 模式,並在腳本中每過 0.2 秒就將文本更改成新的隨機數。

在這個例子中,引擎會在運行時生成一張包含數字 0 到 9 的 BMFont 存在內存中,另外由於我將所有 Label 都聚合在一起,所以所有 Label 的渲染合併成了 1 個 DrawCall,另外請特別關注左下角的幀時間、幀率和渲染耗時

光看上面的圖似乎看不出個所以然來,那我們增加一個對照組,將所有文本的 Cache Mode 選項設爲默認的 NONE 模式

此時可以發現幀時間最高達到了 2 ms,平均幀率下降了大概 6 FPS,渲染耗時更是翻了 4 倍最高達到了 1.8 ms。

總結

結論已經很明顯了,對於大量頻繁更改的文本,使用 CHAR 模式帶來的性能提升是非常明顯的。

同時 CHAR 模式的侷限也很明顯,一般用於場景中出現大量數字文本,類似於經驗值增加、血量減少之類的特效的情況。


UI 層級調整

除了以上的優化方案,我們還可以在遊戲場景中下功夫,將性能優化做到極致。

其實上文也有提到,我們可以通過優化節點層級,分離圖像節點和文本節點,文本使用 BMFont 或 Cache Mode 選項,儘量出現避免文本打斷渲染合批的情況

特別是對於戰鬥場景中大量的文本提示(傷害值、血量值和法力值等等)或合成遊戲中大量的經驗值文本,因爲這些文本基本都是數字,使用這種方式即使再多文本也只需要 1 個 DrawCall 就可以全部渲染出來。

舉個栗子

下面的場景中,文本開啓 CHAR 模式,使用腳本每秒生成 50 個左右的隨機數字,文本節點統一放在 labelLayer 節點下,讓所有文本可以共享 1 個 DrawCall,另外背景和椰子頭佔 1 個,左下角 Profile 佔 1 個。

可以看到即使場景中瞬間出現這麼多文本,整體性能也還是比較可觀的。

在這個例子中,引擎在運行時爲我們生成了一份包含數字 0 到 9 的全局共享位圖(BMFont)。

當然如果可以在 Label 中直接使用 BMFont 的話那就更好了。


補充

再次提醒

  1. 改變渲染狀態會打斷渲染合批,例如改變紋理狀態(預乘、循環模式和過濾模式)或改變 Material(材質)、Blend(混合模式)等等,所以使用自定義 Shader 也會打斷合批。

  2. 圖集默認不參與動態合圖,手動開啓自動圖集資源的 Packable 選項後如果最終圖集符合動態合圖要求也可以參與動態合圖。

  3. 紋理開啓 Packable 選項參與動態合圖後無法使用自定義 Shader,因爲動態合圖會修改原始貼圖的 UV 座標。

  4. 使用 Cache Mode 的 BITMAP 模式需要注意內存情況,CHAR 模式需要注意文本內容是否多且不重複。

最後還需要注意

在 Cocos Creator 2.0.7 之前的版本中,改變節點的顏色或透明度、Sprite 組件使用九宮格(Sliced)都會打斷渲染合批。

蒜我球球你了快更新吧 : (


相關資料

Cocos Creator 用戶手冊
https://docs.cocos.com/creator/manual/zh/


傳送門

微信推文版本

個人博客:菜鳥小棧

開源主頁:陳皮皮

Eazax-CCC 遊戲開發腳手架


更多分享

爲什麼選擇使用 TypeScript ?

高斯模糊 Shader

一文看懂 YAML


公衆號

菜鳥小棧

我是陳皮皮,這是我的個人公衆號,專注但不僅限於遊戲開發、前端和後端技術記錄與分享。

每一篇原創都非常用心,你的關注就是我原創的動力!

Input and output.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章