Godot モデル動的構築 自分の事例2
前回の続き
今回は5つのクラスのうちの1つ、Workshopの説明です。モデルを分解して、データをストックするシングルトンです。下図の緑色の箱!
分解するワケ
分解ってどういうことだ? モデルそのまま取り込むのはそんなに駄目なのか? カメラや光源くらいならインポート設定で取り除けるやろ?
いやーそれが違うんすよ。いくつか例示します。
なんだこりゃあ!
これは前作『ポテト』で作った、プレイアブル種族であるエルフのblendファイルです。
アーマチュア、各服装のボディ、髪型、顔パーツ、被り物(着替えができる)、サイズや形状のアタリ(目安)を付ける参照モデル、そして新規ボディの元となる素体。200個以上が1つのblendファイルに入ってます。
オーク、ドワーフ、ゴブリン、ノーム、ヒューマンの各blendも同様になってます。
管理やデザイン作業の面からいって、これらを別々のファイルにしたくないんですよ。アーマチュアや素体のコピーを分散させたくないし、服装×髪型×被り物の、組み合わせの見た目を常時確認したいですしね。
アニメーション用のblendファイルは別に存在します。こればっかりは、同じにするとBlenderが重くて仕方が無かったので、プレイヤー種族だけはメッシュとアニメを別ファイルにしました。
同じく『ポテト』の武器と盾のblendファイルです。100種類以上が同じファイルに入ってます。さらに見ての通り、位置がゼロではありません。このスクショの撮影用に並べたわけではないです。
サイズ感の比較、デザインの統一性や差別化、並んでるのを見てニヤニヤしたいなどの理由から、1つのファイルにまとめたいんです。あ、そうそう、何より、これ全部別ファイルだったら管理大変です。
同じく『ポテト』のジャッカロープのblendファイルです。ボディは共通にして、角と目のパーツを変えて、テクスチャも変えることで、アルミラージにもなるようになってます。モンスターの類はこういう風に、パーツを一部変えて差別化することをしてます。逆を言うと、共通部分は同じメッシュにしたいんです。容量削減のため、管理のしやすさのため。
こんな感じなので、ファイルからのオブジェクトの抽出が必須なわけです。自分の作り方だと。
Q. Blenderのスクリプト(マクロ)で吐き出せばいいんじゃないの?
A.
いやあ、それはね、まずUnityだとfbxを使いたくなかったんです。
今回はGodotなのでglTFが使えるんですけど、結局インポート設定を変えたりインポート後に操作したりして、マテリアル周りでひと手間必要です。さらに前述の通り、キャラクターのパーツは、複数のパーツから成ってたり、共通オブジェクトを持ってたりするので、インポート前にバラバラになるのも都合が悪かったりするんです。
エクスポートでひと手間、インポート後にひと手間、で中途半端になるなら、やっぱりもう全部実行時のスクリプトでつじつま合わせよう、という目論見です。
Workshopの大枠
Workshopはシングルトンとして常駐していて、「どのモデルファイルの何々が欲しい」というリクエストを受けます。例えば
武器クラス曰く
「"res:/Item/Weapon.blend"の"HandAxe"大枝が欲しい」
ムービーシーンクラス曰く
「"res:/Character/Hero_CutsceneAnim_Chapter0.blend"の
アニメーションライブラリが欲しい」
などです。
キャッシュに無い場合はロードしてこしらえます。
ファイルをリソースとしてロードする
得られたPackedSceneリソースをインスタンス化する
インスタンス化したシーンノードを分解する
得られたデータ(ノードやリソース)をファイル別にキャッシュする
インスタンス化したシーンノードをfreeする
データを返す
モデルデータは次の4種類のノードやリソースに分解されます。
Bough: 大枝。MeshInstance3Dを含むノードのツリー。
アーマチュア: Skeleton3Dとその親ノード。
AnimationLibrary: 同名のリソース。
バリエーション用テクスチャ: Texture2D。モデルの替えテクスチャ。
シーンノードはfreeするので、Node派生であるBoughとアーマチュアは、実際にはそのコピーを保持しておきます。
分解データ1. Bough
筆者が勝手に名付けた概念、Boughがモデルの最小単位です。
いわばパーツです。Node3DとMeshInstance3Dの組み合わせで出来ています。
収集Boughの決定
基本的にはPackedScene(モデルファイルから変換されたリソース)のルート直下にある各ノードが、子孫含めて1つのBoughです。なので1つのファイルに複数存在します。最上位ノードの名前がBoughの名前です。下の画像の左の例で言うと、"CubeA"と"Cylinder"です。
そしてBoughの名前引きの辞書に、ノードを複製して格納します。C言語風に書くとDictionary <StringName, Node3D>です。これに入れます。
また、ファイルがいわゆるキャラクターモデルで、Skeleton3Dノードを含む場合、Skeleton3Dの直下のノードがBoughになります。上の画像の右の例で言うと、"GiantRat"、"ZombieRat_MainBody"、"ZombieRat_SubBody"です。
(なぜMainとSubに分かれているかというと、ゾンビネズミはモディファイアの違う2つのオブジェクトで出来ています)
複数のオブジェクトが兄弟として集まって一つのモデルを形成してるような場合があると思います。上の画像は3つの草が1セットになったモデルです。こういう場合、Blender上ではEmptyでまとめておきます。EmptyはGodotではただのNode3Dになります。このノード(青枠)がBoughになります。
【余談】
この記事の趣旨とはあまり関係無いんですが、この画像の黄色いアイコン(メッシュリソース)のID、3つとも一致してますよね。Blenderのオブジェクトが同じメッシュを共有する場合は、Godotインポート後もちゃんと同一の単一メッシュリソースにしてくれます。
よかった~! ナイスだぜGodot。
マテリアル
当然ながらBoughにはMeshInstance3Dが付いていて、メッシュリソースが適用されています。そしてメッシュには、インポート時にGodotが自動作成した、ほぼデフォルト設定のStandardMaterial3Dが付いています。
このマテリアルをゲームで使うわけにはいきません。設定すべきブレンドモードがあったり、ラフネスやメタリックを変えたかったり、そもそもマテリアルをShaderMaterialに変えたかったりしますよね。
なので、このマテリアルを元に、テクスチャ等を引き継いで、新たにマテリアルを作成します。
マテリアルを作るのはstaticクラスであるPaintboxです。マテリアルの内容は、マテリアル名の接尾辞を元に判断されます。
作成したマテリアルはメッシュに適用します。
Transformのリセット
前述の武器モデルのように、blend内の各オブジェクトは位置がゼロで保存されていなかったりするので、Bough最上位ノードのpositionとrotationをゼロにセットしておきます。
分解データ2. アーマチュア
アーマチュアという名前の概念はGodotに無いです。勝手に名付けました。
Skeleton3Dノードとその親(Node3D)の2つのノードのセットをアーマチュアという名前で管理してます。
アーマチュア=親Node3D+子Skeleton3D
このアーマチュアに対して、1つまたは複数、Boughを子としてくっ付けたものがキャラクターになります。
なぜSkeleton3D単体ではなく親まで保存するかというと、アニメーションの都合です。Godotのアニメーションデータは、例えば次のようにトラックパスを記録しています。
"RatArmature/Skeleton3D:Hand.L"
この場合の"RatArmature"は、Skeleton3Dの親(Node3D)の名前であり、元はBlender上のアーマチュアオブジェクトの名前です。この名前が必要なんです。(Blenderでは新規作成したアーマチュアは、デフォルトで"アーマチュア"とか"Armature"って名前だけど、変えたりするでしょう? 変えない?)
なので名前の保持のために、Node3Dを一緒に保管してます。
分解データ3. AnimationLibrary
blendファイルにアクションが含まれていると、インポート後のGodot上のシーンにはAnimationPlayerノードが追加されます。
AnimationPlayerにはAnimationLibraryが1個だけ登録されています。これを取得して保持しておきます。
AnimationPlayerには複数のAnimationLibraryを登録できますが、blendファイルからインポートしたAnimationPlayerには、常に1個だけセットされています。ちなみにこのライブラリ名は「""(無し)」です。
分解データ4. バリエテクスチャ
モデルの替えテクスチャ集です。カラバリならぬテクバリ。
特定の名前("-MatPool"または"-TexPool")のMeshInstance3Dノードがあれば、そのノードのメッシュに含まれている全マテリアルの各テクスチャを取得し、
マテリアル名引きの辞書に格納します。C言語風に書くとDictionary <StringName, Texture2D>です。これに入れます。
MatPoolオブジェクトのメッシュはダミーなので、形状は何でもいいです。上記画像のような、一覧性のある感じだと管理しやすいですよね。
Workshopはこんな感じ。じゃあ他のクラスはどうなっているかというと・・・
つづく
この記事が気に入ったらサポートをしてみませんか?