SkinnedMeshRenderer
蒙皮网格渲染器。蒙皮是指将Mesh中的顶点附着(绑定)在骨骼之上,而且每个顶点可以被多个骨骼所控制。
骨骼是皮肤网格内的不可见对象,它们影响动画过程中网格变形的方式。其基本思想是将骨骼连接在一起形成一个层次化的“骨架”,动画通过旋转骨架的关节以使其移动。Mesh上的顶点附着在骨骼上。播放动画时,顶点会随着骨骼或骨骼的连接而移动,因此“皮肤(Mesh)”会跟随骨骼的移动。
在一个简单的关节(例如肘)中,Mesh顶点受到在那里遇到的两个骨骼的影响,并且Mesh将在关节弯曲时实际拉伸和旋转。多个骨骼的影响相同的顶点和权重。使用Unity骨骼蒙皮的主要优势是可以使骨骼受到物理影响,制作你的角色布娃娃。
Mesh.bindposes 绑定的姿势
bindposes : Matrix4x4[]
- 绑定的姿势。每个索引绑定的姿势使用具有相同索引的骨骼。
- 当骨骼在绑定的姿势时,绑定的姿势是骨骼变换矩阵的逆矩阵。
这里阐述下BindPose是如何参与在骨骼蒙皮运算中的 根据Unity文档, Unity中BindPose的算法如下:
OneBoneBindPose = bone.worldToLocalMatrix * transform.localToWorldMatrix;
骨骼的世界转局部坐标系矩阵乘上Mesh的局部转世界矩阵
bindposes的主要作用在骨骼变换前预制一些骨骼变换,使得人物可以在同一动画上有不同的骨骼位置表现,简化工作流等
骨骼动画
骨骼动画的基本原理可概括为:在骨骼控制下,通过顶点混合动态计算蒙皮网格的顶点,而骨骼的运动相对于其父骨骼,并由动画关键帧数据驱动。
一个骨骼动画通常包括骨骼层次结构数据,网格(Mesh)数据,网格蒙皮数据和骨骼的动画(关键帧、动画曲线)数据。
网格蒙皮数据:顶点受哪些骨骼影响,这些骨骼影响该顶点的权重(boneWeight),参与骨骼蒙皮计算的BinePose矩阵。
骨骼:理解骨骼动画
首先要明确一个观念:骨骼决定了模型整体在世界坐标系中的位置和朝向。 静态模型没有骨骼,我们在世界坐标系中放置静态模型时,只要指定模型自身坐标系在世界坐标系中的位置和朝向。在骨骼动画中,不是把 Mesh 直接放到世界坐标系中, Mesh 只是作为 Skin 使用的,是依附于骨骼的,真正决定模型在世界坐标系中的位置和朝向的是骨骼。在渲染静态模型时,由于模型的顶点都是定义在模型坐标系中的,所以各顶点只要经过模型坐标系到世界坐标系的变换后就可进行渲染。而对于骨骼动画,设置模型的位置和朝向,实际是在设置根骨骼的位置和朝向,然后根据骨骼层次结构中父子骨骼之间的变换关系计算出各个骨骼的位置和朝向,然后根据骨骼对 Mesh 中顶点的绑定,计算出顶点在世界坐标系中的坐标,从而对顶点进行渲染。要记住,在骨骼动画中,骨骼才是模型主体, Mesh 不过是一层皮,一件衣服。
骨骼就是坐标空间,骨骼层次就是嵌套的坐标空间。为什么要将骨骼组织成层次结构呢?答案是为了做动画方便,设想如果只有一块骨骼,那么让他动起来就太简单了,动画每一帧直接指定他的位置即可。如果是n块呢?通过组成一个层次结构,就可以通过父骨骼控制子骨骼的运动,牵一发而动全身,改变某骨骼时并不需要设置其下子骨骼的位置,子骨骼的位置会通过计算自动得到。上文已经说过,父子骨骼之间的关系可以理解为,子骨骼位于父骨骼的坐标系中。我们知道物体在坐标系中可以做平移变换,以及自身的旋转和缩放变换。子骨骼在父骨骼的坐标系中也可以做这些变换来改变自己在其父骨骼坐标系中的位置和朝向等。那么如何表示呢?由于4X4矩阵可以同时表示上述三种变换,所以一般描述骨骼在其父骨骼坐标系中的变换时使用一个矩阵,也就是DirectX SkinnedMesh中的FrameTransformMatrix。实际上这不是唯一的方法,但应该是公认的方法,因为矩阵不光可以同时表示多种变换还可以方便的通过连乘进行变换的组合,这在层次结构中非常方便。
下面是Unity文档的例子,实际操作一下帮助理解骨骼动画:
using UnityEngine;
// 此示例从头开始创建四边形网格,创建骨骼并分配它们,
// 并根据简单的动画曲线设置骨骼动作的动画以使四边形网格生成动画。
public class BindPoseExample : MonoBehaviour
{
void Start()
{
gameObject.AddComponent<Animation>();
gameObject.AddComponent<SkinnedMeshRenderer>();
SkinnedMeshRenderer rend = GetComponent<SkinnedMeshRenderer>();
Animation anim = GetComponent<Animation>();
// 构建基本网格
Mesh mesh = new Mesh();
mesh.vertices = new Vector3[] { new Vector3(-1, 0, 0), new Vector3(1, 0, 0), new Vector3(-1, 5, 0), new Vector3(1, 5, 0) };
mesh.uv = new Vector2[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(0, 1), new Vector2(1, 1) };
mesh.triangles = new int[] { 0, 1, 2, 1, 3, 2 };
mesh.RecalculateNormals();
rend.material = new Material(Shader.Find("Diffuse"));
// 将骨骼权重指定给网格
// 可以用一个,两个或四个骨骼对每个顶点进行修饰,所有骨骼的权重总和为1
// 下面的例子,我们创建了两个骨骼,一个在Mesh的底部,另一个在Mesh的顶部,
// 由于每个顶点只受到一个骨骼影响,所以对应weight0都是1
// 同时,BoneWeight数组与顶点数组一一对应
// 附着在0,1索引顶点是第0个骨骼, 所以boneIndex0为0
// 附着在2,3索引顶点是第1个骨骼, 所以boneIndex0为1
BoneWeight[] weights = new BoneWeight[4];
weights[0].boneIndex0 = 0;
weights[0].weight0 = 1;
weights[1].boneIndex0 = 0;
weights[1].weight0 = 1;
weights[2].boneIndex0 = 1;
weights[2].weight0 = 1;
weights[3].boneIndex0 = 1;
weights[3].weight0 = 1;
mesh.boneWeights = weights;
// 创建 Bone的Transforms 和 Bind poses
Transform[] bones = new Transform[2];
Matrix4x4[] bindPoses = new Matrix4x4[2];
bones[0] = new GameObject("Lower").transform;
bones[0].parent = transform;
// 设置相对于父级的位置
bones[0].localRotation = Quaternion.identity;
bones[0].localPosition = Vector3.zero;
// 绑定姿势是骨骼的逆矩阵。在这种情况下,我们也相对于根生成这个矩阵。 这样我们就可以自由地移动根游戏对象
bindPoses[0] = bones[0].worldToLocalMatrix * transform.localToWorldMatrix;
bones[1] = new GameObject("Upper").transform;
bones[1].parent = transform;
// 设置相对于父级的位置
bones[1].localRotation = Quaternion.identity;
bones[1].localPosition = new Vector3(0, 5, 0);
// 绑定姿势是骨骼的逆矩阵。在这种情况下,我们也相对于根生成这个矩阵。 这样我们就可以自由地移动根游戏对象
bindPoses[1] = bones[1].worldToLocalMatrix * transform.localToWorldMatrix;
// 之前创建了bindPoses,并使用所需的矩阵进行了更新。
// 现在将bindPoses数组分配给Mesh中的bindposes。
mesh.bindposes = bindPoses;
// 分配骨骼和Mesh
rend.bones = bones;
rend.sharedMesh = mesh;
// 将简单的波动动画分配给底部骨骼
AnimationCurve curve = new AnimationCurve();
curve.keys = new Keyframe[] { new Keyframe(0, 0, 0, 0), new Keyframe(1, 3, 0, 0), new Keyframe(2, 0.0F, 0, 0) };
// 创建带Curve曲线的Clip
AnimationClip clip = new AnimationClip();
clip.SetCurve("Lower", typeof(Transform), "m_LocalPosition.z", curve);
clip.legacy = true;
// 添加并运行Clip
clip.wrapMode = WrapMode.Loop;
anim.AddClip(clip, "test");
anim.Play("test");
}
}
Unity 蒙皮算法
把顶点附着在骨骼上的过程,称为蒙皮。 蒙皮用的网格是通过顶点联系上骨骼,每个顶点可以绑定一个或者多个骨骼(一般最多允许4根骨骼)。若某顶点只绑定至一根骨骼,它就会完全跟随该骨骼移动。若绑定至多个骨骼,该顶点的位置就等于把它逐一绑定个别骨骼,在通过权重加权平均。
能把网格顶点从原来位置(绑定姿势)变换至骨骼的当前姿势的矩阵称为蒙皮矩阵。蒙皮矩阵把顶点变形至新位置,顶点在变换前后都在模型变换空间中。
求蒙皮矩阵时的一个诀窍是:顶点绑定到关节的位置时,在该关节空间中是不变的(其实变的只是骨骼,所以才叫骨骼动画)。
通俗点一点理解就是:模型加载到内存中的位置是在模型空间中的坐标(并不是其绑定的骨骼坐标系下)在做骨骼动画时,动的其实是骨骼,而绑定到该骨骼的顶点会跟随骨骼做动画,所以顶点相对于骨骼位置是不变的(在关节空间下是不变的,变的只是骨骼的位置),因此可以利用这个特性,求出蒙皮矩阵。
Unity中的 LBS蒙皮算法
for (int vert = 0; vert < verts.Count; ++vert)
{
Vector3 point = verts[vert];
BoneWeight weight = Weights[vert];
List<Transform> transSet = bones;
Transform trans0 = bones[weight.boneIndexs[0]];
Transform trans1 = bones[weight.boneIndexs[1]];
Transform trans2 = bones[weight.boneIndexs[2]];
Transform trans3 = bones[weight.boneIndexs[3]];
Matrix4x4 tempMat0 = trans0.localToWorldMatrix * bindPoses[weight.boneIndexs[0]];
Matrix4x4 tempMat1 = trans1.localToWorldMatrix * bindPoses[weight.boneIndexs[1]];
Matrix4x4 tempMat2 = trans2.localToWorldMatrix * bindPoses[weight.boneIndexs[2]];
Matrix4x4 tempMat3 = trans3.localToWorldMatrix * bindPoses[weight.boneIndexs[3]];
Vector3 temp = tempMat0.MultiplyPoint(point) * weight.weights[0] +
tempMat1.MultiplyPoint(point) * weight.weights[1] +
tempMat2.MultiplyPoint(point) * weight.weights[2] +
tempMat3.MultiplyPoint(point) * weight.weights[3];
verts[vert] = srender.transform.worldToLocalMatrix.MultiplyPoint(temp);
}
MATRIX4X4 .MultiplyPoint 通过此矩阵转换位置