e.blog

主にUnity/UE周りのことについてまとめていきます

ランタイムでAvatarを生成してアニメーションに利用する

今日の記事は、UnityAdvent Calendarの9日目の記事です。

qiita.com

概要

VR開発をしていると、キャタクター(アバター)を表現する方法として頭と手だけの簡易的アバターか、通常のキャラクターモデルを用いたアバターの2種類から選ぶことになります。
そして後者、キャラクターモデルを利用したアバターを制作する際に利用できそうな方法を見つけたので、それのメモです。

具体的には、Avatarインスタンスをランタイム時に生成し、それをMecanimとしてキャラクターにアサインする方法となります。

ちなみに、今回の実装のヒントは、OptiTrackというモーションキャプチャーのシステム用に提供されているプラグインの中のソースコードを参考にしました。

プラグインはフリーで、以下からダウンロードすることができます。

optitrack.com

実際に使ってみたのがこちら。Avatarなのでアニメーションを簡単にコピー、複製することができます。

必要クラス

今回利用するクラスは以下です。
それぞれが、アバターを構成するための情報を保持、伝達するためのものです。

  • Avatar
  • AvatarBuilder
  • HumanBone
  • SkeltonBone
  • HumanDescription
  • HumanPose
  • HumanPoseHandler
  • HumanTrait

大まかな手順

大まかな手順は、(Avatarの設定を行ったことがある方であればイメージしやすいと思いますが)Configure Avatarボタンを押下して編集モードに入ったときにできることをプログラムから行う、というイメージです。

具体的には、HumanBoneSkeletonBoneを用いてスケルトンとモデルの構造を定義し、それらを関連付け、最後に関節の曲がり具合などを設定した「設定オブジェクト(HumanDescription)」とともに、Avatarを生成する、という形です。

この設定を行う際、適切にセットアップが終わっていないと最後のAvatarBuilderでビルドする段階でエラーが出てしまうので、セットアップは気をつける必要があります。

百聞は一見にしかず、ということで、まずは実際にコードを見てもらったほうがいいでしょう。

Avatarのセットアップコード

各種ボーンの設定と、アバターのビルドを行うメソッドの抜粋です。

/// <summary>
/// アバターのセットアップ
/// </summary>
private void Setup()
{
    // HumanBoneのためのリストを取得する
    string[] humanTraitBoneNames = HumanTrait.BoneName;

    List<HumanBone> humanBones = new List<HumanBone>(humanTraitBoneNames.Length);
    for (int i = 0; i < humanTraitBoneNames.Length; i++)
    {
        string humanBoneName = humanTraitBoneNames[i];
        Transform bone;
        if (_transformDefinision.TryGetValue(humanBoneName, out bone))
        {
            HumanBone humanBone = new HumanBone();
            humanBone.humanName = humanBoneName;
            humanBone.boneName = bone.name;
            humanBone.limit.useDefaultValues = true;

            humanBones.Add(humanBone);
        }
    }

    List<SkeletonBone> skeletonBones = new List<SkeletonBone>(_skeletonBones.Count);
    for (int i = 0; i < _skeletonBones.Count; i++)
    {
        Transform bone = _skeletonBones[i];

        SkeletonBone skelBone = new SkeletonBone();
        skelBone.name = bone.name;
        skelBone.position = bone.localPosition;
        skelBone.rotation = bone.localRotation;
        skelBone.scale = Vector3.one;

        skeletonBones.Add(skelBone);
    }

    // HumanDescription(関節の曲がり方などを定義した構造体)
    HumanDescription humanDesc = new HumanDescription();
    humanDesc.human = humanBones.ToArray();
    humanDesc.skeleton = skeletonBones.ToArray();

    humanDesc.upperArmTwist = 0.5f;
    humanDesc.lowerArmTwist = 0.5f;
    humanDesc.upperLegTwist = 0.5f;
    humanDesc.lowerLegTwist = 0.5f;
    humanDesc.armStretch = 0.05f;
    humanDesc.legStretch = 0.05f;
    humanDesc.feetSpacing = 0.0f;
    humanDesc.hasTranslationDoF = false;

    // アバターオブジェクトをビルド
    _srcAvatar = AvatarBuilder.BuildHumanAvatar(gameObject, humanDesc);

    if (!_srcAvatar.isValid || !_srcAvatar.isHuman)
    {
        Debug.LogError("setup error");
        return;
    }

    _srchandler = new HumanPoseHandler(_srcAvatar, transform);
    _destHandler = new HumanPoseHandler(_destAvatar, _targetAnimator.transform);

    _initialized = true;
}

ボーンのセットアップ

まず冒頭で行っているのが、ボーンのセットアップです。
ボーンには2種類あり、HumanBoneSkeletonBoneの2種類です。

「人間の構造」を定義する「HumanBone」と実際の「SkeletonBone」

あくまで自分の理解で、という前置きが入りますが、HumanBone人間の構造を定義するためのボーンです。
そして実際のモデル(アバターに適用するオブジェクト)のボーン構造を示すのがSkeletonBoneです。

なぜこのふたつのボーン情報が必要なのかというと、モデルの中身を見たことがある人であればすぐにピンと来ると思いますが、モデルデータには人間にはないボーンが仕込まれている場合があります。
MMDなどは特にそれが顕著で、「よりよく見せるため」のボーンが仕込まれていたりします。
(例えばスカート用のボーンだったり、髪の毛用のボーンだったり)

そのため、人間の構造と同じ構造でボーンを定義することはほとんどなく、いくらかのボーンが人間の構造とは違った形になっているため、「実際のボーン構造のうち、どれが人間の構造としてのボーンか」を定義する必要がある、というわけです。(と理解しています)

そして、その関連付けを行っているのが、それぞれnameプロパティで指定される名称です。
どうやらUnity内では名称でそのマッチングを行っているようです。

なので、「人間としてのこのボーンは、対象モデルではこういう名称ですよ」という関連付けが必要、というわけですね。

コードとしては以下の部分ですね。

humanBone.boneName = bone.name;

// ... 中略 ...

skelBone.name = bone.name;

そして、「人間としてのどこのボーンか」という情報はhumanBone.humanName = humanBoneName;で指定しています。

こうして、人間としてのボーンがどれか、というマッチングを行うことで、Mecanimではその情報を元にアニメーションしている、というわけのようです。

最初、SkeletonBoneが人間のボーン構造を示すものだと思って、それだけのTransformを指定して配列を生成していたんですが、「hoge Transformは fuga Transformの親じゃないとダメだよ」みたいなエラーが出て、少しハマりました。
今回の実装ではSkeletonBoneは以下のように、ルートから再帰的にTransform情報を拾って配列化して設定しています。

/// <summary>
/// 再帰的にTransformを走査して、ボーン構造を生成する
/// </summary>
/// <param name="current">現在のTransform</param>
private void RecursiveSkeleton(Transform current, ref List<Transform> skeletons)
{
    skeletons.Add(current);

    for (int i = 0; i < current.childCount; i++)
    {
        Transform child = current.GetChild(i);
        RecursiveSkeleton(child, ref skeletons);
    }
}

Unityが規定した名称を基に各ボーンの関連付けを行う

さて、もうひとつ重要なのがこの「Unityが規定した名称を基に関連付けを行う」という点です。
どういうことかというと、まずは以下のコードを見てください。

string[] humanTraitBoneNames = HumanTrait.BoneName;

List<HumanBone> humanBones = new List<HumanBone>(humanTraitBoneNames.Length);
for (int i = 0; i < humanTraitBoneNames.Length; i++)
{
    string humanBoneName = humanTraitBoneNames[i];
    Transform bone;
    if (_transformDefinision.TryGetValue(humanBoneName, out bone))
    {
        HumanBone humanBone = new HumanBone();
        humanBone.humanName = humanBoneName;
        humanBone.boneName = bone.name;
        humanBone.limit.useDefaultValues = true;

        humanBones.Add(humanBone);
    }
}

HumanTrait.BoneNameというstring型の配列から値を取り出し、それと、自分が定義した_transformDefinisionの中に値が含まれているかのチェックをしています。

このHumanTrait.BoneNameが、Unityが規定しているボーンの名称で、具体的にはNeckなどの人体の部位の名称が設定されています。

そしてここで定義されている名称とのマッピングを行っているのが_transformDefinisionなのです。

これ自体はシンプルに、インスペクタから手で設定してもらったTransformを設定しているだけです。
生成部分は以下のようになります。

/// <summary>
/// アサインされたTransformからボーンのリストをセットアップする
/// </summary>
private void SetupBones()
{
    _transformDefinision.Clear();

    _transformDefinision.Add("Hips", _hips);
    _transformDefinision.Add("Spine", _spine);
    _transformDefinision.Add("Chest", _chest);
    _transformDefinision.Add("Neck", _neck);
    _transformDefinision.Add("Head", _head);
    _transformDefinision.Add("LeftShoulder", _leftShoulder);
    _transformDefinision.Add("LeftUpperArm", _leftUpperArm);
    _transformDefinision.Add("LeftLowerArm", _leftLowerArm);
    _transformDefinision.Add("LeftHand", _leftHand);
    _transformDefinision.Add("RightShoulder", _rightShoulder);
    _transformDefinision.Add("RightUpperArm", _rightUpperArm);
    _transformDefinision.Add("RightLowerArm", _rightLowerArm);
    _transformDefinision.Add("RightHand", _rightHand);
    _transformDefinision.Add("LeftUpperLeg", _leftUpperLeg);
    _transformDefinision.Add("LeftLowerLeg", _leftLowerLeg);
    _transformDefinision.Add("LeftFoot", _leftFoot);
    _transformDefinision.Add("RightUpperLeg", _rightUpperLeg);
    _transformDefinision.Add("RightLowerLeg", _rightLowerLeg);
    _transformDefinision.Add("RightFoot", _rightFoot);
    _transformDefinision.Add("LeftToes", _leftToes);
    _transformDefinision.Add("RightToes", _rightToes);
}

あとは、この設定されたリストとマッチングして、該当のボーンの名称を、前述のように設定していく、という感じになります。
再掲すると以下の部分です。

humanBone.boneName = bone.name;

// ... 中略 ...

skelBone.name = bone.name;

人間の特性を定義する「HumanDescription」

ボーンのセットアップが終わったら、そのボーン構造を持つ人の特性がどんなものか、を定義する「HumanDescription」構造体を利用して、手の関節の回転などの状態を定義します。

HumanDescription humanDesc = new HumanDescription();
humanDesc.human = humanBones.ToArray();
humanDesc.skeleton = skeletonBones.ToArray();

humanDesc.upperArmTwist = 0.5f;
humanDesc.lowerArmTwist = 0.5f;
humanDesc.upperLegTwist = 0.5f;
humanDesc.lowerLegTwist = 0.5f;
humanDesc.armStretch = 0.05f;
humanDesc.legStretch = 0.05f;
humanDesc.feetSpacing = 0.0f;
humanDesc.hasTranslationDoF = false;

アバターをビルド

以上で必要なデータが揃いました。
あとは、そのデータを利用して、アバターオブジェクトをビルドしてやれば完了です。

_srcAvatar = AvatarBuilder.BuildHumanAvatar(gameObject, humanDesc);

if (!_srcAvatar.isValid || !_srcAvatar.isHuman)
{
    Debug.LogError("setup error");
    return;
}

注意点として、ビルド後にエラーがないかのチェックが必要です。

上でも書きましたが、ボーンの構造などの状態がおかしいと、この時点でエラーが表示されます。
構造として適切でない場合はビルド時にエラーが出るのと同時に、isValidisHumanのフラグがfalseになるので、それをチェックして、失敗していた場合はやり直すなどの処置が必要になります。
(最初はここでエラーが出て、若干ハマった)

ただ、エラーを見てみるとしっかりと理由が書かれているので、それを元に修正していけば問題は解決できると思います。

無事ビルドが終わったら、HumanPoseHandlerを設定し、以後はUpdateメソッド内でアバターの状態をコピーしてやれば完成です。

HumanPoseHandlerは、現在の状態を取得するためのハンドラ。

_srchandler = new HumanPoseHandler(_srcAvatar, transform);
_destHandler = new HumanPoseHandler(_destAvatar, _targetAnimator.transform);

上記で取得したハンドラを用いてGet/Setメソッドを用いて、Getしたポーズを、以下のようにして対象のアバターにコピーします。

private void Update()
{
    if (!_initialized)
    {
        return;
    }

    if (_srchandler != null && _destHandler != null)
    {
        _srchandler.GetHumanPose(ref _humanPose);
        _destHandler.SetHumanPose(ref _humanPose);
    }
}

GetHumanPoseメソッドでポーズ情報を取得し、SetHumanPoseでコピー先のHumanPoseHandlerにセットしてやれば動きが同期されるようになります。

ちなみに、感のいい方であればピンと来ているかもしれませんが、このコピー先を複数用意してやれば、いくらでも同じ動きをするモデルを用意することができます。
Mecanimによるアバター制御の恩恵が受けられる、というわけですね。

f:id:edo_m18:20170923153401p:plain

最後に

冒頭で載せた動画は、このアバターの仕組みを使って「アバターの現在のアニメーション状態をコピー」することで実現しています。

Avatarをランタイムで生成できることで、色々な値を調整することが可能になるのでVRには非常に適したものかなと思っています。