由淺到淺入門批量渲染(完)
簡單來說,它們基本的思路,都是將骨骼蒙皮動畫的“結果”預先儲存在一張紋理中;然後在執行時通過GPU從這張紋理中取樣,並使用取樣結果來更新頂點屬性;再結合例項化技術(GPU instancing),達到高效、大批量渲染的目的。
如果你之前對這類優化方案並不瞭解,看了上面的描述,也仍然一頭霧水;那太好了,這篇文章(沒準)可以幫助你快速入門這類優化方案。
下面我們就簡單的介紹一下它的工作流程及原理吧。
烘焙頂點動畫
可以簡單的將它的工作流程分為兩個階段:
- 非執行狀態下的烘焙階段
- 執行狀態下的播放階段。
烘焙階段
這個階段其實是為後面的播放階段準備動畫資源,你也可以把這個資源簡單的理解為一種特殊型別的動畫檔案。
首先,在編輯狀態下,讓攜帶骨骼蒙皮動畫的角色,按照一定幀率播放動畫。
非執行狀態下播放動畫
我們知道:動畫播放時,角色網格會被蒙皮網格渲染器(SkinnedMeshRenderer)更新而產生變形(頂點變化);如果在動畫播放的同時,記錄下每一個頂點在這一時刻相對於角色座標系(通常是角色腳下)的位置。那麼,當動畫播放完畢時,我們會得到一張“每一個頂點在每一個關鍵幀時的位置表”,我們就叫它“幀頂點位置表”吧。
可是“幀頂點位置表”名字太長,你可能不太好記,我打字也比較麻煩,所以我們後面就叫它“表表”吧。
播放某動畫時,記錄下的表表
除了表表,我們還能得到與它對應的動畫資訊,比如這個動畫的名稱、時長、幀率、總幀數、是否需要迴圈播放等資訊。
到這裡,第一個階段我們需要的內容就準備就緒了,可以進入下一個階段:播放動畫階段。
播放階段
在動畫播放時,通過一個變數(播放時長)來更新播放進度;結合上一階段我們記錄下來的動畫總長度、動畫總幀數資訊,就可以計算出當前動畫播放到了第幾幀(當前幀數 = 已播放時長 / 動畫總時長 x 動畫總幀數)。
一旦得到當前動畫幀,就表示可以通過表表鎖定一行頂點資料。
通過關鍵幀找到的頂點資料
接下來,只要遍歷這行頂點資料;然後根據索引找到網格中對應的頂點並更新它的位置,就等於完成了蒙皮工作。
每一幀都通過表表來更新頂點屬性,動畫就播放起來了
如你所見,使用這種方式來更新角色動畫,其實是直接使用了預先處理好的骨骼動畫、蒙皮網格渲染器的作用結果,是一種用空間換時間的策略。
既然通過一個播放進度、表表和一些簡單的動畫資訊,就能直接完成蒙皮(對頂點屬性的更新),那麼我們就不再需要以下內容了:
- 骨骼資訊
- 動畫控制器
- 蒙皮網格渲染器
使用CPU來讀取表表,並遍歷更新所有頂點,顯然不如GPU來的高效;所以接下來,我們將這一步放到渲染管線中的頂點變換階段,藉著GPU處理頂點的契機,完成使用表表中的資料對頂點屬性進行更新。
使用紋理儲存動畫資料
首先,為了便於GPU讀取表表,我們將其儲存為一張紋理,可以稱之為動畫紋理。
比如,對於一個擁有505個頂點的模型來說,我們可以將表表中的資訊儲存到一張 512 x Height 大小的紋理中。
這其中,紋理的寬度用來表示頂點的數量,而紋理的高度用來表示關鍵幀,所以Height的值取決於動畫長度以及動畫幀率。
將表表中的頂點位置“烘焙”到紋理中
我們通過UV座標來獲取這張紋理上的畫素,就可以被理解為:取第U個頂點在第V幀時的座標。
當然,除此之外,我們還會將動畫資訊儲存成為動畫資源,將重要資訊進行序列化(動畫名稱、動畫長度、總幀數、是否迴圈等)。
使用ScriptableObject儲存動畫資訊
後面的事情就簡單了:在播放動畫時,CPU將當前播放的關鍵幀傳給頂點著色器;頂點著色器計算出對應的V座標;結合頂點索引及動畫紋理的寬度計算出U,既可取樣出這個頂點基於角色座標系下的座標;接下來用這個座標再進行後面的空間變換就可以了。
法向量
由於動畫播放時,頂點的實時位置是從紋理中取樣,而非從網格中讀取的(不再使用蒙皮網格渲染器,頂點緩衝區內的資料不會被修改),所以頂點屬性中的法線資訊也無法使用了(永遠是靜止狀態下的);如果需要獲取正確的法向量,那就需要在烘焙頂點座標時也同樣將法線烘焙下來,並在頂點變換階段將這個法向量也取樣出來。
烘焙的法線紋理
存在多個動畫
通常情況下,角色不會只包含一個動畫;比如小兵通常擁有空閒、移動、攻擊三個動畫。如果對每一個動畫都烘焙一或兩張(法向量)紋理,那貼圖的數量將很快不受控制。鑑於所有動畫對應的頂點數量一致,也就是紋理的寬度都相同,我們可以將多個動畫紋理進行合併。
三個動作被合併到了一張紋理中
將多張動畫紋理進行上下排列、合併後,我們只要將每個動畫的起始、終止的V座標範圍追加到動畫資源中即可。然後在播放、切換動畫時,根據當前動畫所在的起始位置以及播放進度,就能算出正確的V座標了。
儲存動作資訊時需要額外記錄一些屬性
當然,如果網格的頂點數量少,而動畫數量多,我們也可以多列放置動畫(前提是放的下)。
多列放置動作
動畫過渡
簡單的動畫過渡很容易實現,只要在切換動畫時,分別計算出當前動畫和下一個動畫的播放位置,然後傳給GPU進行兩次頂點位置取樣,再對兩次取樣的結果進行插值即可。
帶動畫過渡
不帶動畫過渡
使用例項化渲染
例項化渲染的特點是使用相同網格,相同材質,通過不同的例項屬性完成大批量的帶有一定差異性的渲染;而烘焙頂點恰好符合了例項化渲染的使用需求。
所以,我們只需將控制動畫播放的關鍵屬性:比如過渡動畫播放的V座標、當前和下一個動畫的插值比例等,放入例項化資料陣列中進行傳遞;再在頂點著色器中,對關鍵屬性獲取並使用即可。
例項化渲染時獲取到的關鍵屬性
當然,如果你想實現更多不同的效果,比如你附帶了一張遮罩紋理,用來調整diffuse或做某些特殊的計算,那你也需要將控制引數加到例項化資料中就可以了。
頂點著色器取樣
在頂點著色器中是無法使用tex2D進行取樣的,需要使用tex2Dlod進行代替,但是這個特性需要shader model 3.0(#pragma target 3.0)才可以支援。
頂點索引
可以通過語義SV_VertexID來獲取頂點索引,但是在移動平臺上這個特性需要OpenGL ES3.0(#pragma target 3.5)才可以使用(當然也可以在烘焙階段將頂點索引儲存到網格屬性中)。
與蒙皮網格渲染器的比較
- 不再需要CPU計算動畫和蒙皮,提升了效能
- 可以通過例項化技術批量化渲染角色,減少DC
烘焙頂點的主要問題
- 模型頂點數量受限:如果紋理的最大尺寸限制在2048 x 2048,那麼只能烘焙下頂點數在2048個以下的模型
- 記錄頂點動畫的紋理過大:(頂點位置)紋理格式需設定為TextureFormat.RGBAHalf
- 儲存的動作長度有限
烘焙頂點的動畫長度參考表
烘焙骨骼矩陣
除了烘焙頂點,另一種常用的優化方案是烘焙骨骼矩陣動畫。
聽名字就知道,烘焙骨骼矩陣與烘焙頂點位置,原理十分相似;最大的差異在於它們在烘焙時所記錄的內容不一樣:烘焙頂點記錄下來的是每個頂點的位置,而烘焙骨骼矩陣記錄下來的是每一根骨骼的矩陣,僅此而已。
記錄下每一根骨骼在每一幀動畫播放後的矩陣
烘焙骨骼矩陣最大的意義在於它補上了烘焙頂點的短板:受頂點數量限制、烘焙的動畫紋理過大 及 紋理數量較多。
動畫紋理使用量差異
烘焙頂點對於紋理(面積)的使用是受頂點數量決定的,可以簡單理解為:
紋理面積使用量 = 頂點數量 x 動畫長度 x 內容數量
所以,當模型的頂點數量過多時(數以千計),這種烘焙方式或者無法烘焙下整個模型(頂點數量>2048),或者需要一張或多張(法線、切線)大尺寸紋理(<2048 && > 1024)。
但是,烘焙骨骼矩陣主要取決於骨骼的數量,可以簡單理解為:
紋理面積使用量 = 骨骼數量 x 動畫長度 x 矩陣烘焙方式(x1、x2 或 x3)
在移動平臺上,通常20根左右的骨骼就可以取得不錯的表現效果,所以相對於烘焙頂點,烘焙骨骼可以記錄下更長的動畫,同時它也不再受頂點數量的限制,也無需對法線或切線進行特殊處理(因為可以在取樣後通過矩陣計算得出)。
兩種方式烘焙的動畫紋理尺寸差異較大
烘焙階段
與烘焙頂點相似,烘焙骨骼也需要先在非執行狀態下,預先播放一次動畫;並在動畫播放時,記錄下每個關鍵幀下每根骨骼的轉換矩陣。
這裡有三點需要注意。
第一,記錄下的矩陣是每根骨骼從網格座標系轉換到角色座標系下的矩陣:
Matrix_meshToRole = Matrix_boneLocalToRole x Matrix_meshToBoneLocal
Matrix_meshToBoneLocal可以通過mesh的bindposes獲取;而Matrix_boneLocalToRole可以通過bone的transform計算獲得。
第二,需要將每個頂點與骨骼的關係記錄到網格資訊中,這個關係是指頂點會被哪根骨骼影響(骨骼索引)以及影響的大小(權重值)。
每個頂點最多可以受4根骨骼影響,但是被越多骨骼影響意味著播放時會有越多的取樣和矩陣計算,通常限制在2根骨骼就能得到不錯的效果;骨骼索引和權重可以通過mesh的boneWeights得知,在烘焙紋理時可以將它儲存在mesh中不用的uv中,以便在頂點著色器中獲取。
每個uv可以儲存下一根骨骼的索引和權重,通常使用兩個uv就可以了
第三,對於不同的骨骼動畫,烘焙矩陣的方式也不一定相同。
例如,如果骨骼動畫中每根骨骼只會相對於上層骨骼進行旋轉變換,那我們烘焙一個四元數就夠了,也就是一根骨骼的一個矩陣只佔用一個畫素;但是如果骨骼除了旋轉,還有平移甚至縮放的操作,那我們就需要2-3個畫素來儲存一個骨骼的矩陣了。
擁有特長的角色,需要特殊處理(來自《匹諾曹》)
也可以簡粗的將一個矩陣完整的儲存在三個畫素中
播放階段
與烘焙頂點相同的是,烘焙骨骼矩陣也是在頂點變換階段,通過對動畫紋理的取樣,完成頂點座標計算的;但是它的計算方式相對於烘焙頂點要複雜一些。
這裡主要是需要根據烘焙矩陣的方式,通過取樣來還原從網格座標系到角色座標系的轉換矩陣;例如,在烘焙階段將完整的矩陣儲存在三個畫素中,那轉換時就需要取樣三次才能拼湊出一個完整的矩陣。
當然,一旦得到轉換矩陣,角色座標系下的頂點位置、法向量等就可以通過計算獲取,後面的變換就可以繼續了。
相比於烘焙頂點
正如前文所說,烘焙骨骼不再受頂點數量的限制,可以記錄下更長的動畫時間,烘焙紋理的尺寸和數量也有明顯的優勢。
烘焙骨骼的動畫長度參考表
相同模型下的烘焙動畫長度對比
但是,烘焙骨骼這種方式會在頂點著色器中需要進行多次頂點取樣,在模型頂點數較多、或渲染單位數量較多時,其效率會略低於烘焙頂點。
烘焙骨骼的在頂點著色器中的取樣更多
兩種烘焙方式分組比較
華為P20上的兩種烘焙方式的對比
小結
以上,就是針對批量渲染骨骼蒙皮動畫單位的兩種優化方案。
針對這兩種方案,我個人認為並沒有絕對的孰優孰劣;正如你所看到的,烘焙骨骼在處理複雜模型或多動作時更具優勢;但如果渲染數量較多、模型頂點數較少、表現需求較弱(無需法向量參與計算)時,烘焙頂點也是值得嘗試的,因為它在效能上會更好一些。
其他優化方案
其實,除了上述兩種優化方案外,坊間還有一些特殊的小技巧:比如在手遊《三國志大戰M》的開場戰鬥表演中,某些帶動畫的模型角色,也通過例項化達到了批量渲染的目的。
部分小兵不是簡單的“片”,而是帶模型的單位
但是通過GPA抓幀工具,會發現同一個模型的網格會在同一幀存在多個不同的“姿勢”;我推測這裡應該是將利用了若干個、使用相同模型的、骨骼蒙皮動畫的播放“結果”,通過例項化渲染的方式,複製到多個位置上,由於每個骨骼蒙皮動畫的播放進度都略有不同,它們混在一起後的效果就會比較自然,不失為一種巧妙的方法。
渲染時每個模型的“姿勢”都略有差異
寫在最後
至此,這回的《由淺到淺入門批量渲染》系列就全部更新完了。
相關閱讀:
由淺到淺入門批量渲染(四)
由淺到淺入門批量渲染(三)
由淺到淺入門批量渲染(二)
由淺到淺入門批量渲染(一)
來源:騰訊遊戲學院
原文:https://gameinstitute.qq.com/community/detail/133631
相關文章
- 由淺到淺入門批量渲染(一)
- 由淺到淺入門批量渲染(二)
- 由淺到淺入門批量渲染(四)
- promise由淺入深Promise
- RabbitMQ由淺入深入門全總結(一)MQ
- RabbitMQ由淺入深入門全總結(二)MQ
- JavaScript Promise由淺入深JavaScriptPromise
- MySQL索引由淺入深MySql索引
- 物件導向-由淺入深物件
- iOS架構由淺入深 | MVVMiOS架構MVVM
- 純手寫Promise,由淺入深Promise
- 由淺入深理解 IOC 和 DI
- Vue.js 2.0 由淺入深Vue.js
- 由淺到深瞭解工廠模式模式
- 淺入深出Vue:資料渲染Vue
- Vue入門淺析Vue
- Elasticsearch從入門到放棄:淺談算分Elasticsearch
- lua淺淺入門瞭解一下
- 第十八節:Skywalking由淺入深
- [轉帖]由淺入深瞭解GC入門篇(一):什麼是垃圾回收?GC
- 由淺入深 docker 系列: (2) docker 構建Docker
- 由淺入深 docker 系列: (3) docker-composeDocker
- 由淺入深理解Dubbo的SPI機制
- 由淺入深完全理解Java動態代理Java
- 由淺入深 docker 系列: (6) 映象分層Docker
- 【Fastjson】Fastjson反序列化由淺入深ASTJSON
- 由淺入深 學習 Android Binder(三)- java binder深究(從java到native)AndroidJava
- Git 由淺入深之細說變基 (rebase)Git
- 由淺入深 docker 系列: (5) 資源隔離Docker
- 淺入淺出webpackWeb
- 淺入kubernetes(1):Kubernetes 入門基礎
- 零基礎深度學習入門:由淺入深理解反向傳播演算法深度學習反向傳播演算法
- MVP架構由淺入深篇一(基礎版)MVP架構
- 前端如何理解正則-由淺入深的學習前端
- 【由淺入深_打牢基礎】HOST頭攻擊
- C#非同步程式設計由淺入深(一)C#非同步程式設計
- 淺入淺出 MySQL 索引MySql索引
- 由淺入深 docker 系列:(4) 容器與虛擬機器Docker虛擬機