【Unity】用于BlendShape的LookAt效果控制器
为了控制游戏中角色眼球的朝向,通常会使用 LookAtIK
。但LookAtIK需要有眼球骨骼,如果动画师使用BlendShape制作了面部表情,可能会省略掉眼球骨骼。当然,更多的情况是,动画师为角色单独制作了一个高精度的带BlendShape的面部模型,这时,即使角色主体带有眼球骨骼,也无法直接控制到这个独立的面部模型。解决上述问题的关键点是为眼球分配一个骨骼,并将LookAtIK的计算结果换算成BlendShape权重。
如果角色不带有眼球骨骼,可以在角色的 Head
骨骼下建立一个空GameObject,将其位置对齐到两眼中间,不要有旋转,用来模拟眼球骨骼,然后将这个假的骨骼设置到LookAtIK的 Eyes
骨骼参数上;如果已有眼球骨骼,可以省略上一步。
有了眼球骨骼后,就可以在LookAtIK计算完成后(注册LookAtIK的 OnPostUpdate
回调),将眼球骨骼的旋转角度换算成BlendShape权重。最简单的换算就是线性地将骨骼旋转角度乘以一个系数得到BlendShape权重。如果需要更复杂的控制,可以增加一个 AnimationCurve
来动态调整换算系数。
如果没有使用LookAtIK,也可以利用眼睛的位置和眼睛直视目标的位置,通过三角函数手动计算眼球旋转,再换算成BlendShape参数。
下面给出一个利用FinalIK实现的线性换算控制器:
using RootMotion.FinalIK;
using UnityEngine;
/// <summary>
/// 用于BlendShape的LookAt效果控制器。
/// <br/>
/// 在Animator完成眼睛的BlendShape控制后,
/// 通过LookAtIK的计算结果重新计算并更新BlendShape参数,
/// 实现BlendShape的LookAt效果。
/// <br/>
/// 此组件需要配合BlendShape、LookAtIK和眼睛骨骼使用。
/// </summary>
public class BlendShapeLookAtController : MonoBehaviour
{
[Tooltip("带有眼球BlendShape的蒙皮组件。")]
[SerializeField]
private SkinnedMeshRenderer _skin;
[Tooltip("FinalIK的LookAtIK组件。")]
[SerializeField]
private LookAtIK _lookAtIK;
[SerializeField]
[Tooltip("眼球骨骼。")]
private Transform _eyeBone;
[Tooltip("视线每偏转1°时眼球BlendShape权重增加的百分点数。")]
[SerializeField]
[Range(-20f, 20f)]
private float _eyeBlendShapeWeightPerDegree = 2;
[Tooltip("眼球向上的BlendShape权重最大值。")]
[SerializeField]
[Range(0f, 100f)]
private float _maxEyeUpBlendShapeWeight = 60;
[Tooltip("眼球向下的BlendShape权重最大值。")]
[SerializeField]
[Range(0f, 100f)]
private float _maxEyeDownBlendShapeWeight = 60;
[Tooltip("眼球向左和向右的BlendShape权重最大值。")]
[SerializeField]
[Range(0f, 100f)]
private float _maxEyeLeftRightBlendShapeWeight = 60;
[SerializeField]
[Tooltip("眼球向上的BlendShape权重索引(索引从0开始)。")]
private ushort _eyeUpBlendShapeIndex = 10;
[SerializeField]
[Tooltip("眼球向下的BlendShape权重索引(索引从0开始)。")]
private ushort _eyeDownBlendShapeIndex = 11;
[SerializeField]
[Tooltip("眼球向左的BlendShape权重索引(索引从0开始)。")]
private ushort _eyeLeftBlendShapeIndex = 12;
[SerializeField]
[Tooltip("眼球向右的BlendShape权重索引(索引从0开始)。")]
private ushort _eyeRightBlendShapeIndex = 13;
private void Reset()
{
// SkinnedMeshRenderer
var skins = GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var skin in skins)
{
if (skin.name.ToUpper().Contains("FACE"))
{
_skin = skin;
break;
}
}
// LookAtIK
_lookAtIK = GetComponentInParent<LookAtIK>();
// Transform(Eye Bone)
if (_lookAtIK && _lookAtIK.solver.eyes.Length > 0)
{
_eyeBone = _lookAtIK.solver.eyes[0].transform;
}
}
private void Awake()
{
// IK 在 LateUpdate 中进行更新,此脚本在 IK 更新后再更新
_lookAtIK.solver.OnPostUpdate += UpdateEyeLookAtBlendShapes;
}
/// <summary>
/// 计算并更新眼球的BlendShape权重。
/// </summary>
private void UpdateEyeLookAtBlendShapes()
{
// 通过眼睛骨骼的本地空间旋转角计算BlendShape值
var horizontal = _eyeBone.localEulerAngles.x;
var vertical = _eyeBone.localEulerAngles.z;
var eyeLeftBlendShapeWeight = 0f;
var eyeRightBlendShapeWeight = 0f;
var eyeUpBlendShapeWeight = 0f;
var eyeDownBlendShapeWeight = 0f;
// 计算眼球BlendShape权重
if (horizontal > 180)
{
eyeRightBlendShapeWeight = Mathf.Min(_maxEyeLeftRightBlendShapeWeight, (360 - horizontal) * _eyeBlendShapeWeightPerDegree);
}
else
{
eyeLeftBlendShapeWeight = Mathf.Min(_maxEyeLeftRightBlendShapeWeight, horizontal * _eyeBlendShapeWeightPerDegree);
}
if (vertical > 180)
{
eyeDownBlendShapeWeight = Mathf.Min(_maxEyeDownBlendShapeWeight, (360 - vertical) * _eyeBlendShapeWeightPerDegree);
}
else
{
eyeUpBlendShapeWeight = Mathf.Min(_maxEyeUpBlendShapeWeight, vertical * _eyeBlendShapeWeightPerDegree);
}
// 设置眼球BlendShape权重
_skin.SetBlendShapeWeight(_eyeUpBlendShapeIndex, eyeUpBlendShapeWeight);
_skin.SetBlendShapeWeight(_eyeDownBlendShapeIndex, eyeDownBlendShapeWeight);
_skin.SetBlendShapeWeight(_eyeLeftBlendShapeIndex, eyeLeftBlendShapeWeight);
_skin.SetBlendShapeWeight(_eyeRightBlendShapeIndex, eyeRightBlendShapeWeight);
}
}