読者です 読者をやめる 読者になる 読者になる

Unityの備忘録的な

つくったものとか

MMD4Mecanimアドオン MecanimJitter作ってみた


*プロシージャルの誤字です。

前置き

 キャラクターに会いにいけるツールとしてのVR。
 しかしただ単に配置するだけでは、キャラクターは虚空を見つめるばかりです。ダンスを踊ってもらっても、こちらを意に介さない様子から疎外感を感じたことのある人も多いのではないかと思います。

 そんな中、キャラクターに命を吹き込んだようなMikulusを初めて体験した時の衝撃は大きく、それ以降生きてる感を出す為のアプローチを続けているいちかです。こんにちは。


 モデル製作者であるおんだ氏のご厚意に甘えて、当時から引き続きモデルを使わせて頂いております。足を向けて寝られませんね!時津風かわいい!

 あえてストーリーテリングをせず、呼吸や視線など受動的なインタラクションに割り切ることで実在感を高めることに成功したMikulus。では次に試みるべきは何だろうかと勝手に考えた結果、生理的な反射の再現という結論に至りました。勝手に。
 反射であれば入力に対して出力がほぼ一意に定まるので、プレイヤーが期待した反応から大きく逸れることはありません。技術的に難易度が低いという点も大きい。


 VRにおいて、物を掴もうと伸ばした手がすり抜ける等、期待が外れるというのは没入感を損ねるトリガーとなり得ます。キャラクターにおいてもこれは同様で、大きな音に反応しない、目の前を物が通り過ぎてもまばたきせず、目で追うこともない、というような生理的な反射が見られないと気味悪ささえ感じてしまいます。


まだ前置き

 生理的な反射を再現する過程でモーフや関節をランダムに揺らす必要が出てきた為、自前でアセットを作成しました。ようやくタイトルに触れますが、今回作成したものはこれになります。

 実は同様の機能を持ったものは過去に制作/公開しています。周期毎にランダムに周期や振幅を設定しループ再生するというものです。
 が、再生中の波形に任意のタイミングで別の波形を足す必要が出てきました。

 この動画は、ループ中のまばたきを強制的に次の周期へ移行する形で実装しています。
 しかし周期の長い緩やかな波形に対して同様の手法をとると、モーフや関節の動きが不連続になってしまいます。結局全面改修する運びとなりました。前回のがnull参照やらかすような拙作だったのでいい機会ではあります。

アセット

 今回作成したアセットはこちらです。初のGitHub利用で少々使い方を誤っているような気がしないでもないですが、DLは問題なく出来るかと思います。
github.com

解説

 チュートリアルとして動画を用意しました。



チュートリアル MMD4Mecanim MorphJitter



チュートリアル MMD4Mecanim BoneJitter


 相変わらずの拙作ではありますが、感想等頂けるととても励みになります。

数値入力欄付きのMinMaxSlider (PropertyDrawer)

 Random.Rangeのパラメータ調整にいいエディタ拡張がないものかと調べていたところ、MinMaxSliderを発見。

f:id:ichika292:20170425154108p:plain

 これは…カスタマイズ前提な?
 数値入力欄を含めて1行に押し込んだものが欲しかったので、PropertyDrawerを使って作成してみました。indentLevelを弄ったときにレイアウトが崩れたので、IndentedRectでそれとなく調整しています。

実装

using UnityEngine;

public class Test : MonoBehaviour 
{
    //FloatRange(float minLimit = 0f, float maxLimit = 1f,
    //           bool clampMin = true, bool clampMax = true)

    public FloatRange a = new FloatRange();
    public FloatRange b = new FloatRange(-1, 2);
    public FloatRange c = new FloatRange(1, -1);
    public FloatRange d = new FloatRange(0, 1, false, false);

    void Start()
    {
        Debug.Log(a.max);           //0.826
        Debug.Log(a.min);           //0.209
        Debug.Log(a.Range);         //0.617      max - min
        Debug.Log(a.RandomRange()); //0.7034022  Random.Range(min, max)
    }
}

f:id:ichika292:20170425144319p:plain

FloatRange.cs

using UnityEngine;
using UnityEditor;

/// <summary>
/// MinMaxSlider with DelayedFloatField
/// </summary>
[System.Serializable]
public class FloatRange
{
    public float min;
    public float max;

    public float minLimit;
    public float maxLimit;
    public bool clampMin;
    public bool clampMax;

    /// <summary>
    /// return max - min
    /// </summary>
    public float Range { get { return max - min; } }

    /// <summary>
    /// return Random.Range(min, max)
    /// </summary>
    public float RandomRange() { return Random.Range(min, max); }

    /// <summary>
    /// MinMaxSlider with DelayedFloatField
    /// </summary>
    public FloatRange(float minLimit = 0f, float maxLimit = 1f, bool clampMin = true, bool clampMax = true)
    {
        this.minLimit = minLimit;
        this.maxLimit = Mathf.Max(minLimit, maxLimit);
        this.clampMin = clampMin;
        this.clampMax = clampMax;
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(FloatRange))]
    public class FloatRangeDrawer : PropertyDrawer
    {
        const int NUMBER_OF_DECIMAL_DIGITS = 3;
        const int CLEARANCE_X = 4;
        const int FLOAT_FIELD_WIDTH = 50;

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            using (new EditorGUI.PropertyScope(position, label, property))
            {
                //各プロパティー取得
                var minProperty = property.FindPropertyRelative("min");
                var maxProperty = property.FindPropertyRelative("max");
                var min = minProperty.floatValue;
                var max = maxProperty.floatValue;
                var minLimit = property.FindPropertyRelative("minLimit").floatValue;
                var maxLimit = property.FindPropertyRelative("maxLimit").floatValue;
                var clampMin = property.FindPropertyRelative("clampMin").boolValue;
                var clampMax = property.FindPropertyRelative("clampMax").boolValue;

                //表示位置を調整
                var labelRect = new Rect(position)
                {
                    width = EditorGUIUtility.labelWidth
                };

                float indentWidth = labelRect.width - EditorGUI.IndentedRect(labelRect).width;
                labelRect.width -= indentWidth;
                float inputFieldWidth = position.width - labelRect.width;

                var minFloatRect = new Rect(position)
                {
                    x = position.x + labelRect.width,
                    width = FLOAT_FIELD_WIDTH + indentWidth
                };

                var sliderRect = new Rect(minFloatRect)
                {
                    x = minFloatRect.x + FLOAT_FIELD_WIDTH + CLEARANCE_X,
                    width = inputFieldWidth - 2f * (FLOAT_FIELD_WIDTH + CLEARANCE_X)
                };

                var maxFloatRect = new Rect(sliderRect)
                {
                    x = sliderRect.x + sliderRect.width + CLEARANCE_X - indentWidth,
                    width = FLOAT_FIELD_WIDTH + indentWidth
                };

                EditorGUI.LabelField(labelRect, label);

                //数値入力
                EditorGUI.BeginChangeCheck();
                {
                    min = EditorGUI.DelayedFloatField(minFloatRect, min);
                    max = EditorGUI.DelayedFloatField(maxFloatRect, max);
                }
                if (EditorGUI.EndChangeCheck())
                {
                    //min優先でClamp
                    min = Mathf.Min(min, clampMax ? maxLimit : float.MaxValue);
                    max = Mathf.Clamp(max, min, clampMax ? maxLimit : float.MaxValue);
                    min = Mathf.Clamp(min, clampMin ? minLimit : float.MinValue, max);
                    maxProperty.floatValue = max;
                    minProperty.floatValue = min;
                }

                //スライダー入力
                EditorGUI.BeginChangeCheck();
                {
                    EditorGUI.MinMaxSlider(sliderRect, ref min, ref max, minLimit, maxLimit);
                }
                if (EditorGUI.EndChangeCheck())
                {
                    GUI.FocusControl("");
                    min = Mathf.Min(min, maxLimit);
                    max = Mathf.Max(max, minLimit);
                    minProperty.floatValue = Round(min, NUMBER_OF_DECIMAL_DIGITS);
                    maxProperty.floatValue = Round(max, NUMBER_OF_DECIMAL_DIGITS);
                }
            }
        }

        //少数桁数n桁に四捨五入
        static float Round(float val, int n)
        {
            var unit = Mathf.Pow(10, n);
            return Mathf.Round(val * unit) / unit;
        }
    }
#endif
}

 この手のものは名前空間かクラスか、どのように分類するべきなのかいまいちセオリーが分かってないです。なので今回はこのまま。
 アセットとして公開するものに関しては名前空間を切っていこうと思います。(以前話題になってましたね)

参考

エディター拡張入門 : http://anchan828.github.io/editor-manual/web/

アウトプットの練習

大事らしい。

歴1年に満たない趣味グラマなので、ツッコミどころは多いと思います。