卡通驱动项目ThreeDPoseTracker——模型驱动解析
前言
之前解析過ThreeDPoseTracker這個項目中的深度學習模型,公眾號有兄弟私信一些問題,我剛好對這個項目實現有興趣,就分析一波源碼,順便把問題解答一下。
這個源碼其實包括很多內容:3D姿態估計,坐標平滑,骨骼驅動,物理仿真等,非常值得分析。
參考博客:
ThreeDPoseTracker源碼
理論與實現
核心代碼是源碼中的VNectModel.cs,主要是用預測出的3D坐標驅動卡通人體模型,包括內容有:
- 根關節位置
- 各關節旋轉信息
其核心在于旋轉量的確定,至于根關節位置的確定,感覺涉及到很多亂七八糟的內置參數,就不詳細介紹了,但是會額外提供一個我用到天荒地老的計算方法。
如果下面的理論看不懂,推薦看看我按照源碼復現的一套簡化流程,一千行源碼直接重寫成兩百多行。
預備知識——“LookRotation”
源碼中有個至關重要的函數LookRotation(a,b),它的作用是:
- 使得z軸(藍色)始終精準指向a方向
- 使得y軸(綠色)始終偏向b方向
為什么一個是精準指向,一個是偏向,因為y和z軸是垂直的,如果a和b不垂直,那么此函數就會保證z與a同方向,y和b大致同向,看圖
正方體為物體,藍色和綠色分別為y和z軸,兩個小球分別為y和z的目標方向。
左圖為標準的指向,中間和右圖為調整了目標方向后,物體的y和z的指向,可以發現,藍色z軸始終指向目標,但是綠色y軸是偏向那個方向,因為是立體圖,看著綠軸偏的很遠,其實是差不多的。
總而言之,藍軸始終是向著oa方向,綠軸向著ob與oa組成的平面中與oa垂直的方向。
驅動問題解讀
如果掃過一眼代碼,會發現有很多重復代碼,無外乎以下幾類:
初始化Init()的時候有:
AddSkeleton(xx,yy) xx.Inverse = Quaternion.Inverse(Quaternion.LookRotation(xx.position - xxx.position, yy)); xx.InverseRotation = xx.Inverse*xx.InitRotation驅動PoseUpdate的時候有:
xx = TriangleNormal(aa,bb,cc) xx.rotation = Quaternion.LookRotation(yy) * xx.InverseRotation;會發現初始化和驅動時候貌似是一種反向計算,所以才出現這么多逆(inverse)。
為什么要用LookRotation計算各個關節的旋轉,而非對某根骨骼直接通過Quaternion.FromToRotation(a,b)計算出初始姿態到新的姿態下需要的旋轉矩陣呢?
- 如果只用FromToRotation計算向量到向量的旋轉,只能控制位置正確,無法控制方向正確,比如根關節到頸部是直著上去的,這時候人也可以側身也可以面向前方,所以對關節必須控制至少兩個方向的旋轉,因此必須使用LookRotation去控制z和y軸朝向。
為什么用了LookRotation還要求這么多逆(inverse)?
- 同一個模型的不同關節具有不同的初始旋轉量(即InitRotation),而且不同人的同一個關節也可能具有不同的旋轉量,甚至初始的局部坐標軸都不同,比如源碼中提供的兩個模型的膝蓋部分局部坐標系如下
此時如果用LookRotation,不同的人就需要設置對應的規則,比如同一個姿態,左邊的人藍色z軸向后,右邊人藍色z軸向前,搞不好還有其它的情況,此時就無法用基于LookRotation的同一套代碼去驅動這個人了。
如果還是不懂為什么不能用同一套代碼驅動,可以舉個例子:小腿向后收起的時候,左圖的LookRotation必須保證藍軸向上,右圖必須保證藍軸向下,如下圖:
由于坐標軸最終的方向都不同,所以即使是同一個姿勢,對于具有不同坐標系的相同關節也需要針對性LookRotation。
那么為什么源碼里面可以使用LookRotation去玩驅動,很簡單,因為源碼將所有的關節都利用初始姿態做了LookRotation的對齊,得到了一個中間矩陣,即源碼中的xx.InverseRotation,利用這個中間矩陣就能在驅動的時候對齊所有坐標系,達到通用目的。
源碼分析與復現
如何實現上述問題中的坐標系對齊呢?
利用初始姿態下各關節的坐標和旋轉來確定,具體是:
當前關節的lookrotation=初始旋轉InitRotation×對齊矩陣當前關節的lookrotation = 初始旋轉InitRotation \times 對齊矩陣 當前關節的lookrotation=初始旋轉InitRotation×對齊矩陣
所以源碼中的下面類似代碼就是為了求解對齊矩陣:
看不懂就可以寫成
Quaternion.LookRotation(xx.position - xxx.position, yy) = xx.InitRotation * Quaternion.Inverse(xx.InverseRotation)這就對應上述公式,其中Quaternion.Inverse(xx.InverseRotation)就對應了對齊矩陣。
因此我在復現時候,以根關節的對齊矩陣為例,把上述代碼改成了
root = animator.GetBoneTransform(HumanBodyBones.Hips); midRoot = Quaternion.Inverse(root.rotation) * Quaternion.LookRotation(forward);其中forward是按照源碼的要求,指示人體的當前方向。
如何設置LookRotation的方向?
繼續分析源碼,發現對于所有關節都做了
var forward = TriangleNormal(jointPoints[PositionIndex.hip.Int()].Transform.position, jointPoints[PositionIndex.lThighBend.Int()].Transform.position, jointPoints[PositionIndex.rThighBend.Int()].Transform.position); jointPoint.Inverse = GetInverse(jointPoint, jointPoint.Child, forward);jointPoint.InverseRotation = jointPoint.Inverse * jointPoint.InitRotation;第一行,基于根關節和左右胯關節坐標計算出人體朝向,然后以此作為所有關節的LookRotation的y方向,以及每個關節與其子關節的方向作為z方向,計算出中間矩陣。
注意,在接下來,分別對頭部和手掌單獨又計算了一遍,因為他倆比較特殊
對于頭部,直接求解出頭到鼻子的向量作為LookRotation的z方向,未設置y方向。
var gaze = jointPoints[PositionIndex.Nose.Int()].Transform.position - jointPoints[PositionIndex.head.Int()].Transform.position; // head的方向是head->Nose head.Inverse = Quaternion.Inverse(Quaternion.LookRotation(gaze));然后計算頭部的中間矩陣
head.Inverse = Quaternion.Inverse(Quaternion.LookRotation(gaze));head.InverseRotation = head.Inverse * head.InitRotation;對于手腕,直接利用手腕、大拇指、中指的坐標,計算出手掌方向作為LookRotation的y方向,
var lf = TriangleNormal(lHand.Pos3D, jointPoints[PositionIndex.lMid1.Int()].Pos3D, jointPoints[PositionIndex.lThumb2.Int()].Pos3D); // 手掌方向 var rf = TriangleNormal(rHand.Pos3D, jointPoints[PositionIndex.rThumb2.Int()].Pos3D, jointPoints[PositionIndex.rMid1.Int()].Pos3D);而左手腕以大拇指到中指的方向為z方向,而右手腕以中指到大拇指方向為z方向:
lHand.Inverse = Quaternion.Inverse(Quaternion.LookRotation(jointPoints[PositionIndex.lThumb2.Int()].Transform.position - jointPoints[PositionIndex.lMid1.Int()].Transform.position, lf)); rHand.Inverse = Quaternion.Inverse(Quaternion.LookRotation(jointPoints[PositionIndex.rThumb2.Int()].Transform.position - jointPoints[PositionIndex.rMid1.Int()].Transform.position, rf));再分別求解出中間矩陣:
lHand.InverseRotation = lHand.Inverse * lHand.InitRotation; rHand.InverseRotation = rHand.Inverse * rHand.InitRotation;其實完全沒必要區分這么明顯,只需要求解和使用的時候對應好就行了,比如我實現的時候就統一大拇指到中指:
midLhand = Quaternion.Inverse(lhand.rotation) * Quaternion.LookRotation(lthumb2.position - lmid1.position,TriangleNormal(lhand.position, lthumb2.position, lmid1.position)); midRhand = Quaternion.Inverse(rhand.rotation) * Quaternion.LookRotation(rthumb2.position - rmid1.position,TriangleNormal(rhand.position, rthumb2.position, rmid1.position));也就是說,對于某些特定關節,需要單獨設置用于計算中間變換矩陣的LookRotation信息。推薦看我實現的源碼,分為軀干、頭、手掌三個部分,我實現的源碼就不貼了,文末找。
注意這里計算初始姿態中各關節的LookRotation方法與運行時從深度學習預測的3D關節坐標中計算的LookRotation方案要一模一樣。
如何驅動?
通過
當前關節的lookrotation=初始旋轉InitRotation×對齊矩陣當前關節的lookrotation = 初始旋轉InitRotation \times 對齊矩陣 當前關節的lookrotation=初始旋轉InitRotation×對齊矩陣
得到了每個關節的對齊矩陣,那么這個公式很容易得到每個關節的當前旋轉信息:
當前旋轉Rotation=當前關節的lookrotation×Quaternion.Inverse(對齊矩陣)當前旋轉Rotation = 當前關節的lookrotation \times Quaternion.Inverse(對齊矩陣) 當前旋轉Rotation=當前關節的lookrotation×Quaternion.Inverse(對齊矩陣)
然后分析源碼,在PoseUpdate()函數中,前面的不用看,是計算根關節坐標的,我們先關注關節旋轉。
注意因為用對齊矩陣是從初始姿態獲取的,所以如何依據初始姿態計算的lookrotation就要按照同樣的方法從深度學習模型預測的3D關節坐標中計算對應的lookrotation參數。
比如人體方向依舊是根、左右胯部的坐標計算:
var forward = TriangleNormal(jointPoints[PositionIndex.hip.Int()].Pos3D, jointPoints[PositionIndex.lThighBend.Int()].Pos3D, jointPoints[PositionIndex.rThighBend.Int()].Pos3D);而根關節當前的旋轉就是根據上述公式計算得到:
jointPoints[PositionIndex.hip.Int()].Transform.rotation = Quaternion.LookRotation(forward) * jointPoints[PositionIndex.hip.Int()].InverseRotation;其它關節我不貼源碼了,直接描述:
軀干關節:以身體方向為LookRotation的y方向,以當前關節到其子關節的方向為z方向。
手腕:以手腕、大拇指、中指形成的平面的法線方向為y方向,以拇指到中指的方向為z方向。
比如我隨便貼一下我復現的左臂(肩、肘、腕)實時驅動:
// 左臂 lshoulder.rotation = Quaternion.LookRotation(pred3D[5] - pred3D[6], forward) * Quaternion.Inverse(midLshoulder); lelbow.rotation = Quaternion.LookRotation(pred3D[6] - pred3D[7], forward) * Quaternion.Inverse(midLelbow); lhand.rotation = Quaternion.LookRotation(pred3D[8] - pred3D[9],TriangleNormal(pred3D[7], pred3D[8], pred3D[9]))*Quaternion.Inverse(midLhand);其中pred3D就是深度學習模型預測的3D關節坐標。
人體位置
上述講解了旋轉的計算方法,關于整個人體的位置,源碼中自有一套方案,但是里面預設了很多固定參數,不是特別想分析,所以用了萬年不變的方法,計算unity人物模型腿的長度和深度學習預測的腿部長度,然后計算比例系數,乘到深度學習預測的根關節位置即可。
float tallShin = (Vector3.Distance(pred3D[16], pred3D[17]) + Vector3.Distance(pred3D[20], pred3D[21]))/2.0f; float tallThigh = (Vector3.Distance(pred3D[15], pred3D[16]) + Vector3.Distance(pred3D[19], pred3D[20]))/2.0f; float tallUnity = (Vector3.Distance(lhip.position, lknee.position) + Vector3.Distance(lknee.position, lfoot.position)) / 2.0f +(Vector3.Distance(rhip.position, rknee.position) + Vector3.Distance(rknee.position, rfoot.position)); root.position = pred3D[24] * (tallUnity/(tallThigh+tallShin));是不是超級簡單,雖然效果有點偏差,但是后續還是會分析一下源碼中更新人體位置的方案。
復現流程
在VNectModel.cs中的PoseUpdate函數加入以下代碼:
FileStream fs = new FileStream(@"D:\code\Unity\ThreeDExperiment\Assets\Resources\record.txt", FileMode.Append); StreamWriter sw = new StreamWriter(fs); //寫入 foreach(JointPoint jointPoint in jointPoints) {sw.Write(jointPoint.Pos3D.x.ToString() + " " + jointPoint.Pos3D.y.ToString() + " " + jointPoint.Pos3D.z.ToString() + " "); } sw.WriteLine(); sw.Flush(); sw.Close(); fs.Close();將關鍵點寫入到txt中做復現時候用的3D關鍵點數據
然后按照理論進行復現后效果如下:
紅色為預測的3D坐標,人物模型會做出與紅色骨架一樣的姿勢。
結論
這個感覺還是沒有考慮人體運動的動力學特性,如果跑過源碼,很容易發現個別姿勢會出現奇怪的關節扭曲現象,這就是不考慮動力學的后果,給我自己之前的代碼打一波廣告,那個絕對比這個好,哈哈。
完整的unity實現放在微信公眾號的簡介中描述的github中,有興趣可以去找找。或者在公眾號回復“ThreeDPose",同時文章也同步到微信公眾號中,有疑問或者興趣歡迎公眾號私信。
總結
以上是生活随笔為你收集整理的卡通驱动项目ThreeDPoseTracker——模型驱动解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Unity中BVH骨骼动画驱动的可视化理
- 下一篇: 卡通驱动项目ThreeDPoseTrac