卡通驱动项目ThreeDPoseTracker——模型驱动解析
前言
之前解析過ThreeDPoseTracker這個(gè)項(xiàng)目中的深度學(xué)習(xí)模型,公眾號(hào)有兄弟私信一些問題,我剛好對這個(gè)項(xiàng)目實(shí)現(xiàn)有興趣,就分析一波源碼,順便把問題解答一下。
這個(gè)源碼其實(shí)包括很多內(nèi)容:3D姿態(tài)估計(jì),坐標(biāo)平滑,骨骼驅(qū)動(dòng),物理仿真等,非常值得分析。
參考博客:
ThreeDPoseTracker源碼
理論與實(shí)現(xiàn)
核心代碼是源碼中的VNectModel.cs,主要是用預(yù)測出的3D坐標(biāo)驅(qū)動(dòng)卡通人體模型,包括內(nèi)容有:
- 根關(guān)節(jié)位置
- 各關(guān)節(jié)旋轉(zhuǎn)信息
其核心在于旋轉(zhuǎn)量的確定,至于根關(guān)節(jié)位置的確定,感覺涉及到很多亂七八糟的內(nèi)置參數(shù),就不詳細(xì)介紹了,但是會(huì)額外提供一個(gè)我用到天荒地老的計(jì)算方法。
如果下面的理論看不懂,推薦看看我按照源碼復(fù)現(xiàn)的一套簡化流程,一千行源碼直接重寫成兩百多行。
預(yù)備知識(shí)——“LookRotation”
源碼中有個(gè)至關(guān)重要的函數(shù)LookRotation(a,b),它的作用是:
- 使得z軸(藍(lán)色)始終精準(zhǔn)指向a方向
- 使得y軸(綠色)始終偏向b方向
為什么一個(gè)是精準(zhǔn)指向,一個(gè)是偏向,因?yàn)閥和z軸是垂直的,如果a和b不垂直,那么此函數(shù)就會(huì)保證z與a同方向,y和b大致同向,看圖
正方體為物體,藍(lán)色和綠色分別為y和z軸,兩個(gè)小球分別為y和z的目標(biāo)方向。
左圖為標(biāo)準(zhǔn)的指向,中間和右圖為調(diào)整了目標(biāo)方向后,物體的y和z的指向,可以發(fā)現(xiàn),藍(lán)色z軸始終指向目標(biāo),但是綠色y軸是偏向那個(gè)方向,因?yàn)槭橇Ⅲw圖,看著綠軸偏的很遠(yuǎn),其實(shí)是差不多的。
總而言之,藍(lán)軸始終是向著oa方向,綠軸向著ob與oa組成的平面中與oa垂直的方向。
驅(qū)動(dòng)問題解讀
如果掃過一眼代碼,會(huì)發(fā)現(xiàn)有很多重復(fù)代碼,無外乎以下幾類:
初始化Init()的時(shí)候有:
AddSkeleton(xx,yy) xx.Inverse = Quaternion.Inverse(Quaternion.LookRotation(xx.position - xxx.position, yy)); xx.InverseRotation = xx.Inverse*xx.InitRotation驅(qū)動(dòng)PoseUpdate的時(shí)候有:
xx = TriangleNormal(aa,bb,cc) xx.rotation = Quaternion.LookRotation(yy) * xx.InverseRotation;會(huì)發(fā)現(xiàn)初始化和驅(qū)動(dòng)時(shí)候貌似是一種反向計(jì)算,所以才出現(xiàn)這么多逆(inverse)。
為什么要用LookRotation計(jì)算各個(gè)關(guān)節(jié)的旋轉(zhuǎn),而非對某根骨骼直接通過Quaternion.FromToRotation(a,b)計(jì)算出初始姿態(tài)到新的姿態(tài)下需要的旋轉(zhuǎn)矩陣呢?
- 如果只用FromToRotation計(jì)算向量到向量的旋轉(zhuǎn),只能控制位置正確,無法控制方向正確,比如根關(guān)節(jié)到頸部是直著上去的,這時(shí)候人也可以側(cè)身也可以面向前方,所以對關(guān)節(jié)必須控制至少兩個(gè)方向的旋轉(zhuǎn),因此必須使用LookRotation去控制z和y軸朝向。
為什么用了LookRotation還要求這么多逆(inverse)?
- 同一個(gè)模型的不同關(guān)節(jié)具有不同的初始旋轉(zhuǎn)量(即InitRotation),而且不同人的同一個(gè)關(guān)節(jié)也可能具有不同的旋轉(zhuǎn)量,甚至初始的局部坐標(biāo)軸都不同,比如源碼中提供的兩個(gè)模型的膝蓋部分局部坐標(biāo)系如下
此時(shí)如果用LookRotation,不同的人就需要設(shè)置對應(yīng)的規(guī)則,比如同一個(gè)姿態(tài),左邊的人藍(lán)色z軸向后,右邊人藍(lán)色z軸向前,搞不好還有其它的情況,此時(shí)就無法用基于LookRotation的同一套代碼去驅(qū)動(dòng)這個(gè)人了。
如果還是不懂為什么不能用同一套代碼驅(qū)動(dòng),可以舉個(gè)例子:小腿向后收起的時(shí)候,左圖的LookRotation必須保證藍(lán)軸向上,右圖必須保證藍(lán)軸向下,如下圖:
由于坐標(biāo)軸最終的方向都不同,所以即使是同一個(gè)姿勢,對于具有不同坐標(biāo)系的相同關(guān)節(jié)也需要針對性LookRotation。
那么為什么源碼里面可以使用LookRotation去玩驅(qū)動(dòng),很簡單,因?yàn)樵创a將所有的關(guān)節(jié)都利用初始姿態(tài)做了LookRotation的對齊,得到了一個(gè)中間矩陣,即源碼中的xx.InverseRotation,利用這個(gè)中間矩陣就能在驅(qū)動(dòng)的時(shí)候?qū)R所有坐標(biāo)系,達(dá)到通用目的。
源碼分析與復(fù)現(xiàn)
如何實(shí)現(xiàn)上述問題中的坐標(biāo)系對齊呢?
利用初始姿態(tài)下各關(guān)節(jié)的坐標(biāo)和旋轉(zhuǎn)來確定,具體是:
當(dāng)前關(guān)節(jié)的lookrotation=初始旋轉(zhuǎn)InitRotation×對齊矩陣當(dāng)前關(guān)節(jié)的lookrotation = 初始旋轉(zhuǎn)InitRotation \times 對齊矩陣 當(dāng)前關(guān)節(jié)的lookrotation=初始旋轉(zhuǎn)InitRotation×對齊矩陣
所以源碼中的下面類似代碼就是為了求解對齊矩陣:
看不懂就可以寫成
Quaternion.LookRotation(xx.position - xxx.position, yy) = xx.InitRotation * Quaternion.Inverse(xx.InverseRotation)這就對應(yīng)上述公式,其中Quaternion.Inverse(xx.InverseRotation)就對應(yīng)了對齊矩陣。
因此我在復(fù)現(xiàn)時(shí)候,以根關(guān)節(jié)的對齊矩陣為例,把上述代碼改成了
root = animator.GetBoneTransform(HumanBodyBones.Hips); midRoot = Quaternion.Inverse(root.rotation) * Quaternion.LookRotation(forward);其中forward是按照源碼的要求,指示人體的當(dāng)前方向。
如何設(shè)置LookRotation的方向?
繼續(xù)分析源碼,發(fā)現(xiàn)對于所有關(guān)節(jié)都做了
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;第一行,基于根關(guān)節(jié)和左右胯關(guān)節(jié)坐標(biāo)計(jì)算出人體朝向,然后以此作為所有關(guān)節(jié)的LookRotation的y方向,以及每個(gè)關(guān)節(jié)與其子關(guān)節(jié)的方向作為z方向,計(jì)算出中間矩陣。
注意,在接下來,分別對頭部和手掌單獨(dú)又計(jì)算了一遍,因?yàn)樗麄z比較特殊
對于頭部,直接求解出頭到鼻子的向量作為LookRotation的z方向,未設(shè)置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));然后計(jì)算頭部的中間矩陣
head.Inverse = Quaternion.Inverse(Quaternion.LookRotation(gaze));head.InverseRotation = head.Inverse * head.InitRotation;對于手腕,直接利用手腕、大拇指、中指的坐標(biāo),計(jì)算出手掌方向作為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);而左手腕以大拇指到中指的方向?yàn)閦方向,而右手腕以中指到大拇指方向?yàn)閦方向:
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;其實(shí)完全沒必要區(qū)分這么明顯,只需要求解和使用的時(shí)候?qū)?yīng)好就行了,比如我實(shí)現(xiàn)的時(shí)候就統(tǒng)一大拇指到中指:
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));也就是說,對于某些特定關(guān)節(jié),需要單獨(dú)設(shè)置用于計(jì)算中間變換矩陣的LookRotation信息。推薦看我實(shí)現(xiàn)的源碼,分為軀干、頭、手掌三個(gè)部分,我實(shí)現(xiàn)的源碼就不貼了,文末找。
注意這里計(jì)算初始姿態(tài)中各關(guān)節(jié)的LookRotation方法與運(yùn)行時(shí)從深度學(xué)習(xí)預(yù)測的3D關(guān)節(jié)坐標(biāo)中計(jì)算的LookRotation方案要一模一樣。
如何驅(qū)動(dòng)?
通過
當(dāng)前關(guān)節(jié)的lookrotation=初始旋轉(zhuǎn)InitRotation×對齊矩陣當(dāng)前關(guān)節(jié)的lookrotation = 初始旋轉(zhuǎn)InitRotation \times 對齊矩陣 當(dāng)前關(guān)節(jié)的lookrotation=初始旋轉(zhuǎn)InitRotation×對齊矩陣
得到了每個(gè)關(guān)節(jié)的對齊矩陣,那么這個(gè)公式很容易得到每個(gè)關(guān)節(jié)的當(dāng)前旋轉(zhuǎn)信息:
當(dāng)前旋轉(zhuǎn)Rotation=當(dāng)前關(guān)節(jié)的lookrotation×Quaternion.Inverse(對齊矩陣)當(dāng)前旋轉(zhuǎn)Rotation = 當(dāng)前關(guān)節(jié)的lookrotation \times Quaternion.Inverse(對齊矩陣) 當(dāng)前旋轉(zhuǎn)Rotation=當(dāng)前關(guān)節(jié)的lookrotation×Quaternion.Inverse(對齊矩陣)
然后分析源碼,在PoseUpdate()函數(shù)中,前面的不用看,是計(jì)算根關(guān)節(jié)坐標(biāo)的,我們先關(guān)注關(guān)節(jié)旋轉(zhuǎn)。
注意因?yàn)橛脤R矩陣是從初始姿態(tài)獲取的,所以如何依據(jù)初始姿態(tài)計(jì)算的lookrotation就要按照同樣的方法從深度學(xué)習(xí)模型預(yù)測的3D關(guān)節(jié)坐標(biāo)中計(jì)算對應(yīng)的lookrotation參數(shù)。
比如人體方向依舊是根、左右胯部的坐標(biāo)計(jì)算:
var forward = TriangleNormal(jointPoints[PositionIndex.hip.Int()].Pos3D, jointPoints[PositionIndex.lThighBend.Int()].Pos3D, jointPoints[PositionIndex.rThighBend.Int()].Pos3D);而根關(guān)節(jié)當(dāng)前的旋轉(zhuǎn)就是根據(jù)上述公式計(jì)算得到:
jointPoints[PositionIndex.hip.Int()].Transform.rotation = Quaternion.LookRotation(forward) * jointPoints[PositionIndex.hip.Int()].InverseRotation;其它關(guān)節(jié)我不貼源碼了,直接描述:
軀干關(guān)節(jié):以身體方向?yàn)長ookRotation的y方向,以當(dāng)前關(guān)節(jié)到其子關(guān)節(jié)的方向?yàn)閦方向。
手腕:以手腕、大拇指、中指形成的平面的法線方向?yàn)閥方向,以拇指到中指的方向?yàn)閦方向。
比如我隨便貼一下我復(fù)現(xiàn)的左臂(肩、肘、腕)實(shí)時(shí)驅(qū)動(dòng):
// 左臂 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就是深度學(xué)習(xí)模型預(yù)測的3D關(guān)節(jié)坐標(biāo)。
人體位置
上述講解了旋轉(zhuǎn)的計(jì)算方法,關(guān)于整個(gè)人體的位置,源碼中自有一套方案,但是里面預(yù)設(shè)了很多固定參數(shù),不是特別想分析,所以用了萬年不變的方法,計(jì)算unity人物模型腿的長度和深度學(xué)習(xí)預(yù)測的腿部長度,然后計(jì)算比例系數(shù),乘到深度學(xué)習(xí)預(yù)測的根關(guān)節(jié)位置即可。
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));是不是超級(jí)簡單,雖然效果有點(diǎn)偏差,但是后續(xù)還是會(huì)分析一下源碼中更新人體位置的方案。
復(fù)現(xiàn)流程
在VNectModel.cs中的PoseUpdate函數(shù)加入以下代碼:
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();將關(guān)鍵點(diǎn)寫入到txt中做復(fù)現(xiàn)時(shí)候用的3D關(guān)鍵點(diǎn)數(shù)據(jù)
然后按照理論進(jìn)行復(fù)現(xiàn)后效果如下:
紅色為預(yù)測的3D坐標(biāo),人物模型會(huì)做出與紅色骨架一樣的姿勢。
結(jié)論
這個(gè)感覺還是沒有考慮人體運(yùn)動(dòng)的動(dòng)力學(xué)特性,如果跑過源碼,很容易發(fā)現(xiàn)個(gè)別姿勢會(huì)出現(xiàn)奇怪的關(guān)節(jié)扭曲現(xiàn)象,這就是不考慮動(dòng)力學(xué)的后果,給我自己之前的代碼打一波廣告,那個(gè)絕對比這個(gè)好,哈哈。
完整的unity實(shí)現(xiàn)放在微信公眾號(hào)的簡介中描述的github中,有興趣可以去找找。或者在公眾號(hào)回復(fù)“ThreeDPose",同時(shí)文章也同步到微信公眾號(hào)中,有疑問或者興趣歡迎公眾號(hào)私信。
總結(jié)
以上是生活随笔為你收集整理的卡通驱动项目ThreeDPoseTracker——模型驱动解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Unity中BVH骨骼动画驱动的可视化理
- 下一篇: 交行魔都优逸白金卡怎么申请?有哪些申请条