座標と行列


平行移動

さて、今回からいよいよ3次元プログラムの醍醐味である座標と行列について説明します
これを理解しなければ、3次元プログラミングは始まりません

3次元プログラムには、何度も言うように2次元プログラムに用いられる
X 座標、Y 座標に加え Z 座標という物体の深さを表す深度が加えられます
X 座標と Y 座標の考え方は2次元と同様ですが
Z 座標は基本的に左手座標右手座標と呼ばれる2種類に分けられます

左手座標系右手座標系

見てわかるように、左手座標系は奥に向かって数値が上昇するのに対し
右手座標系は手前に向かって値が上昇するという座標系になっています
両方とも、X 軸と Y 軸については2次元プログラムの常識が通用します

DirectX はこのうち左手座標系を採用しています
他の座標系のデータを DirectX Graphics で使う場合は、適切に変換する必要があります

3次元のグラフィックスは、実際の描画の演算は DirectX Graphics が行ってくれます
3次元のグラフィックスといっても、実際に表示する時は単純な2次元です
ディスプレイやプリンタの用紙は、誰がどう見ても2次元であることがわかりますね

この時、3次元の情報を2次元に変換することをジオメトリ変換と呼びます
ジオメトリとは「幾何学」のことで、場合によってはハードウェアレベルで変換を行います
とくに、並行移動、拡大縮小、回転の図形変換をアフィン変換と呼びます
これらは3次元グラフィックスの基本用語なので覚えておいてください

これらの変換をどのように行うかという数学的な立証は省略します
これを知りたい場合は、プログラムではなくグラフィックス理論や数学を学習してください

DirectX Graphics で座標の変換を行う場合は4 × 4の行列を使います
プログラム言語から見れば、この行列は m[4][4] で初期化される多次元配列と考えられます

なぜジオメトリ変換に行列を使うのかという疑問が発生すると思いますが
行列を使うことによって、複数の移動、回転といった要素を
行列を乗算することで、組み合わせて変換できる利点があるからです



これが行列と座標の計算方法を表した式です
現在の座標に対し、変換情報を保有した行列の積を求めることによって
新しい座標 X、Y、Z を取得し、これを独自の頂点フォーマットで指定することができます
念のため、上の式の計算方法を示します



これで、行列を用いて座標変換を行う方法は理解できたと思います
行列に指定するべき値は、DirectX Graphics が提供する数学関数を利用します
また、行列は D3DMATRIX 構造体で次のように表現されます
typedef struct _D3DMATRIX {
    union {
        struct {
            float        _11, _12, _13, _14;
            float        _21, _22, _23, _24;
            float        _31, _32, _33, _34;
            float        _41, _42, _43, _44;

        };
        float m[4][4];
    };
} D3DMATRIX;
見てわかるように、4×4 の行列が表現されています
行列には、配列でも単一のメンバとしてもアクセスできるように共用体が使われています

この構造体を用いて行列を表現し、座標との積を求めて目的の位置を取得します
座標変換の目的(移動や回転など)によって、行列の初期化が決定します
例えば、平行移動については次のように行列を初期化します



Tx には平行移動する分の X 座標値、Ty には Y 座標値、Tz には Z 座標値を指定します
ただし、行列の初期化は DirectX Graphics の数学関数で行えるため
自分で初期化する必要はなく、通常は数学関数を用いて変換行列を取得します

数学関数を用いるには D3dx8math.h ヘッダをインクルードし
D3dx8.lib インポートライブラリをリンカに指定する必要があります

平行移動を行うには D3DXMatrixTranslation() 関数を使います
この関数は、平行移動を行うための行列を生成します
D3DXMATRIX* D3DXMatrixTranslation(
	D3DXMATRIX* pOut ,
	FLOAT x , FLOAT y , FLOAT z
);
pOut には D3DXMATRIX 構造体へのポインタを指定します
x、y、z には、それぞれ X 座標、Y 座標、Z 座標のオフセットを指定します
関数は、平行移動された行列を表す D3DXMATRIX 構造体へのポインタを返します
この関数の戻り値は pOut 引数と同じものになります

D3DXMATRIX 構造体は D3DMATRIX 構造体を継承したものです
この構造体は C++ のオブジェクト指向を活用し、カプセル化を実現しています
もちろん、代償に数学関数は C 言語から扱うことはできません
typedef struct D3DXMATRIX : public D3DMATRIX {
public:
    D3DXMATRIX() {};
    D3DXMATRIX( CONST FLOAT * );
    D3DXMATRIX( CONST D3DMATRIX& );
    D3DXMATRIX( FLOAT _11, FLOAT _12, FLOAT _13, FLOAT _14,
                FLOAT _21, FLOAT _22, FLOAT _23, FLOAT _24,
                FLOAT _31, FLOAT _32, FLOAT _33, FLOAT _34,
                FLOAT _41, FLOAT _42, FLOAT _43, FLOAT _44 );


    // access grants
    FLOAT& operator () ( UINT Row, UINT Col );
    FLOAT  operator () ( UINT Row, UINT Col ) const;

    // casting operators
    operator FLOAT* ();
    operator CONST FLOAT* () const;

    // assignment operators
    D3DXMATRIX& operator *= ( CONST D3DXMATRIX& );
    D3DXMATRIX& operator += ( CONST D3DXMATRIX& );
    D3DXMATRIX& operator -= ( CONST D3DXMATRIX& );
    D3DXMATRIX& operator *= ( FLOAT );
    D3DXMATRIX& operator /= ( FLOAT );

    // unary operators
    D3DXMATRIX operator + () const;
    D3DXMATRIX operator - () const;

    // binary operators
    D3DXMATRIX operator * ( CONST D3DXMATRIX& ) const;
    D3DXMATRIX operator + ( CONST D3DXMATRIX& ) const;
    D3DXMATRIX operator - ( CONST D3DXMATRIX& ) const;
    D3DXMATRIX operator * ( FLOAT ) const;
    D3DXMATRIX operator / ( FLOAT ) const;

    friend D3DXMATRIX operator * ( FLOAT, CONST D3DXMATRIX& );

    BOOL operator == ( CONST D3DXMATRIX& ) const;
    BOOL operator != ( CONST D3DXMATRIX& ) const;

} D3DXMATRIX, *LPD3DXMATRIX;
この構造体は、行列同士の演算をカプセル化し、直感的にしてくれます
行列間のメンバを全て乗算するには C 言語では関数などを使う方法が考えられますが
C++ はオブジェクト指向なので、演算子のオーバーロードによって実現しています
この構造体については、行列同士の演算を直接できるというだけ知っていればよいでしょう
#include <windows.h>
#include <d3d8.h>
#include <D3dx8math.h>
#define TITLE 	TEXT("Kitty on your lap")

IDirect3D8 * pDirect3D;
IDirect3DDevice8 * pD3Device;
D3DPRESENT_PARAMETERS d3dpp;

typedef struct {
	float x , y , z , rhw;
	DWORD diff;
} D3DVERTEX;

LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	D3DXMATRIX d3dm;
	static D3DVERTEX pt[3] = {
		{200 , 10 , 1 , 1 , 0xFFFF0000} ,
		{400 , 200 , 1 , 1 , 0xFF0000FF} ,
		{10 , 200 , 1 , 1 , 0xFF0000FF}
	};

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		SetTimer(hWnd , 1 , 100 , NULL);
		return 0;
	case WM_PAINT:
		if (!pD3Device) break;
		pD3Device->Clear(0 , NULL , D3DCLEAR_TARGET ,
			D3DCOLOR_XRGB(0xFF , 0xFF , 0xFF) , 1.0 , 0);
		pD3Device->BeginScene();

		pD3Device->SetVertexShader(D3DFVF_XYZRHW | D3DFVF_DIFFUSE);
		pD3Device->DrawPrimitiveUP(
			D3DPT_TRIANGLELIST, 1 , pt , sizeof (D3DVERTEX)
		);
		pD3Device->EndScene();
		pD3Device->Present(NULL,NULL,NULL,NULL);
		ValidateRect(hWnd , NULL);
		return 0;
	case WM_TIMER:
		D3DXMatrixTranslation(&d3dm , 1.0f , 0 , 0);
		pt[0].x = pt[0].x * d3dm.m[0][0] +
			pt[0].x * d3dm.m[1][0] + pt[0].x * d3dm.m[2][0] + d3dm.m[3][0];
		InvalidateRect(hWnd , NULL , TRUE);
		return 0;
	case WM_SIZE:
		if (!pD3Device) return 0;
		d3dpp.BackBufferWidth = LOWORD(lp);
		d3dpp.BackBufferHeight = HIWORD(lp);
		pD3Device->Reset(&d3dpp);
		return 0;
	}
	return DefWindowProc(hWnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	MSG msg;
	HWND hWnd;
	WNDCLASS winc;
	D3DDISPLAYMODE d3ddm;

	pDirect3D = Direct3DCreate8(D3D_SDK_VERSION);
	pDirect3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT , &d3ddm);

	ZeroMemory(&d3dpp , sizeof (D3DPRESENT_PARAMETERS));
	d3dpp.BackBufferFormat = d3ddm.Format;
	d3dpp.BackBufferCount = 1;
	d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
	d3dpp.Windowed = TRUE;

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra		= winc.cbWndExtra = 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 0;

	hWnd = CreateWindow(
		TEXT("KITTY") , TITLE , WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
		CW_USEDEFAULT , CW_USEDEFAULT , CW_USEDEFAULT , CW_USEDEFAULT ,
		NULL , NULL , hInstance , NULL
	);
	if (!hWnd) return 0;

	pDirect3D->CreateDevice(
		D3DADAPTER_DEFAULT , D3DDEVTYPE_HAL , hWnd ,
		D3DCREATE_SOFTWARE_VERTEXPROCESSING , &d3dpp , &pD3Device
	);

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	pDirect3D->Release();
	pD3Device->Release();
	return msg.wParam;
}


このプログラムは、三角形の上部の頂点が少しずつ右へ平行移動するというものです
ウィンドウ生成時に SetTimer() 関数を使ってタイマーをセットして
WM_TIMER が発行される度に、1ピクセル毎に右に平行移動します

もちろん、この程度のものであれば単純に pt[0].x++ と書いても同じ動作をしますが
それは、2次元的な処理であり、3次元の世界では行列を用いて変換を行います
ただし、Z 座標を含めた回転などの処理はさらに複雑になります


D3DXMatrixTranslation()

D3DXMATRIX* D3DXMatrixTranslation(
	D3DXMATRIX* pOut ,
	FLOAT x , FLOAT y , FLOAT z
);
オフセットを指定し、行列を生成します
この行列はワールド座標系の任意の位置への平行移動に使えます

pOut - D3DXMATRIX 構造体へのポインタを指定します
x - X 座標のオフセットを指定します
y - Y 座標のオフセットを指定します
z - Z 座標のオフセットを指定します

戻り値 - pOut と同じ D3DXMATRIX 構造体へのポインタ



前のページへ戻る次のページへ