目录
一、顶点数组、索引数组及UV数组
二、Mesh、MeshFilter、MeshRenderer及SkinnedMeshRenderer
1. Mesh
2. MeshFilter
3. MeshRenderer
4. MeshRenderer与SkinnedMeshRenderer(蒙皮网格)
三、Unity中相关组件
1. mesh和material
四、蒙皮骨骼动画
1. 骨骼点
2. 制作蒙皮动画步骤
3. 关键帧
五、网格数据从制作到渲染的过程
六、动态实现从建模到蒙皮动画的整个过程
1. 代码
2. 相关数据
(1)Curve属性效果
(2)mesh数据
3. 整体效果
4. 注意细节
顶点索引在程序中的表现为,把所有顶点放进一个数组里(顶点数组),再用另一个整数数组作为索引来表达三角形的组成(整数代表顶点数组里的index下标)。
有几个索引数据,就有几个UV坐标,它们由两个浮点数组成,这两个浮点数的范围是0到1,0表示贴图的左上角起始位置,1表示贴图的最大偏移位置,也就是右下角,一听UV两个字母我们就应该知道说的是图片上的坐标。
在绘制3D模型时,除了顶点和索引数组外,还有个数组叫UV数组,这个UV数组是用于存储UV坐标而存在的。由于已经有了索引来表达三角形的三个顶点,所以UV数组不需要再用索引来表达了,只需要按照顶点索引形成的三角形来定制UV的顺序即可。
网格(Mesh)是数据资源,它可以有自己的资源文件,比如XXX.FBX。网格里存储了顶点、UV、顶点颜色、三角形、切线、法线、骨骼、骨骼权重等提供渲染所必要的数据。
MeshFilter是承载网格数据的类,网格被实例化后存储在MeshFilter类中。MeshFilter包含两种类型,即实例型和共享型的变量(mesh和sharedMesh),对mesh进行操作将生成新的mesh实例,而对sharedMesh进行操作将改变与其他模型共同拥有的那个指定的网格数据实例。
MeshRenderer是绘制网格的类,具有渲染功能,它会提取MeshFilter中的网格数据,结合自身的materials或sharedMaterials进行渲染。
MeshRenderer与SkinnedMeshRenderer这两个组件分别用于渲染3D模型和3D模型动画,它们的模型数据都存储在MeshFilter中,因此它们都依赖于MeshFilter组件。其中,MeshRenderer只负责渲染模型,我们也可以称它为普通网格渲染组件,它从MeshFilter中提取网格顶点数据。而SkinnedMeshRenderer(蒙皮网格)虽然也渲染模型,也从MeshFilter中提取模型网格顶点数据,但蒙皮网格主要用于渲染动画服务,所以蒙皮网格除了3D模型数据外,还有骨骼数据及顶点权重数据。
如果蒙皮网格上没有存储任何骨骼数据,那么它与普通网格MeshRender的作用没有任何区别,渲染的都是没有动画的3D模型。
总之,MeshRenderer适用于静态不变形的网格渲染,而SkinnedMeshRenderer适用于需要骨骼动画和变形的网格渲染。在游戏中,常见的角色模型通常会使用SkinnedMeshRenderer来实现骨骼动画,而环境模型和其他静态物体通常会使用MeshRenderer。
mesh(见上文)定义了模型的形状和顶点数据,而material定义了模型的外观属性,如颜色、纹理和光照效果。
mesh和material都是实例型的变量,对mesh和material执行任何操作,都是额外复制一份后再重新赋值,即使只是get操作,也同样会执行复制操作。也就是说,对mesh和material进行操作后,就会变成另外一个实例,虽然看上去一样,但其实已是不同的实例了。
sharedMesh和sharedMaterial与前面两个变量不同,它们是共享型的。多个3D模型可以共用同一个指定的sharedMesh和sharedMaterial,当修改sharedMesh或sharedMaterial里面的参数时,指向同一个sharedMesh和sharedMaterial的多个模型就会同时改变效果。也就是说,sharedMesh和sharedMaterial发生改变后,所有使用sharedMesh和sharedMaterial资源的3D模型都会表现出相同的效果。
与material和sharedMaterial一样,materials是实例型的,sharedMaterials是共享型的,只不过现在它们变成了数组形式。
无论对materials进行什么操作,都会复制一份一模一样的来替换,sharedMaterials操作后,指向这个材质球的所有模型都会改变效果。materials&sharedMaterials和material&sharedMaterial的区别是,materials和sharedMaterials可以针对不同的子网格,material和sharedMaterial只针对主网格。也就是说,material和sharedMaterial等于materials[0]和sharedMaterials[0]。
3D模型要做动作,首先是模型网格上的点、线、面要动起来,只有点、线、面动起来了,每帧渲染的时候才能渲染出不同的网格形状,从而才有看起来会动的画面。那么怎么让点、线、面动起来呢?
主要有两种方法:一种是用一种算法来改变顶点位置,我们通常称之为顶点动画;另一种是用骨骼的方式去影响网格顶点,我们称之为骨骼动画。这两种动画方式都是通过在每一帧里偏移模型网格上的各个顶点,让模型变形,从而形成动画的效果的。每一帧模型网格的形状不一样,播放时就形成了动画,两种方法虽然方式不同,但都遵循同一个原理。
刚性层级式动画
起初3D模型动画只有刚性层级式动画(rigid hierarchical animation),它将整个模型拆分成多个部位,然后按照层级节点的方式安装上去。
这样,模型以层级的方式布置在节点上,当父节点移动、旋转、缩放时,子节点也随之而动。刚性层级式动画的问题很多,其中比较严重的是关节连接位置常产生“裂缝”,因为它们并不是由一个模型衔接而成的,而是由多个模型拼凑起来的。
变形目标动画
变形目标动画(morph target animation)的方法常使用在脸部动画中,它将动画制作成几个固定的极端姿势的模型,然后在两个模型的每个顶点之间做线性插值,脸部动作大约需要50组肌肉驱动,这种复杂细微程度的动画用两个网格顶点之间的线性插值来表现会比较合适。
骨骼动画由骨骼点组成,骨骼点可以认为是带有相对空间坐标点的数据实体,骨骼动画中可以有许多个骨骼点,但根节点只有一个。(在现代手机游戏中,每个人物骨骼动画的数量一般为30个左右,PC单机游戏中可达75个左右。骨骼数量越多,动画就越有动感,但同时也会消耗掉更多的运算量。)
骨骼点为树形结构,一个骨骼可以有很多个子骨骼,子骨骼存在于父骨骼的相对空间下,子骨骼与父骨骼拥有相同的功能,由于子骨骼在父骨骼的空间下,因此,当父骨骼移动、旋转、缩放时,子骨骼也随着父骨骼一起移动、旋转、缩放,它们的相对位置、相对角度、相对比例不变。
在Unity3D的蒙皮网格组件中,bones变量用于存储所有骨骼点,骨骼点在蒙皮网格中是以Transform数组的形式存储的,这一点可以从bones变量就是Transform[]数组类型得知。
骨骼点可以影响周围一定范围内的顶点,单一顶点也可以受到多个骨骼的影响。除了骨骼数据,模型中的每个顶点都有对其顶点本身影响最多的4个骨骼的权重值,Unity3D对这4个骨骼的权重值进行了存储,将它们存放在BoneWeight的Struct结构中,每个SkinMeshRender类都有一个boneWeights数组变量,用于记录所有顶点的骨骼权重值,那些没有骨骼动画的网格,就没有这些数据。
从Unity3D的图形质量设置(Quality setting)中,我们可以看到,Blend Weights参数可用于设置一个顶点能被多少骨骼影响。其中有1 Bone、2 Bones、4 Bones等参数,表达的意思分别是一个顶点能被1个骨骼影响,或者被2个骨骼影响,或者被4个骨骼影响。被影响的骨骼数越多,CPU消耗在骨骼计算蒙皮上的时间就越长,消耗量越大。
骨骼动画是以顶点的骨骼权重数据来决定顶点受哪些骨骼点的影响的,每个顶点都可以受到骨骼点的影响。在Unity3D中,每个顶点最多被4个骨骼点影响,这些数据被存储在BoneWeight实例里,该实例用于描述当前顶点分别受到哪4个骨骼点的影响,它们分别占有多少权重。
当骨骼点移动时,引擎就会使用这些顶点权重值来计算顶点的旋转度、偏移量和缩放度。 简单来说就是,用顶点上的骨骼权重数据确定该点受到哪些骨骼点的影响,影响的程度有多大。
我们制作蒙皮动画通常分为三步:
第一步是使用3DMax、Maya等3D模型软件在几何模型上构建一系列的骨骼点(bones),并计算出几何模型的每个顶点受这些骨骼点影响的权重值(BoneWeight)。
第二步是动画师通过3D模型软件制作一系列动画,这些动画都是通过骨骼点的偏移、旋转、缩放来完成的,每一帧都有可能发生变化,关键帧与关键帧之间会补间一些非关键帧的动画。制作完毕后,导出引擎专有的动画文件格式。在Unity3D中,我们以.fbx作为专有格式文件。
第三步则是在Unity3D中导入并播放动画,播放动画时就已经存储了动画师制作的骨骼点位每帧发生变化的数据,动画序列帧会根据每帧的动画数据来持续改变一系列骨骼点,骨骼点的变化又会导致几何模型网格上的顶点发生相应的变化。
通常我们使用的都是关键帧动画,就是Unity3D里的Animation文件。在某个时间点上对需要改变的骨骼做关键帧,而不是在每帧上都执行关键帧的操作。使用关键帧作为骨骼的旋转位移点,好处是不需要为每帧都设置骨骼点的位置变化,在关键帧与关键帧之间,骨骼位置可以由Animation组件做平滑插值计算,这样可以大大减少数据量,相当于关键帧之间做了补间动画。补间动画的目的就是对需要改变的骨骼做平滑的位移、旋转、缩放的插值计算,从而实时得到相应的结果,以减少数据的使用量。
由于4×4矩阵能够完整地表达点位的偏移、缩放和旋转等操作,也能通过连续右乘法计算出从根节点到父节点再到子节点上的具体方位,因此4×4矩阵是骨骼点必要的数据,它表达了相对空间的偏移量,即骨骼节点变化矩阵=根节点矩阵×父父父节点矩阵1×父父节点矩阵×父节点矩阵×骨骼节点矩阵。
首先,美术人员制作3D模型并导出成Unity3D能够识别的格式,即.fbx文件,其中已经包含了顶点和索引数据。
然后在程序中将.fbx实例化成Unity3D的GameObject,它们身上附带的MeshFilter组件存储了网格的顶点数据和索引数据(我们也可以自己创建顶点数组和索引数组,以手动的方式输入顶点数据和索引数据)。
MeshFilter可用于存储顶点和索引数据,MeshRender或SkinMeshRender可用于渲染模型,这些顶点数据通常都会与材质球结合,在渲染时一起送入图形卡,其中与我们预想的不一样的是,在送入时并不会由索引数据送入,而是由三个顶点一组组成的三角形顶点送入图形卡。接着由图形卡负责处理我们送入的数据,然后渲染帧缓存,并输出到屏幕。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestMesh : MonoBehaviour
{
public SkinnedMeshRenderer rend;
public Animation anim;
public AnimationCurve curve;
public AnimationClip clip;
public string clipName = "test";
// Start is called before the first frame update
void Start()
{
// 新建一个动画组件和蒙皮组件
gameObject.AddComponent();
gameObject.AddComponent();
rend = GetComponent();
anim = GetComponent();
// 新建一个网格组件,并编入4个顶点形成一个矩形形状的网格
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"));
// 为每个顶点定制相应的骨骼权重
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;
// 创建新的骨骼点,设置骨骼点的位置、父骨骼点和位移旋转矩阵
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;
mesh.bindposes = bindPoses;
// 将骨骼点和网格赋值给蒙皮组件
rend.bones = bones;
rend.sharedMesh = mesh;
// 定制几个关键帧
curve = new AnimationCurve();
curve.keys = new Keyframe[] { new Keyframe(0, 3, 0, 0), new Keyframe(2, -3, 0, 0), new Keyframe(4, 3, 0, 0) };
// 创建帧动画
clip = new AnimationClip();
clip.legacy = true; //正确
clip.SetCurve("Lower", typeof(Transform), "m_LocalPosition.z", curve);
//clip.legacy = true; //错误,应先设置legacy为true,再SetCurve,否则发布exe后报错
clip.wrapMode = WrapMode.Loop;
// 将帧动画赋值给动画组件,并播放动画
anim.AddClip(clip, clipName);
anim.playAutomatically = true;
anim.clip = clip;
anim.Play(clipName);
Debug.Log("play");
}
// Update is called once per frame
void Update()
{
}
}
clip.legacy的设置要在SetCurve之前,否则发布exe后会报错: