【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂于分享也博采眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!
這是侑虎科技第1763篇文章,感謝作者楊超wantnon供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群:793972859)
作者主頁:
https://www.zhihu.com/people/wantnon
(本文使用UE 4.27的版本)
早先看到下面用Niagara實現鏈條的教程:
Unreal Niagara - Tentacle effect with physics simulation (FULL TUTORIAL)
https://www.youtube.com/watch?v=vYaHl5bf4ww&t=1934s
后來又看到個進一步的,在此基礎上加了蒙皮:
Rigging With Niagara - Part 1: Dangling Cable - Direct Interpolation
https://www.youtube.com/watch?v=r7c1s7Mwe9w
自己照著做了一遍,發現這方法有很多問題,于是又想了個改進方案。
一、Niagara鏈條
首先根據教程(Unreal Niagara - Tentacle effect with physics simulation (FULL TUTORIAL))實現基礎的鏈條效果,其中幾個要點:
1. 物理迭代是用P osition Based方法,就是對于鏈條上每個點,計算其相對于各鄰接點的平衡位置偏離了多少,偏離多少就移回多少,如果有多個鄰接點,則將分別計算的移回向量相加取平均,作為最終移回量,公式如下:
2. 在這個鏈條模擬中,我們把起始粒子的前驅認為是自身,即存在linkPos=pos的情況,所以上式分母中+0.0001就十分必要,否則會因除以零而產生NAN。
3. 物理迭代通過添加Simulation Stage實現(只有在GPU Sim下才能用),Simulation Stage中可以設置迭代次數,不同的約束可能需要的迭代次數不同,可以分成多個Stage去做,每個Stage單獨設置迭代次數。對于上述長度約束,我試了下需要4次才能效果較好。
4. Calculate Accurate Velocity這一步,若非教程里說了,自己可能想不到。另外Calculate Accurate Velocity可以從迭代次數為4的模擬Stage里移出來,放到后面迭代次數為1的Stage里。
至此,如果是要做鎖鏈之類的效果,只需把粒子替換成圓環模型,就可以了。但我們的目的是想做蒙皮。
二、鏈條蒙皮
教程(Rigging With Niagara - Part 1: Dangling Cable - Direct Interpolation)講得比較籠統,但從中還是能大致了解其思路。要點如下:
1. 把模擬出的鏈條上各粒子坐標寫到一個ParticleCount x 1像素的RT上,傳給材質。并借助RT的雙線性插值實現平滑。
2. MeshRenderer的SourceMode要由Particles改為Emitter,這樣就不會為每個粒子掛一個Mesh,而是整個Emitter掛一個Mesh:
3. 沿著繩子建立切空間,則無論繩子怎么彎曲,蒙皮上各點的切空間坐標不變,利用這點計算蒙皮各點的世界坐標,然后WPO。這一步在材質中進行。
其中3存在一個問題,就是“沿著繩子建立切空間”,即使在鎖定初始點的切空間的情況下,也有無數種結果。
教程中取的是:
T=normalize(p_nxt-p),
B=normalize(cross(T, (1,0,0) )),
N=cross(B,T)。
但這種算法并不能保證當繩子任意彎曲時,相鄰質點的切空間足夠接近,或者說相鄰質點的法線是漸變的。
在劇烈擺動時會出問題,如下視頻所示:
于是針對如何對空間曲線生成法線這個問題,搜了一下,下面這個還算吻合:
opengl es - Vector math, finding co?rdinates on a planar between 2 vectors - Stack Overflow
https://stackoverflow.com/questions/4504331/vector-math-finding-co%c3%b6rdinates-on-a-planar-between-2-vectors/4505658#4505658
主要就是說明給曲線生成漸變的法線,只考慮局部就不行了,需要考慮整體,他給出的代碼是其中最簡單的一種方法,就是用上一個粒子的法線去算下一個粒子的法線。
我按這個簡單實現試了一下,發現效果仍不理想,原因可能有兩點:
1. 我的分段太稀疏,導致即使考慮了上一個粒子的法線影響,仍然會出現劇烈轉折。
2. 此算法嚴格來講需順序執行,而Niagara中GPU粒子的執行順序是亂序的。
于是我暫時放棄了這種基于數學的思路,轉而考慮基于物理的思路。
就想是不是可以引入扭矩約束,從而避免相鄰粒子朝向差異過大,即避免擰麻花。
三、改進:防止扭轉、翻轉
我一開始想的是給粒子添加旋轉屬性,然后想著在模擬階段修正位置的同時對旋轉也作一個修正,但我不知道依據什么更新粒子的旋轉屬性,于是放棄這個思路。
然后想到,如果升維,把繩子看成有內部結構,如圖:
則有可能僅 通過對距離作約束就同時起到防止扭轉的作用。
繩子截面邊數自然是越多越好,擺動起來各方向物理性質更均等,但粒子數也會成倍增加,所以我只試了正三角形截面和正方形截面。出于簡單,以下以正三角形截面為例。
如上圖, 如果僅像(a)那樣連接,側面是平行四邊形,不穩定,可能出現壓扁、扭轉、翻轉三種情況,如圖所示:
為側面 添加支架,變成(b),就穩定多了,壓扁和扭轉都能得到限制,但仍會出現上下翻轉問題,如下視頻所示:
之所以出現這種情況,是因為以截面為鏡面,上下對稱的兩個點的長度一樣,所以從距離約束的角度講,上下兩個位置都是滿足條件的解。為了避免這個問題,對隔一層粒子也添加距離約束,如圖(c)(d)所示,其中(d)在隔層之間作了全約束,而(c)只作了交叉約束,經試驗發現(c)就夠用了。
改為(c)后,上下翻轉不會發生了,但仍存在自穿插翻轉,如下視頻所示:
要解決自穿插翻轉,通過距離約束貌似是不行了(我沒想到),但觀察自穿插翻轉的特點,容易看出當發生自穿插翻轉時,三條側邊一定不再平行。所以想到,如果加一個每段三棱柱三條側邊平行的約束,應該就可以避免自穿插翻轉了。具體算法如下:
pos_new=pos_P +safeNormalize(c-c_P)*distance(pos-pos_P)
注:這里只讓當前粒子pos所關聯的上游邊pos_P->pos與上游中軸線c_P->c平行,沒有讓pos所關聯的下游邊pos->pos_N與下游中軸線c->c_N平行。因為實際試驗發現上下游都處理效果不好,原因可能是當扭轉發生時,如果想通過移動當前粒子緩解,讓上游平行和讓下游平行移動方向必定是沖突的,所以不如只考慮上游。
效果:
可見,無論怎么動,都不會出問題了。
有了魯棒的三棱柱繩子,就可以算出穩定的法線,這里我們是用第一條棱與相應的截面中心點作差作為法線,然后就可以在材質里構建穩定的切空間,進行蒙皮:
之前 是把Position存到了RT里傳入材質,現在還要增加一張RT,用來存法線。
蒙皮后效果:
四、正確的蒙皮法線
上面視頻中繩子的明暗是不正確的,因為沒有考慮繩子彎曲對模型頂點法線的影響。
正確的蒙皮法線計算方法如下:
(1)繩子Tpos切空間基:
T0=(0,0,-1)
N0=(1,0,0)
B0=cross(N0,T0)
(2)vertexNormal在繩子Tpos切空間的坐標:
vertexNormalProjOnT0=dot(vertexNormal,T0)
vertexNormalProjOnN0=dot(vertexNormal,N0)
vertexNormalProjOnB0=dot(vertexNormal,B0)
(3)繩子Bendpos切空間基:
T=采樣posRT并差分得到
N=采樣normRT得到
B=cross(N,T)
(4)根據蒙皮法線Bend前后切空間坐標不變,計算Bend后的頂點法線vertexNormalBend:
vertexNormalBendProjOnT=vertexNormalProjOnT0
vertexNormalBendProjOnN=vertexNormalProjOnN0
vertexNormalBendProjOnB=vertexNormalProjOnB0
vertexNormalBend=mul(vertexNormalBendProjOnT,T)+mul(vertexNormalBendProjOnN,N)+mul(vertexNormalBendProjOnB,B)
最后還有一個細節問題,就是在沒有Bend發生時,Bendpos切空間應該是與Tpos切空間完全重合的。未Bend時T由差分求出來是豎直向下,這和T0=(0,0,-1)吻合,沒問題,但未Bend時normRT采樣出來的N是否也與N0=(1,0,0)重合,就要看一下了。前面說過,我們是用第一條棱與相應的截面中心點作差作為法線N,那也就是說我們要保證在不Bend時第一條棱與截面中心作差指向X軸,所以截面應如下構建:
修正蒙皮法線后效果:
五、補充
1. Niagara的Custom HLSL中語法注意事項:
(1)錯誤:int index=index%10;
正確:int index=index %10;//%前要有空格。
(2)錯誤:NiagaraID idList[2]={PID,NID};
正確:NiagaraID idList[2]={ PID,NID };// { 后和 } 前要有空格
報錯信息去Output Log窗口看,那里才是詳細信息。
2. DebugDraw節點中,不要用drawSphere,其編譯時間很長,drawLine和drawBox都編譯很快。
3. 如果希望不同鏈條約束強度有差異,或者方便忽略掉一些鏈條(權重給0),可以用加權版本公式:
4. Slover模塊連線過多,一堆GetVectorByID+Custom HLSL節點:
可用如下方法簡化:
打開生成的Shader Code:
搜GetVectorByID,可以搜到:
聲明處:
使用處:
所以GetVectorByID可以寫到Custom HLSL里面:
所以眾GetVectorByID+Custom HLSL可合并成一個大的CustomHLSL:
需要注意的一點是,需要保留一個GetVectorByID("Position")節點,以便生成函數聲明,否則會報錯。
同理,Setup模塊也可做同樣簡化:
文末,再次感謝 楊超wantnon 的分享, 作者主頁: https://www.zhihu.com/people/wantnon, 如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群: 793972859 )。
近期精彩回顧
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.