在上篇文章<<Unity 人物捏脸的实现>>中,最后留了一个悬念,就是
这个矩阵是怎么计算来的。这里给大家补上。
几何意义
还是以上图为例,蓝色小球Vertex是Mesh上的一个顶点,它绑定骨骼BN_20(在BN_20本地坐标空间下,坐标值保持不变)。捏脸的时候不可能逐顶点调整位置,那就要调整骨骼,因为存在绑定关系,所以调整骨骼的时候,顶点小球的世界坐标也会跟着变化,以保证在骨骼本地坐标下位置不变。有点绕口。好好理一下就明白了。
假设调整骨骼到一个新的位置,比如我们把骨骼BN_10绕Z轴旋转90度(会带动BN_20移动,进而影响顶点蓝色小球)
现在顶点已经到了新位置,我们的目的是要计算新位置(图2中的篮球位置)相对于调整前的骨骼(图1中的BN_20)的Bindpose(从模型空间到骨骼本地坐标的转换矩阵),这样,骨骼动画把骨骼重置位置后,小球就能保证还在调整后的位置了。我们为了达到下图的效果:
还记得上篇文章中的公式吧:
这里这个M_translation就是一个矩阵,原来的骨骼乘上这个矩阵,就会到新的位置(红色骨骼从图1的位置到图2的位置),同样,顶点位置乘上这个矩阵,也会到新的位置(由图1到图2中篮球位置).所以我们可以叫它差异矩阵。
差异矩阵推导
假设原本(图1中)BN_20本地空间内的一点AA的新位置,即可以用原来骨骼的本地到世界矩阵乘上差异矩阵来求出,也可以用新姿态下骨骼的本地到世界矩阵来求出,则推导出
其中骨骼的新旧姿态下的本地到世界矩阵都是已知量,则差异矩阵就可以求出来了。
第一行,可以看做等号两边的尾部都乘上原骨骼的本地到世界矩阵的逆矩阵,则左边成了差异矩阵乘以单位矩阵,右边就是结果。其中本地到世界的逆矩阵就是世界到本地矩阵,所以推出第二行。这里的M_delta就是图4中的M_translation,命名有点混乱。上篇文章说了图4中下面一行括号中的就是新的bindpose,把图5结果带入图4的括号中,则
对应的代码如下:
Matrix4x4 newBindPose = oldBone.transform.worldToLocalMatrix * newBone.transform.localToWorldMatrix * oldBone.transform.worldToLocalMatrix * mesh.localToWorldMatrix;
至此骨骼的新Bindpose已经计算完毕,下面给Mesh应用上,这样骨骼位置不变,但是顶点相对于骨骼的位置发生了改变,骨骼动画驱动骨骼不停变化过程中,顶点始终和骨骼保持新的相对位置,从而达到捏脸的效果。应用新的bindpose代码入下:
//给模型应用新的BindPose
private static void ApplyNewBindpose(GameObject meshObj, Dictionary<string, Matrix4x4> bindposes)
{
SkinnedMeshRenderer smr = meshObj.GetComponent<SkinnedMeshRenderer>();
if (smr == null)
{
Debug.LogError("Not found SkinnedMeshRenderer " + meshTransform.name);
return;
}
//实例化一份新的mesh,因为要修改mesh的数据,原始的mesh不要动,只读
Mesh mesh = GameObject.Instantiate<Mesh>(smr.sharedMesh);
Matrix4x4[] bindposes = mesh.bindposes;
Transform[] bones = smr.bones;
for (int i = 0; i < bones.Length; ++i)
{
if (bindposes.ContainsKey(bones[i].name))
{
bindposes[i] = bindposes[bones[i].name];
}
}
mesh.bindposes = bindposes;
smr.sharedMesh = mesh;
}
今天的内容都是数学公式,比较枯燥,大家如果不喜欢推导过程,可以直接拿结果去用,bindpose_new那个公式。
好了,捏脸系统的完整思路就是这样了,欢迎大家指出错误和不足之处,共同进步。
【转载请注明出处】