移动游戏性能优化建议与字体剥离精简工具
/
在 Unity 中制作游戲時對動態字體的剝離和精簡是現在常用的手段,現在有兩篇博客是大家閱讀和參照較多的,分別是?如何精簡Unity中使用的字體文件?和?FontPruner 字體精簡工具。他們各自提供了一個用于精簡字體的工具,?FontSubsetGUI?和?FontPruner。前者是網絡上一個作者提供的免費軟件,現在不是很好找,后者是西山居開源的內部工具,基于?google 的 sfntly?制作。
我分別使用兩個工具裁剪同一個字體,使用同一套文本,發現結果還是有區別。字體:仿宋;大小10Mb;裁剪文本:“abcdefg0123我要喝咖啡”;裁剪后查看工具:FontCreator(試用了眾多工具后發現是最好用和專業的工具)。
使用 FontSubsetGUI 裁剪后的字體大小:182Kb,FontCreator 打開后如圖:
圖一
圖二
注意紅色線框部分,Glyphs 總共定義了 28562 個,同原始字體一樣,但是 Empty 了 28542 個,就是如上 圖一 中所有空白方快,他們都是缺失字體外形定義,但有字體符號表,也就是說有這個字但沒有定義和映射字體外形,處于不可用狀態。下面 Characters 是當前擁有的字體定義和外形,一共19個,奇怪的是多出了一個“雙引號”和一個“M”,而這一部分空的 Glphys 定義其實也是需要占空間的。
下面看 FontPruner 的裁剪結果,環境和數據都相同,裁剪后文件大小6Kb,結果如下:
圖三
圖四
如 圖三 所示,我的裁剪文本是16個,字體文件內可用的 Characters 也是16個,顯示的 Glyphs 定義是17個,其中包含一個 .notdef 系統預留的定義(圖四),值為0;也就是其余所有沒用到的 Glyphs 都被裁剪掉了,變得更加精簡,所以文件就更小。總體結果如下:
圖五
以上兩種導入到項目中使用都沒有問題,但是 FontPruner 更加精簡準確,所以我最終選擇后者集成到項目中,做一個批處理能夠自動根據設定的文本來處理所有使用的字體,也非常方便。
但是以上兩個工具都不能直接處理 otf 字體,均會報錯。在處理前建議使用 FontCreator 將 otf 轉換為 ttf 然后在處理,選擇 “File->Export Font As...->Export as TrueType/OpenType Font...” 彈出的對話框中第一個選項 “Outline Format” 一定要選擇 “TrueType”,否則即使最終在保存對話框選擇 ttf 格式后導出了也無法使用。
經過此步驟,就可以處理 otf 字體了(比如安卓默認的那一套思源字體等)。
另外,原本 FontPruner 在處理臨時路徑時有一點點小 bug,我修復提交了 pr 且已經合并了;不過每次運行裁剪命令 Temp 目錄都不會自動創建需要先手動創建好,這樣使用有點不方便,我修改成自動創建,但是沒有 pr,需要這個功能的請在這里下載:https://github.com/yaukeywang/FontPruner/tree/extend。
發現很多朋友在找 FontSubsetGUI 的下載地址,現在不是太好找了,這里給出下載地址:FontSubsetPack1.0.4.125最新版-Android工具類資源-CSDN下載。
///
分清主次
優化性能首先要找出性能瓶頸,對性能影響最大的地方先優化,接著對次影響的進行優化,以此類推。
如果能遵守這條規則,優化效果和花費時間的曲線關系大致如下圖:
GPU廠商工具
幾乎每個GPU大廠都提供了調試自家產品的GPU分析工具。它們與引擎和IDE的工具最大的不同點是:可以查看每次Draw Call的渲染狀態/引用的資源/繪制的畫面,以及其它獨特參數。
1. Tegra Graphics Debugger(NV)
工具本身可運行在各個主流系統,也可以分析OpenGL和Vulkan,但只支持NV旗下的Tegra K1和X1系列GPU。
2. Mali Graphics Debugger(Arm)
類似于Tegra Graphics Debugger, 但全面支持OpenGL的各種版本,還支持Vulkan和OpenCL的調試。
3. PVRTrace(Imagination Technology)
只可調試使用Imagination Technology公司GPU的App。
4. Adreno Profiler(高通)
只可調試使用高通旗下GPU的App。
5. PerfHud(NV)
只支持PC程序,非常強大的GPU調試工具,但無法調試移動設備,需要依賴模擬器。
6. PerfHud ES(NV)
PerfHud移動版,支持移動設備調試, 需要下載整個CodeWorks for Android開發包。
1.3.4 其它第三方工具
1. PIX(MS)
只運行在windows平臺,只支持DirectX的分析。
紋理優化
紋理優化的目的是讓它們占用的內存盡量的小,那么紋理加載進內存后,大小計算公式如下:
紋理內存大小(字節) = 紋理寬度 x 紋理高度 x 像素字節
像素字節 = 像素通道數(R/G/B/A) x 通道大小(1字節/半字節)
從上面公式可以看到,紋理加載進內存后的大小跟尺寸/像素通道數/通道大小都有關系,我們就從它們著手優化,還可以通過提高復用率和合成圖集達到優化的目的。
紋理尺寸
根據項目實際情況,我將所有角色貼圖都縮小至256x256,角色貼圖占用縮小至1/16。
除了角色貼圖,武器/裝備/特效/場景等等所有涉及的貼圖都縮小至合適的大小。這里的合適大小是指渲染對象在畫面中大多數情況下不可能達到的最大尺寸,這個尺寸最好保持2的N次方。
2.1.2 紋理通道
通道優化的目的是降低像素所占的大小,可以通過以下方法達到目的:
1. 去除Alpha通道。可以減少通道數量,適用于不需要Alpha混合或Alpha Test的角色和物件。
2. 應用單通道圖。也可以減少通道數量,比如灰度圖,地形高度圖,掩碼圖,Shader掩碼圖等等。
3. 使用16位代替32位圖。例如RGB444/RGBA4444就可以減少像素通道大小。
4. 壓縮貼圖適應不同的平臺。例如:
Windows可以壓縮成DDS,其中DDS細分DX1~DX5共5種格式,每種應用場景略有不同,但它們只能用于DirectX。
Android可以壓縮成ETC1(不帶Alpha)或ETC2(可帶Alpha)。
iOS可以壓縮成PVRTC格式。
它們都是GPU直接支持的紋理格式,可以顯著減少內存/顯存/帶寬的占用。
5. 避免使用JPG/高壓縮率的PNG/GIF等低質量格式。因為當前主流商業引擎在游戲發布過程中,會自動壓縮所有紋理,而保留原畫質的紋理可以減少紋理壓縮后的畫質損失。
2.1.3 提高紋理復用率
以下方法提高貼圖復用率:
1. 建立共享圖庫。將通用的元素放至共享庫,例如按鈕/進度條/背景/UI通用元素等。
2. 用九宮格圖代替大塊背景圖。九宮格在游戲開發中是比較常見的UI組件。
3. 紋理元素通過變換可組合成復合紋理。例下圖,上下左右對稱的背景圖可以用4張相同貼圖實例通過旋轉/翻轉后獲得。
4. 九宮格+UI元素可以組合成很復雜但消耗相對較小的UI界面
紋理圖集
圖集可以降低IO加載次數,也可以減少Draw Calls(詳見4.2 Batch合批)。但也有副作用,一是可能超出設備支持的最大尺寸,二是可能出現大片空白像素
對于副作用一,可以限制圖集的最大尺寸(通常不要超過2048x2048),分拆成多張圖集。
對于副作用二,可以針對性地調整紋理元素的布局或尺寸,使得合成的圖集盡可能占滿有效像素。
適合生成圖集的資源有:UI界面,道具圖標,角色頭像,技能圖標,序列幀,特效等等。
如果是Unity引擎,可以用SpritePacker很方便地生成和預覽圖集。
如果是自研引擎,可以用TexturePacker的命令行工具合成。
UI圖集
1. 如果界面A確實要用到界面B圖集的某個元素,怎么辦?
參考解決方法:要看被引用元素的通用度,如果只是界面A和B在用,可以將被引用元素拷貝到界面A圖集下;如果其它界面也會引用到,就可以將它移到共享圖庫。
2. 有些UI紋理很大且很多界面都有用到,如果放在共享圖庫會導致共享圖庫急劇膨脹,怎么辦?
參考解決方法:大尺寸紋理建議用九宮格+細節圖,或通過組合的方式來代替。
3. 如何保證美術制作的UI只引用到自身圖集和共享圖集?
參考解決方法:實現批處理檢查工具,找出每個UI界面引用到的圖集列表,引用的圖集超過2個便是不合格。
UI層次
由于UI元素很多,主流商業引擎都會對它們合批以減少Draw Calls,但合批優化是有條件的:
1. 使用相同的材質。使用引擎默認UI材質+UI圖集可以滿足這個條件。
2. 繪制順序是連續的。UI的繪制順序通常就是在場景中的節點順序。
在實際制作UI過程中,經常會破壞UI合批優化的條件:
1. 同個界面往往會引用自身圖集和共享圖集,破壞優化條件1。
2. 界面通常都帶有文本,而文本通常是引擎自動生成的另外一張或若干張圖集,破壞優化條件1。
3. UI節點層次混亂,不同圖集的元素相互交叉,破壞優化條件2。
4. 部分UI元素使用了自定義材質或Shader,破壞優化條件1。
UI的其它優化
1. 禁用MipMaps。MipMaps的原理是根據繪制對象在繪制空間的大小選取合適的紋理層級,它會增加30%的內存/顯存開銷。而UI通常都是等長等寬的,跟攝像機距離無關,所以要禁用UI的MipMaps。
2. 保持UI紋理的原始尺寸。縮放通常會帶來額外的開銷,而且會使UI變模糊,降低畫質。
3. 避免使用大尺寸的背景圖。大背景圖耗內存,通常還不能共用。可用九宮格+細節圖組合而成。
字體
. 控制字體文件數量。除了系統默認字體,自定義字體控制在1~2個為宜。
2. 少用字體的陰影/描邊/發光等效果。
3. 剔除字庫中無用的字形。可以借助FontSubsetGUI或FontPruner給字庫瘦身。
模型
模型特別是帶有骨骼動畫的模型在性能消耗中占據非常大的比重,它們會顯著增加CPU/GPU/內存/顯存的負擔。所以,模型的優化尤為重要。
模型涉及的數據比較多,包含了頂點/索引/材質等,而頂點又可能包含pos/color/uv/normal/tagent/skin等數據,我們可以從這些數據著手優化。
模型數據
模型的頂點數據通常包含pos/uv,但color/normal/skin等數據視不同類型的物件區分對待。比如,對于靜態物體,可以去除skin;如果無需頂點變色,則可以去除color。
模型內pos.xyz/uv.xy/color.rgba等等數據默認用32位浮點數存儲,它們的數據表示范圍遠遠超出大多數游戲的應用場景,可以將它們壓縮至16位浮點數。
模型的索引數據存儲了三角形引用的頂點序號,默認用32位unsigned int存儲,但絕大多數模型不可能超出16位unsigned short的范圍,故用16位整型足矣
MeshBaker:模型合并插件,可以對多個模型合成一個模型,從而減少模型個數,降低Draw Calls。多用于靜態物體合并,比如場景和地面靜態物體。
SimpleLOD:模型減面庫,可以離線或運行時給模型進行減面優化,也可以方便地做成批處理工具。
?場景
地形若是不復雜(比如王者榮耀/LOL的戰斗場景),盡量不用Terrain,用簡易模型代替,地表細節可用一張紋理表示,地表紋理取合適大小,通常不超過1024x1024。
地形網格和地面靜態物體去掉陰影,如果某些物件確實需要陰影,可以讓美術在制作地表紋理時加上陰影。
地形有很多不可見的地方,可以刪減那里的模型網格。
地面通常有很多裝飾物和特效,要關注它們的面數等規格是否超出了限制。
一個場景只用一個平行光,實時像素光不要超過一個。用Lighting Map代替實時光,Lighting Map紋理尺寸不宜太大。
對場景的面數和物體數量做限制。使用合批工具離線將地表相似的靜態物體合并,減少場景復雜度。
對場景使用畫質分級策略,比如低畫質下,用最低模的場景,隱藏場景特效等。
地表如有導航網格,導航網格可以采用更精簡的模型,復雜的邊緣可以簡化成簡單的幾何多邊形。
粒子
1. 每幀計算量大。涉及發射器/效果器/曲線插值等,耗費CPU性能。
2. 頻繁操作內存。粒子在生命周期里,實例持續不斷地創建/刪除,即便有緩存機制下,依然避免不了內存的頻繁讀取和碎片化。
3. 每幀更新數據到GPU。從Lock頂點Buffer到寫入數據到GPU,會引發線程等待,也加重CPU到GPU帶寬的負擔。
4. 增加大量Draw Calls。粒子特效通常五花八樣,使用很多材質,導致引擎無法合批優化,大量增加繪制次數。
5. 導致Overdraw(過繪制)。粒子一般會開啟Alpha Blend,在同屏粒子多的情況下,會造成嚴重的Overdraw。
首先得從資源上著手優化和規范。具體方式有:
1. 優化粒子屬性。關閉陰影,關閉光照;若可以去掉紋理的Alpha通道,并關閉Alpha Blend和Alpha Test;
2. 禁用粒子的高級特效。如模型粒子/模型發射器/粒子碰撞體等。
3. 用最少的粒子效果器。關閉不必要的粒子效果器,采用簡單的方式代替。
4. 控制粒子的材質數量。一個特效通常包含了若干個粒子系統,它們盡量使用內置材質,使用相同的材質實例。
5. 控制粒子的尺寸和貼圖大小。可以粒子的尺寸,可以減緩過繪制,控制貼圖大小,可以減少帶寬和提高渲染性能。
6. 制定粒子特效美術規范。下面是游戲Z的粒子規范。
粒子美術規范
? 單個粒子的發射數量不超過50個。
? 減少粒子的尺寸,面積越大就會消耗更多的性能。
? 粒子貼圖必須是2的N次方,盡量控制64x64以內,極少量128x128或256x256,最大不超過256x256。
? 盡可能去掉粒子貼圖的Alpha通道。
? 盡量不用Alpha Test。
? 盡量使用已有的材質,提高合并渲染的優化概率。
? 材質優先用Mobile目錄下的材質。
? 盡可能不用模型做粒子,如果使用,要控制模型面數在100以內,最大粒子數在5以內。
? 單個特效渲染數據限制:
? 小型特效(如受擊特效、Buff特效)的面數和頂點數在80以內,貼圖在64*64以內,材質數2個以內。
? 中型特效(如技能特效)的面數和頂點數在150以內,貼圖在128*128以內,材質數4個以內。
? 大型特效(如全局特效、大火球)的面數和頂點數在300以內,貼圖在256*256以內,材質數6個以內。
材質
材質若控制不好,會破壞引擎的合批優化,提高渲染消耗。所以在項目前期,就有必要對材質做管理和規范。
1. 充分使用引擎內置材質。引擎內置材質能滿足基本材質需求,而且通常做了優化,所以首選內置材質。如果是移動游戲,一般引擎也有移動版的材質庫。
2. 建立共享的自定義材質庫。如果引擎內置材質不滿足項目需求,就需要通過添加自定義材質來解決。對于自定義材質,要妥善管理,對它們進行分類,按規律命名。這樣對材質共享和管理都大有裨益。
3. 定期檢查新加入的材質是否與已有的重復。如果有相似的材質,則刪除重復的。這個工作可以由主程或主美執行,也可以通過工具協助檢查
CPU優化
緩存計算結果
適用場景舉例:
1. 復雜數學計算。Sin/Cos/Pow/Sqrt等運算要花費一定計算量,如果是第一次計算,可以將結果緩存起來,下次遇到相同的計算,直接從緩存中取值。
2.?物理模擬結果。物體的物理模擬過程耗費大量計算,但有些物體模擬完之后就處于靜止狀態,可以將它之前的模擬結果存下來,防止每幀更新計算。現代主流商業引擎都支持這種優化。
3. 光照貼圖。光照貼圖是離線將場景的靜態光影計算并緩存成貼圖,渲染時只需要采樣光照貼圖的顏色,極大降低了光照計算復雜度。
4.?搜索結果。例如場景節點搜索,場景節點一般采用樹形結構,如果查找的節點很深,將顯著增加遍歷次數,此時很有必要將查找結果緩存起來。
5. 邏輯模塊復雜的計算。游戲的邏輯模塊,涉及到復雜的計算都可嘗試用緩存法降低CPU負擔。
預處理
緩存法利用空間換時間的思想,會增加內存開銷;而預處理是將時間轉移的思想,它并不會增加內存消耗。
將需要花費大量時間加載或運算的邏輯,在啟動程序后/加載場景時/切換界面前/進入戰斗前等時機預先計算或加載,避免渲染時因CPU負載過高出現幀率波動或卡頓現象。
限幀法
限幀法簡單粗暴,但效果顯著,是常見的一種優化手段。
限制頻率的對象可以是World.Update,物理模擬,粒子計算,角色AI處理,角色狀態更新等等。
限幀可以通過以下方法實現:
1. 計數法。用一個變量記錄更新次數,每累計到某個數才執行更新。
2. 計時器。利用Timer機制觸發,每隔固定時間觸發一次更新。
3. 協程。協程是運行于主線程的偽線程,但可以模擬異步操作,沒有多線程的副作用。故而也可以用于限幀操作。
4. 事件觸發。每幀查詢狀態改成事件觸發,也是游戲常用的一種優化手段,用來限幀也非常有效。
主次法
主次法跟LOD技法有異曲同工之妙。思路也是將物件按重要程度劃分為高中低級別,然后不同級別采用不同復雜度的效果或計算。
這種思路在游戲中可以廣泛應用,基本所有消耗高的邏輯或模塊都可以采用這個技法。例如:
1. 畫質等級。將游戲分為若干等級,畫質最高到最低采用不同的渲染技術或資源,區別對待。
2. 資源等級。場景/特效/燈光/物理效果/導航等等模塊都可以根據畫質等級或物件等級對應不同級別的資源,整體上可以減少消耗,又能兼顧畫質效果。
3. 角色分級。主角英雄/Boss等和小怪物/NPC區分開來,前者更新頻率更高,AI行為更復雜更智能,而后者采用簡單效果或降頻更新。
多線程
可獨立成線程處理的模塊:
1.?文件IO。建立一個IO線程,定時檢查文件隊列是否有數據,若有便啟動IO加載,直至文件隊列為空,又回到空閑輪詢狀態。主線程就不會以為文件IO而處于等待狀態。
2. 骨骼動畫。如果同屏動畫角色多,將占據大量CPU計算。可創建一個或多個線程處理骨骼動畫計算,加速渲染流程。但隨之而來的是同步問題。
3. 粒子計算。粒子的多線程跟骨骼動畫類似,也存在同步問題。
4.?渲染線程。將渲染抽離出一個線程,主要是解決CPU與GPU相互等待的問題。常見的一種做法是建立一個渲染線程,定期去查詢渲染隊列是否有數據,如果有就提交至GPU進行繪制。
5. 音視頻編解碼。如果游戲有涉及音頻頻播放,而它們又占據了較大的消耗,那么開辟獨立的線程處理音視頻編解碼是有必要的。
6. 加密解密。加密解密涉及的算法通常較復雜,占用較多CPU性能,而且常常伴隨著文件IO或網絡IO,所以此時非常有必要將它們交給獨立的線程處理。
7. 網絡IO。目前大多數游戲都會開辟一個線程專門收發網絡數據,以避免網絡處理影響主線程。
值得注意的是,線程切換會帶來額外開銷,同步和死鎖問題也會提高邏輯復雜度和調試難度
引擎模塊
動畫
降低動畫采用頻率。
減少關鍵幀數據。
緩存動畫的插值結果。
用簡單曲線插值(線性插值)代替復雜插值(貝塞爾曲線)。
判斷動畫所附對象的可見性,如不可見,則不更新動畫。
控制同屏動畫的個數。
不同畫質加載不同級別的動畫資源。
物理
靜態物理只用靜態碰撞體。
降低物理模擬頻率。
禁用復雜的碰撞體(如模型碰撞體),取而代之的是用若干個簡單幾何碰撞體組合。
若物體不可見,則關閉物理模擬。
控制同屏物理物體個數。
對物體的重要程度做分級,重要性低的角色采用簡單物理效果或者刪除物理效果。
Raycast射線檢查雖好用,但性能消耗也高,要盡量降低視頻頻率,防止重復調用。
粒子
部分粒子特效考慮轉成序列幀渲染。
考慮是否可用緩存/預加載/預計算的優化方式。
對特效的重要程度分級,不重要的粒子不計算或者降頻。
若GPU的負擔比CPU小,可以將粒子更新計算移至GPU端。
粒子物體可見性判斷,不可見則不計算和渲染。
航
簡化導航網格,用最少的面數表達復雜地面的導航構造,比如用平面代替地面凹凸不平的路面。
尋路計算復雜,邏輯上須控制調用頻率,避免扎堆尋路。
邏輯優化
邏輯的消耗也是CPU負擔高的罪魁禍首,主要體現在AI/算法/腳本等等模塊。
AI優化
為了簡化玩家操作和讓怪物更加擬人化,目前大多數游戲都加入了AI功能,而AI行為樹是實現AI的關鍵。
優化AI行為樹:
1. 緩存當前Action節點路徑。即將當前正在更新的節點和其父親節點都緩存起來,下次要更新時優先這個路徑搜尋,避免遍歷整顆樹。如果當前Action節點是持續性的,則這段時間內無需搜索節點,直接更新該節點即可。
2. 降低AI的更新頻率。即強制降低AI頻率達到減負的目的,另外還可以嘗試主次法/攤幀法/動態調幀法優化AI的更新。
算法優化
思路是找出最耗CPU的算法或邏輯,優化之。
1. 空間換時間。利用預排序/預處理/緩存/動態規劃等等思路換取CPU的性能。
2. 選取更快的算法。屬于數據結構和算法的范疇,思路是將O(n2)降低成O(n)或O(logn),具體可以參看《算法導論》《游戲編程算法與技巧》《游戲核心算法編程內幕》等書籍。
腳本優化
通常引擎底層是用C++等Native語言實現,而腳本用動態語言(Java/C#/Lua/Python)實現,它們中間隔著一層厚重的模擬器或封裝層。Unity引擎與C#之間的關系如下圖:
Unity在打包游戲時會通過Mono將C#代碼生成IL中間語言,如果是iOS平臺,還會通過IL2CPP生成C++代碼。簡單點說,Unity引擎核心和C#等腳本語言的交互要通過Mono厚重的中間層。由此產生了額外的開銷,導致腳本語言運行效率低下。
可以通過以下一些方法降低腳本的開銷:
1. 刪除腳本內的空回調。即便腳本對象的回調函數為空,但也會產生引擎核心與腳本層的開銷。
2. 腳本對象如果引用其他對象,可以在初始化時緩存。
3. 幀更新/循環語句內避免產生堆的臨時對象。可以將臨時對象移至循環語句外,或聲明成類的成員,在初始化時賦值。
4. 利用可見性回調。在可見性回調內做禁止/恢復比較耗時的操作。
5. 字符串接很容易引起臨時對象,需警惕。可采用更高效的拼接方式,如C#的StringBuilder。
條件測試
條件測試主要用于耗時的調用優化,將每幀必然更新的操作,加入各種條件檢查,以減少耗時操作的概率。
避免重復
游戲一般涉及的模塊眾多,角色狀態機復雜,觸發事件多且雜,往往會在同一幀內多次調用同一個耗時API,引發額外的開銷。
可以通過條件測試,時間間隔,Log輸出,調用棧調試等方法解決這個問題。
?渲染優化
渲染優化的目的是減少Draw Calls,減少渲染狀態切換開銷,降低顯存占用,降低帶寬和GPU負擔。
在講解渲染優化之前,先了解渲染性能消耗點。
1. Draw Call數量。
Draw Call有些引擎也稱為SetPass Call。一個Draw Call就是游戲調用OpenGL/D3D等圖形渲染的繪制API一次(如OpenGL的glDrawArray和glDrawElements)。
一次Draw Call完整地跑完了整個渲染管線(下圖),期間要涉及的數據/狀態/計算很多,繪制前會先創建各種GPU數據,還可能每幀更新這些數據,數據更新又涉及到帶寬。
所以,每幀Draw Call數量是衡量渲染性能的關鍵指標。
2. 渲染狀態切換。
每次Draw Call前會對圖形渲染層設置一系列的渲染狀態,如是否開啟深度測試/是否開啟Alpha Test/是否開啟Alpha Blend等等。這些狀態通過圖形渲染的驅動層最終應用到GPU中(下圖)。
從上圖可以看到,應用程序(游戲)發送的渲染指令,會經過OpenGL/DirectX等圖形層和顯卡驅動層,最終才能應用到GPU硬件。
由于當代顯卡驅動做了很多工作:狀態管理/容錯處理/邏輯計算/顯存管理等等,屬于重度封裝,會消耗較多性能。
所以,盡可能減少狀態切換,是優化渲染性能的重要措施。
3. 帶寬負載。
此處的帶寬是指CPU經過主板總線傳輸數據到GPU的能力,單位通常是GB/s。當然GPU也可通過總線傳輸數據到CPU,但傳輸能力遠遠低于CPU到GPU。
上圖所示,CPU和GPU通過PCI-e總線相連,它們之間的傳輸能力是有上限的,這個上限就是帶寬。如果繪制需要傳輸的數據大于帶寬(即帶寬負載過高),就會出現畫面卡頓/跳幀/撕裂/延遲/黑屏等等各種異常。
4. 顯存占用。
顯存即顯卡的內存,是集成在GPU內部的專用內存。通常用于存儲頂點/索引/紋理/各種Buffer等數據。
如果游戲顯存占用過高,便會出現顯存分配失敗,導致畫面異常甚至程序崩潰。
5. GPU計算量。
現代顯卡基本都支持可編程渲染管線,涉及Vertex Shader/Geometry Shader/Fragment Shader(下圖),還涉及光柵化/片元操作。所以,如果Shader過于復雜或者片元過多,會極大提高GPU計算量,降低渲染性能。
合批(Batch)
合批(Batch)是將若干個模型合成一個模型,從而可以只調用一次Draw Call的優化手段。合批解決的是Draw Call數量問題。
合批的條件是所有被合的所有模型都引用同一個材質,否正無法正常合批。
離線合批(Offline Batch)
離線合批就是在游戲運行前,先用工具把相關資源做合批處理,以減輕引擎實時合批的負擔。
適合離線合批的是靜態模型和場景物件。如場景地表裝飾面:石頭/磚塊等等。
離線合批方式有:
1. 美術利用專業建模工具合批。如3D Max/Maya等。
2. 利用引擎插件或工具。如Unity的插件MeshBaker和DrawCallMinimizer,可以將靜態物體進行合批。
3. 自制離線合批工具。如果第三方插件無法滿足項目需求,就要程序專門實現離線合批工具。
實時合批(Runtime Batch)
不同于離線合批,實時合批是游戲引擎在游戲運行期完成的。Unity引擎分為靜態合批和動態合批。
1. 靜態合批(Static Batch)
符合靜態合批的條件有兩個:一是模型有Static標記(即物體是靜態的,不能有移動/動畫/物理等),二是引用同一個材質實例。
為了提高靜態合批的概率,盡可能將場景物件設為靜態,并且類似的物件引用相同的材質。
2. 動態合批(Dynamic Batch)
動態合批是針對可以運動的模型,但有更苛刻的要求,例如Unity要求:
- 模型少于300個頂點,少于900個頂點屬性。
- 不能有鏡像Transform。
- 使用同一材質實例(注意:是實例,相同的材質不同的實例,也是不行的)。
- 使用相同的光照圖(Lightmaps)。
- 不能用多Pass Shader。
合批副作用
合批優化雖能降低Draw Calls,但也有副作用:
1. 增加CPU消耗。需要消耗CPU計算將多個模型合成一個,還涉及材質排序和搜集等操作。
2. 增加內存。需要額外開辟內存存儲合成的模型。
3. 合成的模型頂點數有限制。移動游戲通常用16位索引,若合成的模型超出16位無符號整數的范圍,渲染會出現異常。
?渲染狀態優化
狀態緩存
在引擎側,可以使用狀態緩存減少渲染管線的切換。
渲染狀態建議
1. 少用Alpha Blend。開啟Alpha Blend了一般會關閉深度測試,無法利用深度測試剔除多余片元,導致片元數量增加,造成過繪制。所以要盡量少用。
2. 禁用Alpha Test。現代部分移動端GPU采用了特殊的渲染優化方式,如PowerVR采用Tile Based Deferred Rendering方式(下圖右),而Alpha Test會破壞Early-Z優化技術,可用Alpha Blend代替。更多看這里。
3.?開啟背面裁剪。背面裁剪可以將背向攝像機的面片剔除,減少頂點和片元的數據量。
4. 開啟MipMaps。開啟后,渲染時會自動根據畫面尺寸選擇合適大小的紋理,從而降低帶寬,也可以降低鋸齒,提高畫質效果。但UI界面不能開啟,原因見2.2.3。
5. 關閉霧。只在固定管線適用。
6. 少用抗鋸齒。圖形API內置的抗鋸齒通常會增加紋理采樣次數數倍之多(下圖),所以要慎用。
控制繪制順序
控制模型繪制順序的目的是充分利用深度測試,減少片元后續操作。特別是Early-Z技術的引入,此法效果更明顯。
繪制順序是:先繪制已做好排序的不透明物體,再繪制Alpha Tested物體,最后渲染透明物體。
多線程渲染
在單線程渲染架構中,CPU性能消耗過高會影響GPU的渲染幀率,反之,GPU渲染過慢也會讓CPU一直處于等待狀態。
多線程渲染就是為了解決CPU和GPU相互等待的問題。
以Metal/Vulkan等架構出現為界限,將它們分成兩個階段。
軟件級多線程渲染
早期的圖形API和硬件架構都不支持多線程渲染,此階段多線程渲染能做的優化比較受限,只能將渲染提交獨立成一個線程,使之不會卡邏輯線程。
開源圖形渲染引擎OGRE的多線程渲染實現方式有兩種:
1.?Middle-level Multithread。
每個渲染物體都有兩份實例,主線程改變其中一份數據,在下一幀給渲染線程使用。(下圖)
這種方式實現很復雜,要維護物體的兩份實例,也不容易在多核CPU做擴展,不能充分發揮多核CPU的優勢。
2. Low-level Multithread。
這種實現方式是將渲染物體的頂點等數據拷貝一份,邏輯線程修改其中一份數據,下一幀給渲染線程使用。
除了以上兩種方案外,可以給邏輯線程的若干邏輯(如Update/粒子/動畫)開辟多個線程(下圖),并行計算,縮短整體處理時間。
硬件級多線程渲染
近幾年,Metal/Vulkan圖形架構橫空出世,基于硬件級別的多線程渲染的時代終于到來。它們的特點:
1. 輕量化的驅動層。
OpenGL的API和驅動做了很多邏輯封裝,用狀態機的方式實現渲染(下圖左)。而Metal/Vulkan與之不同的是,在驅動層只做少量的工作,為應用程序提供直接訪問GPU硬件的接口,屬于輕量級封裝(下圖右)。
從API架構上看,Metal/Vulkan的性能已勝出一大籌。
2. 支持硬件級的多線程渲染。
Metal/Vulkan支持并行渲染指令,方便CPU各個線程各自提交渲染指令和數據。
下圖展示的是其中一種渲染方式,由多個線程創建不同的繪制命令,再由單獨的線程管理渲染命令隊列,統一提交給GPU繪制。
由于圖形API已經支持多線程渲染指令提交,再結合上一節講到的若干方案,將如虎添翼,渲染性能也會發生質的提升。
目前主流商業引擎已經支持Metal/Vulkan,Unity2018.3已經支持Metal/Vulkan:
Unity在Rendering設置面板可以開啟多線程渲染:
光照模型(Lighting/Illumination Model)
4.5.1 Flat Shading(平面著色)
根據表面法向量計算光照,并應用到整個面片上。速度最快,效果最差,容易暴露物體的多邊形本質(下圖)。
4.5.2?Gouraud Shading(高洛德著色)
根據頂點法向量計算光照,再用插值計算出整個面的光照。效果比Flat shading稍好,但高光部分有瑕疵,過渡不夠自然(下圖)。
可結合Phong Shading做優化,高光弱時用Gouraud Shading,高光強時用Phong Shading,可平衡效果和效率。
4.5.3 Lambert Shading(蘭伯特著色)
物體表面向各個方向等強度的反射光,這種等同地向各個方向散射的現象稱為光的漫反射。
Lambert定律:反射光線的強度與表面法線和光源方向之間的夾角成正比(下圖)。它是一種理想的漫反射模型,但著色效果比高洛德要平滑。
計算公式:
4.5.4 Half Lambert Shading(半蘭伯特著色)
Lambert著色有個缺陷,就是背面受光少,經常處理死黑狀態,與受光面反差太大(下圖左)。
于是其中的一種改進方案誕生了,它就是Half Lambert Shading,它渲染的畫面明暗關系沒那么強烈,過渡更加自然(下圖右)。
計算公式:
其中a和b是常數,通常都取0.5,而且a+b = 1.0。
4.5.5 Phong Shading(馮氏著色)
Phong著色將光照分成自發光(Emissive)/環境光(Ambient)/漫反射(Diffuse)/高光(Specular)四個部分,每個部分獨自計算光照貢獻量。是當前廣泛應用的一種光照模型。
其中高光計算公式:
Phong著色效果如下:
4.5.6 Blinn-Phong Shading
由于Phong模型要用到反射矢量r,而r計算比較耗時(下圖),故有了Blinn Phong。
Blinn Phong是Phong的一個改進,做法是摒棄反射矢量r,引入l和v的中間矢量h,然后利用n和h的夾角進行計算。
高光計算公式:
它渲染出的高光范圍更大(下圖),真實感不如Phong著色,但勝在效率更高。
?4.5.7 光照模型的選擇
從性能上做比較:Flat >?Gouraud >?Lambert > Half?Lambert >?Blinn-Phong >?Phong。
但畫質效果剛好相反,所以每個游戲需根據具體需求做選擇。也可以采用分級策略,高中低畫質分別采用不同的光照模型。
渲染路徑(Rendering Path)
4.6.1 經典頂點光(Legacy Vertex Lit)
嚴格來說,它也是前向渲染的一種,但有些引擎(如Unity)將它單獨抽離出來。由于光照計算在頂點,所以效果和消耗跟4.5.2?Gouraud Shading類似,是早期GPU使用較多的一種渲染方式。
4.6.2 前向渲染(Forward Rendering)
前向渲染是傳統的一種渲染方式,受到廣泛的硬件支持。它渲染的思路就是按照渲染管線的流程一步步渲染,最終將顏色繪制到Render Target(下圖)。
它的消耗跟物體數量和燈光數量有關,是O(Nobject?* Nlight)的關系,對于燈光數量較多的場景,顯得力不從心。
有些引擎(如Unity)在燈光數量多的情況下,會做一些優化:對所有燈光按亮度進行排序,將最亮的那部分燈光做逐像素計算,中間的一部分做逐頂點計算,排在后面的用球諧函數(SH,Spherical Harmonics)模擬。(見下圖)
4.6.3 延遲渲染(Deferred Shading)
? 延遲渲染的精髓在于將燈光計算延后,與場景物體數量解耦。
具體做法是:先將所有物體渲染一遍,但不計算光照,將物體渲染后的像素數據(Position/Normal/DiffuseColor和其他參數)存于各自的GBuffer;然后,利用這些數據采用后處理方式做光照計算。(下圖)
由于最耗時的光照計算延遲到后處理階段,所以跟場景的物體數量解耦,只跟Render Targe尺寸相關,復雜度是O(Nlight?* Wrendertarget?* Hrendertarget)。
延遲渲染并沒有在低端設備支持,它要求OpenGL ES 3.0以上,多渲染紋理以及更多的顯存和帶寬。
4.6.4?基于瓦片的延遲渲染(Tile-Based Deferred Rendering,TBDR)
針對Deferred Shading的缺點,出現了一種改進方案,它就是Tile-Based Deferred Rendering。此種渲染方式已廣泛應用于GPU圖形渲染架構中。
實現思路:
1. 將渲染紋理分成一個個小塊(Tile),通常是32x32。
2. 根據Tile內的Depth計算出其Bounding Box。
3. 判斷Tile的Bounding Box和Light是否求交。
4. 摒棄不相交的Light,得到對Tile有作用的Light列表。
5. 遍歷所有Tile,計算每個Tile有作用的Llight列表的光照。
4.6.5 渲染路徑總結
前面已經描述了各個方式的優缺點,下面詳細列出它們的性能消耗及平臺要求。
此外,還有Forward+,Physically Based Rendering(PBR),Legacy Deferred Rendering(Unity)等渲染方式,這里不詳細描述,有興趣的可以找資料了解。
場景管理和遮擋剔除
4.7.1 場景管理
場景管理的是在游戲場景內所有具有空間屬性的物體。目的是為了快速查找物體,減少物體更新,加快物理碰撞,以及渲染的遮擋剔除。
常見的場景管理方式有二叉空間分割樹(BSP)/四叉樹(平面空間)/八叉樹(三維空間)/入口(Portail)。
上圖左展示的是四叉樹,圖右展示的是八叉樹。具體的原理和實現方式這里不描述,有興趣的另行搜索。
4.7.2 遮擋剔除(Occlusion Culling)
遮擋剔除技術是將不在相機視截體內的物體進行剔除,不送入渲染管線處理,從而減少很多渲染物體。
上圖左是沒有啟用遮擋剔除的場景,右圖是啟用剔除后的場景,可以看出,超過一半的物體被剔除渲染,優化效果非常明顯。
與場景管理(4.5)的方式結合,可以實現快速剔除算法。
Unity的遮擋剔除需要設置遮擋體(Occluder)和被遮擋體(Occludee)
陰影
陰影的實現方式有很多種,消耗和效果各異。
4.8.1 貼圖陰影
貼圖的方式最簡單,做法是制作一張陰影紋理,放到物體腳下(下圖),跟隨物體一起運動。
貼圖陰影渲染非常簡單,只需要兩個三角面,適用于低端機型。
如果地面是起伏不平的,貼圖會被地面遮擋,可以將陰影貼圖用貼花技術緊貼地面。
但貼花依然有可能跟其它動態物體發生異常遮擋。
4.8.2 Projector(投射陰影)
Projector技術是預先指定光源位置和截頭體(Frustum),然后算出物體在其它物體的投影。
它可以將物體投影到任意平面上,但跟貼圖陰影一樣不能表達被投影物體的輪廓。適用于中等畫質效果。
4.8.3 Shadow Map(陰影圖)
陰影圖技術是將物體放入燈光空間(上圖)渲染,得到燈光空間的深度圖(也稱陰影圖),然后在正常渲染時只要讓某個片元在燈光空間的深度與陰影圖做比較,就可判斷出該片元是否處在陰影之中(下圖)。
Shadow Map技術可以渲染物體在任意平面的投影,渲染的效果最接近真實世界(下圖),但性能消耗會高很多,它會增加Draw Calls,增加顯存占用。通常適用于高端機型或重要角色。
陰影的分級策略
ShadowMap效果最好,但最耗性能,而貼圖方式剛好相反。
這就要根據項目具體情況做出分級策略,針對不同畫質不同重要程度的物體采用不同的陰影渲染方式。下表是游戲Z的分級策略。
帶寬優化
帶寬優化的目的是減少CPU與GPU之間的數據傳輸。
4.9.1 LOD(Level Of Detail)
LOD即細節層次,根據物體在畫面的大小選用不同級別的資源,以減少渲染和帶寬的消耗。
LOD在圖形渲染中應用廣泛,適用的對象有模型LOD,地表LOD,材質LOD,植被/樹LOD,燈光LOD,紋理LOD(MipMaps)等等。
.9.2 GPU Instance
GPU Instance技術應用于繪制相同Mesh的多個實例,每個實例都可以有獨自的參數(如Color/Position/Scale等)。這種技術可以減少帶寬,只需傳入一份Mesh數據,就可以繪制任意多個實例。
常用于繪制建筑,地表裝飾物,粒子,草,樹,植被等等。
4.9.3 GPU Skin
CPU Skin最基本的角色動畫實現方式,它可以方便地實現很復雜的動畫操作,如融合/漸隱/組合/串接等等。但它的缺點也顯而易見,占用大量CPU計算性能,難以并行計算,每幀需傳送模型頂點數據到GPU,增加帶寬負載。
GPU Skin的做法是將骨骼矩陣列表作為Uniform傳入Shader,然后在Vertex Shader中對模型頂點進行蒙皮計算。它可以并行計算,減輕CPU負載,此外,由于每幀傳入GPU的數據是骨骼矩陣,不是頂點數據,極大降低了帶寬負載。
當然,GPU Skin也有缺陷。它會提高GPU負載,最高骨骼數往往受限于平臺(如OpenGL ES 2.0不能超過250個Vector4數據量),而且也不利于做復雜的動畫操作。
Unity引擎可以在PlayerSettings面板開啟GPU skin.
此外,GPU Skin還可以結合GPU Instance技術,以渲染大量相同角色的骨骼動畫。
4.9.4 GPU粒子
GPU粒子的優缺點和GPU Skin類似,支持粒子的并行運算,減少帶寬負載,但它同樣難以實現粒子的高級特性(軟粒子/碰撞等)。
Unity對模型粒子做了特殊優化,支持GPU Instancing
4.9.5 正確使用Buffer標記
OpenGL的Buffer標記有以下幾種:
GL_STREAM_DRAW
GL_STREAM_READ
GL_STREAM_COPY
GL_STATIC_DRAW
GL_STATIC_READ
GL_STATIC_COPY
GL_DYNAMIC_DRAW
GL_DYNAMIC_READ
GL_DYNAMIC_COPY
1. DRAW:Buffer數據將會被送往GPU進行繪制。
2. READ:Buffer數據會被CPU應用程序讀取。
3. COPY:Buffer數據會被用于繪制和讀取。
4. STATIC:一次修改,多次使用。可用于靜態模型頂點數據。
5. DYNAMIC:多次修改,多次使用。可用于帶動畫的模型頂點數據,粒子系統的數據。
6.?STREAM:多次修改,一次使用。可用于特殊場合,比如編輯器數據。
Buffer的標記各有用途,所以選擇合適的類型可以減少帶寬負載和顯存占用。
4.10 Shader優化
1. 避免使用耗時的數學運算。如pow,exp,log,sin,cos,tan等等。
2. 使用更低精度的浮點數。OpenGL ES的浮點數有三種精度:highp(32位浮點), mediump(16位浮點), lowp(8位浮點),很多計算不需要高精度,可以改成低精度浮點。
precision mediump float; // Defines precision for float and float-derived (vector/matrix) types. uniform lowp sampler2D sampler; // Texture2D() result is lowp. varying lowp vec4 color; varying vec2 texCoord; // Uses default mediump precision.3. 禁用discard操作。
4. 避免重復計算。
precision mediump float; float a = 0.9; float b = 0.6;varying vec4 vColor;void main() {gl_FragColor = vColor * a * b; // a * b每個像素都會計算,導致冗余的消耗。 }5. 向量延遲計算。
highp float f0, f1; highp vec4 v0, v1;v0 = (v1 * f0) * f1; // v1和f0計算后返回一個向量,再和f1計算,多了一次向量計算。 // 改成: v0 = v1 * (f0 * f1); // 先計算兩個浮點數,這樣只需跟向量計算一次。6. 充分利用向量分量掩碼。
highp vec4 v0; highp vec4 v1; highp vec4 v2; v2.xz = v0 * v1; // v2只用了xz分量,比v2 = v0 * v1的寫法要快。7. 避免計算數組下標。在shader使用動態下標會導致較大的開銷。
8. 警惕動態紋理采樣(Dynamic Texture Lookup,也叫Dependent Texture Read)。也就是說在shader中,紋理坐標做了更改,就是動態紋理采樣(也稱依賴式紋理讀取)。在OpenGL ES 2.0的架構下,動態紋理采樣會出現較大的性能問題;3.0則沒有這問題。
varying vec2 vTexCoord; uniform sampler2D textureSampler;void main() {vec2 modifiedTexCoord = vec2(1.0 - vTexCoord.x, 1.0 - vTexCoord.y); // 紋理坐標改變了gl_FragColor = texture2D(textureSampler, modifiedTexCoord); // 觸發了Dynamic Texture Lookup/Dependent Texture Read。 }9.?避免臨時變量。
10. 避免使用for等循環語句。可以嘗試展開。
11.?盡量將Pixel Shader計算移到Vertex Shader。例如像素光改成頂點光。
12.?將跟頂點或像素無關的計算移到CPU,然后通過uniform傳進來。
13. 分級策略。不同畫質不同平臺采用不同復雜度的算法。
4.11 UI優化
. 避免不同圖集的控件交叉。交叉會增加draw calls,所以要避免。
2. 動態區域和靜態區域分離。即文字/道具等動態控件放在同一層,而其它靜態的控件盡量放至同一層,可以提高合批的概率。
4.12 其它渲染優化
4.12.1 避免后處理
后處理是場景物體渲染完成后,對渲染紋理做逐像素處理,以便實現各種全屏效果,包含以下效果:
- 抗鋸齒:Anti-aliasing (FXAA & TAA)
- 環境光散射:Ambient Occlusion
- 屏幕空間反射:Screen Space Reflection
- 霧:Fog
- 景深:Depth of Field
- 運動模糊:Motion Blur
- 人眼調節:Eye Adaptation
- 發光:Bloom
- 顏色校正:Color Grading
- 顏色查找表:User Lut
- 色差:Chromatic Aberration
- 顆粒:Grain
- 暗角:Vignette
- 噪點:Dithering
Unity的后處理棧
雖然后處理可以渲染出非常多的很酷很真實的效果,但是消耗也不容小覷。
特別是在移動端,由于移動設備硬件架構的特殊設計,會導致更為嚴重的性能問題,主要原因是:
1. 更慢的依賴式紋理讀取(slower dependent texture reads)。關于依賴式紋理讀取的解釋看這里。
2. 缺少硬件特性(missing hardware features)。
3. 額外的渲染紋理解析消耗(extra render target resolve costs)。
所以移動游戲要盡量避免使用后處理。
4.12.2 降分辨率
降分辨率是最粗暴最有效的提升渲染性能的方法。
由于當前很多智能設備分辨率都是超高清,動輒2K以上,但CPU/GPU卻跟不上,如果使用原始屏幕分辨率,就會出現嚴重的卡幀/掉幀現象。
通常可以將屏幕分辨率降到一半,這樣渲染紋理/深度Buffer/紋理等等數據都可以縮減到原來的1/4,極大降低了CPU/GPU/帶寬各項指標的消耗。
內存優化
內存優化目的是加快IO,防止卡主線程,防止頻繁操作(創建/刪除)內存,避免內存碎片化和占用過高。
5.1 緩存法
與CPU的緩存計算類似,思路是將需要重復創建的對象緩存起來,銷毀時將它放入緩存列表,再次創建時優先從緩存列表中讀取。
緩存法可以降低內存的創建/刪除頻率,避免碎片化。
常用于數量多且創建頻繁的物體,如小兵,NPC,血條,特效,道具,各類圖標等等。
5.2 內存池
內存池技術是現代主流引擎的標配,目的是避免內存碎片化,加速內存分配和管理。
實現思想通常是由引擎預先創建一塊較大的內存(也可動態調整),這塊內存通過有效的數據結構和算法策略,統一管理小塊內存的分配和回收,并為邏輯層提供內存相關的操作接口。
5.3 資源管理器
資源管理器是將所有需要用到的文件資源統一管理起來,統一創建,加載,釋放,回收等,為的是提高復用率,減少資源冗余和內存開銷,也是現代引擎必備的一個模塊。
假如沒有資源管理器,勢必會造成資源的冗余,同一份資源可能存在很多份內存數據.
5.4?控制GC
GC是Garbage Collect的簡稱,意為垃圾回收,是游戲引擎中采用一定策略回收內存池或托管堆里的無用內存和緩存區無用對象的一種技術。
GC機制就是防止內存占用過多過久,是一種自動調節內存占用的常用技法。
GC的觸發一般分為兩種:
1. 引擎觸發。一般是時間間隔到了,或者內存占有量到了某個閾值,引擎便會觸發GC。
2. 用戶調用。通常引擎也提供了API給游戲應用,以便邏輯層可以控制GC的時機。例如Unity的GC.Collect()接口可以觸發GC操作。
但是觸發GC需要遍歷內存池/托管堆/各類緩存表,還可能引發內存碎片整理操作,所以它需要耗費一定的CPU性能,是引起掉幀和卡頓的罪魁禍首之一。
那么,我們就需要在邏輯層采用一些方法,避免觸發GC,或者減少觸發GC的處理時間。
常用的方法:
1.?避免頻繁創建/刪除。這個好理解,頻繁創建刪除對象,會引起很多內存碎片和無用對象,增加觸發GC的幾率和時間。
2. 幀更新內盡量避免臨時對象和創建內存。
3. for/while等循環內避免避免臨時對象和創建內存。
4. 盡量避免申請大塊內存。申請大塊內存會導致內存暴漲,提升GC的幾率。
5. 避免內存泄漏。這個需要每個技術人員的職業技能和覺悟,也可以通過一些輔助工具檢查內存泄漏,詳見1.3。
6. 主動調用GC。比如在進入戰斗前后,切換場景前后,切換主要界面前后調用GC,可以一定程度上減少內存占用,避免掉幀/卡頓。
5.5?邏輯優化
邏輯優化的目標是盡量避免無用的內存操作,防止內存泄漏,盡快釋放內存,減少全局變量的使用,關注第三方庫的內存消耗。
卡頓優化
引發卡頓的原因有很多,但主要有:
1. 突發大量IO。
2. 短時大量內存操作。
3. 渲染物體突然暴漲。
4. 觸發GC。
5. 加載資源量多的場景或界面。
6. 觸發過多過復雜的邏輯。
避免或者緩解卡頓的技法也是圍繞以上原因展開。
6.1 降幀法
跟3.3的方法類似,通過強制降低更新頻率,減緩卡頓的時間。
6.2 攤幀法
攤幀法就是本來需要在同一幀處理的邏輯分為若干份,分攤到若干幀去處理,從而緩解同一幀的處理時間,減緩卡頓現象。
例如,本來在同一幀需要創建10個小兵,這個很可能會引發卡頓,那么可以每幀只創建2個,分攤到5幀創建完。
適用此法的還有資源的加載,AI的更新,物理的更新,耗時邏輯的處理等等。
此外,還可以用預處理(3.2),主次法(3.4)來避免卡頓。
6.3 限制數量法
如果降幀法,攤幀法,預處理,主次法都無法解決現象,卡頓原因又剛好是因為物體數量過多,那么限制數量就非常有必要了。
做法就非常簡單,當場景內創建某種物體(角色,特效,血條等)的數量到底最大值時,便強制不再創建。
此法可能會引起邏輯的一些錯誤和不好的游戲體驗,需謹慎使用和處理。
6.4 邏輯優化
如果卡頓是邏輯過于復雜引起的,就需要針對性地優化邏輯。每個項目的邏輯不一樣,這里無法給出具體的優化措施。
6.5 IO優化
因IO慢引起主線程等待,從而導致游戲卡頓的現象非常普遍,下面有一些常用的優化技法。
6.5.1 預加載
將耗時的IO提前到某個時刻(游戲啟動時,場景加載時,進入主界面時等)加載,比如有些角色資源大,可以在加載戰斗場景時提前加載,以免戰斗過程中卡頓。
6.5.2 異步加載
將IO異步化,以避免卡主線程。此技法應用非常普遍了,不再累述。
6.5.3?壓縮資源
將本來零散的文件壓縮成單個文件,或者對大文件利用一定算法(如哈夫曼編碼)壓縮,減少文件大小。這樣也可以降低IO時間。
當然,壓縮資源也有副作用,需占用多一份內存,解壓縮過程也要耗費額外的CPU。
6.5.4 多級緩存
我們都知道CPU的頻率是最高的,目前家用PC的主頻可達3.2GHz甚至更高,CPU內有L1~L3緩存,它們速度略有差別;內存的存取速度遠低于CPU,一般是2~3GHz,約是CPU的1/10。硬盤存取速度又遠低于內存,普遍是0.1Gb/s,遠低于內存讀取速度。而網絡更慢,目前即便是光纖,也不過0.02Gb/s。
通常我們能操控的是內存/磁盤和網絡的數據,所以只要關注它們的速度,它們的速度關系大致如下圖。
所以,多級緩存策略應運而生。
做法跟緩存法類似,只是多了層磁盤緩存
6.5.5 控制Log
游戲的Log通常會隔一段時間存檔,如果邏輯處理不好,很可能引發卡頓。比如,每幀輸出大量調試log,會引發頻繁存檔。
游戲Z在早期,也曾發生卡頓現象,后來經Profiler分析發現是Log存檔引發的。
所以,有必要對Log做出一些優化。
1. 避免幀更新輸出Log。防止Log數據迅速膨脹引起頻繁存檔或增加存檔時間。
2. 改進Log存檔機制。可以適當改進Log存檔機制,比如每隔多少時間存檔一次,或者Log數據到達一定量級觸發。
3. 建立Log等級。可以將Log分為Info,Warning,Error幾個級別,不重要的log不存檔。
4. 異步存檔。將存檔Log的邏輯防止單獨的線程,防止卡主線程。
5. 避免無用的log。這就要在邏輯層控制log輸出,避免無效的log。
6.5.6 JSON代替XML
游戲數據存儲一般有兩種:二進制和文本格式。二進制格式數據量最小,但可讀性和擴展性差,適合存儲模型/紋理/字體/音頻等數據。文本格式的特點跟二進制剛好相反,適合存儲配置信息。
最常見的文本格式有JSON和XML兩種,其中JSON對比XML有諸多優點:
1. 數據量少。表達同樣的數據,JSON格式可以比XML少40%
2. 可讀性更佳。上面兩段分別是XML和JSON表達相同的數據,誰可讀性更佳一目了然。
3. 更快的解析。JSON因為數據量更小,IO也會更快,解析速度當然也更快。
每個游戲都有大量邏輯數據需要存檔,比如角色信息,技能信息,場景信息,配置信息等等。這些數據如果適合用文本格式存儲,首選JSON無疑。
6.6 使用進度條
如果上面那些章節都無法解決卡頓現象,可以嘗試使用進度條。
思路是將卡頓邏輯抽離出來,分成若干階段(step),每完成一個step,給一幀時間刷新UI進度條。當然也可以用異步方式實現。
耗電優化
游戲耗電和游戲卡并無必然聯系,有些游戲在某些設備上雖然運行很流暢,但發現耗電很厲害,玩了不到半個小時,電量已經出現警報。
游戲耗電的原因主要是因為:CPU占用普遍高,內存操作頻繁,磁盤IO頻繁,渲染消耗普遍高,導致帶寬負載和GPU消耗高。
前面介紹的章節基本可以降低耗電,也是優化耗電的必要措施。尤其是以下章節對耗電優化更明顯:
2. 資源優化
3.3 限幀法
3.4 主次法
3.6 引擎模塊優化
4 渲染優化
5.4 控制GC
6. 5 IO優化
除了以上章節,還可以用動態調整幀率和畫質的優化技法。
7.1 動態調整限幀
游戲邏輯通常可以獲取當前設備的電量,若可以,則每隔一段時間獲取一次電量信息,可以統計出單位耗電量,如果發現單位時間耗電量過高,而游戲幀率又很高(比如大于50),可以主動降低幀率(比如30)。
7.2 動態調整畫質
做法跟動態限幀類似,只是調整的是畫質等級,而不是幀率。當然也可以一起結合使用。
網絡優化
網絡優化的目的是讓網絡包更小,響應更及時,消耗更少流量,不卡主線程。
8.1 減少無用字段
網絡包中通常包含了很多信息,諸如角色位置,朝向,狀態等。
如果是2.5D游戲,則位置z分量可以棄掉;朝向只在xz平面上,所以只需要發送RotationY。
通過這種減少無用字段,可以一定程度上降低網絡包大小。
8.2 降低字段精度
通常邏輯里的很多信息都是4字節,包括角色位置,朝向,技能或Buff信息等。但很多時候,這些信息不可能達到4字節數的最大值,可以壓縮至2字節甚至1字節。
比如,同樣是位置,場景的尺寸通常在2字節數的表示范圍內(-32512~32512),可以將位置的x/y/z壓縮至2字節發送。同樣地,朝向RotationY可以2字節表示。
8.3 避免重復發送
游戲網絡模塊須有效限制部分協議在短時間內重復發送,例如玩家在短時間內按了很多次抽獎按鈕。
所以需要一種機制來限制。比如可以在網絡協議定義時,加個標記,表明該協議不能在某個時間段內重復發送。
8.4 網絡異步化
開辟獨立的線程處理收發網絡協議包,是游戲常見的優化手段,可以避免與主線程相互等待。
8.5 壓縮無效字節
壓縮無效字節是指通過一種方式剔除每個字段內高位全0的數據。
比如角色等級50,如果用int32表示,是00000000 00000000 00000000??00110010?,高位3個字節全是0,可以壓縮至1字節。
這里有一種壓縮字節的方法,跟utf8編碼方式類似。
?utf8的編碼方式(x代表有效位):
1字節(最大有效位7)? :0xxxxxxx?
2字節(最大有效位11):110xxxxx?10xxxxxx?
3字節(最大有效位16):1110xxxx?10xxxxxx?10xxxxxx?
4字節(最大有效位21):11110xxx?10xxxxxx?10xxxxxx?10xxxxxx?
5字節(最大有效位26):111110xx?10xxxxxx?10xxxxxx?10xxxxxx?10xxxxxx?
6字節(最大有效位31):1111110x?10xxxxxx?10xxxxxx?10xxxxxx?10xxxxxx?10xxxxxx?
比如角色等級50,有效位是6,用1字節便夠了,壓縮成00110010?(紅色是壓縮位標記)。
如果是數字1000,int32表示為00000000 00000000 0000?0011 11101000?,有效位是10,需要2字節編碼,壓縮后是11001111?10101000?(紅色是壓縮位標記)。
采用這種壓縮方式,普遍可以用1~2個字節取代4字節數據,壓縮效果比較明顯。
8.6 壓縮協議包
8.5壓縮的是字段內數據,每個數據包其實有很多相同的數字,可以用目前主流的壓縮方法再對網絡包做一次壓縮。
游戲最常用的壓縮方法是zlib開源庫,還可以用lz4方法
優化誤區
9.1.1 文件小的圖片占用內存也小
有些人以為圖片文件小,它占用的內存也會小,所以有些人將圖片轉成高壓縮率的jpg格式。
那么這個看法和做法是否妥當呢?
根據2.1章節,可以看出圖片占用的內存大小跟圖片的尺寸和像素格式相關,跟文件格式沒關系。
所以,圖片轉成jpg只是壓縮了文件大小,但并不能降低內存開銷!
9.1.2 合批的數據越大越好
有人會認為即然合批能夠降低渲染消耗,是不是讓合批后的數據越大越好,以便更多地降低Draw Call呢?
答案是否定的。
原因有二:
1. 移動游戲的模型索引通常做了優化,只用16位表示,也就是說如果合批后的頂點數超過65535,便會越界,導致渲染異常。
2. 太大的數據量可能無法充分利用LOD,遮擋剔除等技術,導致過多的數據送入GPU,反而增加帶寬和GPU消耗。
9.1.3 片元等同于像素
片元(fragment)是GPU內部的幾何體光柵化后形成的最小表示單元,它經過一系列片元操作(alpha測試,深度測試,模板測試等)后,才可能最終寫入渲染紋理成為像素(pixel)。
所以,片元不是像素,但有概率成為像素。
參考文獻:
《OpenGL編程指南(第四版)》
《OpenGL ES 2.0編程指南》
《計算機圖形學(第三版)》
《實時計算機圖形學(第二版)》
OpenGL ES 2.0 Reference Pages
Unity3D Docs Manual
Unity3D Optimization
Understanding optimization in Unity
Performance Optimization for Mobile Devices
General Performance Tips
Unity 5 Game Optimization
50 Tips for Working with Unity (Best Practices)
移動游戲圖形渲染分析工具
OpenGL渲染管線詳細版
多線程渲染
游戲引擎多線程模型渲染解決方案
實時渲染中常用的幾種Rendering Path
Shader中常用的光照模型
WWDC 2018:寫給 OpenGL 開發者們的 Metal 開發指南
Lighting in vertex and fragment shader
OpenGLInsights-TileBasedArchitectures
基于GPU Skin的骨骼動畫Instance的實現
Post Process Effects on Mobile Platforms
Best Practices for Shaders
內存池設計和原理
///
總結
以上是生活随笔為你收集整理的移动游戏性能优化建议与字体剥离精简工具的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [DA45] 信用卡诈骗分析
- 下一篇: 树莓派无显示器连接无线