【Unity】用于Humanoid骨骼的扭曲矫正组件
组件已知限制:
- 可以在一定旋转范围内消除变形,不能无限度的消除变形;
- 不支持连续的两个扭曲矫正骨骼,如果需要,可以自行调整代码。
在Unity中,为了使用骨骼重定向复用动画,需要在模型导入设置中把AnimationType位置为Hunamoid。Humanoid骨骼本身带有一定的限制,比如只支持有限的几个骨骼节点。
骨骼节点过少会导致Animator在播放某些动作时模型发生过度扭曲,为了解决这些问题,通常会增加骨骼数量,来执行扭曲矫正。但在使用Hunamoid骨骼时,这些额外增加的骨骼会被Unity忽略掉,导致扭曲矫正失效。在下图中可以看出,位于Hand与LowerArm之间的1号骨骼和位于LowerArm与UpperArm之间的2号骨骼被Unity忽略掉了。
此时,如果手臂发生大幅度旋转,就会导致手腕附近出现严重的变形。
为了修正模型,可以在脚本中手动执行扭曲矫正,方法也很简单,就是找到两个Humanoid骨骼之间用于扭曲矫正的骨骼,将它的Rotation设置为两个Hunamoid骨骼的Rotation的中间值。
源代码
using System;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Animator))]
public class HumanoidTwister : MonoBehaviour
{
[Tooltip("扭曲矫正强度。值越小,越贴近父Humanoid骨骼;值越大,越贴近子Humanoid骨骼。")]
[SerializeField]
[Range(0, 1)]
private float _twistIntensity = 0.5f;
[Tooltip("扭曲矫正骨骼数据。")]
[SerializeField]
private List<TwistBoneInfo> _twistBoneInfoList = new List<TwistBoneInfo>
{
new TwistBoneInfo("Hips", "Spine"), new TwistBoneInfo("Spine", "Chest"), new TwistBoneInfo("Chest", "UpperChest"),
new TwistBoneInfo("LeftShoulder", "LeftUpperArm"), new TwistBoneInfo("LeftUpperArm", "LeftLowerArm"), new TwistBoneInfo("LeftLowerArm", "LeftHand"),
new TwistBoneInfo("RightShoulder", "RightUpperArm"), new TwistBoneInfo("RightUpperArm", "RightLowerArm"), new TwistBoneInfo("RightLowerArm", "RightHand"),
new TwistBoneInfo("LeftUpperLeg", "LeftLowerLeg"), new TwistBoneInfo("LeftLowerLeg", "LeftFoot"), new TwistBoneInfo("LeftFoot", "LeftToes"),
new TwistBoneInfo("RightUpperLeg", "RightLowerLeg"), new TwistBoneInfo("RightLowerLeg", "RightFoot"), new TwistBoneInfo("RightFoot", "RightToes")
};
private void CollectHierarchyRecursively(Transform root, Dictionary<string, Transform> container)
{
container[root.name] = root;
for (int i = 0; i < root.childCount; i++)
{
var child = root.GetChild(i);
CollectHierarchyRecursively(child, container);
}
}
private void CollectTwistTransforms(TwistBoneInfo twistBoneInfo, HumanBone[] humanBones, Dictionary<string, Transform> actorHierarchy)
{
string parentHierarchyName = null;
string childHierarchyName = null;
for (int i = 0; i < humanBones.Length; i++)
{
var humanBone = humanBones[i];
var noParentName = string.IsNullOrEmpty(parentHierarchyName);
var noChildName = string.IsNullOrEmpty(childHierarchyName);
if (noParentName && humanBone.humanName.Equals(twistBoneInfo.ParentName))
{
parentHierarchyName = humanBone.boneName;
noParentName = false;
}
if (noChildName && humanBone.humanName.Equals(twistBoneInfo.ChildName))
{
childHierarchyName = humanBone.boneName;
noChildName = false;
}
if (!noParentName && !noChildName)
{
break;
}
}
if (string.IsNullOrEmpty(parentHierarchyName) || string.IsNullOrEmpty(childHierarchyName))
{
return;
}
var parent = actorHierarchy[parentHierarchyName];
var child = actorHierarchy[childHierarchyName];
var twist = child.parent;
if (twist && twist.parent == parent)
{
twistBoneInfo.TwistBone = twist;
twistBoneInfo.ParentBone = parent;
twistBoneInfo.ChildBone = child;
}
else
{
twistBoneInfo.SetDisplayNameInvalid();
}
}
private void Reset()
{
var animatorHierarchy = new Dictionary<string, Transform>(transform.childCount);
CollectHierarchyRecursively(transform, animatorHierarchy);
var avatar = GetComponent<Animator>().avatar;
var humanBones = avatar.humanDescription.human;
for (int i = 0; i < _twistBoneInfoList.Count; i++)
{
var twistInfo = _twistBoneInfoList[i];
CollectTwistTransforms(twistInfo, humanBones, animatorHierarchy);
}
}
private void LateUpdate()
{
for (int i = 0; i < _twistBoneInfoList.Count; i++)
{
_twistBoneInfoList[i].Twist(_twistIntensity);
}
}
[Serializable]
class TwistBoneInfo
{
[HideInInspector]
public string TwistName;
[Tooltip("扭曲矫正骨骼(直接连接两个Humanoid骨骼的非Humanoid骨骼)。")]
public Transform TwistBone;
[HideInInspector]
public string ParentName;
[Tooltip("父Humanoid骨骼。")]
public Transform ParentBone;
[HideInInspector]
public string ChildName;
[Tooltip("子Humanoid骨骼。")]
public Transform ChildBone;
#if !UNITY_EDITOR
private bool? _isValid;
#endif
public TwistBoneInfo(string parentName, string childName)
{
ParentName = parentName;
ChildName = childName;
TwistName = $"{ParentName} <-> {ChildName}";
}
public void SetDisplayNameInvalid()
{
TwistName = $"[INVALID] {ParentName} <-> {ChildName}";
}
public bool IsValid()
{
#if UNITY_EDITOR
return TwistBone && ParentBone && ChildBone;
#else
if (!_isValid.HasValue)
{
_isValid = m_twistBone && m_parentBone && m_childBone;
}
return _isValid.Value;
#endif
}
public void Twist(float intensity)
{
if (!IsValid())
{
return;
}
var childRotation = ChildBone.rotation;
TwistBone.rotation = Quaternion.Slerp(ParentBone.rotation, childRotation, intensity);
ChildBone.rotation = childRotation;
}
}
}
#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(HumanoidTwister))]
class HumanoidTwisterEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
UnityEditor.EditorGUI.BeginDisabledGroup(true);
base.OnInspectorGUI();
UnityEditor.EditorGUI.EndDisabledGroup();
}
}
#endif
|