そういうのがいいブログ

アプリ個人開発 まるブログ

アプリ開発覚え書き

【Unity】ランタイムでAvatarを生成する

要件

Unity6000.026f1

はじめに

本記事での"Avatar"とはHumanoid型の3Dモデルを使う時にAnimator
に割りついているAvatar要素のことです。

ランタイムで生成する必要があったためメモします。

必要な情報

Avatarを生成するためには以下の関数を実行します。

AvatarBuilder.BuildHumanAvatar(GameObject型の引数, HumanDescription型の引数);

docs.unity3d.com


引数は次の2つです。
① GameObject 型の引数:Avatarを生成したい対象のゲームオブジェクト
② HumanDescription型の引数:アバターの情報

①は対象のゲームオブジェクトを指定するだけなので簡単です。

問題は②のHumanDescription型の引数です。

HumanDescription型はUnityのスクリプトリファレンスを見ると
以下の10個の変数から成っており、これらを指定する必要があります。

プロパティ名 説明
armStretch IK 使用時の腕の長さの引き伸ばし許容量
armStretch IK 使用時の腕の長さの引き伸ばし許容量
feetSpacing ヒューマノイドモデル脚部の最小距離の調整
hasTranslationDoF Degree of Freedom (自由度:DoF) 表現を持つすべてのヒューマンに対して true を返します。デフォルトで false に設定されます。
human Mecanim ボーン名とリグのボーン名の間におけるマッピング
legStretch IK 使用のときに許容する脚の伸び幅
lowerArmTwist 回転/ひねりを肘と手首にどの割合で反映するか定義します
lowerLegTwist 回転/ひねりを膝や足首にどの割合で反映するか定義します
skeleton モデルに含めるボーン Transform のリスト
upperArmTwist 回転/ひねりを肩と肘にどの割合で反映するか定義します
upperLegTwist 回転/ひねりを脚の付け根おと膝にどの割合で反映するか定義します

docs.unity3d.com

HumanDescriptionの設定

HumanDescriptionを設定するにあたって変数が10個あると言いましたが、
humanとskeleton以外は以下のデフォルト値があるのでその値を使用します。
(デフォルト値はHumanDescriptionの
 スクリプトリファレンスの変数ページに書いてあります。)

HumanDescription humanDescription = new HumanDescription
{
   armStretch = 0.05f,//IK(インバースキネマティクス)使用時の腕の長さの引き伸ばし許容量を設定します。0.05fは、腕が5%まで引き伸ばされることを許容することを意味します。
   feetSpacing = 0.0f,//ヒューマノイドモデルの脚部の最小距離の調整を設定します。0.0fは、脚部の間隔がデフォルトのままであることを意味します。
   hasTranslationDoF = false,//アバターの関節に対して平行移動の自由度を持たせないことを指定します。通常必要ないためfalseに設定します。
   human = 設定方法はこの後説明
   legStretch = 0.05f,//IK使用時の脚の長さの引き伸ばし許容量を設定します。0.05fは、脚が5%まで引き伸ばされることを許容することを意味します。
   lowerArmTwist = 0.5f,//回転/ひねりを肘と手首にどの割合で反映するかを定義します。0.5fは、回転が肘と手首に均等に分配されることを意味します。
   lowerLegTwist = 0.5f,//回転/ひねりを膝と足首にどの割合で反映するかを定義します。0.5fは、回転が膝と足首に均等に分配されることを意味します。
   skeleton = 設定方法はこの後説明
   upperArmTwist = 0.5f,//回転/ひねりを肩と肘にどの割合で反映するかを定義します。0.5fは、回転が肩と肘に均等に分配されることを意味します。
   upperLegTwist = 0.5f,//回転/ひねりを脚の付け根と膝にどの割合で反映するかを定義します。0.5fは、回転が脚の付け根と膝に均等に分配されることを意味します。
};


humanとskeletonについて見ていきます。


skeleton

取得がhumanより簡単なskeletonからいきます。

skeletonにはモデルが持つ配下オブジェクト全てのSkeletonBone型の情報を配列で渡します。

SkeletonBone型の情報とは以下の内容になります。

変数名 内容
name ボーンがマッピングされているトランスフォーム名
position ローカル空間でボーンの T ポーズの位置
rotation ローカル空間でボーンの T ポーズの角度
scale ローカル空間でボーンの T ポーズのスケール


Tポーズになっているという前提ですが、
Transform型を取得できていればSkeletonBone型の情報は次のスクリプトで取得できます。
※Tポーズでない状態の情報を設定した場合、どのような不具合がでるかは未検証です。

    SkeletonBone bone = new SkeletonBone
    {
        name = Transform型の変数.name,
        position = Transform型の変数.localPosition,
        rotation = Transform型の変数.localRotation,
        scale = Transform型の変数.localScale
    };


あとは指定した親ゲームオブジェクトのTrasnform型から
配下全てのTransform型へアクセスすればskeletonの情報は作成できます。

まず、SkeletonBone型のリスト作成と、
配下オブジェクトにアクセスして情報を取得する関数を設置します。

    /// <summary>
    /// HumanDescription.skeleton 用の SkeletonBone 配列を生成
    /// </summary>
    /// <param name="root">スケルトン階層のルート Transform</param>
    /// <returns>SkeletonBone 配列</returns>
    private static SkeletonBone[] GenerateSkeleton(Transform root)
    {
        // 引数が null の場合はエラー
        if (root == null)
        {
            Debug.LogError("Root Transform is null.");
            return null;
        }
        
        // スケルトンボーンのリストを作成
        List<SkeletonBone> skeletonBones = new List<SkeletonBone>();
        
        // 再帰的にスケルトンを構築
        BuildSkeletonRecursive(root, skeletonBones);
        
        // スケルトンボーンのリストを配列に変換して返す
        return skeletonBones.ToArray();
    }
    
    /// <summary>
    /// 再帰的に SkeletonBone を構築
    /// </summary>
    /// <param name="current">現在の Transform</param>
    /// <param name="skeletonBones">構築中の SkeletonBone リスト</param>
    private static void BuildSkeletonRecursive(Transform current, List<SkeletonBone> skeletonBones)
    {
        // SkeletonBone を作成
        SkeletonBone bone = new SkeletonBone
        {
            name = current.name,
            position = current.localPosition,
            rotation = current.localRotation,
            scale = current.localScale
        };
        
        // 作成した SkeletonBone をリストに追加
        skeletonBones.Add(bone);
        
        // 子要素を再帰的に処理
        // 子要素がない場合は何もしない
        // foreachでTransform型に対してループをまわすと子要素に対して処理ができる
        foreach (Transform child in current)
        {
            BuildSkeletonRecursive(child, skeletonBones);
        }
    }


そして、関数GenerateSkeleton に親ゲームオブジェクトのTrasnform型の情報を
渡して実行することで取得可能です。

        // ターゲットのオブジェクトのTransformからモデルのTransformの配列を作成
        SkeletonBone[] skeletonBones = GenerateSkeleton(親ゲームオブジェクトのTrasnform型);

以上がskeletonの取得方法になります。


human

humanにはHumanBone型の情報を配列で渡します。

HumanBone型の情報とは以下の内容になります。

変数名 内容
boneName Mecanim のヒューマノイド型ボーンがマッピングされたボーン名
humanName モデルにおけるボーンのマッピング元となる Mecanim のヒューマノイド型ボーン名
limit ボーンにおける割り込みの可動域を返す


説明欄にマッピングという言葉が出てきて困惑しますね。
HumanBone型は簡単に言えば、
"Unityが定義しているボーン名 humanName "に対する
"3Dモデルが持つボーン名 boneName ”の対応情報です。

Unity上にて3Dモデル配下のAvatarを選択し「アバターを設定」を押すと表示される情報を
作るためのものといえばわかりやすいでしょうか。



設定可能なボーン情報は以下の55種類です。
(最後のLastBoneは飾りみたいなものなのでカウント対象外です。)

そのうち、登録が必須のボーン情報は赤文字の15種類になります。

HumanBone型の humanName に対しては
下表の"ボーン名"のいずれかを指定することになります。

番号 ボーン名
1 Hips
2 LeftUpperLeg
3 RightUpperLeg
4 LeftLowerLeg
5 RightLowerLeg
6 LeftFoot
7 RightFoot
8 Spine
9 Chest
10 UpperChest
11 Neck
12 Head
13 LeftShoulder
14 RightShoulder
15 LeftUpperArm
16 RightUpperArm
17 LeftLowerArm
18 RightLowerArm
19 LeftHand
20 RightHand
21 LeftToes
22 RightToes
23 LeftEye
24 RightEye
25 Jaw
26 LeftThumbProximal
27 LeftThumbIntermediate
28 LeftThumbDistal
29 LeftIndexProximal
30 LeftIndexIntermediate
31 LeftIndexDistal
32 LeftMiddleProximal
33 LeftMiddleIntermediate
34 LeftMiddleDistal
35 LeftRingProximal
36 LeftRingIntermediate
37 LeftRingDistal
38 LeftLittleProximal
39 LeftLittleIntermediate
40 LeftLittleDistal
41 RightThumbProximal
42 RightThumbIntermediate
43 RightThumbDistal
44 RightIndexProximal
45 RightIndexIntermediate
46 RightIndexDistal
47 RightMiddleProximal
48 RightMiddleIntermediate
49 RightMiddleDistal
50 RightRingProximal
51 RightRingIntermediate
52 RightRingDistal
53 RightLittleProximal
54 RightLittleIntermediate
55 RightLittleDistal
56 LastBone


話は少しそれますが、アバター情報をよくみると必須ボーンと任意設定ボーンでは
アイコンが異なっていることがわかります。

設定必須のボーン アイコン

任意設定のボーン アイコン


ではプログラムで
"Unityが定義しているボーン名 humanName "に対する
"3Dモデルが持つボーン名 boneName ”の対応情報を設定します。

繰り返しで処理するためDictionary型を使用して
humanName と boneName の対応関係を書きます。

        Dictionary<string, string> boneNameMap = new Dictionary<string, string>
        {
            ["Hips"] = 対応するボーン名,
            ["LeftUpperLeg"] = 対応するボーン名,
            ["RightUpperLeg"] = 対応するボーン名,
            ["LeftLowerLeg"] = 対応するボーン名,
            ["RightLowerLeg"] = 対応するボーン名,
            ["LeftFoot"] = 対応するボーン名,
            ["RightFoot"] = 対応するボーン名,
            ["Spine"] = 対応するボーン名,
            ["Head"] = 対応するボーン名,
            ["LeftUpperArm"] = 対応するボーン名,
            ["RightUpperArm"] =対応するボーン名,
            ["LeftLowerArm"] = 対応するボーン名,
            ["RightLowerArm"] = 対応するボーン名,
            ["LeftHand"] = 対応するボーン名,
            ["RightHand"] = 対応するボーン名,
        };


具体例としてmixamoからダウンロードできる
"Hip Hop Dancing"というモデルデータで設定してみます。

このモデルのアバター情報は以下です。


必須ボーンの"Hips"や"LeftUpperLeg"に対応するボーン名を
インスペクターから把握して先ほどのプログラムを作ります。


例えば、必須ボーン"Hips"に対応しているボーン名は
"mixamorig:Hips"であることが把握できます。

そのため"Hips"に対応する名前は"mixamorig:Hips"と書きます。

        Dictionary<string, string> boneNameMap = new Dictionary<string, string>
        {
             ["Hips"] = "mixamorig:Hips",
     ・
     ・
      省略
     ・
     ・
        };


今回のモデルでは必須ボーンに対応するボーン名は以下になりました。

        // ボーン名マッピングの情報
        Dictionary<string, string> boneNameMap = new Dictionary<string, string>
        {
            ["Hips"] = "mixamorig:Hips",
            ["LeftUpperLeg"] = "mixamorig:LeftUpLeg",
            ["RightUpperLeg"] = "mixamorig:RightUpLeg",
            ["LeftLowerLeg"] = "mixamorig:LeftLeg",
            ["RightLowerLeg"] = "mixamorig:RightLeg",
            ["LeftFoot"] = "mixamorig:LeftFoot",
            ["RightFoot"] = "mixamorig:RightFoot",
            ["Spine"] = "mixamorig:Spine",
            ["Head"] = "mixamorig:Head",
            ["LeftUpperArm"] = "mixamorig:LeftArm",
            ["RightUpperArm"] = "mixamorig:RightArm",
            ["LeftLowerArm"] = "mixamorig:LeftForeArm",
            ["RightLowerArm"] = "mixamorig:RightForeArm",
            ["LeftHand"] = "mixamorig:LeftHand",
            ["RightHand"] = "mixamorig:RightHand",
        };


このDictionary型の情報から以下のプログラムでHumanBone型の配列を作ります。

    /// <summary>
    /// ボーン名マッピング情報から HumanBone 配列を生成
    /// </summary>
    /// <param name="boneNameMap">ボーン名マッピング情報</param>
    /// <returns></returns>
    private HumanBone[] CreateHumanBones(Dictionary<string, string> boneNameMap)
    {
        // HumanBone のリストを作成
        List<HumanBone> humanBones = new List<HumanBone>();
        
        // マッピング情報を元に HumanBone を作成
        foreach (var pair in boneNameMap)
        {
            // HumanBone を作成
            HumanBone humanBone = new HumanBone
            {
                humanName = pair.Key,
                boneName = pair.Value,
                limit = new HumanLimit { useDefaultValues = true }
            };
            
            // HumanBone をリストに追加
            humanBones.Add(humanBone);
        }
        
        // HumanBone のリストを配列に変換して返す
        return humanBones.ToArray();
    }


上記プログラムの中でHumanBone型を作成する部分は以下です。
こちらをDictionary型の要素分繰り返しています。
HumanBone型の中のlimitはHumanLimit型になります。
こちらはデフォルト値を使用する書き方で書きました。

            // HumanBone を作成
            HumanBone humanBone = new HumanBone
            {
                humanName = pair.Key,
                boneName = pair.Value,
                limit = new HumanLimit { useDefaultValues = true } // デフォルト値を使用
            };

あとは、関数CreateHumanBones に
ボーン名マッピング情報が入ったDictionary型の情報を渡して実行することで取得可能です。

        // ボーン名マッピングの情報からHumanBoneのリストを作成
        HumanBone[] humanBones = CreateHumanBones(Dictionary型のボーンマッピング情報);

以上がhumanの取得方法になります。

プログラム全文

mixamoからダウンロードできる"Hip Hop Dancing"の
モデルデータを使用する前提のプログラムです。

using System.Collections.Generic;
using UnityEngine;

public class GenerateAvatarTest : MonoBehaviour
{
    //アバターを生成したい対象のゲームオブジェクト
    [SerializeField] private GameObject _targetModel;
    
    //アバターを生成する対象のアニメーターコンポーネント
    private Animator _animator;
    
    void Start()
    {
        // アニメーターコンポーネントを取得
        _animator = _targetModel.GetComponent<Animator>();

        // アバターを生成
        GenerateAvatar();
    }

    
    
    /// <summary>
    /// アバターを生成
    /// </summary>
    private void GenerateAvatar()
    {
        //最終的にAvatarBuilder.BuildHumanAvatar (引数1 GameObject型, 引数2 HumanDescription型);を使用してアバターを生成する
        //引数1はアバターを生成したいゲームオブジェクト
        //引数2はアバターの構造を定義するHumanDescription構造体
        
        
        // ボーン名マッピングの情報
        Dictionary<string, string> boneNameMap = new Dictionary<string, string>
        {
            ["Hips"] = "mixamorig:Hips",
            ["LeftUpperLeg"] = "mixamorig:LeftUpLeg",
            ["RightUpperLeg"] = "mixamorig:RightUpLeg",
            ["LeftLowerLeg"] = "mixamorig:LeftLeg",
            ["RightLowerLeg"] = "mixamorig:RightLeg",
            ["LeftFoot"] = "mixamorig:LeftFoot",
            ["RightFoot"] = "mixamorig:RightFoot",
            ["Spine"] = "mixamorig:Spine",
            ["Head"] = "mixamorig:Head",
            ["LeftUpperArm"] = "mixamorig:LeftArm",
            ["RightUpperArm"] = "mixamorig:RightArm",
            ["LeftLowerArm"] = "mixamorig:LeftForeArm",
            ["RightLowerArm"] = "mixamorig:RightForeArm",
            ["LeftHand"] = "mixamorig:LeftHand",
            ["RightHand"] = "mixamorig:RightHand",
        };
        
        // ボーン名マッピングの情報からHumanBoneのリストを作成
        HumanBone[] humanBones = CreateHumanBones(boneNameMap);
        
        // ターゲットのオブジェクトのTransformからモデルのTransformの配列を作成
        SkeletonBone[] skeletonBones = GenerateSkeleton(_targetModel.transform);
        
        HumanDescription humanDescription = new HumanDescription
        {
            armStretch = 0.05f,//IK(インバースキネマティクス)使用時の腕の長さの引き伸ばし許容量を設定します。0.05fは、腕が5%まで引き伸ばされることを許容することを意味します。
            feetSpacing = 0.0f,//ヒューマノイドモデルの脚部の最小距離の調整を設定します。0.0fは、脚部の間隔がデフォルトのままであることを意味します。
            hasTranslationDoF = false,//アバターの関節に対して平行移動の自由度を持たせないことを指定します。通常必要ないためfalseに設定します。
            human = humanBones,
            legStretch = 0.05f,//IK使用時の脚の長さの引き伸ばし許容量を設定します。0.05fは、脚が5%まで引き伸ばされることを許容することを意味します。
            lowerArmTwist = 0.5f,//回転/ひねりを肘と手首にどの割合で反映するかを定義します。0.5fは、回転が肘と手首に均等に分配されることを意味します。
            lowerLegTwist = 0.5f,//回転/ひねりを膝と足首にどの割合で反映するかを定義します。0.5fは、回転が膝と足首に均等に分配されることを意味します。
            skeleton = skeletonBones,
            upperArmTwist = 0.5f,//回転/ひねりを肩と肘にどの割合で反映するかを定義します。0.5fは、回転が肩と肘に均等に分配されることを意味します。
            upperLegTwist = 0.5f,//回転/ひねりを脚の付け根と膝にどの割合で反映するかを定義します。0.5fは、回転が脚の付け根と膝に均等に分配されることを意味します。
        };
        
        // アバターを生成
        var avatar = AvatarBuilder.BuildHumanAvatar (_targetModel, humanDescription);
        
        //確認
        if (avatar.isValid && avatar.isHuman)
        {
            _animator.avatar = avatar;
            Debug.Log("Avatar created successfully!");
        }
        else
        {
            Debug.LogError("Avatar creation failed.");
        }
        
    }
    
    
    /// <summary>
    /// ボーン名マッピング情報から HumanBone 配列を生成
    /// </summary>
    /// <param name="boneNameMap">ボーン名マッピング情報</param>
    /// <returns></returns>
    private HumanBone[] CreateHumanBones(Dictionary<string, string> boneNameMap)
    {
        // HumanBone のリストを作成
        List<HumanBone> humanBones = new List<HumanBone>();
        
        // マッピング情報を元に HumanBone を作成
        foreach (var pair in boneNameMap)
        {
            // HumanBone を作成
            HumanBone humanBone = new HumanBone
            {
                humanName = pair.Key,
                boneName = pair.Value,
                limit = new HumanLimit { useDefaultValues = true } // デフォルト値を使用
            };
            
            // HumanBone をリストに追加
            humanBones.Add(humanBone);
        }
        
        // HumanBone のリストを配列に変換して返す
        return humanBones.ToArray();
    }
    
    
    /// <summary>
    /// HumanDescription.skeleton 用の SkeletonBone 配列を生成
    /// </summary>
    /// <param name="root">スケルトン階層のルート Transform</param>
    /// <returns>SkeletonBone 配列</returns>
    private static SkeletonBone[] GenerateSkeleton(Transform root)
    {
        // 引数が null の場合はエラー
        if (root == null)
        {
            Debug.LogError("Root Transform is null.");
            return null;
        }
        
        // スケルトンボーンのリストを作成
        List<SkeletonBone> skeletonBones = new List<SkeletonBone>();
        
        // 再帰的にスケルトンを構築
        BuildSkeletonRecursive(root, skeletonBones);
        
        // スケルトンボーンのリストを配列に変換して返す
        return skeletonBones.ToArray();
    }
    
    
    /// <summary>
    /// 再帰的に SkeletonBone を構築
    /// </summary>
    /// <param name="current">現在の Transform</param>
    /// <param name="skeletonBones">構築中の SkeletonBone リスト</param>
    private static void BuildSkeletonRecursive(Transform current, List<SkeletonBone> skeletonBones)
    {
        // SkeletonBone を作成
        SkeletonBone bone = new SkeletonBone
        {
            name = current.name,
            position = current.localPosition,
            rotation = current.localRotation,
            scale = current.localScale
        };
        
        // 作成した SkeletonBone をリストに追加
        skeletonBones.Add(bone);
        
        // 子要素を再帰的に処理
        // 子要素がない場合は何もしない
        // foreachでTransform型に対してループをまわすと子要素に対して処理ができる
        foreach (Transform child in current)
        {
            BuildSkeletonRecursive(child, skeletonBones);
        }
    }
    
}


実行

実際に動かします。

モデルをヒエラルキー上に配置すると
Animatorコンポーネントの"Avatar"はすでに設定されているので予めを削除しておきます。

作成した"GenerateAvatarTest"スクリプトを割り当てて"Target Model"を指定します。

実行するとAvatarが生成されたことが確認できます。

ボーンマッピング情報作成方法が肝

今回、必須ボーンに対するボーン名は事前に把握できている状態でしたが、
ランタイムでインポートしたモデルは対応するボーン名が事前にわかりません。

このような場合はボーンマッピング情報をどのようなアルゴリズムで決定するのかが
正常なAvatar生成に関して肝になってきます。

Avatar生成が成功していてもHipsに対応するボーンを足のボーンに設定しまったとしたら
アニメーションした時におかしな動きになるのが想像できると思います。

Unity上ではUnityが勝手に対応するボーンを判定して設定してくれますが、
そのアルゴリズムは公開されていないようなので
自分でアルゴリズムを見出す必要があるようです。

おわりに

アバター生成に必要な必須のボーン情報はスクリプトリファレンスの
HumanTrait.RequiredBoneの例文を実行することで確認することができます。

docs.unity3d.com

Unityバージョンで結果が異なる可能性があるためAvatar生成前に
必須ボーン情報が存在するかを確認すると良さそうですね。

参考

docs.unity3d.com

docs.unity3d.com