株式会社フォワードワークス様が配信・提供する『みんゴル』でのキャラクターメイキングをどのように実装しているかの説明をさせて頂きたいと思います。

着せ替え機能ってどうやって作るの!?

複数のパーツを組み合わせてキャラクターモデルを作りたい!?

同一アニメーションを別モデルに適用するには!?

揺れ物設定が有るモデルを動きながら差し替えるには?

上記に興味があるかたや、Unityで『みんゴル』の様なアプリを作ってみたい!という方にお勧めしたい記事です。

■キャラクターモデルの構成

まず『みんゴル』では7種類の部位モデルを組み合わせてキャラクターモデルを作成しています。

髪 / 顔 / 帽子 / 上半身服 / 下半身服 / 手袋 / 靴 / アクセサリ

※8種類の派生として複数部位をまとめたセット部位があります。

キャラクターメイキングの手法は様々あり有名な所では、

  1. 各部位を個別モデルにし差し替え・組み合わせて繋げる
  2. 素体の上から別モデルを重ねて表示させる
  3. 素体となる形状から変化させて表示させる

大小さまざまな理由はあるのですが『みんゴル』では基本的に【1】の手法を使ってキャラクター作成をしていますが、『みんゴル』では更に骨構造の都合上、Meshだけ付け替える方法を採用しているため、単純にモデルを繋げている訳ではなかったりします。

※Mesh付け替えに関しての詳細は後述します。

【1】の手法を使用している理由はいくつかあるのですが、特に大きい理由は二つあります。

その1:モデル同士の接触干渉を部位モデル側でコントロールできる。

その2:きぐるみ等の特殊な形状でも部位の「差し替え」になるので比較的縛りが少なくできる。


これらの方針を元に、次は細かい部位モデルのルール付けや制限項目を設けていきます。

  • 各部位のポリゴン数
  • ボーン数の設定
  • 使用できるテクスチャ容量
  • 使用できるシェーダー
  • 干渉時の表示排他 等

細かい内容は省きますが『みんゴル』はスマートフォン向けとしては豪華と思われる程にはキャラクターに容量を使っています。

例として、下記が目安としている各設定値となります。

  • ポリゴン:10000
  • テクスチャ容量:15MByte
  • ボーン数:90

1キャラクターに付きこの位を目安に作成しております。

(フェイス部分で2000以上ポリゴン数を割いていますので是非表情豊かな『みんゴル』を楽しんでください!)

■キャラクターのボーン構造

大まかなキャラクターを構成する項目の次は、キャラクターのボーン構造の説明です。

『みんゴル』ではキャラクターのボーン構造を胴体と顔で二つの区分に分けて、それぞれのアニメーションを再生できるように構成しています。

区分わけといっても特別ソースコード上で何かを行っているわけではなく、Unity上で設定をしています。


ボーン構造は画像の様な構成で作り、青線の「Reference」が胴体のアニメーションのRoot、
赤線の「Head」が顔のアニメーションのRootとしています。

■キャラクターアニメーション設定

次に胴体と顔のアニメーション設定に関しての説明です。

『みんゴル』のキャラクターアニメーション設定はHumanoidを使用しています。

通常スマートフォン用の軽量モデルでは処理効率化の観点等からGeneral形式のアニメーションが多用されますが、『みんゴル』では以下の理由からHumanoid形式を使用しています。

  1. IKを使用
  2. Humanoid設定を使用してMask処理を行う
  • 【1】に関しては足と地面の設置感を出す為にUnityのIKを活用したく、Generalでは対応がされていなかったためHumanoidを使用しています。
    ※UnityのIKだけでは姿勢制御が求める動作にはならず、『みんゴル』独自の制御も追加で行っています。こちらも後述します。
  • 【2】に関しては同一のボーン構造で「胴体」と「顔」のアニメーションを別々に駆動させる為に
    Mask設定が必要であり、設定の幅はHumanoidの方が細かく設定できた為です。

またこれらとは別にキャラクターの骨構造上、人体に極力近い形だった事もありHumanoid形式を採用する環境が整っていた為、特に障害なく使用することが出来たのも大きな理由です。

Humanoidの設定は基本的にプロジェクト特有の事はせず、Unityのドキュメントに有る通りに設定を行っています。

https://docs.unity3d.com/jp/540/Manual/AvatarCreationandSetup.html

Mask設定に関しては「AvatarMask」を作成し、「胴体」と「顔」に関するMaskを作成します。


BodyMask


FaceMask

ここで作成したMask設定を各AnimationのImport設定に埋め込みます。

こうする事で余分な骨のAnimationキーの省略や、標準のHumanoid設定にはないAnimationキーをインポートする事が出来ます。

(Animation.fbxファイル毎に個別設定もできますが、AvatarMaskに保存し参照設定すると楽に適用できます。)


上記のMask→Sourceを設定するだけでAnimation単体のAvatarMask設定は終了です。

Body/Faceの各アニメーションにインポート設定を行い、Animatorに各Body/Faceに対応するレイヤーを用意/設定すると胴体と顔の別々のAnimation設定が行えるようになります。

BodyレイヤーをBaseに、FaceレイヤーにMask設定とSync設定を行い制御しています。

Sync設定の為、BodyとFaceで別々のAnimationStateの制御は行えないのですが、Face部分のみ変更等を行いたい場合は「Animator Override Controler」を使用して別アニメーションを設定する事で、
同じStateでも別アニメーションを再生できるようにしています。

表情パターン等が複雑なゲームの場合は、Sync設定をせず各レイヤー毎に遷移条件を設定し制御すると楽です。


AnimatorOverrideController

■キャラクターモデルの差し替え

上記まではモデル作成用の前提処理として長々とUnity上の設定話でしたが、ここからはコードの話題になります。

『みんゴル』ではモデルの差し替え・組み合わせでキャラクターを作成しています。

大まかな手順としては次の様な手順で処理しています。

  1. キャラクターの基礎となる骨構造を取得し、Name/BoneIndexとName/Transformの対となる2つのリストを作成。
  2. 各モデル部位読み込み(以降:部位モデルA)。
  3. 現在の表示しているキャラクターに読み込んだモデルの揺れ物情報が存在するかチェックし適切な処理をする。※1
  4. 部位モデルAのbonesを使用し、2で作成した骨構造に沿うようなTransformのIndexリストを新規作成する。
  5. 新規にGameObjectを生成し、SkinnedMeshRendererを追加する。
  6. 5で作ったSkinnedMeshRendererのsharderMeshに対して、「部位モデルA」のsharderMeshを全てInstantiateで追加する。(マテリアルも同様に処理)
  7. 4で作った骨構造のIndexリストを5で作ったbonesに適用する。

ハイ、文章だけ書いてもさっぱりですね。では1から順にコードで説明します。

※コードは今回用に書き起こして検証した物となります。実際に『みんゴル』で使用しているコードとは異なります。

1.基礎の骨構造の情報を保存

public GameObject RootBone;  // 基礎となる骨構造
protected List<Transform> BoneTransformList
protected Dictionary<string, int> RootBoneIndexList; // IndexList
protected Dictionary<string,BoneTransInitialize> BoneTransInitializeList; // Boneの初期構造リスト

// BoneTransInitialize は pos/rot/scaleを保存する構造体.

---------------------------------------------------------------------------------------
// 素体のボーンを登録する.
Transform[] rootBoneTransforms = RootBone.GetComponentsInChildren<Transform> ();

BoneTransformList.Clear ();
BoneTransformList.AddRange (rootBoneTransforms);

// boneの名前とIndexを登録.
RootBoneIndexList.Clear();

for (int boneIndex = 0; boneIndex < rootBoneTransforms.Length; ++boneIndex ) {
    Transform baseTrans = rootBoneTransforms[boneIndex].transform;

    // bone index登録.
    RootBoneIndexList.Add(baseTrans.name, boneIndex );

    // bone 初期情報保存.
    BoneTransInitialize addBoneTrans = new BoneTransInitialize()
    addBoneTrans.localPos = baseTrans.localPosition;
    addBoneTrans.localRotation = baseTrans.localRotation;
    addBoneTrans.localScale = baseTrans.localScale;
    BoneTransInitializeList.Add( baseTrans.name, addBoneTrans );
}

こちらに関しては特殊な事はおこなっておらず、後述する手順でMesh情報を基礎モデルに適する際に
Indexリストを示し合わせて変更してやるために最初に元となるリストを作成しています。

2.モデルを読み込む

ここは単純に部位モデルを読み込みます。AssetBundleからでもResourcesからでも各環境毎の読み込み方法でGameObjectを読み込みます。

3.揺れ物用の処理

こちらに関しては「2」で読み込んだモデルが揺れ物だった場合、または既に揺れ物をキャラクターが付けていた場合に必要になる処理です。
『みんゴル』では揺れ物設定を各部位設定する事ができ、同じ部位でもモデルによって揺れ物の設定は様々に設定できます。

4.「部位モデルA」のMeshが使用している骨構造を、「キャラクターモデル」の骨構造に合わせるリスト作成

GameObject resourceObject = Resource.Load(“modelA”);

---------------------------------------------------------------------------------------

SkinnedMeshRenderer[] smRenderersParts = resourceObject.GetComponentsInChildren< SkinnedMeshRenderer >();
for (int smrIndex = 0; smrIndex < smRenderersParts.Length; ++smrIndex)
{
    SkinnedMeshRenderer smr = smRenderersParts[ smrIndex ];

    // Boneの取得.
    Transform[] meshBones = smr.bones;

    // SkinnedMeshRenderer毎のBoneIndexに対応したTransformの配列.
    Transform[] localTransforms = new Transform[ meshBones.Length ];

    // Boneの参照を差し替える用のデータ用意。
    for (int boneIndex = 0; boneIndex < meshBones.Length; ++boneIndex)
    {
        // 部位と素体の同名Boneを検索し、部位と素体のindexを整理/保存.
        if (RootBoneIndexList.ContainsKey(meshBones[boneIndex].name))
        {
            //  部位側のBoneIndexリストと同じサイズのリストに、同じindexで素体側のtransformを設定.
            localTransforms[boneIndex] = BoneTransformList[RootBoneIndexList[meshBones[boneIndex].name]];
        }
    }
}

こちらが、「部位モデルA」で使用しているBoneのTransformListを、「キャラクターモデル」側のBoneのTransformListに置き換えた物を作成する処理になります。
各MeshはBoneの名前ではなくTransformListのIndexで「重み付け」を保存しています。このIndex情報が狂うと、例えば「顔周りのMeshが膝を曲げただけで動く」等、BoneとMeshの関係性が壊れてしまいます。

各モデル毎に完全一致するTransformListを用意できればこの処理は必要ないのですが、部位モデルは揺れ物用のBoneを追加したり不要なBoneを排除しデータが作成されているため部位モデル単体ではBoneの総数が違う状態ですので調整しています。

余談ですが開発初期では、「BoneのTransformListのIndexを合わせる」という視点に行かず実直に「全ての頂点の重み付け情報を書き換える」処理を行っていました。
参考までに以下に以前使用していたコードを乗せてみました。
こちらは「boneWeightsがサブメッシュ分増加する」や「全てのMesh分のループを回す」事になり、現行の方法に変更をしています。

List<BoneWeight> boneWeights = new List<BoneWeight>(); // ボーンの重み最終情報。
List<Matrix4x4> bindPoses = new List<Matrix4x4>();

for (int b = 0; b < BoneTransformList.Count; b++)
{
    bindPoses.Add( BoneTransformList[b].worldToLocalMatrix );
}

// boneWeightsはSubMeshCount * BoneNum 分ないと整合性エラーがでる。
for (int subMeshIndex = 0; subMeshIndex < smr.sharedMesh.subMeshCount; ++subMeshIndex)
{
    // 骨の参照を差し替える用のデータ用意。
    Dictionary<int, int> boneIndexRplaceDic = new Dictionary<int, int>(4);

    for (int boneIndex = 0; boneIndex < meshBones.Length; ++boneIndex)
    {
        // meshと素体の同名ボーンを検索し、meshと素体のindex相対値を保存.
        if (RootBoneIndexList.ContainsKey(meshBones[boneIndex].name))
        {
            boneIndexRplaceDic.Add(boneIndex, this.RootBoneIndexList[meshBones[boneIndex].name]);
        }
    }

    // 部位側のインデックスを素体側のボーンインデックスに置き換える
    BoneWeight[] meshBoneweight = smr.sharedMesh.boneWeights;
    for (int index = 0; index < meshBoneweight.Length; ++index)
    {
        BoneWeight boneWeight = meshBoneweight[index];
        if ( !boneIndexRplaceDic.ContainsKey( boneWeight.boneIndex0 ) )
        {
            continue;
        }

        boneWeight.boneIndex0 = boneIndexRplaceDic[ boneWeight.boneIndex0 ];
        boneWeight.boneIndex1 = boneIndexRplaceDic[ boneWeight.boneIndex1 ];
        boneWeight.boneIndex2 = boneIndexRplaceDic[ boneWeight.boneIndex2 ];
        boneWeight.boneIndex3 = boneIndexRplaceDic[ boneWeight.boneIndex3 ];

        boneWeights.Add(boneWeight);
    }
}


// メッシュ生成.
SkinnedMeshRenderer r = meshObject.AddComponent<SkinnedMeshRenderer>();
r.sharedMesh = Instantiate( smr.sharedMesh );
r.materials = smr.sharedMaterials;
r.bones = BoneTransformList.ToArray();
r.sharedMesh.boneWeights = boneWeights.ToArray();
r.sharedMesh.bindposes = bindPoses.ToArray();

5~7.「キャラクターモデル」に「部位モデルA」のメッシュ情報を適用。

三つの手順は一つのコードに纏めたほうが分かりやすいと思ったので一連のコードにしています。

//  キャラクターモデル用のMeshObjectを作成
GameObject newMeshObject = new GameObject();
newMeshObject.transform.SetParent( RootBone.transform );
newMeshObject.name = smr.gameObject.name;

// SkinnedMeshRenderer生成.
SkinnedMeshRenderer r = newMeshObject.AddComponent<SkinnedMeshRenderer>();
// 部位モデルのMesh情報を複製して適用
r.sharedMesh = Instantiate(smr.sharedMesh);
// マテリアルの適用
r.materials = smr.sharedMaterials;
// Bonesの情報を整合性をあわせてリストに更新
r.bones = localTransforms;
// 各種細かい設定
r.receiveShadows = false;
r.quality = SkinQuality.Bone2;

まずMeshObjectを新規に作成している所ですがこれは「部位単位で差し替え」させる事を前提にしている関係上、管理を楽にするためです。
「必要な時に作って、必要がなくなったら破棄する」としてしまい、流用する際に各種Mesh情報の更新や依存関係の整理等を極力無くす為です。
Instantiate(smr.sharedMesh)を行っているのも同様ですが、ここに関しては「同じモデルを再度差し替えに使う場合」に読み込んだオリジナルデータを変更してしまうと、整合性を保つ事が大変だったので複製したデータを使用するようにしています。

そして「手順4」で行った骨構造のリストは r.bones = localTransforms; こちらで使用します。
こちらを行うことで「SkinnedMeshRenderer r」の中のBoneのTransformListのIndexがキャラクターモデル仕様に置き換わり破綻なくmeshが表示されます。

■まとめ

以上がアプリで使用しているモデル生成及び、適用するアニメーションの設定となります。
3Dモデルに限らず2Dでもキャラクター周りはゲームの要件との相談で何を実装し何を実装しないかを
明確にしないと、大量のコードや論文とも言えるような決まり事ができてしまうので、
チームで相談しつつ機能を作っていくといいと思います。