Unity项目捏脸解决方案BlendShapes
引子
最近公司在开发一个捏脸的SDK,使用的核心功能就是使用Unity的SkinnedMeshRenderer上的BlendShapes来实现的。这个功能在Maya和3ds Max中都可以找到。
BlendShapes
BlendShapes是一种动画制作方式,与骨骼动画相比,它主要应用在很小的局部,比如角色的面部表情。
骨骼只适合控制整体的动作,但太局部的动作使用骨骼制作会非常繁琐,同时太多的骨骼对性能也有影响。
BlendShapes的原理很简单,就是在相邻两个网格间做插值运算,从一上形状融合到另一个形状;或者说是单个网格变形以实现许多预定义形状和任何数量之间组合的技术,在Maya/3ds Max中我们称它为变形目标,例如单个网格是默认形状的基本形状(例如无表情的面),并且基本形状的其他形状用于混合/变形,是不同的表达(笑、皱眉、闭合眼皮),这些被统称为混合形状或变形目标。所以美术只要制作若干个形状的模型即可,比摆骨骼要方便很多,特别是对于面部这种很局部的动作来说。
模型在导入时,Import BlendShapes选项默认是选中的。
具体使用
- 要在Maya或者3ds Max里面制作一个含有BlendShapes的形变动画Avator。
- 把Avator导入到Unity中。
- 导入的模型上面有组件SkinnedMeshRenderer,面板上有BlendShapes参数,它的下面有一连串的数值可以手动或编码设置,来实现捏脸效果。
代码
Unity提供的一些相关接口(组件SkinnedMeshRenderer中):
void SetBlendShapeWeight(int index, float value)
:用于直接设置BlendShape的值,使用index
按照面板上的自上而下顺序下标来索引设置的“部位”。float GetBlendShapeWeight(int index)
:根据index
获取对应的BlendShape值。sharedMesh.blendShapeCount
:组建中BlendShape的数量。sharedMesh.GetBlendShapeName(int shapeIndex)
:根据index
获取对应的BlendShape名称字符串。sharedMesh.GetBlendShapeIndex(string blendShapeName)
:根据名称获取索引的下标(第几个)。
这里我只用了了几个,还有一些并没有使用测试过。
void AddBlendShapeFrame(string shapeName, float frameWeight, Vector3[] deltaVertices, Vector3[] deltaNormals, Vector3[] deltaTangents)
:添加一个新的BlendShape框架。int GetBlendShapeFrameCount(int shapeIndex)
:返回BlendShape的帧数。void GetBlendShapeFrameVertices(int shapeIndex, int frameIndex, Vector3[] deltaVertices, Vector3[] deltaNormals, Vector3[] deltaTangents)
:检索BlendShape框架的deltaVertices,deltaNormals和deltaTangents。float GetBlendShapeFrameWeight(int shapeIndex, int frameIndex)
:返回BlendShape框架的权重。
之后下面是我使用的一个脚本,当使用的一个模型(FBX)下有好多分支模型节点时,使用这个脚本统一管理设置BlendShape。
using UnityEngine;
public class BlendShapeModel : MonoBehaviour
{
private SkinnedMeshRenderer[] skinnedMeshRenderers;
private string[] m_bsNameAll;
void Start()
{//获取初始化
skinnedMeshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
m_bsNameAll = new string[skinnedMeshRenderers.Length];
for (int i = 0; i < skinnedMeshRenderers.Length; i++)
{
for (int j = 0; j < skinnedMeshRenderers[i].sharedMesh.blendShapeCount; j++)
{
m_bsNameAll[i] += skinnedMeshRenderers[i].sharedMesh.GetBlendShapeName(j) + ',';
}
}
}
/// <summary>
/// 设置BlendShape值
/// </summary>
/// <param name="name">BlendShape的名字</param>
/// <param name="value">BlendShape设置的值</param>
public void SetBlendShapeValue(string name, float value)
{
for (int i = 0; i < m_bsNameAll.Length; i++)
{
if (m_bsNameAll[i].Contains(name))
{
int lenght = m_bsNameAll[i].IndexOf(name);
int index = m_bsNameAll[i].Substring(0, lenght).Split(',').Length - 1;
skinnedMeshRenderers[i].SetBlendShapeWeight(index, value);
}
}
}
}
代码中,我是使用的名称检索对应的BlendShape,所以各个分支所对应的相同的BlendShape的名称要有相同的字符串,或一定的规律。