サイトアイコン GAMEWORKS LAB

テクスチャーを使った描画

今回の記事ではテクスチャーを利用した描画をしていきます。

テクスチャーとは?

テクスチャーとは主に質感を表現するためのものです。
3Dのゲームなどでは光沢や凹凸の表現等の様々な用途で使用させています。

今回のように2Dの環境では基本的には事前に用意した画像を表示する単純なものです。

テクスチャーを使うことで画面に事前に準備した画像を表示できるようになります。
なのでテクスチャーを使うことが出来れば2Dのある程度のゲームは作れてしまいます。

たとえばこんな画像を表示したりとかですね。

 

テクスチャーの生成

DirectX9に同梱されているD3DXの機能を使えば簡単にテクスチャーを生成することができます。

D3DXとはDirectXに同梱されている開発のサポートキットのようなものです。
DirectX11では廃止されていますが、DirectX9の世代ではよく使用されていました。

では早速、テクスチャーの生成をしていきましょう。

とその前にD3DXの機能を使うには下記の対応が必要です。
DirectX9を使う際も同じ記述があったかと思いますが、インクルードとリンクの指定が必要になります。

// D3DXのヘッダーをインクルード
#include <d3dx9.h>
// D3DXの静的ライブラリをリンク
#pragma comment(lib, "d3dx9.lib")


これでD3DXを使う準備が整いました。
では今度こそテクスチャーの生成です。

// テクスチャーデータ
struct TextureData
{
	UINT			width;		// テクスチャーの幅
	UINT			height;		// テクスチャーの高さ
	UINT			srcWidth;	// 画像の幅
	UINT			srcHeight;	// 画像の高さ
	IDirect3DTexture9*	pTexture;	// 生成したテクスチャーのポインタ
};


今回は先ずテクスチャーを管理する構造体を準備しています。
内容はそれぞれコメントとして書いている通りですが、高さと幅が2つあるのは不思議だと感じる人が多いと思います。

これはテクスチャーの幅と高さが2の累乗でなければ対応していないデバイスがあることが主な理由です。
最近のデバイスは殆ど無いと思いますが、それでもハードウェアからの参照効率等から今でも2の累乗にした方がよいとされています。

今回は2の累乗になる画像を用意していますが、それ以外の場合でも正しいサイズに画像を表示するためにテクスチャーと画像の両方の高さと幅を保持しています。

// テクスチャーの読み込み
bool LoadTexture(const WCHAR* pFileName, TextureData* pOut)
{
	if( pOut == NULL ) return false;
	IDirect3DTexture9* pTexture;
	D3DXIMAGE_INFO info;
	// ファイルからテクスチャーを生成する
	HRESULT hr = D3DXCreateTextureFromFileEx(
		GetDirect3DDevice(),	// Direct3DDevice
		pFileName,		// ファイル名
		D3DX_DEFAULT,		// 横幅(D3DX_DEFAULTでファイルから判定)
		D3DX_DEFAULT,		// 高さ(D3DX_DEFAULTでファイルから判定)
		1,			// ミップマップの数
		0,			// 使用用途
		D3DFMT_A8R8G8B8,	// フォーマット
		D3DPOOL_MANAGED,	// メモリの管理設定
		D3DX_FILTER_NONE,	// フィルター設定
		D3DX_DEFAULT,		// ミップマップフィルターの設定
		0x00000000,		// カラーキー
		&info,			// 画像情報
		NULL,			// パレットデータ
		&pTexture);		// 生成したテクスチャーの格納先
	if( FAILED(hr) )
	{
		return false;
	}
	D3DSURFACE_DESC desc;
	// 生成したテクスチャーの情報を取得
	pTexture->GetLevelDesc(0, &desc);
	// テクスチャー情報の設定
	pOut->pTexture	= pTexture;
	pOut->srcWidth	= info.Width;
	pOut->srcHeight	= info.Height;
	pOut->width	= desc.Width;
	pOut->height	= desc.Height;

	return true;
}
// テクスチャーの解放
void ReleaseTexture(TextureData* pTextureData)
{
	if( pTextureData == NULL ) return;

	SAFE_RELEASE(pTextureData->pTexture);
}


テクスチャーの生成と解放はたったこれだけです。
本来であればテクスチャーのフォーマット解析や必要メモリサイズの計算なども必要なのですが、D3DXが全て対応してくれています。
又、画像フォーマットにも複数対応していてbmp, jpg, png, tga, dds等が対応しています。

なのでD3DXCreateTextureFromFileExを呼び出すだけで基本は終わりです。
今回は実際に使う時のためにテクスチャーの幅や高さも取得しています。

D3DXCreateTextureFromFileExの引数をいくつか簡単に説明しておきます。

第二引数で指定されたファイルをHDDから読み込みテクスチャーとして生成してくれます。
第三引数と第四引数は幅と高さですが、D3DX_DEFAULTを指定した場合は2の累乗になるように調整した値になります。
第七引数は今回はRGBAを持ったフルカラーのテクスチャーを作るためにD3DFMT_A8R8G8B8を指定していますが、他のフォーマットのテクスチャーを作る際にはそれぞれ指定する必要があります。
第十一引数のカラーキーは透明にしたいカラーがあれば指定すると0xFF******と入れると指定されたカラーが透明になります。
第十二引数は読み込んだ画像の情報が入ります。必要のない場合はNULLで問題ありません。
第十四引数は生成したテクスチャーオブジェクトが返ってきます。

D3DSURFACE_DESC desc;
// 生成したテクスチャーの情報を取得
pTexture->GetLevelDesc(0, &desc);
// テクスチャー情報の設定
pOut->pTexture	= pTexture;
pOut->srcWidth	= info.Width;
pOut->srcHeight	= info.Height;
pOut->width	= desc.Width;
pOut->height	= desc.Height;


最後に生成したテクスチャーの情報を設定しています。
内容は最初に記述したTextureDataの説明通りです。

今回はテクスチャーをファイルから生成する方法を紹介しましたが、空のテクスチャーやメモリ上からテクスチャーを生成する方法等もあります。

テクスチャ―を使った描画

ついにテクスチャーを使って画像を描画していきます。

今回はサンプル画像にこちらを用意しました。

bridge.png

先ずはテクスチャーを張るためのUV値を頂点に追加します。

#define FVF_CUSTOM2D    (D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1)

struct SimpleVertex
{
	float x, y, z;
	float rhw;
	DWORD color;
	float u, v;
};


D3DFVF_TEX1はテクスチャーを1枚使いますという定義です。
uとvはそれぞれテクスチャーのどの部分を表示するかという指定に使います。

0.0f~1.0fの間で範囲を表現します。
例えば横幅が128pixelのテクスチャーの64pixel目を指す場合は0.5fとなります。

次に生成処理です。

TextureData g_texData;
IDirect3DVertexBuffer9* g_pVertexBuffer;
//	リソースの初期化
bool InitializeResource(void)
{
	// assetsフォルダ内のbridge.pngをテクスチャーとして読み込み
	if( !LoadTexture(TEXT("assets/bridge.png"), &g_texData) )
	{
		return false;
	}
	// 画像サイズから幅と高さを設定
	float rect[] =
	{
		10.0f, 10.0f,
		10.0f + (float)g_texData.srcWidth, 10.0f + (float)g_texData.srcHeight,
	};
	// テクスチャーサイズから画像サイズのUVを取得(画像が2の累乗であれば1.0fになる)
	float u = (float)g_texData.srcWidth / (float)g_texData.width;
	float v = (float)g_texData.srcHeight / (float)g_texData.height;
	// 画像サイズに合わせてUVも追加した頂点データ
	const SimpleVertex vertices[4] =
	{
		// 1つ目の矩形
		{ rect[0], rect[1], 0.0f, 1.0f, 0xffffffff, 0.0f, 0.0f },
		{ rect[2], rect[1], 0.0f, 1.0f, 0xffffffff,    u, 0.0f },
		{ rect[0], rect[3], 0.0f, 1.0f, 0xffffffff, 0.0f,    v },
		{ rect[2], rect[3], 0.0f, 1.0f, 0xffffffff,    u,    v },
	};
	// 頂点バッファの生成
	g_pVertexBuffer = CreateVertexBuffer(vertices, sizeof(vertices));
	if( g_pVertexBuffer == NULL )
	{
		return false;
	}
	return true;
}
//	リソースの解放
void CleanupResource(void)
{
	ReleaseTexture(&g_texData);
	SAFE_RELEASE(g_pVertexBuffer);
}


今回はソースコードのあるフォルダの直下にassetフォルダを作成してその中にbridge.pngを入れました。
下記の処理でフォルダ階層を指定して画像の読み込みとテクスチャーの生成を行っています。

LoadTexture(TEXT("assets/bridge.png"), &g_texData)


次に画像とテクスチャーのサイズから頂点データの値を設定して頂点バッファを作成しています。

今回は単純な矩形一つなのでインデックスバッファは使いません。
そして毎回ですが作った後はちゃんと解放しておきましょう。

次に描画処理です。

void Render(void)
{
	IDirect3DDevice9* pDevice = GetDirect3DDevice();
	// 描画シーンの開始
	pDevice->BeginScene();
	// バックバッファのクリア
	pDevice->Clear(0, NULL, D3DCLEAR_TARGET, 0xff5f5f5f, 1.0f, 0);

	// デフォルトの描画ステートを設定
	SetDefaultRenderState();
	// 通常書き込みモードに設定
	SetRenderMode(Multiple, false);

	// テクスチャーの設定
	pDevice->SetTexture(0, g_texData.pTexture);
	// 頂点バッファの設定
	pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(SimpleVertex));

	// 頂点フォーマットの指定
	pDevice->SetFVF(FVF_CUSTOM2D);
	// 描画
	pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

	// 描画シーンの終了
	pDevice->EndScene();

	// モニターに絵を表示する
	pDevice->Present(NULL, NULL, NULL, NULL);
}


基本の流れは同じですがいくつか追加している項目があります。

先ずは聞きなれないかもしれませんが描画ステートの設定です。

// デフォルトの描画ステートを設定
SetDefaultRenderState();
// 通常書き込みモードに設定
SetRenderMode(Multiple, false);


こちらの中身は後述しますが、別途関数を追加しています。
テクスチャ―を使って加算や半透明等の表現をする際は重要な役割があります。

次にテクスチャーの設定です。

// テクスチャーの設定
pDevice->SetTexture(0, g_texData.pTexture);


ここは単純で0番目のステージにテクスチャーをセットしています。
テクスチャーが複数になった場合は0の場所を1, 2と変えていきます。

これで一部飛ばしましたがテクスチャーの描画が出来ます。
実行結果がこれです。

無事にbridge.pngがWindowの上に表示されました。

次のページの準備のために背景の色がグレーに変えています。

描画ステートとは?

前のページでは後述するとだけ書いて流しましたが、描画ステートは描画に置いてかなり重要なものになっています。
ゲームでエフェクト等の演出を作る為には必ず必要になってきます。

// 初期の描画ステートを設定
void SetDefaultRenderState(void)
{
	IDirect3DDevice9* pDevice = GetDirect3DDevice();
	// カリングモード
	pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
	// フィルモード
	pDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
	// Zバッファを有効にするか?
	pDevice->SetRenderState(D3DRS_ZENABLE, true);
	// Zバッファの書き込みを有効にするか?
	pDevice->SetRenderState(D3DRS_ZWRITEENABLE, true);
	// Zバッファの書き込みを有効にするか?
	pDevice->SetRenderState(D3DRS_ZFUNC, D3DCMP_ALWAYS);
	// αブレンドを有効にするか?
	pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
	// αテストを有効にするか?
	pDevice->SetRenderState(D3DRS_ALPHATESTENABLE, true);
	// αテストの判定方法
	pDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_NOTEQUAL);
	// αテストの参照値
	pDevice->SetRenderState(D3DRS_ALPHAREF, 0);

	// 描画設定
	pDevice->SetRenderState(D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA);
	pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

	// カラーステージ設定
	pDevice->SetTextureStageState(0, D3DTSS_COLOROP,   D3DTOP_MODULATE);
	pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
	pDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_CURRENT);
	// αステージ設定
	pDevice->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_MODULATE);
	pDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
	pDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_CURRENT);
	// テクスチャーフィルターの設定
	pDevice->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_NONE);
	pDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
	pDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
	// テクスチャーのアドレッシングモードの設定
	pDevice->SetSamplerState(0, D3DSAMP_ADDRESSU,  D3DTADDRESS_WRAP);
	pDevice->SetSamplerState(0, D3DSAMP_ADDRESSV,  D3DTADDRESS_WRAP);
}


DirectX9における描画ステートの設定はSetRenderState関数で行います。
第一引数で何の設定を行うかを指定します。
第二引数で指定された項目の設定を行います。

今回初期設定として設定している項目は以下の通りです。

・ D3DRS_CULLMODE

カリングモードの設定です。
この設定ではポリゴンのどちらの面を描画するかの設定です。

D3DCULL_CCWで背面を左回りでカリングするように設定しています。
他にも右回りでカリングする設定とカリングせずに両面を描画する設定があります。

・D3DRS_FILLMODE

フィルモードの設定です。
ポリゴンを描画する際の塗り方を設定です。

D3DFILL_SOLIDは面を全て塗りつぶす設定です。
他にもワイヤーフレームや点を塗りつぶす設定があります。

・D3DRS_ZENABLE

Zバッファを有効にするかを設定できます。

TRUEかFALSEで設定します。

・D3DRS_ZWRITEENABLE

Zバッファの書き込みを有効にするかを設定できます。

TRUEかFALSEで設定します。

・D3DRS_ALPHABLENDENABLE

αブレンドを有効にするか設定できます。

TRUEかFALSEで設定します。

・D3DRS_ALPHATESTENABLE

αテストを有効にするか設定できます。

TRUEかFALSEで設定します。

・D3DRS_ALPHAFUNC

αテストの判定方法の設定です。

D3DCMP_NOTEQUALは次項のαテストの参照値と!=ならテストに通るようになっています。
他にも比較演算子が一通り用意されています。

・D3DRS_ALPHAREF

αテストの参照値の設定です。

数値で0-255を(0~1.0f)として判定されます。
今回は0なので透明な箇所は描画されないようになっています。

・D3DRS_SRCBLEND

出力するカラーのブレンド方法を指定できます。

D3DBLEND_SRCALPHAは出力α値を掛け合わせるようにしています。
式:SrcColor * α

・D3DRS_DESTBLEND

書き込み先のカラーに対するブレンド方法を指定できます。

D3DBLEND_INVSRCALPHAは1 – 出力αの値を掛け合わせるようにしていしています。
式:DestColor * (1 – α)


次にテクスチャ―ステージの設定をSetTextureStageStateで行っています。
テクスチャ―ステージの設定とは設定したテクスチャーをどのように合成するかの設定です。
第一引数の数値はテクスチャーを設定する際に指定するステージの番号です。

・D3DTSS_COLOROP

テクスチャ―のカラーステージの合成方法を指定できます。

D3DTOP_MODULATEは乗算を意味しています。
次に出てくるD3DTSS_COLORARG1とD3DTSS_COLORARG2に対して処理されます。

・D3DTSS_COLORARG1

カラーの一つ目の引数です。

D3DTA_TEXTUREではテクスチャーのカラーが該当します。

。D3DTSS_COLORARG2

カラーの二つ目の引数です。

D3DTA_CURRENTは1つ前のステージの値を意味しています。
0ステージ目に設定された場合はD3DTA_DIFFUSEと同じ意味となり頂点カラーの値が使用されます。
頂点カラーが無い場合は白(0xFFFFFFFF)として扱われます。

・D3DTSS_ALPHAOP
・D3DTSS_ALPHAARG1
・D3DTSS_ALPHAARG2

こちらの3項目はαステージの設定です。
内容としては基本的にカラーステージと同じになります。


最後にテクスチャーのサンプラーステートを設定しています。
こちらは3Dでミップマップ辺りを使う時に説明しようかと思うので今回は割愛させてもらいます。

描画ステートの初期設定が完了したら次は描画モードの設定を行います。

// 通常書き込みモードに設定
SetRenderMode(Multiple, false);


ここではテクスチャーを画面にどう合成方法をするかを設定しています。
前のページで簡単に説明したブレンドの設定を使って複数の描画モードを用意しています。

第二引数はαを有効にするかのフラグを設定できます。
有効にするとテクスチャーのα値を使って半透明などの強弱を付けた描画が可能です。

今回用意した描画モードは下記の4種類です。

// 描画モード
enum ERenderMode
{
	Normal,		// 書き込み
	Add,		// 加算
	Subtract,	// 減算
	Multiple,	// 乗算
};

Normal

単純な上書きです。
描画すると指定した範囲にそのまま書き込みます。

Add

加算合成です。
背景の色に対して描画した絵の色を足して描画します。

Subtract

減算合成です。
背景の色に対して描画した絵の色を引いて描画します。

Multiple

乗算合成です。
背景の色に対して描画した絵の色を掛け合わせて描画します。


ではそれぞれどういった設定になっているのか関数の中を見ていきましょう。

// 描画ステートの設定
void SetRenderMode(ERenderMode mode, bool enableAlpha)
{
	IDirect3DDevice9* pDevice = GetDirect3DDevice();
	// αが無効な場合は入力されたカラーをそのまま使う
	DWORD srcColorblend = D3DBLEND_ONE;
	DWORD dstColorblend = D3DBLEND_ZERO;
	if( enableAlpha )
	{
		// αが有効な場合はα値をカラーに影響させる
		srcColorblend = D3DBLEND_SRCALPHA;
		dstColorblend = D3DBLEND_INVSRCALPHA;
		// αテストでα値が0の場合は描画をしないように設定
		pDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_NOTEQUAL);
		pDevice->SetRenderState(D3DRS_ALPHAREF, 0);
	}
	// αブレンドとαテストの有無を設定
	pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
	pDevice->SetRenderState(D3DRS_ALPHATESTENABLE, enableAlpha);

	switch (mode)
	{
	case Normal:
		// αが無効:(SrcColor * 1) + (DestColor * 0)
		// αが有効:(SrcColor * α) + (DestColor * (1 - α))
		pDevice->SetRenderState(D3DRS_BLENDOP,   D3DBLENDOP_ADD);
		pDevice->SetRenderState(D3DRS_SRCBLEND,  srcColorblend);
		pDevice->SetRenderState(D3DRS_DESTBLEND, dstColorblend);
		break;
	case Add:
		// αが無効:(SrcColor * 1) + (DestColor * 1)
		// αが有効:(SrcColor * α) + (DestColor * 1)
		pDevice->SetRenderState(D3DRS_BLENDOP,   D3DBLENDOP_ADD);
		pDevice->SetRenderState(D3DRS_SRCBLEND,  srcColorblend);
		pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
		break;
	case Subtract:
		// αが無効:(DestColor * 1) - (SrcColor * 1)
		// αが有効:(DestColor * 1) - (SrcColor * α)
		pDevice->SetRenderState(D3DRS_BLENDOP,   D3DBLENDOP_REVSUBTRACT);
		pDevice->SetRenderState(D3DRS_SRCBLEND,  srcColorblend);
		pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
		break;
	case Multiple:
		// 共通:(SrcColor * 0) + (DestColor * SrcColor)
		pDevice->SetRenderState(D3DRS_BLENDOP,   D3DBLENDOP_ADD);
		pDevice->SetRenderState(D3DRS_SRCBLEND,  D3DBLEND_ZERO);
		pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);
		break;
	}
}


一通りの簡単な説明はコメントに記載しています。
それぞれ描画した際の色の扱い方が少しずつ異なっています。

ただ言葉だけではどんな見た目になるのか想像できないかと思います。
なので今回はそれぞれの実行結果を用意しました。
※今回の画像にはαが入っていないので全てαは無効にしてあります。

Normal

今回既に紹介していた描画結果なので見た目は画像のそのままです。

Add

加算合成を行っているので背景のグレーの分だけ少し明るくなっています。
加算合成は何度も重ねるとどんどん白くなっていきます。

Subtract

減算合成なので背景のグレーから画像の色だけ反転したような色になっています。
今回の描画だと正確には違うのですが、見た目としてはネガポジですね。

Multiple

乗算合成をすると背景のグレーの色以上の明るさは出ないので暗い色になっています。
色は0~1.0の間で制御されているので背景の色より明るくなることはありません。

 

このように描画ステートを変更するだけで同じ画像でも見た目を変えることができます。
ゲームではこの設定に対して様々な工夫をしながら演出に組み込んでいます。

ゲームの描画を行う上では基本部分になるので、知らなければ奇麗なエフェクト等は作れません。
最初は難しく感じるかもしれませんが、実際に色々と設定をいじってどんな見た目になるのか触りながら覚えるのが分かりやすいかと思うので、今回のプログラムを作成したら色々と遊んでみてください。

今回の記事の実装まで行ったサンプルプロジェクトを下記にアップしてあります。
プロジェクトファイル

モバイルバージョンを終了