クラスまみれのゲームプログラミング入門

21:変換行列によるスプライト移動

 これまではスプライトの表示位置を、ID3DXSprite::Draw()の引数で直接指定していましたが、移動方法は他にもあります。それは幾何変換と呼ばれる方法です(座標軸自体の移動をイメージするとよいでしょう)。幾何変換は単純な移動だけではなく、回転や拡大縮小処理を同時に加えたスプライトを描画するときに便利です。


低価格PC・自作パーツ充実!ツクモネットショップ

スプライト自体を移動座標軸自体を移動

 移動元の座標行列に次の行列を掛け合わせたものが、移動後の座標となります。


[lはx、mはy、nはz方向の移動量]

 おのおのの値の算出方法は以下の通りです。mは4×4の行列、角括弧([])内は行列の列方向(左から右)、行方向(上から下)を意味します。

x' = x * m[0][0] + y * m[0][1] + z * m[0][2] + 1 * m[0][3]
y' = x * m[1][0] + y * m[1][1] + z * m[1][2] + 1 * m[1][3]
z' = x * m[2][0] + y * m[2][1] + z * m[2][2] + 1 * m[2][3]
 上の式に先ほどの行列の値を代入すると、実際に平行移動が行われていることがわかるでしょう。
x' = x * 1 + y * 0 + z * 0 + 1 * l = x + l
y' = x * 0 + y * 1 + z * 0 + 1 * m = y + m
z' = x * 0 + y * 0 + z * 1 + 1 * n = z + n

 しかし、移動計算を行うたびに行列やその計算を意識するには結構つらいものがあります。うれしいことにDirect3Dには、幾何変換に関する行列計算を行うための変数や関数が用意されています。

 Dirext3Dにおける4×4の行列は「D3DXMATRIX」構造体にて宣言することができます。平行移動における値の代入には「D3DXMatrixTranslation」関数をつかいましょう。行列をスプライトに反映させるには「ID3DXSprite::SetTransform()」を入力してください。たとえば、特定のスプライトをx方向に10、y方向に10平行移動するには次のようにします。

D3DXMATRIX mtrx;
D3DXMatrixTranslation(&mtrx, 10.0f, 10.0f, 0.0f);
pSprite->SetTransform(&mtrx);

 以上をふまえて、プログラムの制作に取りかかるわけですが、このままだと計算結果の座標位置がスプライトの左上となります。背景グラフィックであればこれでも不便な点はないのですが、敵キャラクターなど動きのあるグラフィックならば、キャラクターの中心と座標位置が重なるようにすると、あたり判定の計算など、いろいろ都合のよい点が出てきます。

 
[原点が中央にある方が描画結果をイメージしやすい]

 そこで、グラフィックの原点を変更できるようにし、その位置を中心にスプライトを描画できるようにする機能を付け加えることにします。位置はx方向とy方向をOR演算で組み合わせるようにします。

position.h
#define CP_MIDDLECENTER 0	// 中央(デフォルト値)
#define CP_LEFT 1			// x方向左端
#define CP_CENTER 2			// x方向中央
#define CP_RIGHT 4			// x方向右端
#define CP_TOP 8			// y方向上端
#define CP_MIDDLE 16		// y方向中央
#define CP_BOTTOM 32		// y方向下端

 スプライトの中央位置を取得するには、スプライトの元となるテクスチャのサイズがわからないといけません。テクスチャのサイズと原点の取得はテクスチャの読み込みが成功したときに行うことにします。

sprite.h
#pragma once

#include "Texture.h"

#define CP_MIDDLECENTER 0
#define CP_LEFT 1
#define CP_CENTER 2
#define CP_RIGHT 4
#define CP_TOP 8
#define CP_MIDDLE 16
#define CP_BOTTOM 32

CSprite : public CGameObject
{
private:
	CTexture *texture;

	BYTE cpos;	// 基点の位置
	float orig_x, orig_y;	// 原点の位置
	
	int swidth, sheight;	// スプライトのサイズ

	void Reset();

public:
	CSprite();
	CSprite(CTexture *source);

	void SetTexture(CTexture *source);

	void SetCenterPosition(BYTE pos);
	BYTE GetCenterPosition();

	/* 省略 */
};

sprite.cpp
#include "Sprite.h"

CSprite::CSprite()
{
	Reset();
}

CSprite::CSprite(CTexture *source)
{
	Reset();
	SetTexture(source);
}

void CSprite::Reset()
{
	texture = NULL;
	cpos = CP_MIDDLECENTER;
	orig_x = orig_y = 0.0f;
}

void CSprite::SetTexture(CTexture *source)
{
	if(source == NULL) return;
	texture = source;

	texture->GetTextureSize(&swidth, &sheight);
	
	// 原点位置の更新
	SetCenterPosition(cpos);
}

void CSprite::SetCenterPosition(BYTE pos)
{
	// 原点の位置を計算
	cpos = pos;

	if(pos & CP_LEFT){
		orig_x = 0.0f;
	}else if(pos & CP_RIGHT){
		orig_x = (float)swidth;
	}else{
		orig_x = (float)swidth / 2.0f;
	}

	if(pos & CP_TOP){
		orig_y = 0.0f;
	}else if(pos & CP_BOTTOM){
		orig_y = (float)sheight;
	}else{
		orig_y = (float)sheight / 2.0f;
	}
}

BYTE CSprite::GetCenterPosition()
{
	return cpos;
}

 最後にテクスチャサイズや原点を元にした移動を行う処理を記述します。原点が左上以外の場合は、一度座標軸の原点とイメージの原点が一致するように移動させてから、改めて[x, y]方向に移動させる必要があります。

 2回以上の平行移動を行う場合は、既存の行列に、移動する分量を記録した行列を掛け合わせていかなくてはいけません。この「行列に行列を掛け合わせる」計算は「D3DXMatrixMultiply」にて行います。

sprite2.h
CSprite : public CGameObject
{
private:
	/* 省略 */
	
public:
	/* 省略 */

	void Draw(float x, float y, int alpha = 255);
};

sprite2.cpp
void CSprite::Draw(float x, float y, int alpha)
{
	if(texture == NULL){
		DXTRACE_MSG(_T("テクスチャが読み込まれていません"));
		return;
	}

	D3DXMATRIX mtrx1, mtrx2;

	// 原点を重ね合わせる平行移動
	D3DXMatrixTranslation(&mtrx1, -orig_x, -orig_y, 0.0f);

	// 指定の場所へ移動する行列との合成
	D3DXMatrixTranslation(&mtrx2, x, y, 0.0f);
	D3DXMatrixMultiply(&mtrx1, &mtrx1, &mtrx2);


	pSprite->Begin(NULL);

	pSprite->SetTransform(&mtrx1);
	pSprite->Draw(texture->GetTexture(), &drawrect, NULL, NULL, 0x00FFFFFF | ((BYTE)alpha << 24));

	pSprite->End();
}

 これは余談ですが、スプライトを描画する位置によっては補完処理が行われて、ぼやけたイメージになるときがあります。この補完処理が気になるようであれば、以下の2行を追加することで、補完処理をやめさせることが可能です。

noanti.cpp
	//アンチエイリアスの無効
	pD3Ddevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_NONE);
	pD3Ddevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_NONE);


低価格PC・自作パーツ充実!ツクモネットショップ

 それともう一つ注意。一度設定した行列変換はすべてのスプライトに対して有効となります。これはフォント描画も例外ではありませんので、フォント描画処理の関数にも行列変換の処理を加えるようにしましょう。

font.cpp
void CDxFont::Draw
	(LPCTSTR text, int count, LPRECT pRect,
	DWORD Format, D3DXCOLOR Color)
{
	if(font){
		pSprite->Begin(NULL);

		D3DXMATRIX mtrx;
		D3DXMatrixTranslation(&mtrx, 0.0f, 0.0f, 0.0f);
		pSprite->SetTransform(&mtrx);

		font->DrawText(pSprite, text, count, pRect, Format, Color);
		
		pSprite->End();
	}
}