捏臉:
不知什么時(shí)候涡尘,捏臉幾乎已經(jīng)成為游戲的標(biāo)配,用戶通過捏臉系統(tǒng)創(chuàng)建出個(gè)性化角色响迂,彰顯獨(dú)特的形象考抄,增強(qiáng)了沉浸感。從技術(shù)角度講蔗彤,捏臉也有很多種實(shí)現(xiàn)方式川梅,比如:
- BlendShape
- 骨骼捏臉
- AI智能捏臉
BlendShape可以很好的表現(xiàn)不同的面部表情,但是制作工作量大然遏,一般CG,影視等應(yīng)用較多贫途。
AI智能捏臉現(xiàn)在發(fā)展勢(shì)頭很猛,通過對(duì)輸入的圖片做AI智能圖像識(shí)別啦鸣,提取出面部特征潮饱,再根據(jù)特征定制角色Mesh,從而快速完美的還原現(xiàn)實(shí)世界中的人物形象来氧。具體實(shí)現(xiàn)可以參考 面由 AI 生|虛擬偶像“捏臉”技術(shù)解析
骨骼捏臉
今天我們主要討論骨骼捏臉以及在Unity中的實(shí)現(xiàn)诫给。說(shuō)到骨骼捏臉,就要先了解一下骨骼啦扬,骨骼動(dòng)畫中狂,蒙皮Mesh,權(quán)重這些名詞。這里假設(shè)讀者已經(jīng)知道這些概念扑毡,從而可以盡快進(jìn)入主題胃榕。
骨骼捏臉也有很多實(shí)現(xiàn)方式,比如:
- 每幀動(dòng)畫后瞄摊,給骨骼一個(gè)變化補(bǔ)償值勋又。
- 給骨骼掛載一個(gè)不參與動(dòng)畫的子骨骼苦掘,Mesh的權(quán)重僅受這些子骨骼影響。
- 修改骨骼的Bindposes實(shí)現(xiàn)楔壤。
第一種 是先計(jì)算好捏臉骨骼和原始骨骼的差異值鹤啡,然后在每幀動(dòng)畫后,再給每根骨骼應(yīng)用一次這個(gè)差異值蹲嚣,比如捏臉的時(shí)候縮小了臉部骨骼0.5倍递瑰。 當(dāng)骨骼動(dòng)畫的每幀完成后,動(dòng)畫數(shù)據(jù)會(huì)把臉部骨骼設(shè)置回原本的值隙畜,這時(shí)候在LateUpdate中抖部,把臉部骨骼再縮小0.5倍,從而達(dá)到還原捏臉效果的目的议惰。
優(yōu)點(diǎn): 實(shí)現(xiàn)簡(jiǎn)單
缺點(diǎn): 由于每幀都要做額外的補(bǔ)償計(jì)算慎颗,所以效率不高。
第二種 由于把影響蒙皮的骨骼和驅(qū)動(dòng)動(dòng)畫的骨骼進(jìn)行了分離言询,動(dòng)畫數(shù)據(jù)不直接改變蒙皮骨骼的數(shù)據(jù)哗总,捏臉時(shí)指定的變化,不會(huì)被動(dòng)畫數(shù)據(jù)覆蓋倍试,所以讯屈,只要調(diào)整蒙皮骨骼就能達(dá)到捏臉效果。
優(yōu)點(diǎn): 實(shí)現(xiàn)更簡(jiǎn)單
缺點(diǎn): 增加了額外的骨骼县习,影響效率
參與動(dòng)畫的骨骼不能進(jìn)行調(diào)整(會(huì)被骨骼動(dòng)畫還原)涮母,從而限制了捏臉的自由度,對(duì)捏臉效果產(chǎn)生不利影響躁愿。
第三種: 修改骨骼Bindposes達(dá)到捏臉效果叛本。
優(yōu)點(diǎn): 只計(jì)算一次,效率高彤钟。
缺點(diǎn): 實(shí)現(xiàn)稍微復(fù)雜来候,需要良好的數(shù)學(xué)基礎(chǔ)。
由于第三種優(yōu)點(diǎn)最為突出逸雹,我們今天就來(lái)討論它是如何實(shí)現(xiàn)的营搅。
Bindpose:
首先說(shuō)一下對(duì)骨骼的簡(jiǎn)單理解:
-
骨骼就是一系列具有父子關(guān)系的Transform。由于骨骼具有父子關(guān)系梆砸,所以對(duì)父骨骼的修改會(huì)影響其子骨骼转质,比如抬手臂會(huì)帶動(dòng)手指骨骼。
-
骨骼動(dòng)畫就是在關(guān)鍵幀中指定骨骼的transform帖世。 位置休蟹,旋轉(zhuǎn),縮放.。關(guān)鍵幀之間通過插值得到中間狀態(tài)赂弓,從而提高動(dòng)畫流暢度绑榴。
- 骨骼的bindpose是骨骼在TPose下,從模型空間轉(zhuǎn)到該骨骼空間的矩陣盈魁。
前兩者很好理解彭沼,最后一點(diǎn)不是那么直觀,而且是我們捏臉的關(guān)鍵备埃,所以我們?cè)敿?xì)介紹一下姓惑。
TPose是骨骼的一個(gè)初始狀態(tài),模型和骨骼的綁定就是在這個(gè)狀態(tài)下進(jìn)行的按脚,所謂的綁定于毙,就是指定模型的頂點(diǎn)受哪些骨骼影響,每個(gè)骨骼影響的權(quán)重是多少辅搬,這些一般都是美術(shù)在美術(shù)工具軟件中做好唯沮,導(dǎo)入到Unity中直接使用,我們?yōu)榱苏f(shuō)明原理堪遂,直接用一個(gè)藍(lán)色小球代表模型上的一個(gè)頂點(diǎn)介蛉。把他和骨骼BN_20綁定。權(quán)重為1溶褪。也就是它僅受BN_20影響币旧。
現(xiàn)在這個(gè)頂點(diǎn)(藍(lán)色小球),位于BN_20上方一個(gè)單位猿妈,但是看右邊面板注意到吹菱,Mesh以及其上的Vertex和骨骼并沒有父子關(guān)系,所以現(xiàn)在變換骨骼彭则,頂點(diǎn)vertex并不會(huì)跟隨移動(dòng)鳍刷。我們需要把兩者關(guān)聯(lián)起來(lái)(綁定).
假設(shè)BN_10向右移動(dòng)了一個(gè)單位(會(huì)帶動(dòng)子骨骼BN_20一起向右),那藍(lán)色小球也應(yīng)該跟隨綁定的骨骼BN_20一起向右移動(dòng)一個(gè)單位俯抖,但是如果BN_10圍繞Z軸旋轉(zhuǎn)90度输瓜,藍(lán)色小球又該在什么位置呢?他們是怎么綁定的芬萍,有什么規(guī)律呢尤揣?
答案就是頂點(diǎn)相對(duì)于綁定的骨骼,相對(duì)位置不變担忧,也可以描述為芹缔,頂點(diǎn)在綁定骨骼的坐標(biāo)系下坯癣,坐標(biāo)不變瓶盛。這樣BN_20.transform.position.x += 1.那Vertex.transform.position += 1(世界坐標(biāo)系下). 如果BN_10繞z軸旋轉(zhuǎn)了,那BN_20的坐標(biāo)系也跟著旋轉(zhuǎn)了,但是頂點(diǎn)在該坐標(biāo)系下惩猫,位置還是不變芝硬。
上圖是理想的位置,它是怎么計(jì)算來(lái)的呢轧房?上面說(shuō)了頂點(diǎn)和骨骼相對(duì)位置不變拌阴,就是頂點(diǎn)在骨骼坐標(biāo)系下坐標(biāo)位置不變。那我們先求出在初始位置(TPose)下奶镶,頂點(diǎn)在骨骼BN_20坐標(biāo)系下的坐標(biāo)位置迟赃。直接轉(zhuǎn)換坐標(biāo)系不好轉(zhuǎn)換,但是我們可以先把vertext坐標(biāo)轉(zhuǎn)換到世界空間厂镇,然后再由世界空間轉(zhuǎn)換到BN_20空間纤壁。
對(duì)應(yīng)代碼如下:
Vector3 vertexPositionInBone_20 = (BN_20.transform.worldToLocalMatrix * mesh.transform.localToWorldMatrix).MultiplyPoint3x4(vertext.transform.localPosition);
好了,現(xiàn)在頂點(diǎn)相對(duì)于綁定骨骼的相對(duì)位置已經(jīng)計(jì)算出來(lái)了捺信,接下來(lái)就保證骨骼在各種變換后酌媒,頂點(diǎn)的vertextPositionInBone不變就好了。反過來(lái)想迄靠,假設(shè)BN_20變換到了新的位置秒咨,只要把BN_20本地坐標(biāo)下的vertexPositionInBone轉(zhuǎn)換到世界坐標(biāo)系下得到位置A,然后掌挚,把vertext的世界坐標(biāo)移動(dòng)到A,就能保障相對(duì)位置不變了雨席。
對(duì)應(yīng)代碼如下:
vertext.transform.position = bone.transform.localToWorldMatrix.MultiplyPoint3x4(vertexPositionInBone_20);
這里要明確一點(diǎn),圖5中bone的矩陣是TPose時(shí)的矩陣吠式,這個(gè)矩陣是固定不變的舅世,圖6中是骨骼變換后的矩陣,這個(gè)矩陣隨著骨骼的變換而不斷變化奇徒,從而驅(qū)動(dòng)頂點(diǎn)不斷變化雏亚。
說(shuō)到這里基本把骨骼動(dòng)畫,蒙皮摩钙,權(quán)重(特例只受一根骨骼影響)的原理講清楚了罢低,但是讀者還是不明白什么是Bindposes. 其實(shí)圖5中最后一行,用括號(hào)括起來(lái)的部分就是骨骼的Bindpose, 很意外吧胖笛,原來(lái)早就認(rèn)識(shí)了网持,只是不知道對(duì)方的名字,它的數(shù)學(xué)意義是 在TPose下长踊,從模型的本地空間到骨骼的本地空間的轉(zhuǎn)換矩陣功舀,這里面每個(gè)詞都是有意義的,首先一定是骨骼的初始狀態(tài)(TPose下), 再就是從一個(gè)坐標(biāo)系身弊,到另一個(gè)坐標(biāo)系的轉(zhuǎn)換矩陣.
捏臉:
好了辟汰,啰里啰嗦一大堆列敲,終于知道bindpose是啥了,那它和捏臉有什么關(guān)系呢帖汞?下面我們來(lái)說(shuō)明戴而,假設(shè)vertext是鼻子上的一個(gè)頂點(diǎn),現(xiàn)在TPose下翩蘸,這個(gè)頂點(diǎn)離骨骼BN_20一個(gè)單位遠(yuǎn)所意,如果我在TPose下,把vertex再多遠(yuǎn)離BN_20一個(gè)單位催首,會(huì)發(fā)生什么呢扶踊?發(fā)現(xiàn)鼻子變高了,vertext在BN_20坐標(biāo)系下郎任,坐標(biāo)位置由(0,1,0)變成(0,2,0)了姻檀。 按照上面說(shuō)的骨骼動(dòng)畫的原理,骨骼動(dòng)起來(lái)涝滴,頂點(diǎn)相對(duì)于骨骼坐標(biāo)位置不變绣版,所以骨骼動(dòng)畫動(dòng)的過程中,vertex相對(duì)于BN_20始終位于(0,2,0)的位置歼疮,這樣杂抽,角色怎么動(dòng),鼻子始終是高的韩脏。這就是捏臉?biāo)枨蟮乃豸铩O旅婵纯淳唧w計(jì)算過程:
圖7中括號(hào)里面赡矢,就是骨骼新的bindpose.只要用這個(gè)bindpose,頂點(diǎn)就相對(duì)于骨骼始終在新的相對(duì)位置杭朱。
給Mesh賦值新的bindpose的代碼如下:
//Dictionary<string,Matrix4x4> newBindposes; //這里面存儲(chǔ)根據(jù)用戶捏臉后計(jì)算出的新的Bindpose
SkinnedMeshRenderer smr = face.GetComponent<SkinnedMeshRenderer>();
//實(shí)例化一份新的mesh
Mesh mesh = GameObject.Instantiate<Mesh>(smr.sharedMesh);
Matrix4x4[] bindposes = mesh.bindposes;
Transform[] bones = smr.bones;
for (int i = 0; i < bones.Length; ++i)
{
bindposes[i] = newBindposes[bones[i].name];
}
mesh.bindposes = bindposes;
smr.sharedMesh = mesh;
OK,現(xiàn)在看看捏臉后的效果吧:
感興趣的朋友可以自行推導(dǎo)一下,你會(huì)發(fā)現(xiàn)其中數(shù)學(xué)的樂趣空民。如果疑惑較多刃唐,我可以單開一章用來(lái)說(shuō)明這塊。
另外吐槽一下界轩,Unity資源商店竟然沒有找到輕量級(jí)捏臉的插件画饥,是我的搜索方式不對(duì)嗎?有知道的推薦個(gè)好用的給我唄浊猾,先行謝過抖甘!
【轉(zhuǎn)載請(qǐng)注明出處】