好像又快一年没写博客啦 🙁
前一段时时间在做角色表情相关的方案,由于项目中的骨骼计算相关方案都是自己实现的,没有使用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少女]