Unity | 从建模到蒙皮动画的整个过程

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

一、顶点数组、索引数组及UV数组

二、Mesh、MeshFilter、MeshRenderer及SkinnedMeshRenderer

1. Mesh

2. MeshFilter

3. MeshRenderer

4. MeshRenderer与SkinnedMeshRenderer蒙皮网格

三、Unity中相关组件

1. mesh和material

2. sharedMesh和sharedMaterial

3. materials和sharedMaterials

四、蒙皮骨骼动画

1. 骨骼点

2. 制作蒙皮动画步骤

3. 关键帧

五、网格数据从制作到渲染的过程

六、动态实现从建模到蒙皮动画的整个过程

1. 代码

2. 相关数据

1Curve属性效果

2mesh数据

3. 整体效果

4. 注意细节


一、顶点数组、索引数组及UV数组

        顶点索引在程序中的表现为把所有顶点放进一个数组里顶点数组再用另一个整数数组作为索引来表达三角形的组成整数代表顶点数组里的index下标。 

        有几个索引数据就有几个UV坐标它们由两个浮点数组成这两个浮点数的范围是0到10表示贴图的左上角起始位置1表示贴图的最大偏移位置也就是右下角一听UV两个字母我们就应该知道说的是图片上的坐标。

        在绘制3D模型时除了顶点和索引数组外还有个数组叫UV数组这个UV数组是用于存储UV坐标而存在的。由于已经有了索引来表达三角形的三个顶点所以UV数组不需要再用索引来表达了只需要按照顶点索引形成的三角形来定制UV的顺序即可。

  • 4个顶点 [(0,0,0),(0,1,0),(1,1,0),(1,0,0)]
  • 顶点索引表示2个三角形[0,1,2,2,3,0]
  • UV数组表示两个三角形上贴图的绘制范围[(0,0),(0,1),(1,1),(1,1),(1,0),(0,0)]

二、Mesh、MeshFilter、MeshRenderer及SkinnedMeshRenderer

1. Mesh

        网格Mesh是数据资源它可以有自己的资源文件比如XXX.FBX。网格里存储了顶点、UV、顶点颜色、三角形、切线、法线、骨骼、骨骼权重等提供渲染所必要的数据。

2. MeshFilter

        MeshFilter是承载网格数据的类网格被实例化后存储在MeshFilter类中。MeshFilter包含两种类型即实例型和共享型的变量mesh和sharedMesh对mesh进行操作将生成新的mesh实例而对sharedMesh进行操作将改变与其他模型共同拥有的那个指定的网格数据实例。

3. MeshRenderer

        MeshRenderer是绘制网格的类具有渲染功能它会提取MeshFilter中的网格数据结合自身的materials或sharedMaterials进行渲染。

4. MeshRenderer与SkinnedMeshRenderer蒙皮网格

        MeshRenderer与SkinnedMeshRenderer这两个组件分别用于渲染3D模型和3D模型动画它们的模型数据都存储在MeshFilter中因此它们都依赖于MeshFilter组件。其中MeshRenderer只负责渲染模型我们也可以称它为普通网格渲染组件它从MeshFilter中提取网格顶点数据。而SkinnedMeshRenderer蒙皮网格虽然也渲染模型也从MeshFilter中提取模型网格顶点数据但蒙皮网格主要用于渲染动画服务所以蒙皮网格除了3D模型数据外还有骨骼数据及顶点权重数据。

        如果蒙皮网格上没有存储任何骨骼数据那么它与普通网格MeshRender的作用没有任何区别渲染的都是没有动画的3D模型。

        总之MeshRenderer适用于静态不变形的网格渲染而SkinnedMeshRenderer适用于需要骨骼动画和变形的网格渲染。在游戏中常见的角色模型通常会使用SkinnedMeshRenderer来实现骨骼动画而环境模型和其他静态物体通常会使用MeshRenderer。

三、Unity中相关组件

1. mesh和material

        mesh见上文定义了模型的形状和顶点数据而material定义了模型的外观属性如颜色、纹理和光照效果。

        mesh和material都是实例型的变量对mesh和material执行任何操作都是额外复制一份后再重新赋值即使只是get操作也同样会执行复制操作。也就是说对mesh和material进行操作后就会变成另外一个实例虽然看上去一样但其实已是不同的实例了。

2. sharedMesh和sharedMaterial

        sharedMesh和sharedMaterial与前面两个变量不同它们是共享型的。多个3D模型可以共用同一个指定的sharedMesh和sharedMaterial当修改sharedMesh或sharedMaterial里面的参数时指向同一个sharedMesh和sharedMaterial的多个模型就会同时改变效果。也就是说sharedMesh和sharedMaterial发生改变后所有使用sharedMesh和sharedMaterial资源的3D模型都会表现出相同的效果。

3. materials和sharedMaterials

        与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组肌肉驱动这种复杂细微程度的动画用两个网格顶点之间的线性插值来表现会比较合适。

1. 骨骼点

        骨骼动画由骨骼点组成骨骼点可以认为是带有相对空间坐标点的数据实体骨骼动画中可以有许多个骨骼点但根节点只有一个。在现代手机游戏中每个人物骨骼动画的数量一般为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个骨骼点的影响它们分别占有多少权重。

        当骨骼点移动时引擎就会使用这些顶点权重值来计算顶点的旋转度、偏移量和缩放度。 简单来说就是用顶点上的骨骼权重数据确定该点受到哪些骨骼点的影响影响的程度有多大。

2. 制作蒙皮动画步骤

        我们制作蒙皮动画通常分为三步

        第一步是使用3DMax、Maya等3D模型软件在几何模型上构建一系列的骨骼点bones并计算出几何模型的每个顶点受这些骨骼点影响的权重值BoneWeight。

        第二步是动画师通过3D模型软件制作一系列动画这些动画都是通过骨骼点的偏移、旋转、缩放来完成的每一帧都有可能发生变化关键帧与关键帧之间会补间一些非关键帧的动画。制作完毕后导出引擎专有的动画文件格式。在Unity3D中我们以.fbx作为专有格式文件。

        第三步则是在Unity3D中导入并播放动画播放动画时就已经存储了动画师制作的骨骼点位每帧发生变化的数据动画序列帧会根据每帧的动画数据来持续改变一系列骨骼点骨骼点的变化又会导致几何模型网格上的顶点发生相应的变化。

3. 关键帧

        通常我们使用的都是关键帧动画就是Unity3D里的Animation文件。在某个时间点上对需要改变的骨骼做关键帧而不是在每帧上都执行关键帧的操作。使用关键帧作为骨骼的旋转位移点好处是不需要为每帧都设置骨骼点的位置变化在关键帧与关键帧之间骨骼位置可以由Animation组件做平滑插值计算这样可以大大减少数据量相当于关键帧之间做了补间动画。补间动画的目的就是对需要改变的骨骼做平滑的位移、旋转、缩放的插值计算从而实时得到相应的结果以减少数据的使用量。

        由于4×4矩阵能够完整地表达点位的偏移、缩放和旋转等操作也能通过连续右乘法计算出从根节点到父节点再到子节点上的具体方位因此4×4矩阵是骨骼点必要的数据它表达了相对空间的偏移量即骨骼节点变化矩阵=根节点矩阵×父父父节点矩阵1×父父节点矩阵×父节点矩阵×骨骼节点矩阵。

五、网格数据从制作到渲染的过程

        首先美术人员制作3D模型并导出成Unity3D能够识别的格式即.fbx文件其中已经包含了顶点和索引数据。

        然后在程序中将.fbx实例化成Unity3D的GameObject它们身上附带的MeshFilter组件存储了网格的顶点数据和索引数据我们也可以自己创建顶点数组和索引数组以手动的方式输入顶点数据和索引数据。

        MeshFilter可用于存储顶点和索引数据MeshRender或SkinMeshRender可用于渲染模型这些顶点数据通常都会与材质球结合在渲染时一起送入图形卡其中与我们预想的不一样的是在送入时并不会由索引数据送入而是由三个顶点一组组成的三角形顶点送入图形卡。接着由图形卡负责处理我们送入的数据然后渲染帧缓存并输出到屏幕。

六、动态实现从建模到蒙皮动画的整个过程

1. 代码

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<Animation>();
        gameObject.AddComponent<SkinnedMeshRenderer>();
        rend = GetComponent<SkinnedMeshRenderer>();
        anim = GetComponent<Animation>();

        // 新建一个网格组件并编入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()
    {

    }
}

2. 相关数据

1Curve属性效果

2mesh数据

3. 整体效果

4. 注意细节

        clip.legacy的设置要在SetCurve之前否则发布exe后会报错

 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6