ホーム < ゲームつくろー! < DirectX技術編 < Xファイルのカスタムテンプレートを使ってみよう!
その32 Xファイルのカスタムテンプレートを使ってみよう!
Xファイルのお世話になるといえばメッシュオブジェクトを読み込む(D3DXLoadMeshFromX関数)やフレーム情報を読み込む(D3DXLoadMeshHierarchyFromX関数)がありますが、何もそれだけの為にあるわけではありません。実はXファイルは独自のデータを読むことも出来ます。しかも、DirectX6.0になってからそれを読み書きするための専用インターフェイスも用意されました。これにより、ユーザは統一した方法であらゆるデータを定義することができるようになったんです!
この章ではDirectXが用意してくれた読み書きの機構であるXファイルのカスタムテンプレートについて見ていく事にしましょう。
@ Xファイルのフォーマットはテンプレート指向
Xファイルは「テンプレート指向」という書式を採用しています。これは、いわば外部定義された構造体です。1つ例を見てみましょう。
template SkinWeights {
<6f0d123b-bad2-4167-a0d0-80224f25fabb>
STRING transformNodeName;
DWORD nWeights;
array DWORD vertexIndices[nWeights];
array FLOAT weights[nWeights];
Matrix4x4 matrixOffset;
}
これはアニメーションで使われる「SkinWeightsテンプレート」を抜粋したものです。見た目は構造体そっくりですよね。templateという宣言の後にそのテンプレートの名前(SkinWeights)を書きます。C言語と同じように中括弧で内容を包みます。その次に来る<6f0d123b-bad2-4167-a0d0-80224f25fabb>という数字の羅列は「ユニークID」と呼ばれるものです。これはこのテンプレートを識別するための番号でして、世界に1つしかない固有のものなんです。プログラムでは、このユニークIDを用いてテンプレートを識別しています。その後に来るSTRINGやDWORD、arrayそしてMatrix4x4というのはすべて変数もしくは他のテンプレートの名前です。この辺りは本当に構造体そっくりですよね。唯一配列を表すarrayの書き方が独特ですが、書式自体は明確です。最後は中括弧で閉じますが、この後に「;(セミコロン)」は付けません!ご注意下さい。
このように、テンプレートを宣言した後、実際にデータを格納します。同様のSkinWeightsのデータを見てみましょう。
SkinWeights {
"Frame2_Bone01";
4;
0,
1,
4,
5;
1.000000,
0.500000,
0.500000,
1.000000;
-1.000000,-0.000000,0.000000,0.000000,-0.000000,0.000000,1.000000,0.000000,-0.000000,1.000000,-0.000000,0.000000,0.000000,0.000000,0.000000,1.000000;;
}
これが実際のデータです。最初の"Frame2_Bone01"というのが「STRING transformNodeName」に相当するものです、STRINGは文字を格納できる宣言というわけです。1つのデータの区切りはセミコロン(;)です。以下、そのような目線でご覧下さい。次の4はnWeightsに格納されます。次は0,1,4,5までですね。これはvertexIndices[nWeights]というDWORD型の配列に格納されます。要素の数がちゃんと合っていますよね。次の1.0,0.5,0.5,1.0も同じです(weights配列)。最後のずら〜っとならんだ数字。これは16個あるのですが、これはMatrix4x4という4×4行列に値をいれるときの方法です。これはMatrix4x4テンプレートで定義されています。このように、データはちゃんとテンプレートに従って区切られ、並べられています。
テンプレートにはもう1つ「オープン変数」という素晴らしい書式が定義できます。次のFrameテンプレートを見て下さい。
template Frame {
< 3D82AB46-62DA-11CF-AB39-0020AF71E433 >
[...]
}
「?」と思われるかもしれません。このフレーム構造体にはただ1つ「[...]」という変数(?)が定義されています。この[...]というのは「この部分にどんなテンプレートが来てもいいですよ」という印なんです。これを「開かれたテンプレート(Open Template)」と言います。まだハテナかもしれませんので、実際にフレームのデータを見てみましょう。
Frame Frame_SCENE_ROOT {
FrameTransformMatrix {
1.000000, 〜 ,1.000000;;
}
Frame Frame1_Plane1_Layer1 {
FrameTransformMatrix {
1.000000, 〜 ,1.000000;;
}
Mesh Mesh_Plane1 {
6;
// 以下省略 //
}
}
Frame_SCENE_ROOTというフレームデータの中には別のフレーム、そしてMesh(メッシュデータ)が格納されています。Frameは、他のどのテンプレートでも受け付けることが出来ます。これによって「データの入れ子」「データの親子関係」を非常にスマートに記述できるのです。ただ、何でもかんでも来られると困るという場合もあります。そういう時にはテンプレートの限定使用というのもあります。これは、例えば[ SkinWeights <6f0d123b-bad2-4167-a0d0-80224f25fabb>]というようにテンプレートの名前と、そのユニークIDを記述します。
あとXファイルで気にすることは「可変長ヘッダー」と呼ばれるファイルの最初に来る識別コードです。これが無いとXファイルとして正しく認識してくれません。可変長ヘッダーの例を挙げます。
xof 0303txt 0032
まず最初の「xof」はいわゆるおまじない(マジック番号)です。ここがxofという文字列でなければXファイルでは無いと判断して読み込みをやめてしまうわけです。半角スペースを空けて次の03はメジャーバージョン、次の03はマイナーバージョンです。お約束のようなものです(Xファイルのバージョンが変わればまた異なる数字になりますが、今はこれで十分です)。次の「txt」というのはこれがテキストファイルであることを示します。Xファイルはバイナリモードでも記述できまして、その場合は「bin」となります。スペースを空けまして最後の0032は、数字を32bitで読み込むことを宣言しています。将来的には64bitも可能にしていますが、今はサポート外です、
ここまで基礎知識があれば、カスタムテンプレートは作れます!!
A まずは超簡単なカスタムテンプレートで
いきなり難しいことはできません。ですから、非常に簡単なカスタムテンプレートを作りましょう。人の名前とIDコードを保持するテンプレートにしてみます。
xof 0303txt 0032
template PersonID {
<B2B63407-6AA9-4618-9563-631EDC204CDE>
STRING Name;
DWORD IDCode;
}
こんな感じです。ユニークIDですが、これは「GuidGen.exe」というグローバルユニークIDを生成するツールを用いて取得しました。このツールはPlatform SDKをインストールした方ならばそのフォルダ内(私の環境ではC:\Program Files\Microsoft Platform SDK for Windows Server 2003 R2\Bin)、Visual Studioのインストールフォルダ(私の環境ではC:\App\VS2005\Common7\Tools\Bin)などにあります。DirectX SDKのフォルダやマイクロソフトのホームページからは取得できないようです。どうしても欲しい方はPlatform SDKをインストールすることをお勧めします。これを用いると、世界に1つしかない数字を作ってくれます。このテンプレートをPersonID.xという名前で保存しておきます。
次にこのテンプレートに従ったデータテキスト作っておきましょう。
xof 0303txt 0032
PersonID Me{
"IKD"; // Name
1234; // ID
}
PersonID Someone{
"WhoAreYou?"; // Name
9999; // ID
}
データテキストはTestID.xと言う名前で保持しておきます。テンプレートとデータのテキストを分けるのには理由があります。テンプレートは再利用が可能です。ですから、他のプログラムでこのテンプレートを使いたい時にデータがあると良くないわけです。これはちょうどヘッダーファイルと実装ファイルの関係に似ていますね。
では、次に進みましょう。
B カスタムテンプレートからデータを読み取る
さ、ここからが実は本番です。先ほど作成したカスタムテンプレートをプログラムから読み込む機構を見ていきます。DirectX6からこれを扱えるインターフェイス群が提供されました。それらを使うことにします。
以下それらのインターフェイスについて説明いたしますが、全てのインターフェイスを使うためには、d3dxof.libライブラリをリンクし、dxfile.hをインクルードする必要があります。
ライブラリとヘッダーの定義 #pragma comment( lib, "d3dxof.lib")
#include <dxfile.h>
まず、最初に使うインターフェイスはIDirectXFileインターフェイスです。このインターフェイスには2つの大きな役割があります。1つはカスタムテンプレートを登録して、独自テンプレートに従ったデータの読み込みができるようにすること、そしてもう1つは読み込んだデータをテンプレート単位で列挙するIDirectXFileEnumObjectインターフェイスを生成することです。
このインターフェイスを取得するには、グローバル関数であるDirectXFileCreate関数を用います。
DirectXFileCreate関数からIDirectXFileインターフェイスを取得 IDirectXFile *pDXFile;
DirectXFileCreate( &pDXFile );
このインターフェイスにカスタムテンプレートを登録するにはIDirectXFile::RegisterTemplates関数を使います。
IDirectXFile::RegisterTemplates関数 HRESULT RegisterTemplates(
LPVOID pvData,
DWORD cbSize
);
pvDataにはテンプレート定義の文字列(もしくはバイナリデータ)を与えます。
obSizeはpvDataに与えた文字列の長さを指定します。
pvDataにはテンプレートの定義をテキストでそのまま与えます。ただ、残念ながらファイルを解析してはくれませんので、自前でテンプレート文字列をファイルから読み込む必要があります。テンプレートテキストを分けたのはこういうわけなんです。ファイルから文字列をごっそりと取り出すのは意外と面倒なんですが、以下のようにします。
ファイルからテキストを全部コピーする char* GetFileText( char* filename ){
ifstream ifs;
ifs.open( filename);
if( !ifs.is_open() )
return NULL;
DWORD FileSize=0;
while(!ifs.eof()){ ifs.ignore(); FileSize++;} // サイズ取得
ifs.clear(); ifs.seekg(0, ios_base::beg); // ファイルポインタを初期位置へ
char* tmp = new char[FileSize];
ZeroMemory( tmp, FileSize );
ifs.read( tmp, FileSize-1 );
return tmp;
}
テキストモードで開いてifstream::read関数を用いるのがコツです(改行が考慮されます)。ただ、実はバイナリモードで保存しても、以下のプログラムについてはうまくいきます。この関数でファイルの中身が文字列としてメモリに保存されますので、それをRegisterTemplates関数の第1引数に渡します。文字列のサイズは…あ、この関数から取得するようにしても良いですね。面倒な人はstrlen関数で十分です。
カスタムテンプレートが正しく登録できた場合、関数はD3D_OKを返します。それ以外の場合、以後の操作はただ1つも出来ませんので注意して下さい。
次に使用するのはIDirectXFileEnumObjectインターフェイスです。これは、データファイルの読み込みを行ってくれるインターフェイスです。このインターフェイスは先ほどテンプレートを登録したIDirectXFileインターフェイスのCreateEnumObject関数で取得します。
IDirectXFile::CreateEnumObject関数 HRESULT CreateEnumObject(
LPVOID pvSource,
DXFILELOADOPTIONS dwLoadOptions,
LPDIRECTXFILEENUMOBJECT *ppEnumObj
);
pvSourceにはデータが格納されているファイルやメモリなどへのポインタを指定します。うれしいことに、こちらはちゃんとファイル解析をしてくれますので、ファイル名を指定するだけでOKです。もちろん、ファイルの拡張子を「.x」にしなくてもかまいません。バイナリデータでXファイルを作成すれば、さらに安全性が増します。メモリやリソースに保存したデータも読み込み可能です。
dwLoadOptuinsは、第1引数に与えた「物」を示すフラグです。ファイルであればDXFILELOAD_FROMFILE、メモリであればDXFILELOAD_FROMMEMORYなどを指定します。この指定フラグの種類はDXFILEマクロとしてマニュアルに記載されています。
ppEnumObjにIDirectXFileEnumObjectインターフェイスが返ります。
この関数は、IDirectXFileインターフェイスに登録したテンプレートだけ扱えます。よって、他のテンプレートによるデータが混在していた場合、読み込みは失敗してしまいます。注意してください。
この関数でIDirectXFileEnumObjectインターフェイスの取得に成功すると、ファイルに登録していたデータを取得できるようになります。データへのアクセスを提供してくれるインターフェイスはIDirectXFileDataインターフェイスで、IDirectXFileEnumObject::GetNextDataObject関数で取得することが出来ます。
IDirectXFileEnumObject::GetNextDataObject関数 HRESULT GetNextDataObject(
LPDIRECTXFILEDATA *ppDataObj
);
ppDataObjにIDirectXFileDataインターフェイスへのポインタのポインタを渡しますと、データがある場合はそこに有効なポインタが格納されます。データが無い場合はNULLが返ります。
有効なIDirectXFileDataへのポインタが取れると、登録したいずれかのテンプレートに従ったデータが取得できています。その「いずれか」を判断するのがIDirectXFileData::GetType関数です。これは取得したIDirectXFileDataインターフェイスがどのテンプレートのデータを示しているのかを知らせてくれます。GetType関数で取得できるのはテンプレートのGUIDです。世界に1つしかない数字ですから、確実にテンプレートを判断できると言うわけです。例えば次のように比較します。
const GUID *guid; // const宣言が必要です
pDXData->GetType( &guid ); // 現在のテンプレートのGUIDを取得
// PersonIDのGUIDを設定
const GUID PersonID_GUID ={ 0xB2B63407,0x6AA9,0x4618, 0x95, 0x63, 0x63, 0x1E, 0xDC, 0x20, 0x4C, 0xDE};
if( *guid == PersonID_GUID ){
// PersonIDのデータであることが確定!
}
if分で上のような比較が出来ることに私は驚きました(^-^;。しかし残念ながら、GUIDはswtch〜case構文には使えません。ですから、複数のテンプレートを解析するときには、条件文をいくつも並べる必要があります。
テンプレートが特定できれば後は簡単です。IDirectXFileData::GetData関数で指定のデータを取得します。
IDirectXFileData::GetData関数 HRESULT GetData(
LPCSTR szMember,
DWORD *pcbSize,
void **ppvData
);
szMemberにはテンプレートで定義した変数の名前を指定します。この関数は変数名でデータを判断してくれるわけです。
pcbSizeには取得されるデータのサイズが返ります。szMemberにNULLを指定すると、データサイズだけを純粋に取得できる仕組みになっています。
ppvDataには実際のデータの値へのポインタが返ります。ここ、注意してください。「データへのポインタ」です(ダブルポインタであることに注意!)。つまり、取得したデータはコピーしなければならないわけです。実際にポインタの先を調べて見たのですが、ヒープではなくてローカルとして取られているようで、スコープからはずれてしまうとフリーメモリとして無くなってしまいます。
こうしてようやく、カスタムテンプレートからデータを抽出することができました。
C データ抽出は習うより慣れよう!
Bでカスタムテンプレートからデータを抽出する方法を見てきました。結構長いプロセスを踏みます。ただ、何だかんだで単なるデータ取得ですから、慣れてしまえば簡単です。そこで、Aで定義した簡単なカスタムテンプレート及びそれに従ったデータテキストからデータを抽出する作業をしてみましょう。
まずは、IDirectXFileインターフェイスにテンプレートの書式を登録します。
IDirectXFile *pDXFile;
DirectXFileCreate( &pDXFile );
char *szMyTemplateStr = GetFileText( "PersonID.x" );
if( !szMyTemplateStr ){
// ファイルが無い
}
if( FAILED( pDXFile->RegisterTemplates( szMyTemplateStr, strlen( szMyTemplateStr ) ))){
// 取得失敗
}
ここを通れば、テンプレートの登録は成功です。つまりRegisterTemplates関数はテンプレートのコンパイラなんですね。次は、データを列挙してくれるIDirectXFileEnumObjectインターフェイスをここから取得します。
IDirectXFileEnumObject *pDXFileEnumObj;
// ファイルを解析してテンプレートデータを列挙する
if( FAILED( pDXFile->CreateEnumObject( "TestID.x", DXFILELOAD_FROMFILE, &pDXFileEnumObj ))){
// 解析失敗
}
ここを通るということは、少なくともデータファイル内が登録したテンプレートのみであった事を示しています。しかし、テンプレートに定義されたデータの整合性は判断しません!もし、テンプレート内のデータ定義や定義順番が間違っていたとしても、ここはあっさり通過します。
テンプレートの列挙に成功したら、次はデータへアクセスするIDirectXFileDataインターフェイスの取得です。これは、IDirectXFileEnumObject::GetNextDataObject関数を使うのでした。インターフェイスを取得できたら、テンプレート型をチェックして、データを取得するだけです。
IDirectXFileData *pDXFileData;
char **ppName; // 文字列は配列なのでダブルポインタにすること
DWORD *pIDCode; // 数字は1つなのでシングルポインタ
// テンプレートデータにアクセスするIDirectXFileDataインターフェイスを取得
while( SUCCEEDED( pDXFileEnumObj->GetNextDataObject( &pDXFileData )))
{
// テンプレートの型チェック
const GUID *pguid;
pDXFileData->GetType( &pguid );
if( *pguid == g_pPersonID_GUID )
{
DWORD Size;
// PersonIDテンプレートデータを取得
pDXFileData->GetData("Name", &pSize, (void**)&pName ); // 名前取得
pDXFileData->GetData("IDCode", &pSize, (void**)&pIDCode ); // ID取得
// データをコピーする
CopyAllData( *pName, *pIDCode ); // 適当な関数ですよ
// 必要なくなったので解放する(重要!)
pDXFileData->Release();
}
}
whileループが成功する限りデータを取り続けてくれます。実際TestID.xファイルには2つのPersonIDテンプレートデータを格納していました。
TestID.xテキストファイル内 xof 0303txt 0032
PersonID Me{
"IKD"; // Name
1234; // ID
}
PersonID Someone{
"WhoAreYou?"; // Name
9999; // ID
}
よって、1回目のループでは**ppNameには"IKD"、*pIDCodeには1234、2回目のループでは**ppName=WhoAreYou?、*pIDCode=9999となります。ここで重要なのは、IDirectXFileDataインターフェイスからデータを取得した後はすぐに解放してしまうことです。このインターフェイスは使い回しがききません。2回目のループ時には別のインターフェイスポインタが取得されます。
全てのデータを取得しコピーし終わったら、後は解放するだけです。
pDXFileEnumObject->Release();
pDXFile->Release();
これで、綺麗さっぱりインターフェイス群は無くなります。こうみてみると、さほど難しくは無いですよね。むしろ、インターフェイスを用い統一した方法でどのような形のデータでも取得できる点に、私は素晴らしさを感じてしまいます。
D テンプレートの中のテンプレートの取り方
オープンテンプレートという仕様を@で説明しました。テンプレートの中に[...]というのがあると言うやつです。これがある場合、IDirectXFileDataインターフェイスのGetNextObject関数を用いると、この子テンプレートデータにアクセスする有効なインターフェイスを取得できます。まず、関数から見てみます。
IDirectXFileData::GeetNextObject関数 HRESULT GetNextObject(
LPDIRECTXFILEOBJECT *ppChildObj
);
ppChildObjにはIDirectXFileObjectインターフェイスへのポインタを渡します。もし子テンプレートデータがあれば、ここに有効なポインタが渡されます。無ければNULLが返ります。
IDirectXFileObjectインターフェイスとは何なのでしょうか?実はこれ、IDirectXFile**関連の親インターフェイスのようなんです。このインターフェイス自体は実は殆ど何もできません。このインターフェイスの役目は、IDIrectXFileDataインターフェイスを取得することにあります。ただ、その取得にはQueryInterface関数を使うしかありません。次のように実装します。
IDirectXFileObjectインターフェイスからIDirectXDataインターフェイスを取得する IDirectXFileData *pDXChildData;
pDXFileObj->QueryInterface( IID_IDirectXFileData , (void**)&pDXChildData );
IDirectXFileDataインターフェイスのGUIDはIID_IDirectXFileDataマクロで定義されています。もし、これでインターフェイスの取得に失敗したならば、それは[...]が別の形式のデータであることを表します。有効なポインタが取れれば、それは間違いなく子テンプレートデータです。これが取得できれば、そのデータ抽出はBやCで説明した方法と全く同じです。
E 結局カスタムテンプレートを有効に使うには?
ここまでの説明で、もう殆どありとあらゆるテンプレートを定義して、データを抽出することが出来ます。最後に、カスタムテンプレートを有効に利用する方法論について簡単に説明したいと思います。
テンプレートの定義は厳密に決まっています。それこそGUIDを指定するのですから、世界にたった1つだけのテンプレートであるわけです。今回は分かりやすさを考慮してテンプレートをファイルに定義しましたが、IDirectXFile->RegisterTemplates関数の第1引数には文字列を与えているだけでした。つまり、何もテンプレートの情報を外から読み込まなくても、プログラムの内部で定数文字列(リテラル文字)として定義してしまえばいいんです。そうすれば、無駄なファイル解析をせずに済みます。
Xファイルに格納されているのはある情報群です。あるオブジェクトを作成するために必要な情報のみがしっかりと格納されています。使われるテンプレートやデータの組み方も決まっています。つまり、あるXファイルとその読み込み部分は1対1の関係になるわけです。これは何を暗示するかと言いますと、「専用読み込みクラス(もしくは関数)を作りなさい」ということを示しています。みなさん、DirectX技術編その25「アニメーションの根っこ:オブジェクトを読み込む」で出てきたD3DXLoadMeshHierarchyFromX関数を覚えておりますでしょうか?この関数は、正にそのお手本のような関数です。裏で行っていることを完全に隠蔽して、ファイルからオブジェクトを生成する機構を提供しています。クラス図を描くと、
依存関係で十分
という風に、オブジェクトの生成ローダーを作るわけです。インターフェイスを派生させて、今回のカスタムテンプレートを読み込む機構をきちっと作っていけば、もう、何でも出来そうですよね(^-^)。
IDirectXFileインターフェイスには、実は「セーブ機構」も備わっています。もちろん、全てインターフェイスで操作できます。独自のデータをXファイルに書き出すことが出来るわけです。こうなると「オブジェクトの透過性」も確保できてしまいます。セーブ機構については、またいずれお話したいと思います。