この記事はUnreal Engine 4 (UE4) その3 Advent Calendar 2020 16日目の記事です。
#概要
この記事では、「UE4のスケルタルメッシュアセットをプログラムで作成する方法」について書きます。
先に結果を見せると、このようなスケルタルメッシュをプログラムから作れます。
「これスケルタルメッシュどうこうでなくレイマーチ表現じゃん」とおっしゃりたい方がいらっしゃるかもしれません。
全くそのとおりです。
一応、あえてスケルタルメッシュにしたことによる利点もあるので後述します。
この記事では、テーマとはずれるので描画に関することは書きません。
描画表現の面に興味がある方は、参考にしたものを後述しますのでそちらを御覧ください。
#背景
スケルタルメッシュアセットを、FBXファイルをインポートするのでなく、プログラムで作りたいと思ったことはないでしょうか。
僕はたまにあります。
いまやマーケットプレイスで探せば多くのアセットは手に入る時代ですが、ゲームに使うならまだしも、技術的な実験用途に使うにはそれらは複雑すぎることが多いです。
FBXファイルならともかく、マーケットプレイスで購入したものはスケルトンやメッシュもいじれません。
技術的な実験では、プリミティブなメッシュと簡素なスケルトンが必要になることが多いです。
__プログラマならそれをプログラムから作りたいと思うのは当然(?)__でしょう。
UE4はバージョン4.26の時点では、スケルタルメッシュを外部ファイルを使わずに作る機能は提供されていません。
(あったら教えて下さい!)
今年はもうひとつアドベントカレンダーに記事を書くのですが、その記事のために特定の構造のスケルタルメッシュが必要になりました。
そこで、プログラムで作ることにしました。
ここまで読んだ多くの方は、簡単なモデルならMayaとかのDCCツールで作ればいいじゃん、と思ったと思います。
そうです。全くそのとおりです。
なぜプログラムで作ったかと言うと、DCCツールの使い方を覚えるのがプログラム書くより面倒だった、という理由につきます。
あと、__モデル作成を軽い気持ちで依頼できるような友人がいない__というのもあります。
(ちなみに僕はモデリングもリギングも習得してないくせして、Mayaのカスタムノードやツールは作ったりしてます。いい加減DCCツールを覚えないと怒られそうですね。反省してます!)
#スケルタルメッシュの作り方
ソースコードは以下に置いてます。
Githubレポジトリ
エンジン改造はしていないので、UE4のC++プロジェクトとしてビルドして挙動を確認できます。
Contentフォルダ直下にEditorUtilityWidgetアセットがあり、実行してボタンを押すとスケルタルメッシュが作られるようにしています。
(作った直後は、作ったアセットがContentフォルダに表示されないので、コンテンツブラウザの表示更新のために一度別のフォルダを表示してから戻ってきてください)
以下の画像のようなスフィアを敷き詰めたメッシュを作るボタンなどありますのでお試しあれ。
さて、スケルタルメッシュを作る方法ですが、一行で説明すると、FSkeletalMeshImportDataで構造を指定してFSkeletalMeshBuilderを使えばできます。
UE4はfbxやusdファイルをインポートしてスケルタルメッシュを生成できますが、それらもFSkeletalMeshImportDataとFSkeletalMeshBuilderを用いて実装されています。
つまり、FSkeletalMeshImportDataとFSkeletalMeshBuilderを用いれば、どんな形式のモデルデータファイルであってもインポータを自分で実装できると言うことです。
たとえば、VTuber用のツールで有名なVRM4Uも、vrmファイルからスケルタルメッシュをインポートする処理を実装しておられます。
(VRM4UはFSkeletalMeshImportDataやFSkeletalMeshBuilderのような上位レイヤーのAPIは使っておらず、もう少し低レイヤーのAPIを使って実装しておられます。)
VRM4UのGithubレポジトリ
今回は、インポータを実装するのでなく、FSkeletalMeshImportDataの値を直接プログラムから設定します。
FSkeletalMeshImportDataの値設定は、DirectXなどの描画APIの頂点バッファやインデックスバッファをTriangleList形式で直接指定するのに近いものがあります。
自分で一からソースコードを読みたい人は、必要な処理は上記GithubレポジトリのProceduralSkeletalMeshFunctionLibrary.cppにまとめてあるので、そちらを読んでください。
以下では、主要な部分を簡単に解説します。
もっとも簡単な、1トライアングル2ボーンの例を見てみましょう。
MakeTriangleSkeletalMeshImportData()関数となります。
こういうものを作ります。
まずは3頂点の位置の設定です。
座標は適当です。
頂点バッファのようなものだと思ってください。
FSkeletalMeshImportData SkeletalMeshData;
SkeletalMeshData.Points.Emplace(-10.0f + 50.0f, 10.0f, 0.0f);
SkeletalMeshData.Points.Emplace(10.0f + 50.0f, 10.0f, 0.0f);
SkeletalMeshData.Points.Emplace(-10.0f + 50.0f, -10.0f, 0.0f);
次にポリゴンごとの頂点の設定です。
同じ位置でもポリゴンが違えば、別のFVertexとしてWedges配列に登録します。
次に説明するFaces配列と合わせて、TriangleList形式のインデックスバッファのようなものだと思ってください。
あとから手動でマテリアルを作って割り振ることを考えて、UVはちゃんと設定しておきます。
VertexIndexには先に設定したPoints配列のインデックスを指定します。
SkeletalMeshImportData::FVertex V0, V1, V2;
V0.VertexIndex = 0;
V0.UVs[0] = FVector2D(0.0f, 0.0f);
V0.MatIndex = 0;
V1.VertexIndex = 1;
V1.UVs[0] = FVector2D(1.0f, 0.0f);
V1.MatIndex = 0;
V2.VertexIndex = 2;
V2.UVs[0] = FVector2D(0.0f, 1.0f);
V2.MatIndex = 0;
SkeletalMeshData.Wedges.Add(V0);
SkeletalMeshData.Wedges.Add(V1);
SkeletalMeshData.Wedges.Add(V2);
次に、トライアングルポリゴンの設定。
WedgeIndexがWedges配列のインデックスになります。
SkeletalMeshImportData::FTriangle T0;
T0.WedgeIndex[0] = 0;
T0.WedgeIndex[1] = 1;
T0.WedgeIndex[2] = 2;
T0.MatIndex = 0;
T0.SmoothingGroups = 0;
SkeletalMeshData.Faces.Add(T0);
次は、骨の設定です。
今回はRootとChildの2つにしています。
Length, XSize, YSize, ZSizeはUE4内で現状使われてないので適当な値を入れておいて構いません。
ParentIndexをINDEX_NONEにしておくとルート骨として扱われます。
Flagsも現状では使われていません。
SkeletalMeshImportData::FJointPos J0, J1;
J0.Transform = FTransform::Identity;
J0.Length = 1.0f;
J0.XSize = 100.0f;
J0.YSize = 100.0f;
J0.ZSize = 100.0f;
J1.Transform = FTransform(FVector(50.0f, 0.0f, 0.0f));
J1.Length = 1.0f;
J1.XSize = 100.0f;
J1.YSize = 100.0f;
J1.ZSize = 100.0f;
SkeletalMeshImportData::FBone B0, B1;
B0.Name = FString("Root");
B0.Flags = 0x02;
B0.NumChildren = 1;
B0.ParentIndex = INDEX_NONE;
B0.BonePos = J0;
B1.Name = FString("Child");
B1.Flags = 0x02;
B1.NumChildren = 0;
B1.ParentIndex = 0;
B1.BonePos = J1;
SkeletalMeshData.RefBonesBinary.Add(B0);
SkeletalMeshData.RefBonesBinary.Add(B1);
次に、各頂点のスキニングウェイトを設定します。
VertexIndexにはPoints配列のインデックスを指定します。
Points配列の要素数だけの設定が必要になります。
今回はウェイトをChild骨に全振りします。
SkeletalMeshImportData::FRawBoneInfluence I0, I1, I2;
I0.Weight = 1.0f;
I0.VertexIndex = 0;
I0.BoneIndex = 1;
I1.Weight = 1.0f;
I1.VertexIndex = 1;
I1.BoneIndex = 1;
I2.Weight = 1.0f;
I2.VertexIndex = 2;
I2.BoneIndex = 1;
SkeletalMeshData.Influences.Add(I0);
SkeletalMeshData.Influences.Add(I1);
SkeletalMeshData.Influences.Add(I2);
UE4内で頂点位置の配列を別途作るときがあり、そのために、もとのPoints配列のインデックスへのマッピングテーブルを作っておく必要があります。
もとのPoints配列のインデックス値をそのまま入れておいて構いません。
SkeletalMeshData.PointToRawMap.AddUninitialized(SkeletalMeshData.Points.Num());
for (int32 PointIdx = 0; PointIdx < SkeletalMeshData.Points.Num(); PointIdx++)
{
SkeletalMeshData.PointToRawMap[PointIdx] = PointIdx;
}
最後に、スケルタルメッシュ生成のための各種設定をします。
こちらは、FBXをインポートするときのダイアログで同じ名前のオプションがあるものが多いので、それを見ればおおよその役割はわかるかと思います。
SkeletalMeshData.NumTexCoords = 1; // UVチャンネルの数
SkeletalMeshData.MaxMaterialIndex = 0; // この数プラス1が、マテリアルスロットの数になる
SkeletalMeshData.bHasVertexColors = false;
SkeletalMeshData.bHasNormals = false;
SkeletalMeshData.bHasTangents = false;
SkeletalMeshData.bUseT0AsRefPose = false;
SkeletalMeshData.bDiffPose = false;
これで、FSkeletalMeshImportDataの値設定は終わりです。
メッシュ構造と骨構造の必要最低限の情報が設定できています。
次に、FSkeletalMeshImportDataからスケルタルメッシュアセットとスケルトンアセットを生成する処理を解説します。
CreateSkeletalMesh()関数となります。
物理アセットやマテリアルはエディタ上で作った方が手っ取り早いので、今回はプログラムからは作りません。
CreateSkeletalMesh()の中身は、UE4のfbxインポート処理(FbxSkelMeshImport.cpp)やusdインポート処理(USDSkeletalDataConversion.cpp)を参考にして実装しました。
長くなるので、肝となる処理のみ抜粋します。
FSkeletalMeshBuilder::Build()によりスケルタルメッシュの内部データを生成します。
FSkeletalMeshBuilder().Build(SkeletalMesh, ImportLODModelIndex, bRegenDepLODs);
SkeletalMeshRenderDataを作ります。
SkeletalMesh->Build();
スケルトンの骨構造をスケルタルメッシュから設定します。
Skeleton->MergeAllBonesToBoneTree(SkeletalMesh);
スケルタルメッシュにスケルトンを対応させます。
SkeletalMesh->Skeleton = Skeleton;
スケルタルメッシュ生成の応用:スライム
前節まででスケルタルメッシュ生成の解説は終わりです。
ですが、何のためにこんなことをやるのか伝わらないと思うので、マテリアルも追加して何か面白い表現を作ってみましょう。
前節では1トライアングル2ボーンでしたが、今回は立方体型メッシュを作ります。
骨は正四面体配置で4本作りました。
前述したEditorUtilityWidgetのBoxTetrahedronボタンで生成できますのでお試しあれ。
これに対して、それぞれの骨の位置を中心としたメタボールを描画してみます。
そうすると、冒頭で見せたスライム状の何か?ができます。
骨が動くことを前提にしているので、立方体メッシュのスキニングウェイトは4つの骨に均等に割り振っています。
これは、立方体メッシュのサーフェイスの範囲内でのみレイマーチでメタボールを描画するからです。
メタボールを描画するマテリアルは、以下のレポジトリにあるものを改造させていただいて使用しています。
メタボールマテリアルの改造元のレポジトリ
(僕のGithubレポジトリのREADMEにも書いていますが、マテリアルをご自分の何かで利用したい場合は、上記のレポジトリに記載されているライセンス条項に従ってください。)
この記事は、描画でなくスケルタルメッシュ生成がテーマなので、メタボールマテリアルの解説はしません。
興味がある方は、このレポジトリの元となったEpicのYoutube動画で作り方を解説しているので、そちらを見てみるといいでしょう。
Custom Material Node: How to use and create Metaballs | Live Training | Unreal Engine
立方体メッシュにしているのは、レイマーチのためです。
頂点数が少なく、ボリュームがある形状なら何でもよかったのです。
それはスタティックメッシュだろうとスケルタルメッシュだろうと変わりません。
上記レポジトリやEpicの動画では、スタティックメッシュに描画しています。
今回、スケルタルメッシュにしたことで、骨によってメタボールの位置を制御できるようになっています。
このようなスライム表現を作った時、頂点アニメーションにしたり頂点シミュレーションにすると、ゲームプレイ中に登場するキャラクターに使うには取り回しが難しくなるし負荷も大きいです。
骨であればプロシージャルアニメーションや物理アニメーションの低負荷なノウハウがたくさんあります。
また、頂点アニメーションよりも骨アニメーションの方がアニメータにとってモーションを量産しやすいというメリットもあります。
今回はボールが4個でしたが、もっとボールの数を増やせば、そこそこのクオリティのスライムができるのでないでしょうか。
と、もっともらしくまとめようとしてますが、
それはスケルタルメッシュを使う理由にはなっても、プログラムでスケルタルメッシュを作る理由にはならないんですけどね。
DCCツールで作ればいいんで。
まあ9日後にはクリスマス、15日後には大晦日ということで、広い心で受け止めてくださると助かります。
今回、せっかくスライム表現を作ったので物理で動かしてみたいですが、それはこの記事のテーマからはずれるので別の記事に書こうと思います。
#(少し脱線)骨の入ってないメッシュの作り方
UE4では骨が入ってないメッシュについては、ProceduralMeshComponentやCustomMeshComponentによってBPからでも生成する手段が提供されています。
興味のある方は、ProceduralMeshComponentとStaticMeshComponentの違いをまとめつつGeometryProcessingプラグインについて書かれているこちらの記事や
Mesh Generation and Editing at Runtime in UE4.26
こちらの記事を読まれるといいかと。
UE4 ComputeShaderでMeshの頂点バッファをリアルタイム書き換えする
手前みそですが以前、コンピュートシェーダの実験のためにグリッド状のメッシュをプログラムから生成していました。
そちらのソースコードも参考になるかと思います。
UE4で海面シミュレーションと描画を行う
UE4でクロスシミュレーションをコンピュートシェーダで行う
あとがき
FSkeletalMeshImportDataの設定後は、objファイルを手書きしてるかのような疲労を感じました。。。
1トライアングル2ボーンくらいならまだしも、少し複雑なメッシュになってくるとしんどいです。
twitterのフレンドから紹介されたのですが、プログラムでスケルタルメッシュを作るなら、FBX SDKを使ってfbxファイルを作るという手段もあります。
自分の関心がFBXよりもUE4内でのスケルタルメッシュデータの方に向いたため、今回はUE4側で実装しました。
パイプラインに強くなりたい人はFBX SDKを使って実装するのもいい勉強になるのではないかと思います。
明日はaltaltさんのGroomに関する記事です!楽しみですね!