Unity中BVH骨骼动画驱动的可视化理论与实现
前言
找了很久使用BVH到unity中驅動骨骼動畫的代碼,但是都不是特別好用,自己以前寫過,原理很簡單,這里記錄一下。
理論
初始姿態
在BVH或者其它骨骼動畫中,一般涉及到三種姿勢:A-pose,T-pose,其它姿勢。其中A-pos或者T-pos通常是作為骨骼定義的姿勢或者第一幀骨骼姿勢。
比如在unity中,導入某個模型時,通常為T-pose的姿態,如unity娘模型剛導入的時候:
這個Tpose在CMU提供的BVH骨骼動畫數據中,第一幀數據也是T-pose,比如:
但是在Maya中設計角色,或者做服裝綁定的時候,有些模型需要保持Apose的姿態,如:
驅動理論
因為使用BVH數據在unity中驅動角色,而unity中的角色大部分是基于Tpose的,因而可以利用Tpose作為中間轉換姿態,將BVH中的所有動畫幀全部遷移到unity中。
unity動作模型必須有與BVH一致的動作,否則不適用此博客的理論,如果unity角色與BVH角色的一致動作非Tpose,也可以按照博客理論去轉換。
整個轉換流程如下圖所示
為了解釋上圖,我們使用以下數據為示例:
- BVH數據:CMU動捕數據BVH格式
- Unity數據:Unity-Chan! Model
內置姿態
所謂內置姿態,即模型的所有骨骼旋轉量為0,直接可視化其關節位置得到的結果
BVH的內置姿態可通過bvhacker的setT按鈕可視化(或者對我比較熟悉的人知道我前面關于動捕的博客有提供可matlab版本的可視化代碼):
unity-chan的內置姿態可視化就需要通過代碼將所有的關節旋轉量設置為單位四元數,得到所有
變換矩陣T1/T2
這一步在BVH和unity中都涉及到一個變換矩陣,如果內置姿態本來就是Tpose,那么變換矩陣就是單位陣。如果不是,那么也可以直接獲取到。
CMU提供的BVH骨骼動畫中,第一幀通常就是Tpose,所以直接計算出第一幀每個關節的全局旋轉矩陣就是對應的Tpose變換矩陣T1,記住:
當前關節全局旋轉=父關節全局旋轉×當前關節局部旋轉當前關節全局旋轉=父關節全局旋轉\times 當前關節局部旋轉 當前關節全局旋轉=父關節全局旋轉×當前關節局部旋轉
unity中骨骼動畫因為導入以后通常就是T姿勢,所以直接獲取所有關節最開始的全局旋轉矩陣就是對應的Tpose變換矩陣T2
變換矩陣T3
BVH中記錄了很多動畫數據,這些數據都以歐拉角的形式存儲,其中根關節額外多了一個位置信息,按照根關節的坐標信息、關節層級關系、關節相對父關節偏移量(BVH初始姿態)、關節局部旋轉量就能推出所有關節的坐標位置:
當前關節坐標=父關節坐標+父關節全局旋轉?當前關節相對于父關節定義的偏移量當前關節坐標=父關節坐標+父關節全局旋轉*當前關節相對于父關節定義的偏移量 當前關節坐標=父關節坐標+父關節全局旋轉?當前關節相對于父關節定義的偏移量
簡而言之:這個T3就是BVH所記錄的所有動畫幀的根關節坐標以及各關節的旋轉數據。
變換矩陣T4
因為我們是以Tpose為媒介,將BVH動作遷移到unity中,這個T4能夠達到這種效果:
BVHT×T4=UNITYT×T4\text{BVH}_T\times T4 = \text{UNITY}_T\times T4 BVHT?×T4=UNITYT?×T4
注意上面的=代表姿勢相等,不是關節坐標相等。
怎么找到這個T4呢,非常簡單,這樣想:先將內置姿態通過T1變換成Tpose,然后再通過T4轉換成動畫姿態,那么原始BVH記錄的全局旋轉量就應該等于T4和T2的累計旋轉量。
T3=T4×T2T3 = T4\times T2 T3=T4×T2
所以
T4=T3?T2?1T4=T3*T2^{-1} T4=T3?T2?1
變換矩陣T5
這個就是我們最終需要應用到unity每個關節的旋轉數據
T5=T4×T1T5 = T4\times T1 T5=T4×T1
位置調整
準確來說還有一步是調整人體的位置,因為BVH的人物大小和unity的人物大小不同,所以可以根據某根骨骼的長度計算一下縮放比例,然后對BVH的根關節位置乘以對應縮放比例就是unity人物的對應位置了。
實現
完整代碼在github中獲取,公眾號和CSDN都有寫地址。
使用BVHTool這個工程里面讀取BVH數據的代碼以及對應的數據結構進行后續開發。
核心代碼有:
-
獲取關節父子關系:
public Dictionary<string,string> getHierachy(){Dictionary<string, string> hierachy = new Dictionary<string, string>();foreach (BVHBone bb in boneList){foreach (BVHBone bbc in bb.children){hierachy.Add(bbc.name, bb.name);}}return hierachy;} -
歐拉角轉四元數(要注意你的bvh數據是不是ZYX記錄的,如果不是這個,請自行書寫轉換代碼,但是一定要轉成全局旋轉量即可):
private Quaternion eul2quat(float z, float y, float x) {z = z * Mathf.Deg2Rad;y = y * Mathf.Deg2Rad;x = x * Mathf.Deg2Rad;// 動捕數據是ZYX,但是unity是ZXYfloat[] c = new float[3];float[] s = new float[3];c[0] = Mathf.Cos(x / 2.0f); c[1] = Mathf.Cos(y / 2.0f); c[2] = Mathf.Cos(z / 2.0f);s[0] = Mathf.Sin(x / 2.0f); s[1] = Mathf.Sin(y / 2.0f); s[2] = Mathf.Sin(z / 2.0f);return new Quaternion(c[0] * c[1] * s[2] - s[0] * s[1] * c[2],c[0] * s[1] * c[2] + s[0] * c[1] * s[2],s[0] * c[1] * c[2] - c[0] * s[1] * s[2],c[0] * c[1] * c[2] + s[0] * s[1] * s[2]); } -
獲取關鍵幀的全局旋轉數據:
public Dictionary<string,Quaternion> getKeyFrame(int frameIdx) {Dictionary<string, string> hierachy = getHierachy();Dictionary<string, Quaternion> boneData = new Dictionary<string, Quaternion>();boneData.Add("pos", new Quaternion(boneList[0].channels[0].values[frameIdx],boneList[0].channels[1].values[frameIdx],boneList[0].channels[2].values[frameIdx],0));boneData.Add(boneList[0].name, eul2quat(boneList[0].channels[3].values[frameIdx],boneList[0].channels[4].values[frameIdx],boneList[0].channels[5].values[frameIdx]));foreach (BVHBone bb in boneList){if (bb.name != boneList[0].name){Quaternion localrot = eul2quat(bb.channels[3].values[frameIdx],bb.channels[4].values[frameIdx],bb.channels[5].values[frameIdx]);boneData.Add(bb.name, boneData[hierachy[bb.name]] * localrot);} } return boneData; } -
獲取骨骼定義時候,每個關節相對于父關節的偏移量:
public Dictionary<string,Vector3> getOffset(float ratio) {Dictionary<string, Vector3> offset = new Dictionary<string, Vector3>();foreach(BVHBone bb in boneList){offset.Add(bb.name, new Vector3(bb.offsetX * ratio, bb.offsetY * ratio, bb.offsetZ * ratio));}return offset; } -
獲取BVH的T姿態變換矩陣(如果你的第一幀不是T,內置姿態就是T,那么這個變換矩陣就是單位陣)
bvhT = bp.getKeyFrame(0); -
根據BVH的Tpose和BVH其它動畫幀的旋轉量,以及unity的Tpose變換矩陣,求解unity驅動所需的全局旋轉:
if (FirstT) {Transform currBone = anim.GetBoneTransform(bm.humanoid_bone);currBone.rotation = (currFrame[bm.bvh_name] * Quaternion.Inverse(bvhT[bm.bvh_name])) * unityT[bm.humanoid_bone]; } else {Transform currBone = anim.GetBoneTransform(bm.humanoid_bone);currBone.rotation = currFrame[bm.bvh_name] * unityT[bm.humanoid_bone]; }FirstT代表BVH數據第一幀是Tpose,否則內置姿態為Tpose。在工程中提供了temp.bvh和13_29.bvh分別代表內置T和第一幀T的bvh數據例子。
代碼運行結果:
紅色為BVH可視化,unity-chan為驅動結果。
后記
理論超級簡單,不過需要注意,BVH全局旋轉的計算一定要正確,不同動捕設備定義的旋轉軸順序不同。
完整的unity實現放在微信公眾號的簡介中描述的github中,有興趣可以去找找,同時文章也同步到微信公眾號中,有疑問或者興趣歡迎公眾號私信。
[外鏈圖片轉存中…(img-FEg8MV6D-1610182566145)]
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Unity中BVH骨骼动画驱动的可视化理论与实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡销卡会影响信用吗?看完再也不敢乱办
- 下一篇: 卡通驱动项目ThreeDPoseTrac