動態骨骼Dynamic Bone優化

Dynamic Bone是基於彈簧質點算法的彈性節點模擬組件,可以用於柔性繩索和其他的簡單的柔體,上一篇我們已經詳細的對於算法進行過研究,想回顧的可以到這裏查看

上週主要在對原版代碼進行優化以適應大規模的應用,優化過程主要是減少了向量、矩陣的數學運算的消耗,緩存每個節點的狀態變化,降低計算頻度高,減少其一幀內的計算量,並且進行了ECS化和Job System的嘗試,最終效果目前符合預期。

場景內50個模型,共450個組件對象,2700個節點 ,測試爲PC環境,CPU爲i7-8700,屬於高配,數據僅供參考。

 


優化的相關結果

 

DynamicBone經歷一系列優化之後,單幀耗時下降非常明顯,目前只有原始C#版本代碼開銷的4%不到。不考慮Culling和Distance機制僅有原版的2.5%的開銷,對於distance的計算後面不會每幀去計算,最後會採用間隔幾幀來處理,原版插件沒有culling這部分。

上面數據的這一系列優化除了C#到C++的遷移之外,還涉及到Unity引擎內部的相關調整,在下面會一併的進行介紹。


一、Transform的結構與算法優化

Dynamic Bone優化過程遇到的Transform的最主要的性能坑點,或者說最主要的不必要的高額性能開銷,就是Transform提供的最通用的各項全局數據(例如位置、旋轉、變換矩陣等)的獲取和設置接口開銷相當之高。從上圖的數據可以看出與Transform進行數據交互至少佔了整個計算流程30%的時間;

Transform的數據讀寫接口的高耗時與其數據結構有關。由於Unity場景內所有GameObject都是以層級關係相互關聯的,所以Transform使用層級關係結構來儲存變換數據,每個Transform對應層級關係樹中的一個節點,其只儲存本身的局部位置、局部旋轉和局部縮放;

這樣Transform只用關心自身局部變換數據的修改,父節點發生變化後不用修改所有的子孫節點的數據(雖然實現上不是完全沒有修改)。但是由於所有的Transform都只存儲局部變換數據,所以當需要讀取或者設置某個Transform的任何全局變換數據時,這個Transform都需要向上回溯計算得到他的全局變換數據。

 


Unity的Transform結構

 

Transform的全局變換的計算開銷問題本身可以用Cache處理——當每次讀取Transform都將讀取過程計算得到的結果儲存到Cache,而當兩次讀取之間Transform的變換數據沒有發生變化時,Transform不進行計算直接返回Cache值,需要計算時再進行更新計算——由於Unity中整體邏輯流程是串行的,沒有邏輯上的並行(Job System只是計算並行),因此這個功能不難實現。

但是Transform卻沒有任何Cache邏輯,雖然其有一種ChangeMask機制用於保存當前Transform是否有過修改的信息,但是Transform沒有基於ChangeMask建立Cache,這就導致對於Transform全局位置、旋轉、縮放數據的每次讀取和修改都會導致Transform從當前節點出發,逐層遞歸或者迭代直至根節點計算出全局變換數據;

這其中,SetPosition\SetRotation的邏輯也是會先逐層向上求逆,直到求到當前節點應有的局部變換數據之後再進行設置,但是與全局Getter函數不同的是,Setter函數求逆的過程是遞歸的,同樣的簡單邏輯遞歸的實現要比迭代耗時很多,這就導致Setter函數的性能更加低下;我們的優化也就對其進行了CACHE,CACHE後數據看出效率提升明顯。


二、SIMD數學庫對於普通數學庫的修改

原始的代碼是不經過SIMD的,改爲C++代碼以後編譯也就編譯器的默認的simd的優化,而Unity內部幾乎所有的數學計算都使用它自己的數學庫的SIMD優化,因此我們對代碼進行了改寫保持與引擎內部的計算一致。

 

 

順手進行了下測試,測試代碼的編譯環境是VC++2010,優化全開,通過查看反彙編代碼可以看出VC編譯器是會自動進行SIMD優化的;

結果中的向量點乘和四元數乘向量都有明顯的SIMD優化,以至於普通運算耗時接近SIMD運算耗時,這對測試結果影響比較明顯,但考慮到安卓平臺編譯環境依然可能有自動的SIMD優化,所以這裏沒有關閉編譯器的SIMD優化開關;

從測試結果可以看出:

1、SIMD對向量四元數矩陣等數據結構運算優化是比較明顯的,能減少40-50%的耗時;
2、雖然有編譯器的自動SIMD優化,但是其優化幅度有限,實際優化還是要依賴技術手動調用SIMD接口;


三、ECS機制以及Job System化

ECS機制以及Job System是Unity爲了提供代碼性能而提出的設計框架,ECS即Entity-Component-System,核心理念是把原先的OOP思想改爲DOD思想。DOD的好處是可以將同種數據集中到一起並放在內存中密集排布,大幅增加CPU Cache的命中率,降低代碼抽象度,將數據獨立出來,並且斷開各種數據之間的耦合,這樣Job System就可以將同種數據分散到不同的線程進行處理。具體ECS機制官方已經有詳細的DEMO和數篇文章介紹,在這裏也就不再重複,下面說說對於Dynamic Bone原始的結構改進以滿足優化要求。

1.數據的拆分-ECS化

查看插件源碼可以知道DynamicBone是每個實例各自儲存自己的組件數據以及自身粒子(DynamicBone會將每根骨骼抽象爲一個彈簧粒子)的數據,並且在每幀Update和LateUpdate中完成自身數據的更新,

我們增加了一個DynamicBoneManager進行整體的管理,Manager的好處可以統一儲存所有組件和所有粒子的數據,並且所有數據都是以直接數組的形式平坦儲存的;然後Manager會在自身的Update和LateUpdate中通過JobSystem一次性並行更新所有粒子的數據,並且將其Apply至對應的Transform對象上;

 

 

經過這樣設計以後,DynamicBoneManger把原本分散作爲DynamicBone和Particle屬性的數據全部集中在集合DynamicBones中,然後再對集合進行分類,分類依據就是DynamicBone組件對象擁有的節點數量;

 

 

如上圖中所示的DynamicBones對象,紅框部分就是Particle數據,其他是DynamicBone數據以及有效列表數據,而這裏Particle數據集合永遠保持爲其他DynamicBone數據集合大小的兩倍,也就是說這個DynamicBones對象專門容納有2個節點的DynamicBone對象的數據。同理,就可以創建N個DynamicBones對象,分別容納有2到(N+1)節點的DynamicBone對象的數據。這樣的設計完成後,所有組件和粒子的數據都是內存緊密排布的;Manager會一次性申請大量的空閒內存,而當新的組件Entity註冊的時候,Manager只會就近找一個沒有被使用的無效索引分配給這個組件Entity,並且將這個索引置爲有效,隨後初始化Component數據,並且將數據存入索引的空閒內存中。反之,當Entity註銷的時候,Manager並不真正釋放內存,而只是將這個索引置爲無效. 同時,Manager會維護一個有效索引表,Manager每一幀只用按照這個表去更新有效索引指向的所有Component數據。

 

 

2.Dynamic Bone的JobSystem使用

 

 

由於Manager使用索引邏輯,所以Job只需要和有效索引一一綁定就好了,DynamicBoneManager的JobData內只有索引相關的信息,還有Component數組指針。每次當有索引被設置爲有效,就會添加新的Job,反之就會刪除存在的Job。例外補充一下對於Job的拆分後需要注意下本身的Job消耗太小比如處於0.001MS的消耗,建議對於Job進行Batch測試,大家在2018的Job system的使用中需要注意。


這是侑虎科技第484篇文章,感謝作者FrankZhou供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:793972859)

作者主頁:https://www.zhihu.com/people/pkhere,作者也是U Sparkle活動參與者,UWA歡迎更多開發朋友加入USparkle開發者計劃,這個舞臺有你更精彩!

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