2D精靈Batch系統設計
來自專欄 korok引擎開發
對於2D遊戲引擎來說 Batch 是非常必要且重要的一個系統。現在的圖形驅動主要是針對3D場景設計的,2D遊戲會產生大量的簡單四邊形,圖形驅動每次昂貴的GPU調用如果只渲染有限的幾個頂點(四邊形只會產生4個頂點)是得不償失的。
了解更多 - Batch, Batch, Batch - Nvidia
我們從幾個方面來探討Batch系統的設計:
- API介面設計
- Batch穩定性
- 實現/考量
面向用戶的API介面設計
所謂面向用戶的API介面既最終暴露給用戶使用的介面,當前市面上最常見的兩種API設計方式可以按自動化程度分類:
- 手動Batch 提供一個類似 SpriteBatch 的對象/結構/工具,讓用戶來決定如何Batch。早期的 Cocos2D 提供的 SpriteBatchNode,Love2D 提供的 SpriteBatch 都是類似的設計思路。
- 自動Batch 用戶不必參與 Batch 的執行,引擎在底層自動把可批量渲的可視化元素打包在一個 Batch 裡面,比如現在的 Cocos2DX 就有類似的實現,用戶不必介入Batch的過程。
使用手動方案API設計簡單,規則也比較明確但是手動方案需要用戶了解 Batch 的概念同時還需要手動維護 Batch 的執行,並把 Batch 的概念帶到了應用層,這不是友好的設計。所以我們在設計 Batch 系統的時候直接了放棄了這種方案。其次就是自動方案,如果用戶只需要像往常一樣渲染精靈,引擎根據紋理和材質在底層自動合併可以Batch的元素這是非常棒的事情!
但是自動方案常常令開發人員困擾,常常我們認為應該 Batch 的場景卻沒有 Batch。比如在 Cocos2D 論壇上經常會看到的問題, 「為什麼我們的精靈沒有被Batch,我們已經XXX了啊???」 就是是自動 Batch 的問題,自動Batch的規則和用戶理解的情況會不一樣,造成很多問題。
所以我們設計了一種半自動的 Batch 方案,使用 batch-id 的概念來給引擎一定程度的暗示,如果用戶給一批可視化的元素指定了相同的 batch-id,那麼他們應該被 Batch 在一起。如果無法 Batch 的話我們可以檢測到這個異常以便給予一些友好的提示(暫時還沒有實現)。通常情況下 batch-id 是自動生成的,用戶並不需要關心。如果需要配置的話,可以通過下面的介面設置:
func (b *batchId) SetBatchId(id uint16) { b.value = id}func (b *batchId) BatchId() uint16 { return b.value}
目前Korok引擎中,精靈組件和文字組件都實現了這個介面,所以它們是可以實現自動Batch的組件,其它的組件比如簡單網格是無法Batch的。
Batch穩定性探索
一個顯而易見的問題是,如果我們的場景幾乎都是靜態的,那麼我們繼續每一幀都執行一次Batch,那麼這次 Batch 就變得毫無意義,我們完全可以把上次的Batch結果緩存下來,下一次直接使用就可以了,為什麼還要從頭到尾再計算一次呢??這就是Batch的穩定性問題,相反,如果我們渲染的是滿屏的子彈那麼我們需要對屏幕中的所有精靈進行重新Batch變非常有意義。
一個簡單的想法是對遊戲對象進行分類:
- 靜態對象 理想情況下只需要做一次 Batch,然後重複使用,穩定性 100%
- 動態對象 每次都需要重新 Batch,穩定性 0%
對靜態物體 Batch 一次之後就緩存下來,對於動態的物體每幀都進行重新計算。在 Unity 中是有類似的 概念的 :
1. Dynamic batching: for small enough Meshes, this transforms their vertices on the CPU, groups many similar vertices together, and draws them all in one go.
2. Static batching: combines static (not moving) GameObjects into big Meshes, and renders them in a faster way.
實現靜態 Batch 實際上比想像的要複雜,如果待處理的這一組遊戲對象中有部分物體不可見,假如一片森林只有一片樹葉是可見的,那麼完全沒有必要繪製整個 Batch,Unity的方案是 數據共享,部分繪製,這也算是一種折中的解決方案吧。
提高 Batch 的穩定性需要在靜態物體上做文章,大多數時候還需要一些應用層的知識,如果有 IDE 的支持這件事可以做的很漂亮,比如在 IDE 中標記一組遊戲對象為一個Batch,就可以優雅的解決這個問題。
在 korok 中當前的實現只支持完全動態的Batch,這是以後需要改進的地方。
當前實現和考量
Korok 中的 Batch 演算法還是很簡單的,假如有一組待渲染的遊戲對象,我們先根據 batch-id 進行一次排序,這樣變把所有可以 Batch 的遊戲對象聚合在一起。然後遍歷這一組對象,把它們的頂點複製到一個大的 VBO 中,之後直接渲染這個VBO就可以了(看起來有點簡單)。
在設計上我們把這個過程分為兩層:
- 實現層 通過一組標準介面,提供最基礎的 Batch API
- 應用層 調用標準的Batch介面,實現對精靈/文字等的 Batch
標準 Batch 介面:
// 啟動一次 Batchbatch.Begin()// 把可Batch的遊戲對象寫入batch.Draw(batchable)// 結束這次 Batch,並生成一個 BatchObjectbatch.End()// 提交所有的 BatchObject 給底層渲染系統batch.Flush()
我們很習慣於這種系統設計方式,先設計介面再考慮具體實現,這樣可以從宏觀上把握整個系統的設計。
在應用層的Batch代碼:
// sortsort.Slice(bList, func(i, j int) bool { return bList[i].sortId < bList[j].sortId})// batch draw!for _, b := range bList{ ii := b.value if sid := b.sortId; sortId != sid { if begin { render.End() } render.Begin(tex2d, depth) } render.Draw(spriteBatchObject)}if begin { render.End()}render.Flush()
以上的代碼片段是我們做精靈批量渲染的代碼(做了刪減)。這段代碼調用了 Batch 介面,執行 Batch ,最後會生成一組 BatchObject,BatchObject 就是普通的可渲染單元和精靈(Sprite) 類似,只是它裡面打包更多的頂點並共享了紋理。
注意,render.FLush() 並沒有執行渲染只是生成了一組可渲染對象(BatchObject),這給我們以後做穩定性設計提供了可能。如果你閱讀過 Cocos2D-X 的代碼(V3.x) 你會的發現我們的實現和Cocos的不同,形象的說:
Cocos2D 的 Batch 類似於一個分揀流水線的操作工,他旁邊有一個臨時存儲的大框,如果他發現下一個物體和筐里的相同則把物體放入框中,否則把框傾倒(渲染),而我們的實現不同,我們的操作工如果發現下一個物體和筐里的相同則把物體放入框中,否則的話會再拿一個新框來存放物體,最後再把這些框交給下一個流水線來處理(渲染)。
前者的實現個人覺得以後會給引擎的擴展帶來不不要的麻煩,因為他是一個即時系統,這樣他就會喪失了對數據繼續進行編排的空間。
在寫代碼之前我們調研了一些主流遊戲引擎的Batch情況:
- Unity 目前全場最佳,實現了靜態/動態 Batch,有IDE支持.
- Unreal 4.8 版本開始支持(2015/05/18),採用 IDE 編輯方案
- Godot 不支持,Godot 最近兩年挺火所以特別關注了
- Love2D 採用SpriteBatch 手動Batch方案
- Cocos2DX 自動 Batch 方案
(以上是1-2年多前的調研,最近可能會有新的變化)
注意:Batch系統在 CPU 端計算頂點變換併合併到一個VBO中,只有在CPU端計算的成本小於一次 DrawCall的成本時,Batch 才會變得有意義。在主機或者一些現代的圖形驅動上一次Drawcall 的成本已經很低,Batch的意義也不再那麼重要。目前 Korok 的 Batch 系統是區分圖層的這個設計還在考慮之中,區分圖層意味著不同圖層的遊戲對象不能 Batch 在一起。
最後真誠的希望您能夠在 Github 上給我們的項目 Github - KorokEngine 來一套 watch/star/fork 三連擊。 關注一下 twitter 也是歡迎的。
以上(配圖是第一次執行Batch系統時的截圖)。
推薦閱讀:
※卡卡為什麼不受大多數人歡迎?
※又有一家能幫你裝逼的公司倒閉了
※[Android&Ios]【測評】《物種起源》我的征途,是那十億光年外的星辰大海。
※關於試譯
※你怎麼看待《龍之谷》這款遊戲?