首页 > 其他分享 >Unity-利用SkinnedMeshRenderer和Mesh的BindPose实现骨骼动画

Unity-利用SkinnedMeshRenderer和Mesh的BindPose实现骨骼动画

时间:2022-11-29 20:37:55浏览次数:35  
标签:动画 SkinnedMeshRenderer 骨骼 BindPose Unity Mesh 顶点 bones new


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 通过此矩阵转换位置

 

标签:动画,SkinnedMeshRenderer,骨骼,BindPose,Unity,Mesh,顶点,bones,new
From: https://blog.51cto.com/u_6871414/5896971

相关文章

  • Unity判断对象是否在视野内
    判断对象是否在视野内,有两种方式:第一种:不设置固定的目标,使用LayerMask,设置寻找对象的Layer,使用Physics.OverlapSphere方法,以给定的位置为圆心,按照设定距离投射一个球体,返回......
  • Unity--Physics.OverlapSphere的参数LayerMask和GameObject的layer
    Layer介绍:Unity中是用int32来表示32个Layer层。int32表示二进制一共有32位(0—31)在Unity中每个GameObject都有Layer属性,默认的Layer都是Default。在Unity中可编辑的Layer共......
  • Unity-Animator Override Controller
    AnimatorOverrideController是一种资产类型,允许您扩展现有的AnimatorController,替换使用的特定动画,但保留原始结构,参数和逻辑。允许您创建相同基本状态机的多个变体,但每......
  • Unity用户手册-AssetBundle
    AssetBundle什么是AssetBundle?AssetBundle实际上是一个资源管理包。AssetBundle包含了两个部分:数据头以及数据段。数据头内包含了AssetBundle的元数据信息,比如它的标识符......
  • 1-Unity学习计划
    1、Unity3D程序开发基础-从基础讲解C#语言,熟悉字段、属性、接口、委托、事件,掌握C#面向对象编程的核心思想。让学员掌握Unity3d各个方面的知识和基本使用方法,为后面深入的......
  • 01. 魔法球特效_Unity Visual Effect Graph
    效果图01.粒子线条02.中心球体03.周边粒子 特效来源于:GabrielAguiarProd的VisualEffectGraph教程想学习更多的特效可以去UnityVFXGraph-BeginnerToInte......
  • Unity Application Block 3月12 发布的版本
    3月12日,Unity又发布了正式发布之前的版本,这个版本提供了安装程序.并且提供了一个依赖注入在实现方式:Setterinjection的配置API。之前发布的版本,属性......
  • 在Unity中使用C#调用C++动态链接库(DLL)
     在Unity中使用C#调用C++动态链接库(DLL)https://blog.csdn.net/qq_51456342/article/details/125693678 [FNote: 属性页中无C++项时,要先写点代码编译一下,就有了]......
  • Unity-UGUI按钮点击失效
      在Canvas中的三个按钮,不知为什么怎么点击都不响应点击事件,查找资料说是幕布顺序displayorder的问题,将按钮移动至Canvas最下方即可。......
  • unity 实现伤害值显示(飘字)效果
    一:创建伤害预制件①导入插件,如果是第一次导入,以下两个都需要导入 ②将Canvas调整为世界模式,并将主摄像机推入伤害预制件内部结构为: ③调整TMP(text)为合适的大小调......