【C++】DirectX9で学ぶ3D描画プログラミング:デバイス初期化とシェーダー活用テクニック
DirectX9はC++でリアルタイム3Dグラフィックスを描く代表的APIです。
デバイス初期化、レンダーステート管理、頂点・インデックスバッファ送信、テクスチャやシェーダー切替まで一貫制御でき、固定機能とプログラマブルパイプラインの両方に対応します。
古いGPUやOSでも動くため互換性が高く、Windowsゲーム制作や描画エンジン開発の土台として今も有用です。
DirectX9デバイスの仕組み
DirectX9で3D描画を行うためには、まずDirect3Dデバイスの初期化が必要です。
デバイスはグラフィックスハードウェアとアプリケーションの橋渡しをする重要な役割を持ちます。
このセクションでは、DirectX9のデバイス生成に関わる基本的な仕組みを解説します。
COMインターフェイス基礎
DirectX9はCOM(Component Object Model)ベースのAPIです。
COMはMicrosoftが開発したコンポーネント技術で、オブジェクト指向のインターフェイスを言語に依存せずに利用できる仕組みです。
Direct3Dの各種オブジェクトはCOMインターフェイスとして提供されており、ポインタを通じて操作します。
COMインターフェイスの特徴は以下の通りです。
- 参照カウント管理
COMオブジェクトはAddRef()
とRelease()
メソッドで参照カウントを管理し、不要になったら自動的にメモリ解放されます。
これによりメモリリークを防ぎやすくなります。
- QueryInterfaceによるインターフェイス取得
あるCOMオブジェクトが複数のインターフェイスを持つ場合、QueryInterface()
を使って目的のインターフェイスを取得します。
Direct3D9の初期化では、まずIDirect3D9
インターフェイスを取得し、そこからIDirect3DDevice9
を生成します。
これらはCOMのルールに従って操作します。
以下はCOMインターフェイスの基本的な使い方の例です。
#include <d3d9.h>
#include <windows.h>
#include <iostream>
int main() {
// Direct3D9オブジェクトの作成
IDirect3D9* pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if (!pD3D) {
std::cerr << "Direct3D9の作成に失敗しました。" << std::endl;
return -1;
}
// 参照カウントの確認
ULONG refCount = pD3D->AddRef();
std::cout << "参照カウント: " << refCount << std::endl;
// Releaseで参照カウントを減らす
refCount = pD3D->Release();
std::cout << "参照カウント(Release後): " << refCount << std::endl;
// 最後にオブジェクトを解放
pD3D->Release();
return 0;
}
このコードでは、Direct3DCreate9
でIDirect3D9
オブジェクトを作成し、AddRef()
とRelease()
で参照カウントを操作しています。
COMのルールに従い、使い終わったら必ずRelease()
を呼び出してリソースを解放してください。
デバイス生成パラメータ
Direct3Dデバイスを生成する際には、D3DPRESENT_PARAMETERS
構造体で様々なパラメータを指定します。
これらのパラメータは描画の動作や画面表示の設定に大きく影響します。
主なメンバーは以下の通りです。
メンバー名 | 説明 |
---|---|
BackBufferWidth | バックバッファの幅(ピクセル単位) |
BackBufferHeight | バックバッファの高さ(ピクセル単位) |
BackBufferFormat | バックバッファのピクセルフォーマット |
BackBufferCount | バックバッファの数(通常は1か2) |
MultiSampleType | マルチサンプリングの種類 |
SwapEffect | バッファのスワップ方法 |
hDeviceWindow | 描画対象のウィンドウハンドル |
Windowed | ウィンドウモードかフルスクリーンかの指定 |
EnableAutoDepthStencil | 深度ステンシルバッファの自動生成 |
AutoDepthStencilFormat | 深度ステンシルバッファのフォーマット |
PresentationInterval | 垂直同期の設定 |
以下はデバイス生成時のパラメータ設定例です。
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.BackBufferWidth = 800; // 画面幅
d3dpp.BackBufferHeight = 600; // 画面高さ
d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8; // 32bitカラー
d3dpp.BackBufferCount = 1; // バックバッファ1枚
d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE; // マルチサンプリングなし
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // スワップ効果
d3dpp.hDeviceWindow = hWnd; // 描画対象ウィンドウ
d3dpp.Windowed = TRUE; // ウィンドウモード
d3dpp.EnableAutoDepthStencil = TRUE; // 深度バッファ自動生成
d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; // 24bit深度+8bitステンシル
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE; // 垂直同期ON
この設定を使ってIDirect3DDevice9
を生成します。
パラメータは用途に応じて調整してください。
フルスクリーンとウィンドウモード
Direct3D9では、描画モードとして「フルスクリーンモード」と「ウィンドウモード」の2種類があります。
どちらを選ぶかで描画の挙動やパフォーマンスに違いが出ます。
- フルスクリーンモード
画面全体をDirect3Dが占有し、解像度やリフレッシュレートを自由に設定できます。
ゲームなどで高いパフォーマンスが求められる場合に適しています。
ただし、Alt+Tabなどでの切り替え時にデバイスロストが発生しやすいので、復旧処理が必要です。
- ウィンドウモード
通常のWindowsアプリケーションのようにウィンドウ内に描画します。
デスクトップの他のウィンドウと共存でき、デバイスロストの問題も少ないです。
開発時やツール系アプリケーションに向いています。
フルスクリーンとウィンドウモードはD3DPRESENT_PARAMETERS
のWindowed
メンバーで切り替えます。
d3dpp.Windowed = FALSE; // フルスクリーンモード
また、フルスクリーン時はBackBufferWidth
やBackBufferHeight
をモニターの解像度に合わせる必要があります。
d3dpp.BackBufferWidth = 1920;
d3dpp.BackBufferHeight = 1080;
ウィンドウモードでは、ウィンドウのクライアント領域のサイズに合わせてバックバッファサイズを設定します。
マルチサンプリング設定
マルチサンプリングはアンチエイリアスの一種で、ジギーなエッジを滑らかにする効果があります。
Direct3D9ではD3DMULTISAMPLE_TYPE
列挙型で指定します。
主な種類は以下の通りです。
定数名 | サンプル数 | 説明 |
---|---|---|
D3DMULTISAMPLE_NONE | 0 | マルチサンプリングなし |
D3DMULTISAMPLE_2_SAMPLES | 2 | 2サンプル |
D3DMULTISAMPLE_4_SAMPLES | 4 | 4サンプル |
D3DMULTISAMPLE_8_SAMPLES | 8 | 8サンプル |
マルチサンプリングを有効にするには、D3DPRESENT_PARAMETERS
のMultiSampleType
に希望の値を設定し、MultiSampleQuality
も適切に設定します。
d3dpp.MultiSampleType = D3DMULTISAMPLE_4_SAMPLES;
d3dpp.MultiSampleQuality = 0; // 通常は0で問題ありません
ただし、マルチサンプリングはハードウェアの対応状況に依存します。
利用可能かどうかはIDirect3D9::CheckDeviceMultiSampleType
で確認してください。
HRESULT hr = pD3D->CheckDeviceMultiSampleType(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
d3dpp.BackBufferFormat,
d3dpp.Windowed,
D3DMULTISAMPLE_4_SAMPLES,
NULL);
if (hr == D3D_OK) {
// 4サンプルのマルチサンプリングが利用可能
} else {
// 利用不可なので別の設定にする
}
マルチサンプリングを有効にすると描画品質が向上しますが、パフォーマンスに影響が出る場合があります。
用途に応じて適切に設定してください。
描画ループの基本構造
メッセージループ統合
Windowsアプリケーションでは、ユーザー入力やシステムイベントを処理するためにメッセージループが必須です。
DirectX9の描画処理はこのメッセージループ内で行うのが一般的です。
メッセージループを適切に統合することで、スムーズな描画と応答性の高いアプリケーションを実現します。
典型的なメッセージループは以下のようになります。
MSG msg = {0};
while (msg.message != WM_QUIT) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// 描画処理をここで行う
RenderFrame();
}
}
このコードでは、PeekMessage
を使ってメッセージがあるかどうかを非同期にチェックし、メッセージがなければRenderFrame()
関数で描画を行います。
PeekMessage
を使うことで、メッセージがない間もCPUを使って描画を継続できるため、フレームレートの維持に役立ちます。
一方、GetMessage
を使うとメッセージが来るまで処理がブロックされるため、描画が止まってしまいます。
ゲームやリアルタイム描画ではPeekMessage
が推奨されます。
RenderFrame()
の中では、Direct3Dデバイスを使ってシーンの描画を行います。
メッセージループと描画処理を分離することで、コードの見通しも良くなります。
フレームバッファクリア
描画ループの最初に行うべき処理の一つがフレームバッファのクリアです。
これにより前フレームの残像を消し、新しいフレームの描画準備を整えます。
Direct3D9ではIDirect3DDevice9::Clear
メソッドを使います。
主に以下のバッファをクリア可能です。
- バックバッファ(色バッファ)
- 深度バッファ(Zバッファ)
- ステンシルバッファ
典型的なクリア処理は以下のようになります。
HRESULT hr = pDevice->Clear(
0, // クリア範囲の矩形(0で全画面)
NULL, // クリア範囲の配列(NULLで全画面)
D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, // クリアするバッファ
D3DCOLOR_XRGB(0, 0, 64), // 背景色(濃い青)
1.0f, // 深度値(最大値)
0 // ステンシル値(使わない場合は0)
);
if (FAILED(hr)) {
// エラーハンドリング
}
この例では、バックバッファと深度バッファをクリアしています。
背景色は濃い青に設定していますが、用途に応じて自由に変更可能です。
クリア処理はBeginScene()
の前に行うのが一般的です。
これにより、描画開始時に画面がリセットされた状態になります。
Present最適設定
描画が完了したら、IDirect3DDevice9::Present
メソッドでバックバッファの内容をフロントバッファに転送し、画面に表示します。
Present
は描画ループの最後に必ず呼び出します。
HRESULT hr = pDevice->Present(NULL, NULL, NULL, NULL);
if (FAILED(hr)) {
// デバイスロストなどの処理
}
Present
の引数は通常すべてNULL
で問題ありませんが、特定の矩形領域だけ更新したい場合や、別のウィンドウに表示したい場合は指定可能です。
パフォーマンスやティアリング防止のために、D3DPRESENT_PARAMETERS
のPresentationInterval
を設定します。
主な設定値は以下の通りです。
定数名 | 説明 |
---|---|
D3DPRESENT_INTERVAL_DEFAULT | デフォルト(ドライバに任せる) |
D3DPRESENT_INTERVAL_ONE | 垂直同期あり(VSync ON) |
D3DPRESENT_INTERVAL_IMMEDIATE | 垂直同期なし(VSync OFF) |
垂直同期を有効にすると画面のティアリング(映像のズレ)を防げますが、フレームレートがモニターのリフレッシュレートに制限されます。
逆に無効にすると最大フレームレートが出せますが、ティアリングが発生しやすくなります。
また、Present
の戻り値がD3DERR_DEVICELOST
の場合は、デバイスがロストしている状態なので、リセット処理を行う必要があります。
これを適切に処理しないと描画が停止してしまいます。
if (hr == D3DERR_DEVICELOST) {
// デバイスロスト時の処理(Resetなど)
}
描画ループの基本は、メッセージ処理を行いながら、フレームバッファをクリアし、描画を行い、最後にPresent
で画面に表示する流れです。
この流れを守ることで安定した描画が可能になります。
行列変換とカメラ制御
3D描画において、オブジェクトの位置や向き、カメラの視点を制御するために行列変換は欠かせません。
DirectX9ではD3DXMATRIX
構造体を使い、ワールド行列、ビュー行列、プロジェクション行列の3つの主要な行列を組み合わせてシーンを描画します。
ビュー行列構築
ビュー行列はカメラの位置と向きを表現し、ワールド空間の座標をカメラ視点の座標系に変換します。
DirectX9ではD3DXMatrixLookAtLH
関数を使って簡単にビュー行列を作成できます。
関数のシグネチャは以下の通りです。
D3DXMATRIX* D3DXMatrixLookAtLH(
D3DXMATRIX *pOut,
const D3DXVECTOR3 *pEye,
const D3DXVECTOR3 *pAt,
const D3DXVECTOR3 *pUp
);
pEye
:カメラの位置(視点)pAt
:カメラが注視する点(注視点)pUp
:カメラの上方向ベクトル(通常は(0,1,0))
以下はビュー行列を作成する例です。
#include <d3dx9.h>
D3DXMATRIX viewMatrix;
D3DXVECTOR3 eye(0.0f, 5.0f, -10.0f); // カメラ位置
D3DXVECTOR3 at(0.0f, 0.0f, 0.0f); // 注視点
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f); // 上方向
D3DXMatrixLookAtLH(&viewMatrix, &eye, &at, &up);
pDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
このコードでは、カメラがワールド座標の原点を見つめ、少し上方と後方に位置しています。
SetTransform
でビュー行列をデバイスにセットすることで、以降の描画はこの視点で行われます。
プロジェクション行列設定
プロジェクション行列は3D空間の座標を2Dスクリーンに投影するための行列です。
視野角やアスペクト比、近クリップ面・遠クリップ面の距離を指定して作成します。
DirectX9ではD3DXMatrixPerspectiveFovLH
関数を使います。
D3DXMATRIX* D3DXMatrixPerspectiveFovLH(
D3DXMATRIX *pOut,
FLOAT fovy,
FLOAT Aspect,
FLOAT zn,
FLOAT zf
);
fovy
:垂直方向の視野角(ラジアン)Aspect
:アスペクト比(幅÷高さ)zn
:近クリップ面の距離zf
:遠クリップ面の距離
例として、視野角60度、アスペクト比4:3、近クリップ0.1、遠クリップ1000のプロジェクション行列を作成します。
D3DXMATRIX projMatrix;
float fovY = D3DXToRadian(60.0f);
float aspectRatio = 4.0f / 3.0f;
float nearZ = 0.1f;
float farZ = 1000.0f;
D3DXMatrixPerspectiveFovLH(&projMatrix, fovY, aspectRatio, nearZ, farZ);
pDevice->SetTransform(D3DTS_PROJECTION, &projMatrix);
この設定により、カメラの視野が自然な遠近感を持ってスクリーンに投影されます。
ワールド行列階層管理
ワールド行列はオブジェクトの位置、回転、スケールを表現し、ローカル座標系からワールド座標系への変換を行います。
複数のオブジェクトを階層的に管理する場合、親子関係に基づいてワールド行列を合成します。
例えば、親オブジェクトのワールド行列をparentWorld
、子オブジェクトのローカル変換行列をchildLocal
とすると、子のワールド行列は以下のように計算します。
D3DXMATRIX childWorld = childLocal * parentWorld;
DirectXの行列は左から右への乗算で変換が適用されるため、親の変換を後に掛ける形になります。
以下は単純な回転と平行移動を組み合わせたワールド行列の例です。
D3DXMATRIX matScale, matRot, matTrans, worldMatrix;
// スケール行列(1.5倍)
D3DXMatrixScaling(&matScale, 1.5f, 1.5f, 1.5f);
// Y軸回転行列(45度)
D3DXMatrixRotationY(&matRot, D3DXToRadian(45.0f));
// 平行移動行列(X=3, Y=0, Z=5)
D3DXMatrixTranslation(&matTrans, 3.0f, 0.0f, 5.0f);
// ワールド行列を合成(スケール→回転→平行移動)
worldMatrix = matScale * matRot * matTrans;
pDevice->SetTransform(D3DTS_WORLD, &worldMatrix);
このようにワールド行列を階層的に管理することで、複雑なシーンのオブジェクト配置やアニメーションが可能になります。
視野角とクリッピング平面
視野角(Field of View, FOV)はカメラの見える範囲の広さを決める重要なパラメータです。
一般的に垂直方向の視野角を指定し、広すぎると遠近感が強調されすぎて歪みが生じ、狭すぎると視野が限定されてしまいます。
DirectX9のD3DXMatrixPerspectiveFovLH
で指定する視野角はラジアン単位で、通常は30度から90度の範囲で設定します。
クリッピング平面はカメラからの距離で、描画される範囲を制限します。
近クリップ面(near plane)はカメラに近すぎるオブジェクトを描画しないようにし、遠クリップ面(far plane)は遠すぎるオブジェクトを描画しないようにします。
近クリップ面の値は0.1など小さく設定しますが、あまり小さすぎると深度バッファの精度が落ちてZファイティング(ちらつき)が発生しやすくなります。
遠クリップ面はシーンの最大描画距離に合わせて設定します。
視野角とクリッピング平面の関係は以下のようにまとめられます。
パラメータ | 影響内容 |
---|---|
視野角(FOV) | 視界の広さ。大きいほど広範囲を表示。 |
近クリップ面 | カメラに近い描画開始距離。小さすぎ注意。 |
遠クリップ面 | 描画終了距離。大きすぎると深度精度低下。 |
適切な値を設定することで、自然な遠近感と安定した描画が実現できます。
頂点データ管理
3D描画の基盤となる頂点データは、DirectX9において効率的に管理・転送することが重要です。
頂点フォーマットの定義からバッファの生成、インデックスバッファの活用、ストリームソースの設定まで、基本的な頂点データ管理の手法を詳しく解説します。
頂点フォーマット定義
頂点フォーマットは、頂点データの構造をDirect3Dに伝えるための情報です。
頂点には位置情報だけでなく、法線、テクスチャ座標、色など様々な属性が含まれます。
DirectX9ではD3DVERTEXELEMENT9
構造体を使ってカスタム頂点フォーマットを定義するか、D3DFVF
(Flexible Vertex Format)を使って簡易的に指定します。
FVFを使った頂点フォーマット例
struct CUSTOMVERTEX {
FLOAT x, y, z; // 位置
DWORD color; // 頂点カラー
FLOAT tu, tv; // テクスチャ座標
};
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)
この例では、位置(XYZ)、頂点カラー(DIFFUSE)、テクスチャ座標1セット(TEX1)を持つ頂点フォーマットを定義しています。
D3DFVF_CUSTOMVERTEX
を使ってデバイスにフォーマットを設定します。
頂点宣言を使ったカスタムフォーマット
より柔軟なフォーマットが必要な場合は、IDirect3DDevice9::CreateVertexDeclaration
でD3DVERTEXELEMENT9
配列を渡して頂点宣言を作成します。
D3DVERTEXELEMENT9 vertexElements[] = {
{0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{0, 12, D3DDECLTYPE_D3DCOLOR,D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0},
{0, 16, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};
この配列は頂点の各要素の型、オフセット、用途を指定しています。
CreateVertexDeclaration
で頂点宣言オブジェクトを作成し、SetVertexDeclaration
でデバイスにセットします。
頂点バッファ生成とロック
頂点データはIDirect3DVertexBuffer9
オブジェクトに格納します。
頂点バッファはGPUに効率的に転送され、描画時に使用されます。
頂点バッファの生成
IDirect3DVertexBuffer9* pVertexBuffer = nullptr;
UINT vertexCount = 3; // 三角形の頂点数
UINT vertexSize = sizeof(CUSTOMVERTEX);
HRESULT hr = pDevice->CreateVertexBuffer(
vertexCount * vertexSize,
0,
D3DFVF_CUSTOMVERTEX,
D3DPOOL_MANAGED,
&pVertexBuffer,
NULL
);
if (FAILED(hr)) {
// エラーハンドリング
}
CreateVertexBuffer
の引数は、バッファサイズ、使用フラグ、頂点フォーマット、メモリプール、バッファのポインタ、予約用NULLです。
D3DPOOL_MANAGED
はDirect3Dがメモリ管理を行うため便利です。
頂点バッファのロックとデータ書き込み
頂点バッファにデータを書き込むには、Lock
でメモリを取得し、書き込み後にUnlock
します。
CUSTOMVERTEX* pVertices = nullptr;
hr = pVertexBuffer->Lock(0, 0, (void**)&pVertices, 0);
if (SUCCEEDED(hr)) {
// 頂点データの設定(例:三角形)
pVertices[0] = {0.0f, 1.0f, 0.0f, 0xFFFFFFFF, 0.5f, 0.0f};
pVertices[1] = {1.0f, -1.0f, 0.0f, 0xFFFFFFFF, 1.0f, 1.0f};
pVertices[2] = {-1.0f, -1.0f, 0.0f, 0xFFFFFFFF, 0.0f, 1.0f};
pVertexBuffer->Unlock();
}
この例では、三角形の3頂点を設定しています。
Lock
の第2引数を0にするとバッファ全体をロックします。
インデックスバッファ活用
インデックスバッファは頂点の描画順序を指定し、頂点の再利用を可能にするために使います。
これによりメモリ使用量と描画コール数を削減できます。
インデックスバッファの生成
IDirect3DIndexBuffer9* pIndexBuffer = nullptr;
UINT indexCount = 3; // 三角形のインデックス数
hr = pDevice->CreateIndexBuffer(
indexCount * sizeof(WORD),
0,
D3DFMT_INDEX16,
D3DPOOL_MANAGED,
&pIndexBuffer,
NULL
);
if (FAILED(hr)) {
// エラーハンドリング
}
16ビットインデックスD3DFMT_INDEX16
を使う例です。
32ビットインデックスを使う場合はD3DFMT_INDEX32
を指定します。
インデックスバッファのロックと設定
WORD* pIndices = nullptr;
hr = pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);
if (SUCCEEDED(hr)) {
pIndices[0] = 0;
pIndices[1] = 1;
pIndices[2] = 2;
pIndexBuffer->Unlock();
}
この例では、三角形の頂点インデックスを指定しています。
描画時のバッファ設定と描画呼び出し
pDevice->SetStreamSource(0, pVertexBuffer, 0, vertexSize);
pDevice->SetIndices(pIndexBuffer);
pDevice->SetFVF(D3DFVF_CUSTOMVERTEX);
pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, vertexCount, 0, 1);
DrawIndexedPrimitive
でインデックスバッファを使った描画を行います。
これにより頂点の重複を避け効率的に描画できます。
ストリームソースと宣言
Direct3D9では複数の頂点ストリームを使って頂点データを管理できます。
ストリームは複数の頂点バッファを同時にバインドし、頂点宣言で各ストリームのフォーマットを指定します。
ストリームソースの設定
pDevice->SetStreamSource(0, pVertexBuffer, 0, vertexSize);
ここではストリーム0に頂点バッファをバインドしています。
複数ストリームを使う場合はストリーム番号を変えて複数回呼び出します。
頂点宣言の設定
カスタム頂点宣言を使う場合は、IDirect3DVertexDeclaration9
オブジェクトを作成し、以下のようにセットします。
pDevice->SetVertexDeclaration(pVertexDeclaration);
頂点宣言は頂点フォーマットの詳細をDirect3Dに伝え、複数ストリームの属性を正しく解釈させるために必要です。
これらの頂点データ管理の基本を押さえることで、DirectX9で効率的かつ柔軟な3D描画が可能になります。
特にインデックスバッファの活用はパフォーマンス向上に直結するため、積極的に利用しましょう。
固定機能パイプライン
DirectX9の固定機能パイプラインは、シェーダーを使わずに3D描画の基本的な処理を行う仕組みです。
マテリアルやライティング、テクスチャステージの設定、フォグやカリング、アルファブレンドなどの機能を簡単に利用できます。
マテリアルとライティング
マテリアルはオブジェクトの表面特性を定義し、ライティングは光源からの光の当たり方を計算してリアルな見た目を作ります。
固定機能パイプラインではD3DMATERIAL9
構造体でマテリアルを設定し、IDirect3DDevice9::SetMaterial
でデバイスに適用します。
D3DMATERIAL9 material = {};
material.Diffuse = D3DXCOLOR(1.0f, 0.5f, 0.5f, 1.0f); // 拡散反射色(赤み)
material.Ambient = D3DXCOLOR(0.2f, 0.2f, 0.2f, 1.0f); // 環境光色
material.Specular = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); // 鏡面反射色
material.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f); // 自己発光色
material.Power = 20.0f; // 鏡面反射の鋭さ
pDevice->SetMaterial(&material);
ライティングはD3DLIGHT9
構造体で光源を定義し、SetLight
とLightEnable
で有効化します。
以下はディレクショナルライトの例です。
D3DLIGHT9 light = {};
light.Type = D3DLIGHT_DIRECTIONAL;
light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
light.Direction = D3DXVECTOR3(0.0f, -1.0f, 0.0f); // 下向き
pDevice->SetLight(0, &light);
pDevice->LightEnable(0, TRUE);
pDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
これにより、オブジェクトに光源の影響が反映され、立体感のある描画が可能になります。
テクスチャステージ設定
固定機能パイプラインでは、複数のテクスチャを段階的に合成する「テクスチャステージ」を設定できます。
SetTexture
でテクスチャをバインドし、SetTextureStageState
で合成方法を指定します。
以下は1段階目のテクスチャを単純に描画する例です。
pDevice->SetTexture(0, pTexture);
pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
pDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
この設定では、テクスチャの色と頂点カラーを乗算(MODULATE)して描画します。
複数ステージを使う場合は、ステージ番号を増やしてそれぞれの合成方法を設定します。
フォグとカリング
フォグは遠くのオブジェクトを霧のようにぼかして遠近感を強調する効果です。
SetRenderState
でフォグの種類や色、開始距離・終了距離を設定します。
pDevice->SetRenderState(D3DRS_FOGENABLE, TRUE);
pDevice->SetRenderState(D3DRS_FOGCOLOR, D3DCOLOR_XRGB(128, 128, 128));
pDevice->SetRenderState(D3DRS_FOGSTART, *(DWORD*)&(float){10.0f});
pDevice->SetRenderState(D3DRS_FOGEND, *(DWORD*)&(float){50.0f});
pDevice->SetRenderState(D3DRS_FOGTABLEMODE, D3DFOG_LINEAR);
この例では、10単位から50単位の距離で線形フォグがかかり、色はグレーです。
カリングは裏面(通常はカメラから見えない面)を描画しないことで描画負荷を減らす機能です。
D3DRS_CULLMODE
で設定します。
pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); // 反時計回りの面をカリング
通常、頂点の順序が時計回り(CW)か反時計回り(CCW)かで表裏を判定します。
適切に設定することで不要な面の描画を省けます。
アルファブレンドとテスト
アルファブレンドは透明度を扱い、半透明オブジェクトの描画に使います。
SetRenderState
でブレンドの有効化やブレンド方法を指定します。
pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
この設定は、ソースのアルファ値に応じて透過合成を行います。
描画順序に注意が必要で、通常は遠いオブジェクトから近いオブジェクトへ描画します。
アルファテストは、ピクセルのアルファ値に基づいて描画の可否を判定し、不透明部分だけを描画するのに使います。
pDevice->SetRenderState(D3DRS_ALPHATESTENABLE, TRUE);
pDevice->SetRenderState(D3DRS_ALPHAREF, 0x08); // しきい値
pDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL);
この例では、アルファ値が0x08(約3%)以上のピクセルだけ描画されます。
これにより、透明部分の描画を省略してパフォーマンス向上が期待できます。
固定機能パイプラインのこれらの機能を活用することで、シェーダーを使わずとも基本的な3D描画表現が可能です。
特にマテリアルとライティング、テクスチャステージの組み合わせは多彩な表現を実現します。
プログラマブルシェーダ
DirectX9では固定機能パイプラインに加え、HLSL(High Level Shader Language)を用いたプログラマブルシェーダが利用可能です。
これにより、頂点やピクセルの処理を柔軟にカスタマイズできます。
HLSL基本構文
HLSLはC言語に似た構文を持つシェーダ言語で、頂点シェーダやピクセルシェーダのプログラムを記述します。
基本的な構造は関数定義と変数宣言で構成されます。
以下は簡単な頂点シェーダの例です。
float4x4 WorldViewProj; // ワールドビュー射影行列
struct VS_INPUT {
float4 Pos : POSITION;
float4 Color : COLOR0;
};
struct VS_OUTPUT {
float4 Pos : POSITION;
float4 Color : COLOR0;
};
VS_OUTPUT main(VS_INPUT input) {
VS_OUTPUT output;
output.Pos = mul(input.Pos, WorldViewProj); // 位置変換
output.Color = input.Color; // 色をそのまま渡す
return output;
}
float4x4
は4×4行列型struct
で入力・出力の頂点構造を定義POSITION
やCOLOR0
はセマンティクスで、頂点属性の意味を示すmul
関数で行列とベクトルの乗算を行う
ピクセルシェーダも同様に関数で記述し、色やテクスチャサンプルを計算します。
頂点シェーダバージョン選択
DirectX9の頂点シェーダはバージョンによって機能や命令数の制限が異なります。
主なバージョンは以下の通りです。
バージョン | 機能概要 | 命令数制限 |
---|---|---|
1.1 | 基本的な頂点変換とライティング | 64命令 |
2.0 | より複雑な計算が可能 | 256命令 |
3.0 | 高度な機能と大きな命令数 | 512命令 |
HLSLでコンパイル時にターゲットを指定します。
例えば、vs_2_0
は頂点シェーダバージョン2.0を意味します。
// コンパイル時のターゲット例
// fxc.exe /T vs_2_0 shader.hlsl
バージョンが高いほど表現力が増しますが、古いGPUでは対応していない場合があります。
ターゲットはハードウェアの対応状況に合わせて選択してください。
ピクセルシェーダ作成
ピクセルシェーダはピクセル単位の色計算を行います。
HLSLで記述し、テクスチャサンプルやライティング計算を行うことが多いです。
以下は単純なピクセルシェーダの例です。
sampler2D texSampler : register(s0);
struct PS_INPUT {
float4 Color : COLOR0;
float2 TexCoord : TEXCOORD0;
};
float4 main(PS_INPUT input) : COLOR {
float4 texColor = tex2D(texSampler, input.TexCoord);
return texColor * input.Color; // テクスチャ色と頂点色の乗算
}
sampler2D
で2Dテクスチャを宣言tex2D
関数でテクスチャサンプルを取得- 入力のテクスチャ座標と頂点色を使って最終色を計算
ピクセルシェーダもバージョンがあり、ps_2_0
やps_3_0
などを指定します。
シェーダ定数テーブル
シェーダ内で使う定数(行列やベクトル、スカラー値)は定数テーブルに登録し、アプリケーション側から値をセットします。
HLSLではuniform
キーワードで定数を宣言します。
uniform float4x4 WorldViewProj;
uniform float4 LightDir;
アプリケーション側ではID3DXEffect
やIDirect3DDevice9::SetVertexShaderConstantF
などを使って定数を設定します。
float matrix[16]; // WorldViewProjの値
pDevice->SetVertexShaderConstantF(0, matrix, 4); // レジスタ0から4つのfloat4をセット
定数テーブルのレジスタ番号はHLSLのコンパイル結果やエフェクトファイルで確認できます。
適切に定数を更新することで、動的な変換やライティングが可能です。
ランタイムコンパイル
DirectX9ではHLSLシェーダを実行時にコンパイルすることができます。
これにより、ソースコードの変更を即座に反映したり、複数のシェーダを動的に切り替えたりできます。
ランタイムコンパイルにはD3DXCompileShader
関数を使います。
#include <d3dx9.h>
const char* shaderSrc = R"(
float4x4 WorldViewProj;
struct VS_INPUT {
float4 Pos : POSITION;
};
struct VS_OUTPUT {
float4 Pos : POSITION;
};
VS_OUTPUT main(VS_INPUT input) {
VS_OUTPUT output;
output.Pos = mul(input.Pos, WorldViewProj);
return output;
}
)";
LPD3DXBUFFER pCode = nullptr;
LPD3DXBUFFER pErrors = nullptr;
HRESULT hr = D3DXCompileShader(
shaderSrc,
strlen(shaderSrc),
NULL,
NULL,
"main",
"vs_2_0",
0,
&pCode,
&pErrors,
NULL
);
if (FAILED(hr)) {
if (pErrors) {
OutputDebugStringA((char*)pErrors->GetBufferPointer());
pErrors->Release();
}
// エラーハンドリング
} else {
// pCodeにコンパイル済みバイナリが格納される
// ここから頂点シェーダを作成可能
}
この例では文字列でシェーダコードを渡し、頂点シェーダバージョン2.0向けにコンパイルしています。
エラーがあればpErrors
にメッセージが格納されます。
コンパイル後はIDirect3DDevice9::CreateVertexShader
やCreatePixelShader
でシェーダオブジェクトを生成し、SetVertexShader
やSetPixelShader
でデバイスにセットします。
プログラマブルシェーダを活用することで、固定機能パイプラインでは実現できない高度な表現や最適化が可能になります。
HLSLの基本構文を理解し、適切なバージョンを選択、ランタイムコンパイルを活用して柔軟な描画を実現しましょう。
テクスチャ技法
3Dグラフィックスにおいてテクスチャは表現力を大きく向上させる重要な要素です。
DirectX9では様々なテクスチャ技法が利用可能で、ミップマップ生成やハイトマップ・ノーマルマップ、テクスチャアニメーション、キューブマップを使った環境マッピングなどが代表的です。
ミップマップ生成
ミップマップはテクスチャの縮小版を複数用意し、描画時に適切な解像度のテクスチャを選択する技術です。
これにより遠距離のオブジェクトでのテクスチャのちらつきやジャギーを抑え、描画品質とパフォーマンスを向上させます。
DirectX9ではテクスチャ作成時にミップマップを自動生成できます。
D3DXCreateTextureFromFileEx
関数のMipLevels
パラメータに0を指定すると、全レベルのミップマップが生成されます。
#include <d3dx9.h>
LPDIRECT3DTEXTURE9 pTexture = nullptr;
HRESULT hr = D3DXCreateTextureFromFileEx(
pDevice,
L"texture.png",
D3DX_DEFAULT, D3DX_DEFAULT,
0, // ミップマップレベル0で自動生成
0,
D3DFMT_UNKNOWN,
D3DPOOL_MANAGED,
D3DX_FILTER_TRIANGLE,
D3DX_FILTER_TRIANGLE,
0,
NULL,
NULL,
&pTexture
);
ミップマップはGPUが自動的に適切なレベルを選択し、テクスチャフィルタリングと組み合わせて滑らかな描画を実現します。
ミップマップを使う場合は、SetSamplerState
でミップマップフィルタを設定することも重要です。
pDevice->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
ハイトマップとノーマルマップ
ハイトマップは高さ情報を持つグレースケール画像で、地形の凹凸表現などに使われます。
一方、ノーマルマップは各ピクセルの法線ベクトルをRGB値で表現し、ライティング計算により凹凸感をリアルに表現します。
ハイトマップの利用例
ハイトマップは頂点の高さを変化させるために使われ、地形生成に活用されます。
例えば、グレースケールのテクスチャから高さ値を読み取り、頂点のY座標に反映します。
// ハイトマップのピクセル値を読み取って頂点の高さに反映する例
BYTE heightValue = heightMapData[y * width + x];
float height = heightValue / 255.0f * maxHeight;
vertex.y = height;
ノーマルマップの利用例
ノーマルマップはピクセルシェーダで利用し、光の当たり方を詳細に計算します。
DirectX9の固定機能パイプラインでもD3DTSS_TEXCOORDINDEX
やD3DTSS_COLOROP
を工夫して簡易的に使えますが、プログラマブルシェーダでの利用が一般的です。
// ピクセルシェーダ内でのノーマルマップサンプル例(HLSL)
float3 normal = tex2D(normalMapSampler, input.TexCoord).xyz * 2.0f - 1.0f;
normal = normalize(normal);
float diffuse = saturate(dot(normal, lightDir));
ノーマルマップを使うことで、ポリゴン数を増やさずに複雑な凹凸表現が可能になります。
テクスチャアニメーション
テクスチャアニメーションは、テクスチャ座標を時間経過で変化させることで動きを表現する技法です。
水面の波紋や炎の揺らぎなどに使われます。
DirectX9ではSetTextureStageState
のD3DTSS_TEXTURETRANSFORMFLAGS
やSetTransform
でテクスチャ座標変換行列を設定し、アニメーションを実現します。
// テクスチャ座標の平行移動行列を作成
D3DXMATRIX texTrans;
float offset = fmodf(time * 0.1f, 1.0f); // 時間に応じたオフセット
D3DXMatrixTranslation(&texTrans, offset, 0.0f, 0.0f);
pDevice->SetTransform(D3DTS_TEXTURE0, &texTrans);
pDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
この例では、テクスチャがX方向にゆっくりスクロールし、動きのある表現が可能です。
複数のテクスチャステージを組み合わせて複雑なアニメーションも作れます。
キューブマップと環境マッピング
キューブマップは6面のテクスチャで構成され、環境マッピングや反射表現に使われます。
キューブマップを使うことで、オブジェクトに周囲の環境が映り込むリアルな反射効果を実現できます。
キューブマップの作成
DirectX9ではIDirect3DCubeTexture9
インターフェイスを使い、6面のテクスチャを管理します。
IDirect3DCubeTexture9* pCubeTexture = nullptr;
HRESULT hr = D3DXCreateCubeTextureFromFile(pDevice, L"env_map.dds", &pCubeTexture);
.dds
形式のファイルはキューブマップを含むことが多く、簡単に読み込めます。
環境マッピングの設定例
固定機能パイプラインで環境マッピングを行う場合、テクスチャステージの設定を工夫します。
pDevice->SetTexture(0, pCubeTexture);
pDevice->SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEREFLECTIONVECTOR);
pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
この設定により、カメラ空間の反射ベクトルを使ってキューブマップから色をサンプリングし、反射効果を表現します。
プログラマブルシェーダを使う場合は、反射ベクトルの計算やサンプリングをより自由に制御できます。
これらのテクスチャ技法を組み合わせることで、DirectX9の3D描画においてリアルで動的な表現が可能になります。
ミップマップで品質を保ちつつ、ノーマルマップや環境マッピングで奥行き感や反射を加え、テクスチャアニメーションで動きを演出しましょう。
ライティング拡張
DirectX9の固定機能パイプラインやシェーダを活用して、よりリアルで多彩なライティング効果を実現するための技術を解説します。
ディレクショナルライト、ポイントライト、スポットライトの特徴や設定方法、ライトアッテネーションの調整、さらにシェーダを使った動的ライティングについて詳しく説明します。
ディレクショナルライト
ディレクショナルライト(方向光)は、無限遠から一定方向に照射される光源で、太陽光のように平行光線を表現します。
位置の概念はなく、方向ベクトルだけで光の向きを指定します。
DirectX9ではD3DLIGHT9
構造体のType
にD3DLIGHT_DIRECTIONAL
を指定し、Direction
メンバで光の方向を設定します。
D3DLIGHT9 directionalLight = {};
directionalLight.Type = D3DLIGHT_DIRECTIONAL;
directionalLight.Diffuse = D3DXCOLOR(1.0f, 1.0f, 0.9f, 1.0f); // やや暖色の白色光
directionalLight.Direction = D3DXVECTOR3(-0.5f, -1.0f, 0.0f); // 斜め下方向
directionalLight.Ambient = D3DXCOLOR(0.2f, 0.2f, 0.2f, 1.0f); // 環境光
directionalLight.Specular = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); // 鏡面反射光
pDevice->SetLight(0, &directionalLight);
pDevice->LightEnable(0, TRUE);
pDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
この光源はシーン全体に均一に影響し、影の方向や明暗の基準として使われます。
方向ベクトルは正規化しておく必要があります。
ポイントライトとスポットライト
ポイントライトは光源が一点に存在し、全方向に光を放射します。
距離に応じて光の強さが減衰するため、局所的な照明に適しています。
スポットライトはポイントライトの一種で、特定の方向に光を集中させる光源です。
懐中電灯や舞台照明のような効果を表現します。
ポイントライトの設定例
D3DLIGHT9 pointLight = {};
pointLight.Type = D3DLIGHT_POINT;
pointLight.Diffuse = D3DXCOLOR(1.0f, 0.8f, 0.6f, 1.0f);
pointLight.Position = D3DXVECTOR3(0.0f, 5.0f, 0.0f);
pointLight.Range = 20.0f; // 光の届く距離
pointLight.Attenuation0 = 1.0f; // 定数減衰
pointLight.Attenuation1 = 0.1f; // 線形減衰
pointLight.Attenuation2 = 0.01f; // 二次減衰
pDevice->SetLight(1, &pointLight);
pDevice->LightEnable(1, TRUE);
スポットライトの設定例
D3DLIGHT9 spotLight = {};
spotLight.Type = D3DLIGHT_SPOT;
spotLight.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
spotLight.Position = D3DXVECTOR3(0.0f, 10.0f, 0.0f);
spotLight.Direction = D3DXVECTOR3(0.0f, -1.0f, 0.0f);
spotLight.Range = 30.0f;
spotLight.Falloff = 1.0f; // スポットライトの減衰率
spotLight.Theta = D3DXToRadian(20); // 内側コーン角度
spotLight.Phi = D3DXToRadian(40); // 外側コーン角度
spotLight.Attenuation0 = 1.0f;
spotLight.Attenuation1 = 0.05f;
spotLight.Attenuation2 = 0.0f;
pDevice->SetLight(2, &spotLight);
pDevice->LightEnable(2, TRUE);
スポットライトはTheta
とPhi
で光の集中度合いを調整し、Falloff
で光の減衰の滑らかさを制御します。
ライトアッテネーション
ライトアッテネーションは光の強さが距離に応じてどのように減衰するかを制御するパラメータです。
D3DLIGHT9
のAttenuation0
(定数減衰)、Attenuation1
(線形減衰)、Attenuation2
(二次減衰)で設定します。
光の強度は以下の式で計算されます。
ここで
Attenuation0
が大きいと距離に関係なく一定の光強度が保たれますAttenuation1
は距離に比例して光が減衰しますAttenuation2
は距離の二乗に比例して減衰し、より自然な減衰を表現します
適切な値を設定することで、リアルな光の減衰をシーンに反映できます。
シェーダによる動的ライト
固定機能パイプラインの制限を超えた複雑なライティング効果は、プログラマブルシェーダで実装します。
HLSLを使い、動的に変化する光源の位置や色、影響範囲を計算可能です。
以下は簡単な動的ライトの頂点シェーダ例です。
float4x4 WorldViewProj;
float3 LightPos;
float4 LightColor;
struct VS_INPUT {
float4 Pos : POSITION;
float3 Normal : NORMAL;
};
struct VS_OUTPUT {
float4 Pos : POSITION;
float4 Diffuse : COLOR0;
};
VS_OUTPUT main(VS_INPUT input) {
VS_OUTPUT output;
output.Pos = mul(input.Pos, WorldViewProj);
float3 worldPos = input.Pos.xyz;
float3 lightDir = normalize(LightPos - worldPos);
float NdotL = max(dot(input.Normal, lightDir), 0.0f);
output.Diffuse = LightColor * NdotL;
return output;
}
このシェーダは頂点ごとにライト方向と法線の内積を計算し、拡散反射成分を算出しています。
ピクセルシェーダでさらに詳細な計算を行うことも可能です。
アプリケーション側では、光源の位置や色を毎フレーム更新し、SetVertexShaderConstantF
などでシェーダに渡します。
float lightPos[3] = {x, y, z};
pDevice->SetVertexShaderConstantF(4, lightPos, 1);
シェーダによる動的ライトは複数光源の同時処理や影の表現、物理ベースレンダリング(PBR)など高度な表現に対応でき、リアルなシーン作成に欠かせません。
シャドウ表現
3Dグラフィックスにおけるシャドウ(影)の表現は、シーンのリアリティを大きく向上させます。
DirectX9では主に「シャドウマッピング」と「シャドウボリューム」という2つの手法が用いられます。
ここではそれぞれの技術の基本的な仕組みと実装のポイントを詳しく解説します。
シャドウマッピング
シャドウマッピングは、光源から見たシーンの深度情報をテクスチャ(シャドウマップ)として生成し、描画時にピクセルごとに光源からの距離と比較して影かどうかを判定する手法です。
比較的実装が容易で、柔軟な影表現が可能です。
深度レンダリングパス
シャドウマッピングの最初のステップは、光源の視点からシーンをレンダリングし、深度情報を取得することです。
この深度情報はシャドウマップとしてテクスチャに格納されます。
具体的には、以下の手順で行います。
- レンダーターゲットの切り替え
通常のバックバッファではなく、深度情報を格納するためのテクスチャ(深度レンダーターゲット)に描画します。
- 光源のビュー行列とプロジェクション行列の設定
光源の位置と方向に基づいて、光源視点のカメラ行列を作成します。
これにより、光源から見たシーンの深度を正確に取得できます。
- シーンの描画
深度バッファのみを更新し、色の描画は行いません。
これにより、光源からの距離情報がシャドウマップに記録されます。
DirectX9のコード例(概略):
// 深度レンダーターゲットに切り替え
pDevice->SetRenderTarget(0, pShadowDepthSurface);
pDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0);
// 光源のビュー・プロジェクション行列をセット
pDevice->SetTransform(D3DTS_VIEW, &lightViewMatrix);
pDevice->SetTransform(D3DTS_PROJECTION, &lightProjMatrix);
// シーンを描画(色は不要)
RenderSceneDepthOnly();
この深度レンダリングパスで得られたシャドウマップは、後の描画パスで影判定に使われます。
バイアス調整
シャドウマッピングでは、深度比較の際に「シャドウアクネ(Shadow Acne)」と呼ばれる自己影のちらつきが発生しやすいです。
これは、深度値の丸め誤差やZファイティングによるもので、影がオブジェクト表面に不自然に現れる現象です。
この問題を軽減するために「バイアス」を加えます。
バイアスは深度値に微小なオフセットを加え、比較時に誤差を吸収します。
バイアスの設定例:
float bias = 0.005f; // 小さな値を調整
float shadowDepth = tex2D(shadowMapSampler, projCoords.xy).r;
if (currentDepth - bias > shadowDepth) {
// 影の中
} else {
// 影の外
}
DirectX9の固定機能パイプラインではD3DRS_DEPTHBIAS
やD3DRS_SLOPESCALEDEPTHBIAS
を使ってバイアスを設定できます。
pDevice->SetRenderState(D3DRS_DEPTHBIAS, *(DWORD*)&biasValue);
pDevice->SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, slopeScaleValue);
バイアスは大きすぎると影が浮いて見え、小さすぎるとシャドウアクネが発生するため、シーンに応じて調整が必要です。
シャドウボリューム
シャドウボリュームは、ジオメトリのエッジから影のボリューム(体積)を生成し、ステンシルバッファを使って影の領域を判定する手法です。
正確な影の境界を表現できる反面、計算コストが高く実装が複雑です。
ステンシルバッファ利用
シャドウボリュームの基本は、影の領域をステンシルバッファにマークし、描画時に影の部分だけ色を暗くすることです。
手順は以下の通りです。
- シャドウボリュームの生成
光源から見てシルエットとなるエッジを抽出し、そのエッジから無限遠まで伸びるポリゴン(ボリューム)を作成します。
- ステンシルバッファの設定
ステンシルテストを有効にし、シャドウボリュームの前面と背面ポリゴンを描画してステンシル値を増減させます。
- 影の描画
ステンシル値が0でないピクセルを影として扱い、暗く描画します。
DirectX9のステンシル設定例:
// ステンシルテスト有効化
pDevice->SetRenderState(D3DRS_STENCILENABLE, TRUE);
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);
pDevice->SetRenderState(D3DRS_STENCILREF, 0);
pDevice->SetRenderState(D3DRS_STENCILMASK, 0xFF);
// 前面ポリゴン描画時にステンシル値を増加
pDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCRSAT);
RenderShadowVolumeFrontFaces();
// 背面ポリゴン描画時にステンシル値を減少
pDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_DECRSAT);
RenderShadowVolumeBackFaces();
// ステンシル値が0でない部分を影として描画
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL);
pDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP);
RenderShadowedScene();
エッジ抽出
シャドウボリュームの生成には、光源から見てシルエットとなるエッジの抽出が必要です。
これは、隣接するポリゴンの法線が光源方向に対して片方が表面向き、もう片方が裏面向きであるエッジを特定する処理です。
エッジ抽出のアルゴリズムは以下のようになります。
- メッシュの全エッジを走査
- 各エッジに隣接する2つのポリゴンの法線を取得
- 光源方向ベクトルと法線の内積を計算し、片方が正、もう片方が負ならシルエットエッジと判定
このシルエットエッジを基に、エッジから光源方向に向かって無限遠まで伸びるポリゴンを生成し、シャドウボリュームを構築します。
シャドウマッピングは比較的実装が容易で多くのシーンに適用可能ですが、バイアス調整が重要です。
一方、シャドウボリュームは正確な影の境界を表現できる反面、計算負荷が高く実装が複雑です。
用途やパフォーマンス要件に応じて使い分けることが望ましいです。
ポストプロセス効果
ポストプロセス効果は、3Dシーンのレンダリング後に画面全体に対して適用する画像処理技術です。
DirectX9ではスクリーンレンダーターゲットを活用し、ブルームやトーンマッピング、ガウスブラーなどの効果を実装できます。
これにより映像の質感や雰囲気を大幅に向上させられます。
スクリーンレンダーターゲット
スクリーンレンダーターゲット(Render Target)は、描画結果を画面に直接出力するのではなく、テクスチャとして一旦保存するためのバッファです。
これを利用することで、描画後の画像に対してシェーダを使った加工が可能になります。
DirectX9でレンダーターゲットを作成するにはIDirect3DDevice9::CreateTexture
でテクスチャを生成し、GetSurfaceLevel
でその表面を取得します。
LPDIRECT3DTEXTURE9 pRenderTargetTex = nullptr;
HRESULT hr = pDevice->CreateTexture(
width,
height,
1,
D3DUSAGE_RENDERTARGET,
D3DFMT_A8R8G8B8,
D3DPOOL_DEFAULT,
&pRenderTargetTex,
NULL
);
LPDIRECT3DSURFACE9 pRenderTargetSurface = nullptr;
pRenderTargetTex->GetSurfaceLevel(0, &pRenderTargetSurface);
描画時はSetRenderTarget
でこのサーフェスを指定し、シーンをレンダリングします。
pDevice->SetRenderTarget(0, pRenderTargetSurface);
pDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, 0);
RenderScene();
レンダーターゲットに描画した後は、通常のバックバッファに戻して、保存したテクスチャを使ってポストプロセスシェーダを適用します。
ブルームフィルタ
ブルームは明るい部分がにじんで輝いて見える効果で、光源の強調や幻想的な雰囲気を演出します。
ブルームフィルタは以下の手順で実装します。
- 明るい部分の抽出
シーンのレンダーターゲットから輝度が一定以上のピクセルだけを抽出し、別のテクスチャにコピーします。
- ぼかし(ガウスブラー)適用
抽出した明るい部分に対してガウスブラーをかけ、光のにじみを作ります。
- 合成
元のシーンとぼかした輝度テクスチャを加算合成し、ブルーム効果を完成させます。
HLSLの簡単な輝度抽出例:
float4 main(float4 pos : POSITION, float2 texCoord : TEXCOORD) : COLOR {
float4 color = tex2D(sceneSampler, texCoord);
float luminance = dot(color.rgb, float3(0.299, 0.587, 0.114));
if (luminance > 0.8) {
return color;
} else {
return float4(0,0,0,0);
}
}
このシェーダは輝度が0.8以上の部分だけを残します。
続いてガウスブラーを適用し、最後に元画像と合成します。
トーンマッピング
トーンマッピングはHDR(高ダイナミックレンジ)レンダリングの結果を、ディスプレイの表示可能な範囲に収めるための処理です。
明るすぎる部分を圧縮し、暗い部分のディテールを保ちつつ自然な見た目に調整します。
代表的なトーンマッピング手法には「Reinhardトーンマッピング」などがあります。
HLSLでのReinhardトーンマッピング例:
float4 main(float4 color : COLOR) : COLOR {
float3 mapped = color.rgb / (color.rgb + 1.0);
return float4(mapped, color.a);
}
この式は色の各成分を
トーンマッピングはHDRレンダーターゲットを使ったレンダリング後に適用し、最終的な画面出力に用います。
ガウスブラー
ガウスブラーは画像をぼかす効果で、ブルームや被写界深度、モーションブラーなど様々なポストプロセスで使われます。
ガウス関数に基づく重みを使い、周囲のピクセルを加重平均して滑らかなぼかしを実現します。
DirectX9では2パスの分離ガウスブラーが一般的です。
まず水平方向にぼかし、次に垂直方向にぼかします。
HLSLの水平方向ブラー例:
float4 main(float2 texCoord : TEXCOORD) : COLOR {
float4 sum = float4(0,0,0,0);
float weights[5] = {0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216};
float2 texOffset = float2(1.0 / textureWidth, 0);
sum += tex2D(sceneSampler, texCoord) * weights[0];
for (int i = 1; i < 5; ++i) {
sum += tex2D(sceneSampler, texCoord + texOffset * i) * weights[i];
sum += tex2D(sceneSampler, texCoord - texOffset * i) * weights[i];
}
return sum;
}
垂直方向はtexOffset
をfloat2(0, 1.0 / textureHeight)
に変えるだけです。
この2パス処理により効率的に高品質なぼかしが得られます。
これらのポストプロセス効果を組み合わせることで、DirectX9の3D描画はより美しく、リアルで魅力的な映像表現が可能になります。
スクリーンレンダーターゲットを活用し、シェーダで自由に画像処理を行うことがポイントです。
ジオメトリ生成
3Dシーンの構築において、ジオメトリ生成は重要な役割を果たします。
DirectX9ではスカイボックスやスカイドームによる背景表現、テレインレンダリングによる地形生成、そしてパーティクルシステムによるエフェクト表現がよく使われます。
ここではそれぞれの技術と実装のポイントを詳しく解説します。
スカイボックスとスカイドーム
スカイボックスは、カメラを囲む立方体の内側に空や遠景のテクスチャを貼り付ける手法です。
これにより、広大な空間を簡単に表現できます。
スカイドームは球体または半球体のジオメトリにテクスチャを貼り付ける方法で、より自然な空の表現が可能です。
スカイボックスの実装例
スカイボックスは6面のテクスチャ(キューブマップ)を用い、カメラ位置に追従させて描画します。
カメラの回転は反映しますが、位置は固定し、遠くの背景として常に一定に見えるようにします。
// カメラ位置を取得
D3DXVECTOR3 camPos = camera.GetPosition();
// スカイボックスのワールド行列をカメラ位置に設定
D3DXMATRIX matTrans;
D3DXMatrixTranslation(&matTrans, camPos.x, camPos.y, camPos.z);
pDevice->SetTransform(D3DTS_WORLD, &matTrans);
// スカイボックス用の頂点バッファとキューブマップをセットして描画
pDevice->SetTexture(0, pSkyboxCubeMap);
pDevice->SetFVF(D3DFVF_XYZ);
pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 12);
スカイボックスは深度バッファの設定を工夫し、他のオブジェクトの後ろに描画されるようにします。
スカイドームの特徴
スカイドームは球体のメッシュに空のテクスチャを貼り付けるため、スカイボックスよりも自然なグラデーションや雲の表現が可能です。
頂点数は多くなりますが、テクスチャの歪みが少ないのが利点です。
テレインレンダリング
テレインレンダリングは地形をポリゴンメッシュで表現する技術です。
高さマップ(ハイトマップ)を利用して頂点の高さを決定し、広大な地形を効率的に描画します。
基本的な地形生成手順
- 高さマップの読み込み
グレースケール画像から高さ情報を取得します。
- 頂点生成
グリッド状に頂点を配置し、各頂点のY座標に高さマップの値を反映します。
- インデックスバッファ生成
グリッドの四角形を2つの三角形に分割し、インデックスバッファを作成します。
// 頂点生成例
for (int z = 0; z < terrainHeight; ++z) {
for (int x = 0; x < terrainWidth; ++x) {
float height = heightMap[z * terrainWidth + x] / 255.0f * maxHeight;
vertices[z * terrainWidth + x] = { (float)x, height, (float)z };
}
}
レベルオブディテール(LOD)
広大な地形では遠距離ほどポリゴン数を減らすLOD技術が重要です。
DirectX9では複数のメッシュを用意し、距離に応じて切り替えたり、ジオメトリシェーダを使って動的に調整したりします。
パーティクルシステム
パーティクルシステムは火花、煙、炎、爆発などのエフェクトを多数の小さなパーティクルで表現する技術です。
DirectX9ではCPUで制御する方法とGPUを活用する方法があります。
エミッター制御
エミッターはパーティクルの発生源で、位置、発生速度、方向、寿命などを管理します。
CPU側でパーティクルの状態を更新し、頂点バッファに反映します。
// パーティクル構造体
struct Particle {
D3DXVECTOR3 position;
D3DXVECTOR3 velocity;
float life;
};
// 更新例
for (auto& p : particles) {
p.position += p.velocity * deltaTime;
p.life -= deltaTime;
if (p.life <= 0) {
RespawnParticle(p);
}
}
エミッターはパーティクルの発生頻度や初期速度の分布を制御し、多様な表現を可能にします。
GPUパーティクル
GPUパーティクルは頂点シェーダやジオメトリシェーダを使い、パーティクルの位置や動きをGPU上で計算します。
これにより大量のパーティクルを高速に処理可能です。
DirectX9では頂点バッファのストリーム出力やシェーダによる計算が制限されるため、主に頂点シェーダでの簡易的な制御が中心です。
// 頂点シェーダ内でパーティクルの位置更新例
float4 main(float4 pos : POSITION, float3 velocity : TEXCOORD0, float life : TEXCOORD1) : POSITION {
float time = ...; // 時間経過
pos.xyz += velocity * time;
return pos;
}
GPUパーティクルはCPU負荷を軽減し、リアルタイムで複雑なエフェクトを実現します。
これらのジオメトリ生成技術を組み合わせることで、リアルで動的な3Dシーンを構築できます。
スカイボックスやスカイドームで背景を整え、テレインレンダリングで地形を作り、パーティクルシステムでエフェクトを加えることで、魅力的な表現が可能です。
衝突検出と当たり判定
3Dゲームやシミュレーションにおいて、衝突検出と当たり判定は物理的なリアリティやインタラクションを実現するために不可欠な技術です。
DirectX9の描画機能とは別に、効率的かつ正確な判定を行うためのアルゴリズムやデータ構造が求められます。
ここでは代表的な手法であるバウンディングボリューム、レイキャスティング、オクツリーと空間分割について詳しく解説します。
バウンディングボリューム
バウンディングボリュームは複雑な3Dモデルの衝突判定を簡略化するための外接形状です。
代表的な形状には以下があります。
- バウンディングボックス(AABB、OBB)
軸に平行な最小の直方体(AABB)や任意の方向に回転可能な直方体(OBB)でモデルを囲みます。
計算が高速で広く使われます。
- バウンディングスフィア
中心と半径で表される球体。
回転の影響を受けず、距離計算が簡単です。
AABBの衝突判定例
2つのAABBが衝突しているかは、各軸の範囲が重なっているかで判定します。
struct AABB {
D3DXVECTOR3 min;
D3DXVECTOR3 max;
};
bool CheckAABBCollision(const AABB& a, const AABB& b) {
return (a.min.x <= b.max.x && a.max.x >= b.min.x) &&
(a.min.y <= b.max.y && a.max.y >= b.min.y) &&
(a.min.z <= b.max.z && a.max.z >= b.min.z);
}
この判定は非常に高速で、まずは粗い判定として使い、詳細な衝突判定の前段階として有効です。
OBBの衝突判定
OBBは回転を考慮した直方体で、判定はより複雑ですが、Separating Axis Theorem (SAT)
を用いて実装します。
DirectX9には標準機能はありませんが、数学的な実装が可能です。
レイキャスティング
レイキャスティングは、ある点から特定の方向に伸ばした直線(レイ)とオブジェクトの交差を判定する手法です。
視線判定や射撃判定、マウスピックなどに使われます。
レイと三角形の交差判定
最も基本的な判定はレイと三角形の交差です。
Möller -Trumboreアルゴリズムが高速かつ広く使われています。
bool RayIntersectsTriangle(
const D3DXVECTOR3& rayOrigin,
const D3DXVECTOR3& rayDir,
const D3DXVECTOR3& v0,
const D3DXVECTOR3& v1,
const D3DXVECTOR3& v2,
float* outDist)
{
const float EPSILON = 1e-6f;
D3DXVECTOR3 edge1 = v1 - v0;
D3DXVECTOR3 edge2 = v2 - v0;
D3DXVECTOR3 h = D3DXVec3Cross(&rayDir, &edge2);
float a = D3DXVec3Dot(&edge1, &h);
if (fabs(a) < EPSILON) return false; // 平行
float f = 1.0f / a;
D3DXVECTOR3 s = rayOrigin - v0;
float u = f * D3DXVec3Dot(&s, &h);
if (u < 0.0f || u > 1.0f) return false;
D3DXVECTOR3 q = D3DXVec3Cross(&s, &edge1);
float v = f * D3DXVec3Dot(&rayDir, &q);
if (v < 0.0f || u + v > 1.0f) return false;
float t = f * D3DXVec3Dot(&edge2, &q);
if (t > EPSILON) {
*outDist = t;
return true;
}
return false;
}
この関数はレイが三角形に当たるか判定し、交差距離を返します。
複数の三角形を持つメッシュに対しては、全三角形に対して判定を行い最短距離の交差を求めます。
オクツリーと空間分割
大規模なシーンでは、全オブジェクトや全ポリゴンに対して衝突判定を行うのは非効率です。
空間分割データ構造を使い、判定対象を絞り込むことで高速化します。
オクツリーの概要
オクツリーは3D空間を8分割(オクタント)して階層的に管理する木構造です。
各ノードは空間の一部を表し、子ノードにさらに細分割されます。
オクツリーの利点は、衝突判定時に判定範囲外のノードを無視できるため、計算量を大幅に削減できることです。
オクツリーの構築と利用例
- 空間の境界を決定
シーン全体を覆う立方体を決めます。
- オブジェクトをノードに割り当て
各オブジェクトのバウンディングボリュームがノードの範囲に含まれるか判定し、該当ノードに格納します。
- 再帰的に細分割
ノード内のオブジェクト数が多い場合、さらに8分割して子ノードを作成します。
- 衝突判定時の絞り込み
レイやオブジェクトのバウンディングボリュームとノードの範囲を比較し、交差するノードだけを探索します。
// オクツリーのノード構造例
struct OctreeNode {
AABB bounds;
std::vector<GameObject*> objects;
OctreeNode* children[8];
};
このように空間分割を活用することで、衝突判定の対象を効率的に絞り込み、リアルタイム性を確保できます。
バウンディングボリュームで粗い判定を行い、レイキャスティングで詳細な交差を調べ、オクツリーなどの空間分割で対象を絞ります。
この組み合わせが3Dアプリケーションにおける衝突検出の基本的かつ効果的な手法です。
リソース管理
DirectX9での3D描画プログラミングにおいて、リソース管理は安定した動作と効率的なパフォーマンスを実現するために非常に重要です。
特にテクスチャやバッファなどのGPUリソースの管理、デバイスロスト時の復旧処理、そしてCOMベースのDirectXオブジェクトのリファレンスカウント管理には注意が必要です。
ここではそれぞれのポイントを詳しく解説します。
テクスチャ管理プール
テクスチャはGPUメモリを消費するため、効率的な管理が求められます。
DirectX9ではテクスチャの生成時にメモリプールを指定します。
主なプールは以下の通りです。
プール名 | 説明 | 利用例 |
---|---|---|
D3DPOOL_DEFAULT | GPU上のメモリ。デバイスロスト時にリセットが必要 | 動的に変更するテクスチャやレンダーターゲット |
D3DPOOL_MANAGED | システムメモリとGPUメモリを自動管理 | 静的なテクスチャや頻繁に変更しないもの |
D3DPOOL_SYSTEMMEM | システムメモリ上。GPUから直接アクセス不可 | テクスチャのアップロード元やバックアップ用 |
D3DPOOL_MANAGED
はDirectXが自動的にシステムメモリとGPUメモリを同期してくれるため、通常のテクスチャはこのプールを使うのが便利です。
一方、D3DPOOL_DEFAULT
は高速ですが、デバイスロスト時にリソースを再作成する必要があります。
テクスチャプールの指定例
LPDIRECT3DTEXTURE9 pTexture = nullptr;
HRESULT hr = D3DXCreateTextureFromFileEx(
pDevice,
L"texture.png",
D3DX_DEFAULT, D3DX_DEFAULT,
D3DX_DEFAULT,
0,
D3DFMT_UNKNOWN,
D3DPOOL_MANAGED, // 管理プールを指定
D3DX_FILTER_LINEAR,
D3DX_FILTER_LINEAR,
0,
NULL,
NULL,
&pTexture
);
テクスチャキャッシュの実装
複数のテクスチャを使う場合は、同じテクスチャを何度も読み込まないようにキャッシュ機構を実装すると効率的です。
キー(ファイル名など)を元にテクスチャを管理し、既に読み込まれている場合は再利用します。
std::unordered_map<std::wstring, LPDIRECT3DTEXTURE9> textureCache;
LPDIRECT3DTEXTURE9 LoadTexture(const std::wstring& filename) {
auto it = textureCache.find(filename);
if (it != textureCache.end()) {
return it->second;
}
LPDIRECT3DTEXTURE9 pTex = nullptr;
HRESULT hr = D3DXCreateTextureFromFile(pDevice, filename.c_str(), &pTex);
if (SUCCEEDED(hr)) {
textureCache[filename] = pTex;
}
return pTex;
}
デバイスロスト対処
DirectX9のデバイスは、Alt+Tabや画面解像度の変更、スクリーンモード切替などで「デバイスロスト」状態になることがあります。
この状態ではGPUリソースが無効化され、描画ができなくなります。
復旧処理を正しく実装しないとアプリケーションがクラッシュしたり描画が停止します。
デバイスロストの検出
描画ループでIDirect3DDevice9::Present
やTestCooperativeLevel
の戻り値をチェックします。
HRESULT hr = pDevice->Present(NULL, NULL, NULL, NULL);
if (hr == D3DERR_DEVICELOST) {
// デバイスロスト中。リセットはまだ不可
} else if (hr == D3DERR_DEVICENOTRESET) {
// リセット可能
ResetDevice();
}
デバイスのリセット
Reset
を呼ぶ前に、D3DPOOL_DEFAULT
のリソースを解放し、リセット後に再作成します。
D3DPOOL_MANAGED
のリソースはDirectXが自動的に復旧します。
void ResetDevice() {
// リソース解放
pVertexBuffer->Release();
pIndexBuffer->Release();
// ... 他のD3DPOOL_DEFAULTリソースも解放
HRESULT hr = pDevice->Reset(&d3dpp);
if (SUCCEEDED(hr)) {
// リソース再作成
CreateResources();
}
}
リセット処理のポイント
Reset
はデバイスがロスト状態かつリセット可能なときのみ呼ぶ- リセット前に
IDirect3DDevice9::TestCooperativeLevel
で状態を確認 - リセット後は全ての
D3DPOOL_DEFAULT
リソースを再生成 D3DPOOL_MANAGED
リソースは自動復旧されるため再生成不要
リファレンスカウント注意点
DirectX9のCOMオブジェクトはリファレンスカウント方式で管理されます。
AddRef
で参照カウントを増やし、Release
で減らします。
カウントが0になるとオブジェクトは破棄されます。
適切なReleaseの呼び出し
リソースを使い終わったら必ずRelease
を呼び、メモリリークを防ぎます。
if (pTexture) {
pTexture->Release();
pTexture = nullptr;
}
二重解放や解放忘れの防止
- 二重解放はクラッシュの原因になるため、
nullptr
チェックを行う - スマートポインタ(例:
CComPtr
やMicrosoft::WRL::ComPtr
)を使うと安全 - ループやコンテナで管理する場合は、解放タイミングを明確にする
参照カウントのトラブル例
- 参照カウントを増やし忘れてオブジェクトが早期破棄される
Release
を呼び忘れてメモリリークが発生する- 複数箇所で同じポインタを管理し、解放タイミングが不明確になる
リソース管理はDirectX9プログラミングの安定性とパフォーマンスに直結します。
テクスチャプールの適切な利用、デバイスロスト時の確実な復旧処理、そしてリファレンスカウントの正しい扱いを徹底することが重要です。
パフォーマンス測定
3Dアプリケーションの快適な動作を実現するためには、パフォーマンスの測定と最適化が欠かせません。
DirectX9環境でのパフォーマンス測定には、FPS計測やGPUパイプラインのプロファイリング、描画バッチの最適化などの手法があります。
ここではそれぞれの方法を詳しく解説します。
FPS計測
FPS(Frames Per Second)は1秒間に描画されるフレーム数を示し、アプリケーションの描画性能の指標として広く使われます。
FPSが高いほど滑らかな描画が可能です。
FPS計測の基本的な実装
FPSを計測するには、一定時間内に描画したフレーム数をカウントし、その時間で割ります。
一般的には1秒ごとに更新します。
#include <windows.h>
#include <iostream>
class FPSCounter {
private:
int frameCount;
float elapsedTime;
float fps;
LARGE_INTEGER frequency;
LARGE_INTEGER lastTime;
public:
FPSCounter() : frameCount(0), elapsedTime(0.0f), fps(0.0f) {
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&lastTime);
}
void Frame() {
frameCount++;
LARGE_INTEGER currentTime;
QueryPerformanceCounter(¤tTime);
float delta = float(currentTime.QuadPart - lastTime.QuadPart) / frequency.QuadPart;
elapsedTime += delta;
lastTime = currentTime;
if (elapsedTime >= 1.0f) {
fps = frameCount / elapsedTime;
frameCount = 0;
elapsedTime = 0.0f;
std::cout << "FPS: " << fps << std::endl;
}
}
float GetFPS() const { return fps; }
};
このクラスを描画ループの最後に呼び出すことで、1秒ごとにFPSをコンソールに表示できます。
注意点
- 高精度タイマー
QueryPerformanceCounter
を使うことで正確な時間計測が可能 - FPS表示は頻繁に更新しすぎるとパフォーマンスに影響するため、1秒程度の間隔が適切
- 実際のゲームでは画面上にテキスト表示することも多い
GPUパイプラインプロファイル
GPUパイプラインのプロファイリングは、描画処理のどの部分がボトルネックになっているかを分析する手法です。
DirectX9単体では詳細なGPUプロファイリング機能は限定的ですが、以下の方法でパフォーマンスを把握できます。
Direct3Dの統計情報取得
IDirect3DDevice9::GetDeviceCaps
でGPUの性能情報を取得し、描画負荷の目安にします。
PIX for Windowsの利用
MicrosoftのPIXツールはDirectX9対応の強力なGPUプロファイラで、以下の機能があります。
- GPUコマンドのキャプチャと解析
- シェーダの実行時間計測
- DrawCall数や頂点数の統計
- リアルタイムのパフォーマンスモニタリング
PIXを使うことで、どの描画パスが重いか、シェーダの最適化ポイントはどこかを詳細に把握できます。
GPUタイマークエリ
DirectX9では標準でGPUタイマークエリはありませんが、拡張機能やドライバ固有のAPIを使うことでGPU処理時間を計測可能な場合があります。
これによりCPUとGPUの処理時間のバランスを評価できます。
描画バッチ最適化
描画バッチとは、GPUに送る描画コマンドの単位で、バッチ数が多いほどCPU負荷が増加します。
バッチ最適化は描画コール数を減らし、パフォーマンスを向上させる重要な技術です。
バッチ数削減の基本手法
- インスタンシング
同じメッシュを複数描画する場合、頂点データを共有しつつ変換行列だけ変えて一括描画します。
DirectX9ではDrawIndexedPrimitive
の代わりにDrawIndexedPrimitiveInstanced
(拡張機能)を使います。
- テクスチャのバインド切り替え削減
同じテクスチャを使うオブジェクトをまとめて描画し、テクスチャ切り替えによるオーバーヘッドを減らします。
- 頂点バッファの統合
複数の小さなメッシュを一つの大きな頂点バッファにまとめ、描画回数を減らします。
バッチ最適化の実装例
// 複数オブジェクトの描画を一つのバッチにまとめる例
pDevice->SetStreamSource(0, pCombinedVertexBuffer, 0, sizeof(Vertex));
pDevice->SetIndices(pCombinedIndexBuffer);
pDevice->SetTexture(0, pSharedTexture);
pDevice->SetFVF(VertexFVF);
pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, totalVertices, 0, totalTriangles);
注意点
- バッチをまとめすぎると、不要な頂点やインデックスも描画されるため、オーバードローが増える可能性がある
- 描画順序や透明オブジェクトの扱いに注意が必要
- CPUとGPUのバランスを考慮し、最適なバッチサイズを見極めることが重要
FPS計測で全体の描画速度を把握し、GPUパイプラインのプロファイルでボトルネックを特定、描画バッチの最適化で描画コール数を削減します。
この3つのアプローチを組み合わせることで、DirectX9アプリケーションのパフォーマンスを効果的に向上させられます。
デバッグ技法
DirectX9での3D描画プログラミングでは、描画結果の不具合やパフォーマンス問題を効率的に発見・修正するためのデバッグ技法が重要です。
ここでは、Microsoftが提供するD3Dデバッグランタイムの活用、PIX for Windowsによる詳細な解析、そしてシェーダデバッグの方法について詳しく解説します。
D3Dデバッグランタイム
D3Dデバッグランタイムは、DirectX SDKに含まれる特別なランタイムで、通常のリリースランタイムよりも詳細なエラーメッセージや警告を出力します。
これにより、APIの誤用やリソース管理のミスを早期に発見できます。
有効化方法
- DirectX SDKのインストール
SDKに含まれるデバッグランタイムを利用します。
- DirectXコントロールパネルの設定
dxcpl.exe
(DirectXコントロールパネル)を起動し、「Direct3D 9」タブで「デバッグランタイムを使用する」にチェックを入れます。
- ログレベルの調整
エラーや警告の詳細度を設定可能です。
通常は「警告とエラー」を選択します。
デバッグ出力の確認
Visual Studioの出力ウィンドウやDebugViewなどのツールで、Direct3Dからのメッセージを確認できます。
例えば、リソースの不正な使用やAPI呼び出しの誤りが検出されると、詳細なメッセージが表示されます。
D3D9: WARNING: SetTexture: Invalid texture pointer.
D3D9: ERROR: CreateVertexBuffer: Invalid parameters.
注意点
- デバッグランタイムはパフォーマンスが低下するため、開発時のみ有効にし、リリース時は通常ランタイムに戻すこと
- 一部のエラーはデバッグランタイムでしか検出できないため、問題解決に役立ちます
PIX for Windows利用
PIX for WindowsはMicrosoftが提供するDirectXアプリケーション向けの強力なプロファイリング・デバッグツールです。
DirectX9にも対応しており、GPUの動作解析やシェーダのデバッグに活用できます。
主な機能
- フレームキャプチャ
アプリケーションの1フレーム分の描画コマンドをキャプチャし、詳細に解析可能です。
- DrawCall解析
各描画コールの頂点数、インデックス数、使用テクスチャ、シェーダ情報を表示。
- GPUパフォーマンスカウンタ
GPUの負荷やパイプラインのボトルネックを特定。
- シェーダデバッグ
頂点・ピクセルシェーダのステップ実行や変数の値確認。
使い方の概要
- PIXを起動し、対象のDirectX9アプリケーションを指定して実行。
- 問題のあるフレームをキャプチャ。
- キャプチャしたフレームの描画コマンドを順に確認し、異常な描画やパフォーマンス低下の原因を特定。
- シェーダコードのデバッグやリソースの状態を詳細に調査。
利用上のポイント
- フレームキャプチャはパフォーマンスに影響するため、問題のある箇所を絞って実施
- シェーダのデバッグにはHLSLソースコードのシンボル情報が必要でしょう
- PIXはDirectX9のほか、DirectX10/11にも対応しているため、幅広い環境で利用可能です
シェーダデバッグ
シェーダはGPU上で動作するプログラムであり、バグやパフォーマンス問題の原因になりやすい部分です。
DirectX9ではHLSLシェーダのデバッグを行うために以下の方法があります。
ランタイムコンパイル時のエラーチェック
D3DXCompileShader
関数の戻り値とエラーメッセージバッファを確認し、構文エラーや型の不一致を検出します。
LPD3DXBUFFER pErrors = nullptr;
HRESULT hr = D3DXCompileShader(
shaderSrc,
strlen(shaderSrc),
NULL,
NULL,
"main",
"ps_2_0",
0,
&pCode,
&pErrors,
NULL
);
if (FAILED(hr)) {
if (pErrors) {
OutputDebugStringA((char*)pErrors->GetBufferPointer());
pErrors->Release();
}
}
PIXを使ったステップ実行
PIXのシェーダデバッガを使うと、シェーダの命令を1ステップずつ実行し、レジスタや変数の値を確認できます。
これにより、計算ミスや意図しない値の変化を特定しやすくなります。
シェーダコードへのデバッグ用出力
DirectX9のHLSLでは標準的なデバッグ用のログ出力はありませんが、疑似的に色やアルファ値を変化させて異常箇所を視覚的に検出するテクニックがあります。
float4 main(...) : COLOR {
if (someCondition) {
return float4(1, 0, 0, 1); // 赤色で異常を示す
}
// 通常処理
}
注意点
- シェーダのデバッグはGPUの並列処理特性によりCPUのデバッグとは異なるため、問題の切り分けが難しいことがあります
- PIXなどのツールを活用し、段階的に問題箇所を特定することが重要です
これらのデバッグ技法を組み合わせることで、DirectX9の3D描画プログラムの問題を効率的に発見し、修正できます。
特にD3DデバッグランタイムとPIXは強力なツールであり、シェーダの問題解決にも欠かせません。
マルチスレッドレンダリング
DirectX9でのマルチスレッドレンダリングは、CPUの複数コアを活用して描画処理の効率化を図る技術です。
リソース生成や描画コマンドの準備を別スレッドで行い、描画スレッドと分離することでパフォーマンス向上が期待できます。
ただし、DirectX9のCOMベースのAPIはスレッドセーフではないため、適切な同期処理が不可欠です。
リソース生成スレッドと描画スレッド
マルチスレッドレンダリングの基本的な構成は、リソース生成スレッド(ワーカースレッド)と描画スレッド(メインスレッド)に分けることです。
- リソース生成スレッド
テクスチャや頂点バッファ、シェーダなどのGPUリソースを生成・更新します。
これにより、描画スレッドの負荷を軽減し、フレームレートの安定化を図ります。
- 描画スレッド
実際の描画コマンドを発行し、画面に表示します。
リソース生成スレッドから準備されたリソースを利用します。
実装例のイメージ
// リソース生成スレッド関数
void ResourceThreadFunc() {
while (running) {
// 新しいテクスチャやバッファを作成・更新
CreateOrUpdateResources();
// 描画スレッドに通知
resourceReadyEvent.Set();
// 次の更新まで待機
resourceUpdateEvent.Wait();
}
}
// 描画スレッドのループ
void RenderLoop() {
while (running) {
// リソース生成完了を待つ
resourceReadyEvent.Wait();
// 描画処理
RenderFrame();
// リソース生成スレッドに次の更新を指示
resourceUpdateEvent.Set();
}
}
このようにイベントやシグナルでスレッド間の連携を行います。
クリティカルセクションと同期
DirectX9のデバイスやリソースは基本的にスレッドセーフではありません。
複数スレッドから同時にアクセスすると競合や破損が発生するため、同期機構を使って排他制御を行います。
クリティカルセクションの利用
Windows APIのCRITICAL_SECTION
は軽量な排他制御機構で、同時アクセスを防ぎます。
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
// リソースアクセス時
EnterCriticalSection(&cs);
// DirectXリソースの操作
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
描画スレッドとリソース生成スレッドが同じリソースにアクセスする場合は、必ずクリティカルセクションで保護します。
他の同期手段
- イベントオブジェクト
スレッド間の処理完了通知に使います。
- ミューテックス
複数プロセス間での排他制御に使いますが、単一プロセス内ならクリティカルセクションで十分です。
- スピンロック
短時間の排他処理に適していますが、CPU負荷が高くなるため注意が必要です。
DrawCall並列化
描画コマンド(DrawCall)はCPUからGPUへ送る命令単位で、多数のDrawCallがあるとCPU負荷が増大します。
DrawCallの並列化は、複数スレッドで描画コマンドの生成や準備を行い、CPU負荷を分散する手法です。
コマンドバッファの分割
各スレッドが独立して描画コマンドを生成し、後でメインスレッドがまとめてGPUに送る方法があります。
DirectX9では標準でコマンドバッファのマルチスレッド生成をサポートしていませんが、アプリケーション側でコマンドの分割管理が可能です。
マルチスレッドレンダリングの制約
- DirectX9の
IDirect3DDevice9
は基本的に単一スレッドでの使用を想定しているため、複数スレッドからの同時呼び出しは避けます IDirect3DDevice9Ex
(DirectX9Ex)ではマルチスレッドレンダリングのサポートが強化されているため、可能ならこちらを利用します
実装例の考え方
// スレッドごとに描画コマンドを準備(擬似コード)
void PrepareDrawCalls(int threadId) {
// 頂点バッファやインデックスバッファの準備
// 描画パラメータの設定
// コマンドリストに追加(アプリケーション管理)
}
// メインスレッドでコマンドを統合して描画
void ExecuteDrawCalls() {
for (auto& cmdList : allThreadCmdLists) {
for (auto& cmd : cmdList) {
pDevice->SetStreamSource(...);
pDevice->DrawPrimitive(...);
}
}
}
効果と注意点
- CPUの複数コアを活用し、描画準備のボトルネックを軽減できます
- スレッド間の同期やリソース共有の設計が複雑になります
- 描画順序や状態変更の管理に注意しないと描画結果が乱れる可能性があります
マルチスレッドレンダリングはDirectX9の制約を理解しつつ、リソース生成と描画処理を分離し、適切な同期を行うことで効果的にパフォーマンスを向上させられます。
DrawCallの並列化は高度な技術ですが、CPU負荷の分散に大きく寄与します。
互換性と移植性
DirectX9を用いた3D描画プログラミングでは、さまざまな環境やハードウェアでの動作を考慮した互換性と移植性の確保が重要です。
特に古いGPUの対応状況やWindowsのバージョン依存、他のAPIとの共存問題に注意が必要です。
ここではそれぞれのポイントを詳しく解説します。
古いGPUとシェーダモデル
DirectX9は2002年にリリースされ、多くのGPUでサポートされていますが、GPUの世代や性能によって対応可能なシェーダモデル(Shader Model)が異なります。
シェーダモデルは頂点シェーダやピクセルシェーダの機能セットや命令数の上限を示す規格です。
シェーダモデル | 主な特徴 | 対応GPU例 |
---|---|---|
1.1 | 基本的な頂点・ピクセルシェーダ | GeForce 3、Radeon 8500など初期世代 |
2.0 | 命令数増加、より複雑なシェーダ可能 | GeForce FXシリーズ、Radeon 9500以降 |
3.0 | 高度な機能、ループや分岐対応 | GeForce 6シリーズ以降、Radeon Xシリーズ |
互換性確保のポイント
- シェーダモデルの検出
IDirect3D9::GetDeviceCaps
でGPUの対応シェーダモデルを取得し、使用可能な機能を判定します。
D3DCAPS9 caps;
pD3D->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps);
if (caps.VertexShaderVersion >= D3DVS_VERSION(2,0)) {
// シェーダモデル2.0以上対応
}
- フォールバックの実装
高機能なシェーダを使えない環境では、固定機能パイプラインや低バージョンのシェーダに切り替える処理を用意します。
- 命令数制限の考慮
シェーダの命令数や定数数が制限されているため、複雑なシェーダは分割や簡略化が必要です。
- テクスチャフォーマットの対応
古いGPUは一部のテクスチャフォーマットをサポートしない場合があるため、汎用的なフォーマットを選択します。
Windowsバージョン依存
DirectX9はWindows 98以降の多くのWindows OSで動作しますが、OSのバージョンによって利用可能な機能やパフォーマンスに差があります。
主なWindowsバージョンとDirectX9の関係
Windowsバージョン | DirectX9対応状況 | 注意点 |
---|---|---|
Windows 98/ME | DirectX9対応(限定的) | ドライバの安定性や機能制限が多い |
Windows 2000 | DirectX9対応 | 一部の新機能は非対応の可能性あり |
Windows XP | DirectX9標準搭載 | 最も広く使われた環境 |
Windows Vista以降 | DirectX9は互換レイヤーとして動作 | DirectX10/11が標準。パフォーマンス差あり |
OS依存の注意点
- ドライバの互換性
OSごとにGPUドライバのバージョンや機能が異なり、特定の機能が使えない場合があります。
- DirectXランタイムのバージョン
ユーザー環境に適切なDirectX9ランタイムがインストールされているか確認が必要です。
- 管理者権限やセキュリティ設定
一部の環境ではDirectXのインストールや動作に制限がかかることがあります。
他APIとの共存
DirectX9アプリケーションは、他のグラフィックスAPIやミドルウェアと共存するケースがあります。
例えば、OpenGLやDirectX11、Vulkan、または物理演算ライブラリやUIフレームワークとの併用です。
共存時の課題
- デバイスの競合
複数APIが同じGPUリソースを使う場合、デバイスの取得やリセットで競合が発生することがあります。
- メッセージループの統合
複数APIを使う場合、ウィンドウメッセージの処理や描画タイミングの調整が複雑になります。
- リソース共有の困難さ
DirectX9と他API間でテクスチャやバッファを共有するのは基本的に困難で、データのコピーや変換が必要です。
共存のための対策
- レンダリングコンテキストの切り替え管理
APIごとに描画コンテキストを明確に分け、切り替え時に状態をリセットします。
- 別スレッドでのAPI利用
可能であれば、異なるAPIを別スレッドで動作させ、リソース競合を回避します。
- 共通の抽象化レイヤーの導入
複数APIを抽象化するミドルウェアやエンジンを利用し、API依存を減らす。
古いGPUやWindows環境の違いを考慮しつつ、API間の共存問題にも対応することで、DirectX9アプリケーションの互換性と移植性を高められます。
これにより幅広いユーザー環境で安定した動作を実現可能です。
まとめ
本記事では、DirectX9を用いた3D描画プログラミングの基礎から応用まで幅広く解説しました。
デバイス初期化やシェーダ活用、描画ループの構造、行列変換、頂点管理、固定機能パイプライン、プログラマブルシェーダ、テクスチャ技法、ライティング拡張、シャドウ表現、ポストプロセス効果、ジオメトリ生成、衝突検出、リソース管理、パフォーマンス測定、デバッグ技法、マルチスレッドレンダリング、互換性と移植性まで、実践的なポイントを具体例とともに紹介しています。
これにより、DirectX9を活用した安定かつ高品質な3Dアプリケーション開発の理解とスキル向上が期待できます。