【Unity】用于Humanoid骨骼的扭曲矫正组件

【Unity】用于Humanoid骨骼的扭曲矫正组件

组件已知限制:

  1. 可以在一定旋转范围内消除变形,不能无限度的消除变形;
  2. 不支持连续的两个扭曲矫正骨骼,如果需要,可以自行调整代码。

在Unity中,为了使用骨骼重定向复用动画,需要在模型导入设置中把AnimationType位置为Hunamoid。Humanoid骨骼本身带有一定的限制,比如只支持有限的几个骨骼节点。

Humanoid骨骼所支持的骨骼节点

骨骼节点过少会导致Animator在播放某些动作时模型发生过度扭曲,为了解决这些问题,通常会增加骨骼数量,来执行扭曲矫正。但在使用Hunamoid骨骼时,这些额外增加的骨骼会被Unity忽略掉,导致扭曲矫正失效。在下图中可以看出,位于Hand与LowerArm之间的1号骨骼和位于LowerArm与UpperArm之间的2号骨骼被Unity忽略掉了。

被Unity忽略的扭曲矫正骨骼

此时,如果手臂发生大幅度旋转,就会导致手腕附近出现严重的变形。

变形的手腕

为了修正模型,可以在脚本中手动执行扭曲矫正,方法也很简单,就是找到两个Humanoid骨骼之间用于扭曲矫正的骨骼,将它的Rotation设置为两个Hunamoid骨骼的Rotation的中间值。

手动扭曲矫正后的手腕

源代码

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 用于Humanoid骨骼的动画扭曲矫正工具。
/// </summary>
[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")
    };


    /// <summary>
    /// 递归收集Transform的全部节点。
    /// </summary>
    /// <param name="root"></param>
    /// <param name="container"></param>
    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);
        }
    }

    /// <summary>
    /// 收集用于扭曲矫正的骨骼节点。
    /// </summary>
    /// <param name="twistBoneInfo"></param>
    /// <param name="humanBones"></param>
    /// <param name="actorHierarchy"></param>
    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);

            // 查找扭曲矫正骨骼的父Humanoid骨骼
            if (noParentName && humanBone.humanName.Equals(twistBoneInfo.ParentName))
            {
    
    
                parentHierarchyName = humanBone.boneName;
                noParentName = false;
            }

            // 查找扭曲矫正骨骼的子Humanoid骨骼
            if (noChildName && humanBone.humanName.Equals(twistBoneInfo.ChildName))
            {
    
    
                childHierarchyName = humanBone.boneName;
                noChildName = false;
            }

            if (!noParentName && !noChildName)
            {
    
    
                break;
            }
        }

        // Humanoid骨骼不全,退出
        if (string.IsNullOrEmpty(parentHierarchyName) || string.IsNullOrEmpty(childHierarchyName))
        {
    
    
            return;
        }

        // 查找扭曲矫正骨骼(直接连接两个Humanoid骨骼的非Humanoid骨骼)
        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);
        }
    }


    /// <summary>
    /// 扭曲矫正骨骼数据。
    /// </summary>
    [Serializable]
    class TwistBoneInfo
    {
    
    
        /// <summary>
        /// 扭曲矫正骨骼名称。
        /// 仅用于Inspector显示。
        /// </summary>
        [HideInInspector]
        public string TwistName;

        [Tooltip("扭曲矫正骨骼(直接连接两个Humanoid骨骼的非Humanoid骨骼)。")]
        public Transform TwistBone;

        /// <summary>
        /// 父Humanoid骨骼名称。
        /// </summary>
        [HideInInspector]
        public string ParentName;

        [Tooltip("父Humanoid骨骼。")]
        public Transform ParentBone;

        /// <summary>
        /// 子Humanoid骨骼名称。
        /// </summary>
        [HideInInspector]
        public string ChildName;

        [Tooltip("子Humanoid骨骼。")]
        public Transform ChildBone;


#if !UNITY_EDITOR
            /// <summary>
            /// 扭曲矫正骨骼信息是否合法。
            /// </summary>
            private bool? _isValid; 
#endif


        /// <summary>
        /// 构造扭曲矫正骨骼信息。。
        /// </summary>
        /// <param name="parentName">父Humanoid骨骼名称。</param>
        /// <param name="childName">子Humanoid骨骼名称。</param>
        public TwistBoneInfo(string parentName, string childName)
        {
    
    
            ParentName = parentName;
            ChildName = childName;
            TwistName = $"{
      
      ParentName} <-> {
      
      ChildName}";
        }

        /// <summary>
        /// 在扭曲矫正数据的名称中添加非法标识。
        /// </summary>
        public void SetDisplayNameInvalid()
        {
    
    
            TwistName = $"[INVALID] {
      
      ParentName} <-> {
      
      ChildName}";
        }

        /// <summary>
        /// 检查扭曲矫正数据是否合法。
        /// </summary>
        /// <returns></returns>
        public bool IsValid()
        {
    
    
            // 要求3个骨骼齐全
#if UNITY_EDITOR
            return TwistBone && ParentBone && ChildBone;
#else
                if (!_isValid.HasValue)
                {
    
    
                    _isValid = m_twistBone && m_parentBone && m_childBone;
                }

                return _isValid.Value;
#endif
        }

        /// <summary>
        /// 执行扭曲矫正。
        /// </summary>
        /// <param name="intensity">Twist强度。</param>
        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

猜你喜欢

转载自blog.csdn.net/qq_21397217/article/details/120896318