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

前一段时时间在做角色表情相关的方案,由于项目中的骨骼计算相关方案都是自己实现的,没有使用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.闭眼

整合后的效果

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

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

核心代码
[cc lang=”C#”]
void Start()
{
Mf = this.GetComponent();
Mr = this.GetComponent();

_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 tmpBSVertexList = new List();
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; } [/cc] btw:骨骼动画集大成的学习资料还是要靠隔壁国家的小黄油,推荐下最新的[AI少女]