【Unity】3DモデルのScaleFactorをAnimationClipに適用する


3DモデルのScaleFactor変更 → アニメーションがおかしくなる問題

Unityに3Dモデルを読み込んだ後、Unity上でサイズを1/2にしました。
ScaleFactorを 1.0 → 0.5 に変更。メッシュは問題なさそう。
ただ、よーく見るとアニメーションがおかしい…。

左のローポリモデル がScaleFactor変更モデル。右が変更なしモデル。
同じアニメーションのはずが、ローポリの方のステップがキレキレ過ぎる。

実は、アニメーションにはScaleFactorが適用されない仕様でした。
そのため、AnimationClipのPosition値が相対的に大きくなることに。ちなみに、Rotation値はスケールの影響を受けないので元の動きのまま。

AnimationClipをEditorスクリプトで変更する

対処としては、AnimationClipをスケール変換するツールを自作しました。
ソースは記事の下にあります。

指定されたAnimationClipを読み込み、全てのキーフレームの値にScaleFactorをかけています。
今回はScaleFactorの影響を受けるPositionのみに適用しています。
最終的に、「ファイル名_scaled.anim」というAnimationClipを生成します。

ScalerAnimationEditorWindow.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

namespace ScalerAnimation
{
    public class ScalerAnimationEditorWindow : EditorWindow
    {
        private AnimationClip m_animationClip;
        private float m_scaleFactor = 1.0f;

        [MenuItem("Tools/ScalerAnimation")]
        static void Window()
        {
            var window = GetWindow<ScalerAnimationEditorWindow>("ScalerAnimation");
            window.Show();
        }

        private void Update()
        {
        }

        private void OnGUI()
        {
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("AnimationClip");
            m_animationClip = (AnimationClip)EditorGUILayout.ObjectField(m_animationClip, typeof(AnimationClip), true);
            m_scaleFactor = EditorGUILayout.FloatField("Scale Factor", m_scaleFactor);

            if (m_animationClip != null)
            {
                if (GUILayout.Button("Apply"))
                {
                    ScalerAnimation(m_animationClip);
                }
            }
        }

        private void ScalerAnimation(AnimationClip animationClip)
        {
            // Copy Clip
            var copyAnimationClip = CopyAnimationClip(animationClip);
            // Get Properties
            var animationProperties = GetAnimationProperties(copyAnimationClip);
            // Apply Scale Factor
            ApplyScalerAnimation(copyAnimationClip, animationProperties);
        }

        private AnimationClip CopyAnimationClip(AnimationClip animationClip)
        {
            var copyAnimationClip = Instantiate(animationClip) as AnimationClip;
            var path = AssetDatabase.GetAssetPath(animationClip);
            var directoryName = System.IO.Path.GetDirectoryName(path);
            AssetDatabase.CreateAsset(copyAnimationClip, $"{directoryName}/{animationClip.name}_scaled.anim");
            return copyAnimationClip;
        }

        private void ApplyScalerAnimation(AnimationClip animationClip, Dictionary<string, AnimationProperty> animationProperties)
        {
            var bindings = AnimationUtility.GetCurveBindings(animationClip);
            foreach (var binding in bindings)
            {
                var curve = AnimationUtility.GetEditorCurve(animationClip, binding);
                var boneName = System.IO.Path.GetFileName(binding.path);
                var animationProperty = animationProperties[boneName];
                switch (binding.propertyName)
                {
                    case "m_LocalPosition.x":
                        curve.keys = animationProperty.GetScalerLocalPositionX(m_scaleFactor);
                        AnimationUtility.SetEditorCurve(animationClip, binding, curve);
                        break;
                    case "m_LocalPosition.y":
                        curve.keys = animationProperty.GetScalerLocalPositionY(m_scaleFactor);
                        AnimationUtility.SetEditorCurve(animationClip, binding, curve);
                        break;
                    case "m_LocalPosition.z":
                        curve.keys = animationProperty.GetScalerLocalPositionZ(m_scaleFactor);
                        AnimationUtility.SetEditorCurve(animationClip, binding, curve);
                        break;
                }
            }
        }

        private Dictionary<string, AnimationProperty> GetAnimationProperties(AnimationClip animationClip)
        {
            var animationProperties = new Dictionary<string, AnimationProperty>();

            var bindings = AnimationUtility.GetCurveBindings(animationClip);
            foreach (var binding in bindings)
            {
                var curve = AnimationUtility.GetEditorCurve(animationClip, binding);
                var boneName = System.IO.Path.GetFileName(binding.path);
                if (!animationProperties.ContainsKey(boneName))
                {
                    animationProperties.Add(boneName, new AnimationProperty());
                }
                switch (binding.propertyName)
                {
                    case "m_LocalPosition.x":
                        animationProperties[boneName].localPositionX.AddRange(curve.keys);
                        break;
                    case "m_LocalPosition.y":
                        animationProperties[boneName].localPositionY.AddRange(curve.keys);
                        break;
                    case "m_LocalPosition.z":
                        animationProperties[boneName].localPositionZ.AddRange(curve.keys);
                        break;
                }
            }
            return animationProperties;
        }
    }
}

使い方

Tools > ScalerAnimationから実行できます。

アニメーションに適用できました。

AnimationClip スケール変換ツール

githubに置いています。

参考

こちらのミラー変換ツールを参考にしました。
https://github.com/hisakataIsm/MirrorAnimation