Unity —— 針對DynamicBone外掛的迭代來實現布料模擬
這篇部落格介紹了我在近期針對於Unity的DynamicBone外掛進行迭代,從而實現近布料模擬的方法。
由於部分涉及到工作內容,因此詢問了部門老大後決定本部落格將只介紹迭代思路,不涉及到具體實現,也不會放出對應圖片。
背景
來上海也有近兩個月了,近來也差不多安定了下來,也是時候恢復寫部落格的習慣了。
最近的任務就是針對於Unity下的DynamicBone
的迭代,讓其能夠更加適配於我們自己的專案(主要是布料的模擬)。
關於原生的DynamicBone
原生DynamicBone介紹
DynamicBone
是一個Unity的外掛,可以用於針對骨骼進行物理模擬。由於開放原始碼並且工具鏈整合方便,因此頗受專案青睞 —— 我也是通過直接閱讀該外掛的原始碼來逐漸接觸Unity開發的。
DynamicBone
開放了Damping
,Elasticity
,Stiffness
,Inert
等物理屬性介面,並且開發者可以通過配置DynamicBoneCollider
來配置相應的碰撞體,來進行碰撞結算。
DynamicBone
允許開發者指定對應的根骨骼,從而允許該骨骼的子骨骼進行物理結算,而根骨骼將不進行物理結算。換句話說,這樣的物理模型更加適用於例如尾巴、馬尾、Ragdoll等帶有明顯層級結構的物理模擬。
直到我入職為止,DynamicBone
已經更新到了1.1.7版本。需要吐槽的是……在閱讀原始碼之後,我個人認為對應的物理模型和結算邏輯經過作者的多次改動後,似乎已經比較混亂了。例如對應的Force和Gravity結算、外力結算的Integration都不太好閱讀。
原生DynamicBone的工作原理
DynamicBone
的物理邏輯與Tressfx、以及我的HairStrandPlugin(沒錯,這是一條廣告)差不多,都是將需要進行物理模擬的物件抽象為Particles,然後針對於Particles進行物理邏輯運算。在進行完物理邏輯運算後,再根據Particles的位置來進行物件transform的設定。
DynamicBone
的工作原理是使用鬆弛法,每次迭代分為Verlet Integration
,Shape Keeping
,Length Keeping
和Collision Solve
四個階段。
Verlet Integration
不多說,可以參考我之前構建HairStrandPlugin的開發日誌,傳送門。
Shape Keeping
原作者的Shape Keeping
採用的是Local Shape Constraint。也就是說針對於每個粒子,每次迭代都會試圖將其掰到其父粒子的原始相對位置上去。
Length Keeping
Length Keeping
的方法是針對於每個粒子,都會將其限制在以其父粒子為球心,兩個粒子初始距離為半徑的球內。這個方法在01年的Game Developer Conference被 Jakobsen T.提到。
Collision Solve
Collision Solve
的方法是進行球與膠囊體的碰撞檢測,如果相交,則將粒子球推到對應的表面去,簡單的幾何計算。
每次的迭代都將經歷上面的四個步驟,原作者將最大的迭代次數限制在4次以內。
針對DynamicBone的新需求
在現在的專案中,我們需要使用DynamicBone
來實現類似衣料的模擬。布料的物理模型與原生DynamicBone
的物理模型差異較大,經過分析後,物理模型的差異可以歸為如下幾點:
- 彎曲彈性(Bending Resistance),由於在原有的物理模型中,整個骨架的彎曲是沒有專門的Constraint來進行結算的,因此整個DynamicBone的效果看起來會很像一根軟繩。並且如果某根骨骼進行了碰撞的結算,那麼整體的形狀會看起來比較怪異,不夠順滑。
- 彈簧-質心繫統(Springarm-Mass system),由於專案的邏輯涉及到外力,因此物理模擬的構建必定少不了質心繫統,提到質心繫統就肯定是彈簧系統。例如比較靠近根骨骼的粒子,應該將其質量設定的相對較大,而比較靠近尾端的骨骼,質量就會較輕。
- 風力系統,沒啥好說的,暖暖是個換裝遊戲。
- 定製化的彈性約束系統(Customizeable Springarm Constraint system),這一點是重中之重。由於我們嘗試使用
DynamicBone
來模擬衣料,而原生的DynamicBone
是有層級架構的,因此不同骨骼樹下面的骨骼之間是沒有約束的。這樣也就導致了衣料碰撞結算會是一個極不穩定的結算 —— 當碰撞體穿過幾串骨骼之間時,這些骨骼將從碰撞體邊緣滑落,這是完全無法接受的。
針對新需求的迭代思路
彈簧-質心繫統(Springarm-Mass system)
之所以把這個東西放到第一個講是因為這是其他幾個物理模型的基石。其實實現起來挺簡單的,針對與每一個粒子加入一個質量(mass)屬性,然後針對彈簧結算,是依據其質量的比例,來將其推向穩定位置。這樣一來靠近根節點的粒子將更加重一些,而靠近尾端的就會更加飄逸一些。
彎曲彈性(Bending Resistance)
這個簡單,在粒子與其孫子粒子之間加入一個本地形狀約束彈簧即可。
風力系統
也簡單,只是適配一個按照時間變化的向量曲線即可。
定製化的彈性約束系統
雖然前面說的比較重要,但那是功能層面的。實際的實現上還是比較簡單,只需要在對應的骨骼上加上長度約束彈簧。這裡比較麻煩的地方是在將各種的配置儲存至.prefab檔案上的操作,但那一塊不屬於這篇部落格覆蓋的內容,因此略過。
效能分析及優化
效能分析
我使用了Unity自帶的profiler進行了DynamicBone
對應的效能分析後,發現效能hot spot如下:
- 各種ConstraintSolve,例如彈簧的結算,Shape&Length Keeping結算等…
- 碰撞運算
- World2Local,Local2World之類的座標系轉換的運算。
在這其中,ConstraintSolve佔大頭,碰撞其次,座標系轉換運算再次。
值得注意的是,由於Verlet Integration
的特點,導致計算的時間步長必須是固定的。這也就導致了在某些情況如果幀率忽然降低的話,會導致下次的迭代次數增加,從而導致下次的迭代計算時間變得更大,從而惡化下下次的迭代計算時間。
因此,如果專案如果在鎖幀方面有特殊需求的話,需要額外注意這一塊的問題。
優化方案
硬優化
首先是硬優化,一些細枝末節的優化方案(例如距離判斷不要進行開方而是直接判斷其平方的大小關係之類的方案)就不提了。有一塊我做了額外的處理,也就是膠囊體的優化。
膠囊體的碰撞計算需要首先計算出膠囊體的Transform資訊,也就是說Position,Rotation,Scale。其中實際上Scale資訊可以約定好不進行採用。
這樣一來,我們需要獲得Position資訊與Rotation資訊,但是這麼說來在結算過程中,需要額外考慮膠囊體的朝向,這樣也就意味著多了不少的計算操作。
因此我建立了球形的碰撞體,並且與團隊成員約定在保證效果的前提下,儘量多使用球形碰撞體來進行檢測碰撞,這樣可以節省一些效能。
還有,可以定義對應粒子的懶惰程度。每個粒子在運算過程中,可以大略估計出其懶惰程度 —— 如果該粒子經常與碰撞體產生碰撞,並且involve進了很多的彈簧系統中,那麼我們認為這個粒子是很勤快的;否則,我們認為這個粒子比較懶惰。這樣,可以減少一些粒子的迭代次數。
最後就是多執行緒優化了。這一塊在我的HairStrandPlugin中也有提到,這裡不再贅述。
軟優化
然後是軟優化。首先,我將最大迭代次數變為一個可調整量。
為什麼要這麼做呢?因為在迭代過程中,往往第一次迭代的效果是最大的,而越往後的迭代只是將其往最精確的位置推一點點而已。而在Verlet Integration
的邏輯下,哪怕是推一丁點位置,也需要一次完全的迭代。
這也就意味著對於一些並不需要那麼精確的地方,我們可以將最大迭代次數設為一個較小的值,甚至為1。
然後,我們鼓勵美術將動態骨骼在合理的條件下儘量分配為多個DynamicBone
。這一塊是為了碰撞的計算優化。以裙子為例,如果是一整個DynamicBone
,那麼即便是左腿左側的那幾根骨骼,也需要和右腿進行碰撞檢測(儘管他們永遠不可能碰撞)。
但是如果分成兩塊DynamicBone
,那麼就可以避免掉一些完全不需要的碰撞檢測,從而提升效能。
優化結果
最終的優化結果,可以在紅米3s上,支撐300+的動態骨骼的實時結算(這還是多執行緒優化未實裝的測試結果,如果多執行緒優化實裝上去,估計效能會更好)。
總結
讀者可能會覺得我一直在吐槽DynamicBone
的不足之處,但是其實不是的,DynamicBone
的工具鏈適配思想這一塊是我認為非常漂亮的地方。
我所做的工作也是將其迭代成為更加適合我們專案的工具,畢竟原生的DynamicBone
並不是為了我們這個專案而開發的,它要考慮的是更加普遍的適用性。
但是反過來想,將普適的工具迭代為適合專案的特殊工具,這似乎也很了不得呢(此處應有掌聲)!
<全文完>