この記事はDelphi Advent Calender 2015の23日目の記事です。
こんにちは。duskdawnです。
luxideaさんからの紹介で書きにきました。
このような場に身をさらすのは初めてなので、本題に入る前に軽く自己紹介をします。
- Delphi歴3年目の理系の大学院生です
- Delphiは研究の実装手段として使っています
- 一応、2Dと3Dの両方をかじっています
今回のネタである3Dモデルの描画も、研究の過程で必要にかられて手を出しました。
なお、以下の話は私の手元の環境(XE4)準拠で進めます。他のバージョンでは異なる部分もあるかもしれませんが、あらかじめご了承ください。
それでは、本題に入ります。
#やりたいこと
FBXファイル(拡張子が.fbxのシーンファイル)のモデルデータをDelphiアプリ上で描画すること。具体的には、ユニティちゃんをDelphiコンポーネントのTViewport3D上に表示することを目指します。
ちなみに、正しく描画できれば以下のようなユニティちゃんが描けます(画像はこちらのサイトより引用)。
なお、今回描画に用いるFBXファイルはユニティちゃん公式サイト、またはこちらのサイトから入手可能です。
ただし、公式サイトからダウンロードできるのは、特殊なエンコードを施されたファイル(.unitypackage)で、解凍するにはUnity(ゲームエンジン)が必要になるので、ない場合は後者から落としてくるのを推奨します。
もしかすると著作権に触れるかもしれないので、キャラクター利用のガイドラインに則って、ライセンスを貼っておきます。
#ライセンス
このアセットは、『ユニティちゃんライセンス』で提供されています。このアセットをご利用される場合は、『キャラクター利用のガイドライン』も併せてご確認ください。
#FBXファイルとは
実装の話に入る前に、簡単にFBXファイルについてふれておきます。
-
Autodesk社(3D制作ツールのMayaやMaxをつくっている会社、MayaやMaxはこの分野をかじったことのある人で知らない人はたぶんいないくらい有名)というところが提唱している、3Dシーンを保存するための汎用フォーマットです
-
3Dモデルの静的な情報(頂点やポリゴン)だけでなく、アニメーション、ライト、カメラといったような、シーン描画に必要な情報まで保持できます
-
Delphiの標準コンポーネント(TModel3D)では読み込めません
もっと詳しく知りたい方は、こちらを参照してください。
FBXファイルの中身はバイナリなので、このままではにっちもさっちもいきません。まずは、FBXファイルから必要な情報を抽出するところから始めなければなりません。
#FBX SDK
上述のAutodesk社は、プログラマがFBXファイルをいじれるように、FBX SDKというソフトウェア開発キットを無償で提供してくれています。FBXファイルからのデータ抽出は、こいつを利用して行います。
ただ、ここで残念なお知らせです。このFBX SDKですが、RAD Studioからでは使えません。もしかするとやりようはあるのかもしれませんが、私には無理でした。対応言語がC++とのことなので、C++ Builderで使ってみようとしたのですが、「そんなコンパイラ知らねえよ」と怒られてしまい、どうしようもありませんでした。
というわけで、仕方がないのでこの部分はVisual Studioで作りました。この場では畑違いの話になるかと思いますので、以下、データが抽出できたものとして話を進めます。今回抽出したデータは以下の通りです。
- 頂点のインデックス
- 頂点座標
- 法線
- UV座標
これ以外にも、メッシュデータであれば、頂点カラー、tangent、binormalといったデータや、マテリアルデータ(モデルの材質を定義するデータ)、ボーンデータ(モデルの骨組み。スキンメッシュアニメーションをするのに必要)、アニメーションデータ(各フレームでのボーン行列等)のように、多彩なデータを抽出することが可能です。
なお、FBX SDKに興味のある方は、ここやここで実装を交えてデータ抽出の方法を説明してくれているので、よければ参考にしてください。
#Delphiで3Dモデル描画に必要なコンポーネント
3Dモデルの最小単位は三角形ポリゴンです。Delphiには、この三角形ポリゴンレベルの操作を行うことができるTCustomMeshというクラスがあります。TSphereやTConeのような出来合いの3Dオブジェクトクラスは、すべてこのTCustomMeshクラスを継承して作られています。
TCustomMeshはDataというプロパティをもっていて、DataはさらにVertexBufferとIndexBufferというプロパティをもっています。たどり着くのが結構大変ですが、このVertexBuffer(頂点バッファ)とIndexBuffer(インデックスバッファ)に抽出してきたデータを放りこんでやれば、自前の3Dモデルを描画することができます。上述した抽出してきたデータは、それぞれ以下のプロパティに割り当てます。
- 頂点のインデックス → IndexBuffer.Indices
- 頂点座標 → VertexBuffer.Vertices
- 法線 → VertexBuffer.Normals
- UV座標 → VertexBuffer.TexCoord0
いきなり、IndexBuffer.IndicesとかVertexBuffer.Verticesとか言われても初見の方はピンとこないかと思いますので、簡単にどんな動作をするのか説明すると、VertexBuffer.Verticesは描きたいモデルの頂点座標を、IndexBuffer.Indicesはどの頂点を使って三角形を作るのかを決めます。例えば
with VertexBuffer do
begin
Length(4); // Verticesは動的配列なので、使う前にLengthプロパティで領域を確保する
Vertices[0] := Point3D(0, 0, 0);
Vertices[1] := Point3D(1, 0, 0);
Vertices[2] := Point3D(0, 0, 1);
Vertices[3] := Point3D(1, 0, 1);
end;
with IndexBuffer do
begin
Length(6); // Indicesは動的配列なので、使う前にLengthプロパティで領域を確保する
// 0~2で1つ三角形ができる
Indices[0] := 0;
Indices[1] := 1;
Indices[2] := 2;
// 3~5で1つ三角形ができる
Indices[3] := 1;
Indices[4] := 3;
Indices[5] := 2;
end;
のように書くと、3点(0, 0, 0)、(1, 0, 0)、(0, 0, 1)を頂点とする三角形と、3点(1, 0, 0)、(1, 0, 1)、(0, 0, 1)を頂点とする三角形が描画され、結果的には1辺の長さが1の正方形が描画されます。
なぜ頂点データをインデックスを用いて間接的に指定するようなまどろっこしい方法をとるのかというと、上の例ではインデックスの1番と3番、2番と5番はそれぞれ同じ座標の点を指定することを意味していますが、どうせ同じ座標なら、まとめてしまった方がデータ容量を節約できるからです。
同様にして、NormalsとTexCoord0にも値を入れてやれば、それらの値を反映した描画結果を得ることができます。
#とりあえずやってみた
どうでしょうか。なんかそれっぽいものが出てきましたね。
しかし、これでは味気ないので、次に考えることはテクスチャを貼ることですよね。見た目からは分かりませんが、このモデルには一応UV座標も割り当ててあるので、あとはテクスチャのパスを渡してやれば、はれて冒頭のようなユニティちゃんが描画されるはずです。
メッシュにテクスチャを貼るには、TLightMaterialSourceというクラスを用います(TTextureMaterialSourceでも可ですが、ライティングの効果を反映させたいときはTLightMaterialSourceを使います)。擬似コードチックで申し訳ないですが、具体的には以下のようにすると、FILEPATHにある画像データをメッシュのテクスチャとして割り当てることができます。
Mesh := TCustomMesh.Create(AOwner); // メッシュをつくる
MaterialSource := TLightMaterialSource.Create(AOwner); // マテリアルをつくる
MaterialSource.Texture.LoadFromFile(FILEPATH); // テクスチャを割り当てる
Mesh.MaterialSource := MaterialSource; // メッシュのMaterialSourceプロパティに割り当てる
ということで、足取りも軽く、FBXからテクスチャとUV座標の対応関係を抽出しにVisual Studioに戻ったのですが、ここに大きな落とし穴がありました。
#テクスチャとUV座標の対応関係
結論から言いましょう。対応関係は取れませんでした。
上で紹介したサイトの通りにやってみても、どのテクスチャがどのUV座標に対応するのかはわかりませんでした。平たく言ってしまうと、どのテクスチャをユニティちゃんのどの部分に貼ればよいのかがわからなかった、ということです。
困りました。丸1日くらいいじくりまわしたのですが、いくらやっても出てこない。
仕方がないので最終手段に出ることにしました。
はい。全部手作業で貼りました。
ユニティちゃんは幸いなことにメッシュの数が23個でしたので、力技でゴリ押しできましたが、これが3ケタとかになると手ではやっていられなくなりますね。う~む、どうしたものでしょうか。
FBXのテクスチャまわりに詳しい方がいらっしゃいましたら、ぜひ解決策をご教授願いたいものです。
テクスチャがつくと、ぐっとそれっぽくなりますね。
1枚目の画像でバレているかもしれませんが、実はまだマテリアルの解析が不完全なので、ほっぺたが少し妙なことになっています。見た目から推測するに、おそらく透過度のパラメータが足りないのかな、と思います。
うん。でもまあ、今回はこれでよしとしましょう。正直もう疲れました。
無事、FBXで記述されたユニティちゃんをDelphiアプリ上に描画することができました。
#こんな面倒なことをしなくても…
さて、最後に、今回の私の苦労をぶちこわしにする話をしてこの記事をしめたいと思います。
実は、Delphiには標準で3Dモデルを読み込む手段が用意されています。上の方でちらっと出てきたTModel3Dを使う方法です。TModel3Dは、その名の通り3Dモデルを扱う汎用クラスで、以下のファイルフォーマットのモデルデータをインポートし、描画することができます。
- .dae
- .obj
- .ase
モデルデータの割当ても実にシンプルでして、FILEPATHにモデルデータがあるとすると
Model := TModel3D.Create(AOwner); // モデルクラスをつくる
Model.LoadFromFile(FILEPATH); // モデルを読み込む
これだけで、今回私がたどった道筋を踏破することができます。
さらに、.fbx→.obj や .fbx→.dae の変換が可能なツールがAutodeskから無償で提供されています(FBX Converter)。
なおさら、今回やったことの意味がないじゃないか、と思われるかもしれません。私もそう思います。
実は今回の企画はまだ中途段階でして、最終的な目標はユニティちゃんに回し蹴りしてもらおう(スキンメッシュアニメーションを動かそう)、というものでした。私が知る限りですが、Delphiの標準コンポーネントでスキンメッシュアニメーションはサポートされていなかったと思います(そもそもやろうとする人があまりいなさそうですが)。
「じゃあちょっと作ってみようか」と思い立ったまではよかったのですが、実装が間に合わなかったので、やむなく描画までの話となった次第です。いつか機会があれば、この話の完結編をしてみたいと思います。
以上です。長文失礼いたしました。
まだ少し早いですが、皆さま、よいお年を