骨骼动画中的表情制作

好像又快一年没写博客啦 🙁

前一段时时间在做角色表情相关的方案,由于项目中的骨骼计算相关方案都是自己实现的,没有使用Unity SkinnedMeshRenderer, 而且Unity的骨骼方案性能超级烂和各种黑盒代码不能信任,所以表情方案也需要自己来做了。

插个题外话,Unity的骨骼方案到底哪里烂
1.SkinnedMeshRenderer在每帧计算骨骼数据的时候是否有做预计算优化?在抛开动作融合的需求上,实际上所有动画的骨骼计算结果都是可以离线计算的好的,运行时不需要再次从关节骨骼空间变换到模型空间,直接计算好模型空间的数据运行时输出即可。

2.为什么PlayerSetting 里的GPU Skinning选项永远都是*号? 在Opengles3.0以及以下Es2.0 其寄存器数量不会超过256,大部分设备是224个顶点寄存器,假设我在这些设备上上传 1000个骨骼上去,Unity会成功进行GPU Skinning 还是fallback 到Cpu Skinning上?

3.使用Animator制作的大型状态机,如果带有大量动画,那么在加载的时候会加载整个状态机中包含的所有动画,此时加载性能会瞬间达到瓶颈,因此一般MMO项目都会自己来写额外分拆加载逻辑,而且Animator中的State融合时间又很难交给策划灵活配置,最后是很难做网络游戏的表现层和逻辑层分离,似乎是天生对单机游戏亲和力更好。

4.对Instancing的内置没有支持方案,以及没有做JobSystem的整合等等

5.存储空间和计算量大,由于Unity的骨骼使用Matrix4x4 矩阵,实际上骨骼计算使用3×4矩阵就可以满足, 不带缩放的骨骼甚至可以使用 8个float的双四元数就能解决,无论是在叉乘计算还是空间存储极限情况下性能会差距1倍左右

题外话完毕~进入正题啦

业界中制作表情动画的方案大致分2种, 骨骼 或者 BlendShape
什么时候用骨骼? 什么时候用BlendShape?
从要变现的结果上来看,2者都可以实现,但是从制作流程上2者差距很大

骨骼方案
优点:运行时计算量小,离线存储的数据量少
缺点:表情做到一半发现表现不够到位,需要临时增加骨骼,于是又要回炉到3dmax.
针对原画设计的表情,用骨骼细调参数的过程实现需要大量的时间

运行时预设的表情需要策划配置大量不好阅读的参数

BlendShape方案
优点:针对原画设计的表情,美术直接按表情建模即可,效率非常高,对于要做跨人种,高矮胖瘦的体型类变化后的表情支持,可以不受骨骼权重影响,总结就是表现一步到位
缺点:对于三角面较多的模型,做BlendShape的数据量等同于表情数量*模型数量,后期程序可以抽出只发生变化的顶点来存储,但是数据量还是较骨骼来说会多出很多

总结来说,如果不要做挤眉弄眼,只是简单表情的话,直接BlendShape.
如果要做兽人,萝莉,狼人等跨人种 以及变身的话,直接BlendShape.
其他的要做细的表情,可以混合骨骼方案实现

以下是项目中用到的表情资源 BlendShape
1.张嘴

2.闭嘴

3.闭眼

整合后的效果

基于骨骼表情动画资源的话面部会包含较多的骨骼

操作骨骼变化表情的效果主要是通过缩放和旋转骨骼来实现

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
 void Start()
    {
        Mf = this.GetComponent<MeshFilter>();
        Mr = this.GetComponent<MeshRenderer>();

        _baseVertices = BaseMesh.vertices;
        _baseNormal = BaseMesh.normals;
        _tmpVertices = new Vector3[_baseVertices.Length];
        _tmpNormal = new Vector3[_baseNormal.Length];

        _tmpMesh = new Mesh();
        _tmpMesh.vertices = _baseVertices;
        _tmpMesh.normals = _baseNormal;
        _tmpMesh.triangles = BaseMesh.triangles;
        _tmpMesh.uv = BaseMesh.uv;
        _tmpMesh.MarkDynamic();

        Mf.sharedMesh = _tmpMesh;

        _vertexCount = BaseMesh.vertices.Length;
        BlendShapeWeight = new float[BlendShapeMesh.Length];
        _blendShapes = new BlendShape[BlendShapeMesh.Length];

        for (int i = 0; i < BlendShapeMesh.Length; i++)
        {
            _blendShapes[i] = new BlendShape();

            //差异顶点数量
            List<VertexDeltaData> tmpBSVertexList = new List<VertexDeltaData>();
            for(int j = 0; j < _vertexCount; j++)
            {
                //变形器与基础顶点的差异
                if(_baseVertices[j] != BlendShapeMesh[i].vertices[j])
                {
                    VertexDeltaData blendShapeVertex = new VertexDeltaData();
                    blendShapeVertex.vertexIdx = j;
                    blendShapeVertex.deltaVertex = BlendShapeMesh[i].vertices[j] - _baseVertices[j]; //delta position
                    blendShapeVertex.deltaNormal = BlendShapeMesh[i].normals[j] - _baseNormal[j]; //delta normal

                    tmpBSVertexList.Add(blendShapeVertex);
                }
            }

            _blendShapes[i].vertices = tmpBSVertexList.ToArray();
        }

        ModifyWeight();
        _hasInited = true;
    }

    void ModifyWeight()
    {
        System.Array.Copy(_baseVertices, _tmpVertices, _baseVertices.Length);
        System.Array.Copy(_baseNormal, _tmpNormal, _baseNormal.Length);

        for(int i = 0; i < BlendShapeMesh.Length; i++)
        {
            BlendShape curBs = _blendShapes[i];
            int deltaDataCount = curBs.vertices.Length;
            for(int j = 0; j < deltaDataCount; j++)
            {
                VertexDeltaData deltaData = curBs.vertices[j];
                int idx = deltaData.vertexIdx;

                _tmpVertices[idx] += deltaData.deltaVertex * BlendShapeWeight[i];
                _tmpNormal[idx] += deltaData.deltaNormal * BlendShapeWeight[i];
            }
        }

        _tmpMesh.vertices = _tmpVertices;
        _tmpMesh.normals = _tmpNormal;
    }

btw:骨骼动画集大成的学习资料还是要靠隔壁国家的小黄油,推荐下最新的[AI少女]

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.