【Unity】【エディタ拡張】正規化されたAnimationCurveの入力フィールドを表示する

正規化されたAnimationCurveの入力フィールドを表示する方法です。

Unity2018.4.0

やりたいこと

Cinemachineを触っていたら、Virtual Cameraをブレンドする際のAnimation Curveの値が0~1で正規化された状態でしか入力できなくなっていることに気づきました。

f:id:halya_11:20190630125000p:plain

普通、Animation CurveをInspectorから設定できるようにすると当然ですが0~1以外の値を入力できてしまいます。

f:id:halya_11:20190630125144p:plain

エディタ拡張をしていると正規化されたAnimation Curveを入力させたいケースは結構あるので、どう実装しているのか気になって調べてみました。

頑張ってエディタ拡張してた

結論から言うと普通にエディタ拡張で入力時にバリデーションしていました。
Animation Curveを正規化するようなAttributeが用意されているのかな?と期待していましたが、そういうものはないようです。

Animation Curveを正規化するメソッドは次のように定義されていました。

public static AnimationCurve NormalizeCurve(AnimationCurve curve)
{
    Keyframe[] keys = curve.keys;
    if (keys.Length > 0)
    {
        float minTime = keys[0].time;
        float maxTime = minTime;
        float minVal = keys[0].value;
        float maxVal = minVal;
        for (int i = 0; i < keys.Length; ++i)
        {
            minTime = Mathf.Min(minTime, keys[i].time);
            maxTime = Mathf.Max(maxTime, keys[i].time);
            minVal = Mathf.Min(minVal, keys[i].value);
            maxVal = Mathf.Max(maxVal, keys[i].value);
        }
        float range = maxTime - minTime;
        float timeScale = range < 0.0001f ? 1 : 1 / range;
        range = maxVal - minVal;
        float valScale = range < 1 ? 1 : 1 / range;
        float valOffset = 0;
        if (range < 1)
        {
            if (minVal > 0 && minVal + range <= 1)
                valOffset = minVal;
            else
                valOffset = 1 - range;
        }
        for (int i = 0; i < keys.Length; ++i)
        {
            keys[i].time = (keys[i].time - minTime) * timeScale;
            keys[i].value = ((keys[i].value - minVal) * valScale) + valOffset;
        }
        curve.keys = keys;
    }
    return curve;
}

なおこれはCinemachineのEditor/Utility/InspectorUtilityに定義されています。

Timeだけ正規化したい場合

前節のメソッドではAnimation Curveの時間(横軸)も値(縦軸)も正規化しますが、
横軸だけ正規化してほしいケースもよくあります。

その場合には下記のようにしたらよさそうです。

public static AnimationCurve NormalizeCurveTime(AnimationCurve curve)
{
    var keys = curve.keys;
    if (keys.Length > 0)
    {
        var minTime = keys[0].time;
        var maxTime = minTime;
        for (var i = 0; i < keys.Length; ++i)
        {
            minTime = Mathf.Min(minTime, keys[i].time);
            maxTime = Mathf.Max(maxTime, keys[i].time);
        }
        var range = maxTime - minTime;
        var timeScale = range < 0.0001f ? 1 : 1 / range;
        for (int i = 0; i < keys.Length; ++i)
        {
            keys[i].time = (keys[i].time - minTime) * timeScale;
        }
        curve.keys = keys;
    }
    return curve;
}