捏脸:
不知什么时候,捏脸几乎已经成为游戏的标配,用户通过捏脸系统创建出个性化角色,彰显独特的形象,增强了沉浸感。从技术角度讲,捏脸也有很多种实现方式,比如:
- BlendShape
- 骨骼捏脸
- AI智能捏脸
BlendShape可以很好的表现不同的面部表情,但是制作工作量大,一般CG,影视等应用较多。
AI智能捏脸现在发展势头很猛,通过对输入的图片做AI智能图像识别,提取出面部特征,再根据特征定制角色Mesh,从而快速完美的还原现实世界中的人物形象。具体实现可以参考 面由 AI 生|虚拟偶像“捏脸”技术解析
骨骼捏脸
今天我们主要讨论骨骼捏脸以及在Unity中的实现。说到骨骼捏脸,就要先了解一下骨骼,骨骼动画,蒙皮Mesh,权重这些名词。这里假设读者已经知道这些概念,从而可以尽快进入主题。
骨骼捏脸也有很多实现方式,比如:
- 每帧动画后,给骨骼一个变化补偿值。
- 给骨骼挂载一个不参与动画的子骨骼,Mesh的权重仅受这些子骨骼影响。
- 修改骨骼的Bindposes实现。
第一种 是先计算好捏脸骨骼和原始骨骼的差异值,然后在每帧动画后,再给每根骨骼应用一次这个差异值,比如捏脸的时候缩小了脸部骨骼0.5倍。 当骨骼动画的每帧完成后,动画数据会把脸部骨骼设置回原本的值,这时候在LateUpdate中,把脸部骨骼再缩小0.5倍,从而达到还原捏脸效果的目的。
优点: 实现简单
缺点: 由于每帧都要做额外的补偿计算,所以效率不高。
第二种 由于把影响蒙皮的骨骼和驱动动画的骨骼进行了分离,动画数据不直接改变蒙皮骨骼的数据,捏脸时指定的变化,不会被动画数据覆盖,所以,只要调整蒙皮骨骼就能达到捏脸效果。
优点: 实现更简单
缺点: 增加了额外的骨骼,影响效率
参与动画的骨骼不能进行调整(会被骨骼动画还原),从而限制了捏脸的自由度,对捏脸效果产生不利影响。
第三种: 修改骨骼Bindposes达到捏脸效果。
优点: 只计算一次,效率高。
缺点: 实现稍微复杂,需要良好的数学基础。
由于第三种优点最为突出,我们今天就来讨论它是如何实现的。
Bindpose:
首先说一下对骨骼的简单理解:
-
骨骼就是一系列具有父子关系的Transform。由于骨骼具有父子关系,所以对父骨骼的修改会影响其子骨骼,比如抬手臂会带动手指骨骼。
-
骨骼动画就是在关键帧中指定骨骼的transform。 位置,旋转,缩放.。关键帧之间通过插值得到中间状态,从而提高动画流畅度。
- 骨骼的bindpose是骨骼在TPose下,从模型空间转到该骨骼空间的矩阵。
前两者很好理解,最后一点不是那么直观,而且是我们捏脸的关键,所以我们详细介绍一下。
TPose是骨骼的一个初始状态,模型和骨骼的绑定就是在这个状态下进行的,所谓的绑定,就是指定模型的顶点受哪些骨骼影响,每个骨骼影响的权重是多少,这些一般都是美术在美术工具软件中做好,导入到Unity中直接使用,我们为了说明原理,直接用一个蓝色小球代表模型上的一个顶点。把他和骨骼BN_20绑定。权重为1。也就是它仅受BN_20影响。
现在这个顶点(蓝色小球),位于BN_20上方一个单位,但是看右边面板注意到,Mesh以及其上的Vertex和骨骼并没有父子关系,所以现在变换骨骼,顶点vertex并不会跟随移动。我们需要把两者关联起来(绑定).
假设BN_10向右移动了一个单位(会带动子骨骼BN_20一起向右),那蓝色小球也应该跟随绑定的骨骼BN_20一起向右移动一个单位,但是如果BN_10围绕Z轴旋转90度,蓝色小球又该在什么位置呢?他们是怎么绑定的,有什么规律呢?
答案就是顶点相对于绑定的骨骼,相对位置不变,也可以描述为,顶点在绑定骨骼的坐标系下,坐标不变。这样BN_20.transform.position.x += 1.那Vertex.transform.position += 1(世界坐标系下). 如果BN_10绕z轴旋转了,那BN_20的坐标系也跟着旋转了,但是顶点在该坐标系下,位置还是不变。
上图是理想的位置,它是怎么计算来的呢?上面说了顶点和骨骼相对位置不变,就是顶点在骨骼坐标系下坐标位置不变。那我们先求出在初始位置(TPose)下,顶点在骨骼BN_20坐标系下的坐标位置。直接转换坐标系不好转换,但是我们可以先把vertext坐标转换到世界空间,然后再由世界空间转换到BN_20空间。
对应代码如下:
Vector3 vertexPositionInBone_20 = (BN_20.transform.worldToLocalMatrix * mesh.transform.localToWorldMatrix).MultiplyPoint3x4(vertext.transform.localPosition);
好了,现在顶点相对于绑定骨骼的相对位置已经计算出来了,接下来就保证骨骼在各种变换后,顶点的vertextPositionInBone不变就好了。反过来想,假设BN_20变换到了新的位置,只要把BN_20本地坐标下的vertexPositionInBone转换到世界坐标系下得到位置A,然后,把vertext的世界坐标移动到A,就能保障相对位置不变了。
对应代码如下:
vertext.transform.position = bone.transform.localToWorldMatrix.MultiplyPoint3x4(vertexPositionInBone_20);
这里要明确一点,图5中bone的矩阵是TPose时的矩阵,这个矩阵是固定不变的,图6中是骨骼变换后的矩阵,这个矩阵随着骨骼的变换而不断变化,从而驱动顶点不断变化。
说到这里基本把骨骼动画,蒙皮,权重(特例只受一根骨骼影响)的原理讲清楚了,但是读者还是不明白什么是Bindposes. 其实图5中最后一行,用括号括起来的部分就是骨骼的Bindpose, 很意外吧,原来早就认识了,只是不知道对方的名字,它的数学意义是 在TPose下,从模型的本地空间到骨骼的本地空间的转换矩阵,这里面每个词都是有意义的,首先一定是骨骼的初始状态(TPose下), 再就是从一个坐标系,到另一个坐标系的转换矩阵.
捏脸:
好了,啰里啰嗦一大堆,终于知道bindpose是啥了,那它和捏脸有什么关系呢?下面我们来说明,假设vertext是鼻子上的一个顶点,现在TPose下,这个顶点离骨骼BN_20一个单位远,如果我在TPose下,把vertex再多远离BN_20一个单位,会发生什么呢?发现鼻子变高了,vertext在BN_20坐标系下,坐标位置由(0,1,0)变成(0,2,0)了。 按照上面说的骨骼动画的原理,骨骼动起来,顶点相对于骨骼坐标位置不变,所以骨骼动画动的过程中,vertex相对于BN_20始终位于(0,2,0)的位置,这样,角色怎么动,鼻子始终是高的。这就是捏脸所需求的。下面看看具体计算过程:
图7中括号里面,就是骨骼新的bindpose.只要用这个bindpose,顶点就相对于骨骼始终在新的相对位置。
给Mesh赋值新的bindpose的代码如下:
//Dictionary<string,Matrix4x4> newBindposes; //这里面存储根据用户捏脸后计算出的新的Bindpose
SkinnedMeshRenderer smr = face.GetComponent<SkinnedMeshRenderer>();
//实例化一份新的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,现在看看捏脸后的效果吧:
感兴趣的朋友可以自行推导一下,你会发现其中数学的乐趣。如果疑惑较多,我可以单开一章用来说明这块。
另外吐槽一下,Unity资源商店竟然没有找到轻量级捏脸的插件,是我的搜索方式不对吗?有知道的推荐个好用的给我呗,先行谢过!
【转载请注明出处】