第3回「画面描画(1) 〜 the texture and the fonts」


皆様ごきげんよう
本講座は、無料で入手できるプログラミング環境「Cygwin」を用いて本格的なSTG(シューティング)ゲームを製作してみる講座である。
その第二回である前回では、Makefile なるものを使ったプログラミング方法について紹介した。
が、ゲーム製作本体は全く進まず、多分に味気の無い回であったと思う。
ゆえに今回は実際的なプログラミングに移ろうと思う。具体的には、画像ファイルの画面への表示、およびフォントを用いた文字列描画である。
今回紹介する部分、特に画像ファイルの画面表示は、2Dゲームにおける要といっていい部分である。
だが幸いにも内容的には大したこと無いので、さっくりと解説してしまおう。
最後までお付き合い頂ければ幸い。



それでは本編に移る・・・その前に、断っておきたい事がある。
今回から、プログラムのソースコード全てを日記上で紹介し、そいつをコピー&ペーストして云々、といった面倒なことは行わないことにする。
その代わり、こちらで予め用意したファイルをダウンロードして頂いてから、それらの一部分を抽出して説明する形で講座を進めていくので、ご理解いただきたい。


さて、それでは今回のソースコードを紹介しよう:
http://gintenlabo.hp.infoseek.co.jp/dl/kouza03.tar.gz
このファイルを適当なディレクトリにダウンロードし、解凍する。
解凍方法は、まず Cygwincd コマンドでダウンロードしたファイルのあるディレクトリに移動し、

tar -xvzf kouza03.tar.gz

と入力すればよい。これで、カレントディレクトリに ginten_kouza03 というディレクトリが作られているはずだ。
そうしたらまず、中の kouza03.exe を実行してみよう。ウィンドウが開き、画面中央にCGが、左下に「文字列出力テスト」と表示されるはずだ。これが今回の完成品である。
なお、このexeファイルを起動するには、何故か d3dx9.dll が必要になるので、それも圧縮ファイルに同梱しておいた。
これは libd3dx9.a ( あるいは d3dx9.lib ) をリンクすると必要になるのだが、本来既に Windows にインストールしてある筈の DLL なので、何故必要になるのかは今一不明である(詳しい方、情報求む)。まぁとりあえず同じディレクトリに置いておけば問題ないので、本講座はその方法で解決することにしたい。


解説に移ろう。
今回のソースコードは、本体である kouza03.cc ファイルと、そこからインクルードされている gintenlib/com_ptr.hpp ファイル、以上の二つである。


まず先に gintenlib/com_ptr.hpp の中身から説明しよう。
この中で宣言&定義されている gintenlib::com_ptr は、当ホームページで公開している「銀天ライブラリ」の未公開コンテンツであり、全体的に boost::intrusive_ptr を模した振る舞いをするスマートポインタである。
詳しい説明は省くが、これを用いることで、前回まで

	// D3Dデバイスの生成
	{
		IDirect3DDevice9 *pD3DDevice_ = NULL;
		// HAL、HELの組み合わせを変えながら、D3Dデバイスを生成する
		// まず、HALのいいやつ
		if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
				D3DCREATE_HARDWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice_ ) ) )
		{
			// 失敗。普通のHALで試す
			if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
				D3DCREATE_SOFTWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice_ ) ) )
			{
				// それでもダメならREFも試す
				if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, hWnd,
						D3DCREATE_SOFTWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice_ ) ) )
				{
					// ダメでした
					throw std::runtime_error( "D3Dデバイスの生成に失敗しました" ); 
				}
			}
		}
		pD3DDevice = intrusive_ptr< IDirect3DDevice9 >( pD3DDevice_, false );
	}
	
	// ・・・

このような書き方をしていた部分を、

	// D3Dデバイスの生成
	
	// HAL、HELの組み合わせを変えながら、D3Dデバイスを生成する
	// まず、HALのいいやつ
	if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
			D3DCREATE_HARDWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice ) ) )
	{
		// 失敗。普通のHALで試す
		if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
			D3DCREATE_SOFTWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice ) ) )
		{
			// それでもダメならREFも試す
			if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, hWnd,
					D3DCREATE_SOFTWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice ) ) )
			{
				// ダメでした
				throw std::runtime_error( "D3Dデバイスの生成に失敗しました" ); 
			}
		}
	}
	
	// ・・・

このように簡潔で直感的な書き方に改めることが出来る。
また、今回追加した部分でも、例えば

	// 描画するテクスチャの指定
	if( FAILED( pD3DDevice->SetTexture( 0, texture ) ) )
	{
		throw std::runtime_error( "テクスチャの設定に失敗しました" );
	}

この部分を、もし boost::intrusive_ptr を使って書こうと思ったら

	// 描画するテクスチャの指定
	if( FAILED( pD3DDevice->SetTexture( 0, texture.get() ) ) )
	{
		throw std::runtime_error( "テクスチャの設定に失敗しました" );
	}

このように書かなければならない。これらの手間を省くためのものである。


それでは本体である kouza03.cc の説明に移ろう。これは前回までの dx_hello.cc を微改造(主に boost::intrusive_ptrgintenlib::com_ptr に変更)したものに、画像描画と文字列描画を加えたものである。


さて、まずは全体的な話をしよう。そもそも描画とはどうするのか、という話だ。
前回の解説では、その辺は「定型文だ」と言って適当に流してしまったので、今回はそこから説明しよう。
kouza03.cc の main 関数内にある、この部分を見て頂きたい:

// 描画

// まずクリア
pD3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,0), 0, 0 );
// シーンの開始
if( SUCCEEDED( pD3DDevice->BeginScene() ) )
{
	// 描画本体
	draw();
	
	// 終了
	pD3DDevice->EndScene();
}

// バックバッファを表画面に反映させる
HRESULT hr = pD3DDevice->Present( NULL, NULL, NULL, NULL );

順に説明していこう。
まず呼ばれている Clear( 〜 ) は、画面をクリアするための関数だ。ここでは真っ黒な色( D3DCOLOR_XRGB(0,0,0) )にクリアしている。これがないと前回の描画内容が残ってしまい見苦しくなってしまう。
次の BeginScene() は、描画を行う前に呼び出す必要がある関数だ。詳しい説明は省くが、これを呼んでおかないと殆どの描画行為はエラーになってしまう( Clear などの例外はあるが)。
BeginScene が成功したら、その次の draw(); で描画処理を行う。これは DirectX の関数ではなく、kouza03.cc 内で宣言&定義された関数だ。無論、関数にせず直接ここに描画の処理を並べても良いが、そうすると見づらくなるので関数に切り分けている。
そうして描画が終わったら、EndScene() を呼び出して描画が終わったことを DirectX に使える。この EndScene() を呼ばないと、次の Present を呼び出すことが出来ない。
最後に、Present( NULL, NULL, NULL, NULL ) を呼び出して、描画した内容を画面に反映する。実はさっきまで描画を行っていたのは画面そのものではなかったのだが(何故そうなっているのかは各自で調べよう)、ここで初めて描画した内容が画面に実際に表示される。本来はこの後、Present の戻り値を調べて『デバイスロスト』に対応しなければならなかったりするが、その処理は省略してある。


以上が描画処理における定型文の部分だ。それでは、実際の描画について説明しよう。
今回行っている処理は、画像ファイルの表示と文字列の表示だ。その中で一番大事なのは画像ファイルの表示であるので、先に大事ではない文字列表示の方を先に片付けてしまうことにする。

さて DirectX において描画処理を行う場合は一般的に、実際に描画を行う前に何らかの下ごしらえを行う必要がある。
今回の文字列処理の場合も多分にもれず、「フォント」を予め作成してから文字列を描画しなければいけない。
そのために、まず作成した「フォント」を受けるためのポインタをグローバル領域に宣言&定義する:

// フォントへのポインタ
com_ptr<ID3DXFont> font;

こんな感じである。
次に、関数 init_dx に以下のコードを追加する:

// フォントの作成
D3DXFONT_DESCA fontdesc =
{
	24,	// 高さ
	0,	// 幅
	FW_NORMAL,	// 字の太さ( FW_NORMAL とか FW_BOLD とか)
	0,	// ミップレベル
	FALSE,	// イタリック(斜体)
	SHIFTJIS_CHARSET,	// 文字コード
	OUT_DEFAULT_PRECIS,	// 出力精度
	DEFAULT_QUALITY,	// 品質
	DEFAULT_PITCH | FF_DONTCARE,	// ピッチ
	"MS ゴシック",	// フォント名( "MS 明朝" とか "MS ゴシック" とか)

};
if( FAILED( D3DXCreateFontIndirect( pD3DDevice, &fontdesc, &font ) ) )
{
	throw std::runtime_error( "フォントの製作に失敗しました" );
}

なお、これは DirectX のバージョンによって作り方が違う可能性があるので注意すること。
そして実際の描画では、

// 文字列描画( void draw() 内)

// 描画する領域
RECT rect = { 500, 550, 800, 600 };

font->DrawText
( 
	NULL,	// 描画するスプライト。よくわからんので NULL を指定してみた
	"文字列出力テスト", -1,	// 描画する文字列、および文字数( -1 のときは NULL 終端)
	&rect,	// 描画する領域を表した RECT 構造体へのポインタ
	DT_LEFT | DT_NOCLIP,	// フラグ。 DT_LEFT は左揃え、また DT_NOCLIP を指定すると若干高速
	D3DCOLOR_XRGB( 255, 128, 255 )	// 描画色。白だと味気ないので明るい紫
);

こんな感じで描画すればいい(これもバージョンによって違うので注意)。
この方法は簡単だが、一つだけ注意することがある。この文字列描画は DirectX の中では結構遅い処理である、という点だ。
そのため、見た目上は文字列の表示であっても、実際には文字列の画像を予め作っておいて、以下の画像表示を使って描画していくことが多い。先ほどの説明において「重要でない」といったのは、そういう理由からである。


さてそれでは、今回の最重要ポイント、ファイル画像の描画について説明しよう。
今回の例では、ファイル hoge.png (サイズ 256x256 、製作中のゲームから適当に切り抜いてきたもの)を、画面上の領域 ( 200, 100 ) - ( 600, 500 ) へと表示させている。
この画像は適当に他のものに差し替えてよいが、条件が一つだけある。それは、幅と高さがともに二の整数乗( 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, ... )になっている必要がある、ということだ(正方形である必要は無い)。これは専ら DirectX 側の事情である。

それでは実際の描画処理を紹介しよう。
ファイル画像の描画についても、文字列描画と同じように、予め下準備をしておく必要がある。
今回のファイル画像描画において必要なのは「テクスチャ」である。

// 描画するテクスチャへのポインタ
com_ptr<IDirect3DTexture9> texture;

そこで、上記のようにグローバル領域にポインタを用意しておく。
次に、文字列描画の時のように、関数 init_dx に以下のコードを追加する:

// 今回使用するテクスチャの読み込み
if( FAILED( D3DXCreateTextureFromFile( pD3DDevice, "hoge.png", &texture ) ) )
{
	throw std::runtime_error( "テクスチャを構\築できませんでした" );
}

この部分は「テクスチャ」をファイルから読み込む部分だ。なお「テクスチャ」という言葉の意味だが、こいつは2Dゲームを作る上では単に「画像」と考えればよい。
「テクスチャ」は今回のようにファイルから読み込むことも出来るし、真っ白(実際は白一色ではないが)なテクスチャを作って、そいつに例えば文字なんかを書き込む、なんてことも可能だ(詳しいことは後の講座で説明したい)。
こうして作ったテクスチャを描画するために、関数 draw 内に以下のコードを追加する:

// 描画するテクスチャの指定
if( FAILED( pD3DDevice->SetTexture( 0, texture ) ) )
{
	throw std::runtime_error( "テクスチャの設定に失敗しました" );
}
// 描画する矩形領域
float x1 = 200, x2 = 600, y1 = 100, y2 = 500;

vertex vx_data[4] =
	{
		{ x1, y1, 0, 1, 0, 0 },
		{ x2, y1, 0, 1, 1, 0 },
		{ x1, y2, 0, 1, 0, 1 },
		{ x2, y2, 0, 1, 1, 1 },
	};

pD3DDevice->SetFVF( vertex::FVF );
pD3DDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, vx_data, sizeof(vertex) );

この中で使われている構造体 vertex は自前で用意したもので、その定義はこちらだ:

// 描画用構造体
struct vertex
{
	float x, y, z, w;
	float tu, tv;
	
	static const int FVF = D3DFVF_XYZRHW | D3DFVF_TEX1;
};

以上が、画像の描画処理である。
これらは所謂定型文であり、こう書けば SetTexture によって指定されたテクスチャ全体が ( x1, y1 ) - ( x2, y2 ) 領域に合うよう拡大/縮小されて表示される、と考えて欲しい(もし拡大/縮小されるのが嫌なら、画像サイズを前もって調べておき(手っ取り早いのは画像のプロパティを見ることだ)、描画する領域のサイズを画像のサイズに合わせればよい)。
この処理を見ると、ただ単純に画像を描画するだけなのに随分と書くことが多いな、などと感じるかもしれない。
そうなっている理由は明快で、ここで使っている「DirectX Graphics」は本来、3Dを描画するためのライブラリであるからだ。実を言うと DirectX には2Dを描画するための「DirectDraw」というライブラリもあり、そちらを使えばより簡単に画像の表示なども出来る(筈。実を言うと使ったこと無いので分かりません)。
しかし、今は3Dを使う必要が無くても、ゲームを作っていくうちに3D的な処理をしたくならないとは限らないので、本講座では少々面倒でも最初から3D用のライブラリで作っていくことにしたい。


さて、以上で本日の講座は終わりであるが、実のところ本日の画像描画、および次回で説明する「UV指定描画」さえ理解してしまえば、2Dのゲームプログラミングの描画処理の大半は出来たも同然だったりする。
そこで、次回からは早速、ゲーム本体を作り始めることとしよう。
次回までの課題はこちら:

  1. 他の画像ファイルを複数、画面上に表示してみよう
  2. これから作ることになるSTGゲームについて想像してみよう

それでは次回の講座まで、ごきげんよう