【C++】DirectX9ゲーム開発入門:軽量3D描画とシェーダーモデル2.0活用テクニック
DirectX9はC++
で動作する軽量APIで、Windows向け3Dゲームを高速に描画しながら入力やサウンドも一括管理できる点が魅力です。
シェーダーモデル2.0を活用すれば動的ライティングやエフェクトを低負荷で実装でき、古いGPUでも安定して動作します。
最新APIに比べ機能は限定されますが、学習コストが低く、ローエンドPCやレトロゲームの再現に最適な選択肢です。
DirectX9の特徴と利点
DirectX9は、Windowsプラットフォーム上でのゲームやマルチメディアアプリケーション開発において、非常に重要な役割を果たしているAPIセットです。
特に3Dグラフィックスの描画に強みがあり、ゲーム開発者にとって多くの利点を提供しています。
ここでは、DirectX9の特徴と利点について詳しく解説します。
レガシーハードウェア互換性
DirectX9は2002年にリリースされて以来、長期間にわたり多くのWindows環境で利用されてきました。
そのため、古い世代のグラフィックカードやハードウェアでも動作する互換性が高い点が大きな特徴です。
たとえば、DirectX9はシェーダーモデル2.0をサポートしており、これは2000年代初頭のGPUでも対応可能な仕様です。
これにより、最新のハードウェアだけでなく、比較的古いPC環境でも3Dゲームを動作させることができます。
このレガシー互換性は、以下のようなメリットをもたらします。
- 幅広いユーザー層への対応
古いPCを使っているユーザーもゲームを楽しめるため、ターゲットユーザーの拡大につながります。
- 開発コストの削減
新しいハードウェアに特化した開発を行う必要がなく、幅広い環境で動作検証が可能です。
- 安定した動作環境
長期間にわたり多くの環境で使われてきたため、ドライバの安定性やトラブルシューティング情報が豊富です。
このように、DirectX9は古いハードウェアでも動作するため、特にインディーゲームや教育用ソフトウェアなど、幅広い環境での利用を想定した開発に適しています。
シェーダーモデル2.0の実用性
DirectX9のもう一つの大きな特徴は、シェーダーモデル2.0(Shader Model 2.0)をサポートしていることです。
シェーダーモデルとは、GPU上で実行されるプログラム(シェーダー)の仕様を示すもので、グラフィックスの表現力や処理能力に直結します。
シェーダーモデル2.0は、以下のような機能を提供します。
- 頂点シェーダーとピクセルシェーダーのプログラマブル化
頂点の変換やライティング、ピクセル単位の色計算をプログラムで自由に制御できます。
- 複雑なエフェクトの実装が可能
反射、屈折、影、光源の動的計算など、リアルな表現が実現できます。
- パフォーマンスと表現力のバランス
当時のハードウェア性能に最適化されており、比較的軽量ながら高度なグラフィックス表現が可能です。
以下は、シェーダーモデル2.0の頂点シェーダーの簡単な例です。
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;
}
このシェーダーは、頂点の位置をワールド・ビュー・プロジェクション行列で変換し、色をそのまま出力しています。
シェーダーモデル2.0の仕様により、こうした処理をGPU上で効率的に実行できます。
シェーダーモデル2.0の実用性は、以下の点でゲーム開発に役立ちます。
- カスタムシェーダーによる独自表現
固有のビジュアルスタイルやエフェクトを実装しやすいです。
- パフォーマンスの最適化
CPU負荷をGPUに分散できるため、全体の処理効率が向上します。
- 将来の拡張性
後のシェーダーモデルへの移行もスムーズで、段階的な機能追加が可能です。
このように、DirectX9のシェーダーモデル2.0は、当時のハードウェア性能を最大限に活かしつつ、柔軟なグラフィックス表現を実現するための重要な技術基盤となっています。
軽量APIによる高速開発
DirectX9は、比較的軽量なAPI設計であるため、ゲーム開発の初期段階から高速に開発を進められます。
特にC++との親和性が高く、低レベルのハードウェア制御を行いながらも、使いやすいインターフェースを提供しています。
軽量APIの利点は以下の通りです。
- シンプルな初期化と描画フロー
Direct3Dデバイスの作成や描画コマンドの発行が直感的で、学習コストが低いです。
- リアルタイム描画の高速化
無駄な抽象化が少なく、GPUとの通信が効率的に行えます。
- デバッグやチューニングが容易
APIの挙動が明確で、問題発生時の原因特定がしやすいです。
以下は、Direct3D9のデバイス初期化の簡単な例です。
#include <d3d9.h>
#pragma comment(lib, "d3d9.lib")
LPDIRECT3D9 d3d = nullptr;
LPDIRECT3DDEVICE9 d3dDevice = nullptr;
bool InitD3D(HWND hWnd) {
d3d = Direct3DCreate9(D3D_SDK_VERSION);
if (!d3d) return false;
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
HRESULT hr = d3d->CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp,
&d3dDevice
);
return SUCCEEDED(hr);
}
このコードは、ウィンドウハンドルhWnd
を使ってDirect3D9デバイスを作成しています。
D3DPRESENT_PARAMETERS
構造体で描画モードを設定し、CreateDevice
関数でデバイスを生成します。
非常にシンプルな構成で、すぐに描画処理を開始できます。
軽量APIであることは、特に以下のような開発シナリオで効果を発揮します。
- プロトタイプ開発
短期間で動作する3D描画を実装し、アイデア検証が可能です。
- 教育用途
3Dグラフィックスの基礎を学ぶ際に、複雑な抽象化に惑わされずに理解できます。
- リソース制約のある環境
古いPCや組み込み機器でも動作しやすい設計です。
このように、DirectX9の軽量API設計は、開発のスピードアップと効率化に大きく貢献します。
C++との組み合わせで、低レベルの制御と高いパフォーマンスを両立できるため、ゲーム開発の現場で今なお根強い人気があります。
最小限のWin32ウィンドウ生成
WindowsアプリケーションでDirectX9を使う際、まずは描画先となるウィンドウを作成する必要があります。
ここでは、最小限のコードでWin32ウィンドウを生成する方法を解説します。
ウィンドウクラス登録
ウィンドウを作成するには、まずウィンドウクラスを登録します。
ウィンドウクラスは、ウィンドウの基本的な属性や動作を定義する構造体WNDCLASSEX
を使って設定します。
以下は、ウィンドウクラスを登録するサンプルコードです。
#include <windows.h>
// ウィンドウプロシージャの宣言
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
bool RegisterWindowClass(HINSTANCE hInstance, const wchar_t* className) {
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW; // ウィンドウの再描画スタイル
wc.lpfnWndProc = WndProc; // ウィンドウプロシージャの指定
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance; // アプリケーションインスタンスハンドル
wc.hIcon = LoadIcon(nullptr, IDI_APPLICATION); // デフォルトアイコン
wc.hCursor = LoadCursor(nullptr, IDC_ARROW); // デフォルトカーソル
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // 背景色
wc.lpszMenuName = nullptr; // メニューなし
wc.lpszClassName = className; // クラス名
wc.hIconSm = LoadIcon(nullptr, IDI_APPLICATION); // 小アイコン
if (!RegisterClassEx(&wc)) {
MessageBox(nullptr, L"ウィンドウクラスの登録に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
return false;
}
return true;
}
このコードでは、WNDCLASSEX
構造体の各メンバを設定しています。
特に重要なのはlpfnWndProc
で、ここにウィンドウのメッセージを処理する関数を指定します。
RegisterClassEx
関数でクラスを登録し、成功すればtrue
を返します。
className
はウィンドウ作成時に指定するクラス名と一致させる必要があります。
メッセージループとフレーム制御
ウィンドウが作成された後は、Windowsのメッセージを処理するためのメッセージループが必要です。
メッセージループは、ユーザーの入力やシステムからの通知を受け取り、適切に処理します。
以下は、基本的なメッセージループの例です。
int RunMessageLoop() {
MSG msg = {};
while (true) {
// メッセージがあれば取得し、処理する
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break; // WM_QUITが来たらループ終了
}
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// メッセージがない間はここでフレーム処理を行う
// 例: ゲームの更新や描画処理
}
}
return static_cast<int>(msg.wParam);
}
このループでは、PeekMessage
を使ってメッセージを非同期に取得しています。
メッセージが存在しない場合は、ゲームの更新や描画処理を行うことができます。
これにより、ウィンドウが応答しつつリアルタイムな描画が可能です。
TranslateMessage
はキーボードメッセージの変換を行い、DispatchMessage
はメッセージをウィンドウプロシージャに送ります。
ウィンドウプロシージャの簡単な例も示します。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_DESTROY:
PostQuitMessage(0); // アプリケーション終了メッセージを送る
return 0;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
このWndProc
は、ウィンドウが閉じられたときにPostQuitMessage
を呼び出してメッセージループを終了させます。
それ以外のメッセージはデフォルト処理に任せています。
これらを組み合わせると、最小限のWin32ウィンドウ生成とメッセージ処理が実現できます。
以下に、ウィンドウ作成からメッセージループまでをまとめたサンプルプログラムを示します。
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
bool RegisterWindowClass(HINSTANCE hInstance, const wchar_t* className) {
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszClassName = className;
wc.hIconSm = LoadIcon(nullptr, IDI_APPLICATION);
if (!RegisterClassEx(&wc)) {
MessageBox(nullptr, L"ウィンドウクラスの登録に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
return false;
}
return true;
}
int RunMessageLoop() {
MSG msg = {};
while (true) {
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// ここでゲームの更新や描画処理を行う
}
}
return static_cast<int>(msg.wParam);
}
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
const wchar_t* className = L"MinimalWindowClass";
if (!RegisterWindowClass(hInstance, className)) {
return -1;
}
HWND hWnd = CreateWindowEx(
0,
className,
L"DirectX9 最小限ウィンドウ",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
800, 600,
nullptr,
nullptr,
hInstance,
nullptr
);
if (!hWnd) {
MessageBox(nullptr, L"ウィンドウの作成に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
return -1;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return RunMessageLoop();
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
このプログラムを実行すると、800×600ピクセルのウィンドウが表示され、閉じるボタンを押すと正常に終了します。
ゲームやDirectX9の描画処理は、RunMessageLoop
内のコメント部分に追加していきます。
Direct3D初期化とデバイス設定
Direct3D9を使った3D描画の第一歩は、Direct3Dオブジェクトの取得とデバイスの初期化です。
ここでは、Direct3Dオブジェクトの生成から、描画に必要なデバイスの設定、さらにフルスクリーンとウィンドウモードの切り替え方法、そしてデバイスロストへの対応方法まで詳しく解説します。
Direct3Dオブジェクト取得
Direct3D9の機能を利用するには、まずIDirect3D9
インターフェースのオブジェクトを作成します。
これはDirect3Dのエントリーポイントであり、デバイスの作成やハードウェア情報の取得に使います。
以下は、Direct3Dオブジェクトを取得するサンプルコードです。
#include <windows.h>
#include <d3d9.h>
#pragma comment(lib, "d3d9.lib")
LPDIRECT3D9 g_pD3D = nullptr;
bool InitDirect3D() {
// Direct3D9オブジェクトの作成
g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if (!g_pD3D) {
MessageBox(nullptr, L"Direct3D9の作成に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
return false;
}
return true;
}
void CleanupDirect3D() {
if (g_pD3D) {
g_pD3D->Release();
g_pD3D = nullptr;
}
}
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
if (!InitDirect3D()) {
return -1;
}
// ここにデバイス作成やメインループ処理を追加
CleanupDirect3D();
return 0;
}
このコードでは、Direct3DCreate9
関数にD3D_SDK_VERSION
を渡してDirect3D9オブジェクトを作成しています。
失敗した場合はエラーメッセージを表示します。
作成後は必ずRelease
で解放してください。
プレゼンテーションパラメータ構成
Direct3Dデバイスを作成する際には、D3DPRESENT_PARAMETERS
構造体で描画に関する設定を行います。
ここでウィンドウモードやバックバッファのフォーマット、スワップ効果などを指定します。
以下は、ウィンドウモードでの基本的なD3DPRESENT_PARAMETERS
設定例です。
D3DPRESENT_PARAMETERS d3dpp = {};
d3dpp.Windowed = TRUE; // ウィンドウモード
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // スワップ効果
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; // ウィンドウモードではUNKNOWNを指定
d3dpp.BackBufferCount = 1; // バックバッファ数
d3dpp.EnableAutoDepthStencil = TRUE; // 深度ステンシルバッファを自動生成
d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; // 24ビット深度、8ビットステンシル
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; // 垂直同期なし
この設定は、ウィンドウモードで高速に描画するための基本形です。
BackBufferFormat
にD3DFMT_UNKNOWN
を指定すると、ウィンドウのフォーマットに合わせて自動設定されます。
フルスクリーンとウィンドウモード切替
フルスクリーンモードに切り替える場合は、Windowed
をFALSE
にし、BackBufferFormat
に適切なピクセルフォーマットを指定します。
たとえば、32ビットカラーの場合はD3DFMT_X8R8G8B8
を使います。
d3dpp.Windowed = FALSE; // フルスクリーンモード
d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8; // 32ビットカラー
d3dpp.BackBufferWidth = 1920; // 解像度幅
d3dpp.BackBufferHeight = 1080; // 解像度高さ
フルスクリーンモードでは、解像度やリフレッシュレートを明示的に指定することが多いです。
リフレッシュレートはFullScreen_RefreshRateInHz
で設定できますが、0を指定するとデフォルトになります。
ウィンドウモードとフルスクリーンモードの切り替えは、デバイスの再作成が必要になるため、以下のような手順で行います。
- 既存のデバイスを
Release
またはReset
で解放またはリセットします。 D3DPRESENT_PARAMETERS
のWindowed
や解像度を変更します。CreateDevice
またはReset
でデバイスを再作成またはリセットします。
デバイスロストへの対応
Direct3D9では、デバイスが「ロスト」状態になることがあります。
これは、Alt+Tabで他のアプリに切り替えたときや、スクリーンセーバー起動時、またはフルスクリーン切り替え時に発生します。
ロスト状態のデバイスは描画ができず、復旧処理が必要です。
デバイスロストの状態はIDirect3DDevice9::TestCooperativeLevel
関数で判定します。
戻り値によって以下のように処理します。
戻り値 | 意味 | 対応方法 |
---|---|---|
D3D_OK | デバイスは正常 | 通常の描画処理を続行 |
D3DERR_DEVICELOST | デバイスはロスト状態(復旧不可) | 待機し続ける |
D3DERR_DEVICENOTRESET | デバイスはリセット可能 | Reset 関数でデバイスをリセット |
以下は、デバイスロスト対応のサンプルコードです。
HRESULT HandleDeviceLost(LPDIRECT3DDEVICE9 device, D3DPRESENT_PARAMETERS& d3dpp) {
HRESULT hr = device->TestCooperativeLevel();
if (hr == D3DERR_DEVICELOST) {
// デバイスロスト中なので待機
Sleep(100);
return hr;
}
else if (hr == D3DERR_DEVICENOTRESET) {
// デバイスをリセットする
hr = device->Reset(&d3dpp);
if (SUCCEEDED(hr)) {
// リセット成功後、リソースの再作成などを行う
// 例: 頂点バッファやテクスチャの再ロード
}
return hr;
}
return hr;
}
この関数は、メインループ内で呼び出し、D3D_OK
以外の場合は適切に待機やリセット処理を行います。
リセット成功後は、失われたリソースを再作成する必要があります。
これらの手順を踏むことで、Direct3D9の初期化とデバイス設定が正しく行え、安定した描画環境を構築できます。
特にデバイスロストへの対応は、ゲームの安定動作に不可欠な処理です。
描画フローの基礎
DirectX9を使った3D描画では、フレームごとに描画処理を行うための基本的な流れを理解することが重要です。
ここでは、フレームバッファのクリア、シーングラフ設計の考え方、そして描画呼び出しの最適化について詳しく説明します。
フレームバッファのクリア
描画を開始する前に、前フレームの残像を消すためにフレームバッファをクリアします。
これにより、画面が正しく更新され、不要な描画の重なりを防げます。
Direct3D9では、IDirect3DDevice9::Clear
メソッドを使ってクリア処理を行います。
主に以下のバッファをクリアします。
- ターゲットバッファ(バックバッファ):描画対象のカラーバッファ
- 深度バッファ(Zバッファ):奥行き情報を保持するバッファ
- ステンシルバッファ:マスク処理に使うバッファ(必要に応じて)
以下は、フレームバッファをクリアするサンプルコードです。
HRESULT ClearFrame(LPDIRECT3DDEVICE9 device) {
// 画面を黒色でクリアし、深度バッファも初期化
return device->Clear(
0, // クリアする矩形の数(0は全画面)
nullptr, // クリアする矩形(nullptrで全画面)
D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, // クリア対象(色と深度)
D3DCOLOR_XRGB(0, 0, 0), // クリア色(黒)
1.0f, // 深度値の初期値(最大値)
0 // ステンシル値(使わない場合は0)
);
}
この関数は、描画開始時に呼び出し、バックバッファと深度バッファをクリアします。
クリア色は黒(RGB=0,0,0)に設定していますが、ゲームの演出に応じて変更可能です。
クリア後は、BeginScene
とEndScene
で描画処理を囲み、Present
で画面に表示します。
device->BeginScene();
// 描画処理
device->EndScene();
device->Present(nullptr, nullptr, nullptr, nullptr);
シーングラフ設計
シーングラフとは、3Dシーン内のオブジェクトやカメラ、ライトなどの情報を階層的に管理するデータ構造です。
これにより、複雑なシーンの管理や描画順序の制御が容易になります。
シーングラフの基本的な構成要素は以下の通りです。
- ノード(Node):シーン内の各要素を表す基本単位。変換行列や子ノードを持ちます
- トランスフォーム(Transform):位置・回転・スケールなどの変換情報
- ジオメトリ(Geometry):描画対象のメッシュやモデル
- ライト(Light):光源情報
- カメラ(Camera):視点情報
シーングラフの階層構造により、親ノードの変換が子ノードに連鎖的に適用されます。
たとえば、キャラクターの腕の動きは腕ノードの変換に加え、体ノードの変換も反映されます。
簡単なシーングラフノードのクラス例を示します。
#include <vector>
#include <d3dx9math.h>
class SceneNode {
public:
D3DXMATRIX transform; // ローカル変換行列
std::vector<SceneNode*> children;
SceneNode() {
D3DXMatrixIdentity(&transform);
}
void AddChild(SceneNode* child) {
children.push_back(child);
}
void Draw(LPDIRECT3DDEVICE9 device, const D3DXMATRIX& parentTransform) {
D3DXMATRIX world = transform * parentTransform;
// ここでworld行列をセットして描画処理を行う
device->SetTransform(D3DTS_WORLD, &world);
// 子ノードの描画
for (auto child : children) {
child->Draw(device, world);
}
}
};
このクラスは、親の変換行列を受け取り、自身のローカル変換と掛け合わせてワールド変換を計算します。
描画時にはSetTransform
でワールド行列を設定し、子ノードを再帰的に描画します。
シーングラフを使うことで、複雑なシーンの管理がシンプルになり、描画順序や変換の一貫性を保てます。
描画呼び出しの最適化
Direct3D9での描画呼び出し(DrawCall)は、GPUへの命令送信回数を指し、パフォーマンスに大きく影響します。
呼び出し回数が多いとCPU負荷が増え、フレームレートが低下するため、最適化が重要です。
主な最適化手法は以下の通りです。
バッチング(Batching)
複数の小さなメッシュをまとめて一度の描画呼び出しで処理します。
頂点バッファやインデックスバッファを結合し、状態変更を減らすことで効率化します。
状態変更の削減
シェーダーやテクスチャ、レンダーステートの切り替えはコストが高いため、同じ状態のオブジェクトを連続して描画するように順序を工夫します。
カリング(Culling)
視界に入っていないオブジェクトを描画しないようにします。
フラスタムカリングやオクルージョンカリングを使い、無駄な描画を減らします。
レベルオブディテール(LOD)
遠くのオブジェクトはポリゴン数の少ないモデルに切り替え、描画負荷を軽減します。
頂点バッファの効率的利用
動的頂点バッファと静的頂点バッファを使い分け、GPUへのデータ転送を最小限に抑えます。
以下は、描画呼び出しの最適化を意識した簡単な描画ループの例です。
device->BeginScene();
// 状態変更を最小限にするため、同じテクスチャのオブジェクトをまとめて描画
for (auto& batch : batches) {
device->SetTexture(0, batch.texture);
device->SetStreamSource(0, batch.vertexBuffer, 0, sizeof(Vertex));
device->SetIndices(batch.indexBuffer);
device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, batch.vertexCount, 0, batch.primitiveCount);
}
device->EndScene();
device->Present(nullptr, nullptr, nullptr, nullptr);
この例では、batches
は同じテクスチャやシェーダー状態を持つ描画単位の集合です。
状態変更を減らすことで、描画パフォーマンスを向上させています。
描画フローの基礎を押さえることで、DirectX9を使った3Dゲーム開発の土台が築けます。
フレームバッファのクリアからシーングラフの設計、描画呼び出しの最適化まで、効率的な描画処理を心がけましょう。
頂点バッファとインデックスバッファ
DirectX9で3Dモデルを描画する際、頂点データやポリゴンの構成情報を効率的に管理するために「頂点バッファ」と「インデックスバッファ」を使います。
これらはGPUにデータを転送し、高速な描画を実現するための重要な仕組みです。
頂点フォーマット定義
頂点バッファに格納する頂点データは、位置情報だけでなく法線やテクスチャ座標、色など複数の属性を持つことが多いです。
これらの属性の構成を「頂点フォーマット」として定義し、GPUに正しく解釈させる必要があります。
DirectX9では、頂点フォーマットをD3DVERTEXELEMENT9
構造体の配列で定義し、IDirect3DDevice9::CreateVertexDeclaration
で頂点宣言(Vertex Declaration)を作成します。
以下は、位置(3D座標)とテクスチャ座標(2D)を持つ頂点フォーマットの例です。
#include <d3d9.h>
#include <d3dx9.h>
// 頂点構造体
struct Vertex {
float x, y, z; // 位置
float u, v; // テクスチャ座標
};
// 頂点フォーマット定義
const D3DVERTEXELEMENT9 VertexElements[] = {
{0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{0, 12, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};
この配列は、ストリーム0のオフセット0に3つのfloat(位置)、オフセット12に2つのfloat(テクスチャ座標)があることを示しています。
D3DDECL_END()
で終端を示します。
頂点宣言は以下のように作成します。
LPDIRECT3DVERTEXDECLARATION9 vertexDecl = nullptr;
device->CreateVertexDeclaration(VertexElements, &vertexDecl);
device->SetVertexDeclaration(vertexDecl);
頂点フォーマットを正しく定義することで、GPUが頂点データを正しく解釈し、シェーダーや固定機能パイプラインで利用できます。
動的バッファと静的バッファ
頂点バッファやインデックスバッファは、用途に応じて「動的バッファ」と「静的バッファ」に分けて使います。
バッファ種別 | 特徴 | 使用例 |
---|---|---|
静的バッファ | 作成後に内容がほとんど変わらない | モデルの頂点データ、地形など |
動的バッファ | 頻繁に内容を書き換える必要がある | 頂点アニメーション、パーティクル |
静的バッファはD3DUSAGE_WRITEONLY
を指定して作成し、CPUからの書き込みは少なく、GPUからの読み出しに最適化されています。
動的バッファはD3DUSAGE_DYNAMIC
とD3DPOOL_DEFAULT
を指定し、D3DLOCK_DISCARD
やD3DLOCK_NOOVERWRITE
フラグを使って効率的に更新します。
以下は、動的頂点バッファの作成例です。
LPDIRECT3DVERTEXBUFFER9 vertexBuffer = nullptr;
HRESULT hr = device->CreateVertexBuffer(
sizeof(Vertex) * vertexCount,
D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY,
0,
D3DPOOL_DEFAULT,
&vertexBuffer,
nullptr
);
動的バッファの更新は以下のように行います。
void UpdateDynamicVertexBuffer(LPDIRECT3DVERTEXBUFFER9 vb, Vertex* vertices, int count) {
void* pData = nullptr;
vb->Lock(0, sizeof(Vertex) * count, &pData, D3DLOCK_DISCARD);
memcpy(pData, vertices, sizeof(Vertex) * count);
vb->Unlock();
}
D3DLOCK_DISCARD
を使うことで、GPUがまだ使用中のバッファ領域を破棄し、新しい領域に書き込むため高速です。
ストリーム管理
Direct3D9では、頂点データは「ストリーム」と呼ばれる単位で管理されます。
1つの頂点バッファをストリーム0にセットし、複数の頂点バッファを複数ストリームに分けてセットすることも可能です。
ストリーム管理の主なポイントは以下の通りです。
- ストリーム番号
SetStreamSource
関数でストリーム番号(0~3など)を指定し、複数の頂点バッファを同時に使用可能です。
- 頂点宣言との対応
頂点宣言で各頂点要素がどのストリームから読み込まれるかを指定できます。
- 効率的なデータ管理
頂点属性ごとにバッファを分けることで、頻繁に変わる属性だけを更新しやすくなります。
以下は、ストリーム0に頂点バッファをセットする例です。
device->SetStreamSource(0, vertexBuffer, 0, sizeof(Vertex));
複数ストリームを使う場合は、例えばストリーム0に位置データ、ストリーム1に法線データをセットし、頂点宣言でそれぞれのストリームを指定します。
ストリーム管理を適切に行うことで、GPUへのデータ転送を最小限に抑え、描画パフォーマンスを向上させることができます。
これらの頂点バッファとインデックスバッファの基本を押さえることで、DirectX9で効率的かつ柔軟な3D描画が可能になります。
頂点フォーマットの正確な定義、用途に応じたバッファの使い分け、そしてストリーム管理を意識して実装しましょう。
シェーダーモデル2.0の導入
DirectX9でシェーダーモデル2.0を活用するには、HLSL(High Level Shader Language)でシェーダープログラムを記述し、コンパイルしてGPUに適用します。
ここでは、HLSLファイルの構成、コンパイルとシェーダーハンドルの取得方法、さらに定数レジスタへの行列やライトパラメータの転送方法を詳しく説明します。
HLSLファイル構成
HLSLファイルは、頂点シェーダー(Vertex Shader)やピクセルシェーダー(Pixel Shader)を記述するテキストファイルです。
シェーダーモデル2.0では、vs_2_0
やps_2_0
というターゲットプロファイルを指定してコンパイルします。
基本的なHLSLファイルの構成例を示します。
// SimpleShader.hlsl
// 定数バッファ(行列など)
float4x4 WorldViewProj;
// 頂点入力構造体
struct VS_INPUT {
float4 Pos : POSITION;
float4 Color : COLOR0;
};
// 頂点出力構造体
struct VS_OUTPUT {
float4 Pos : POSITION;
float4 Color : COLOR0;
};
// 頂点シェーダー
VS_OUTPUT VSMain(VS_INPUT input) {
VS_OUTPUT output;
output.Pos = mul(input.Pos, WorldViewProj); // 変換行列を掛ける
output.Color = input.Color;
return output;
}
// ピクセルシェーダー
float4 PSMain(VS_OUTPUT input) : COLOR {
return input.Color; // 頂点カラーをそのまま出力
}
この例では、頂点シェーダーで頂点位置にWorldViewProj
行列を掛けて変換し、頂点カラーをそのままピクセルシェーダーに渡しています。
ピクセルシェーダーは単純に色を返すだけの処理です。
HLSLファイルは複数のシェーダー関数を含めることができ、コンパイル時にどの関数をエントリーポイントとして使うか指定します。
コンパイルとハンドル取得
HLSLファイルをコンパイルし、シェーダーオブジェクトを作成するには、DirectX9のD3DXCompileShaderFromFile
関数やD3DXCompileShader
関数を使います。
ここではファイルからコンパイルする例を示します。
#include <d3d9.h>
#include <d3dx9.h>
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
LPDIRECT3DVERTEXSHADER9 g_pVertexShader = nullptr;
LPDIRECT3DPIXELSHADER9 g_pPixelShader = nullptr;
bool CompileShaders(LPDIRECT3DDEVICE9 device) {
ID3DXBuffer* pVSCode = nullptr;
ID3DXBuffer* pPSCode = nullptr;
ID3DXBuffer* pErrorMsgs = nullptr;
// 頂点シェーダーのコンパイル
HRESULT hr = D3DXCompileShaderFromFile(
L"SimpleShader.hlsl",
nullptr,
nullptr,
"VSMain", // 頂点シェーダーのエントリーポイント
"vs_2_0", // シェーダーモデル2.0
0,
&pVSCode,
&pErrorMsgs,
nullptr
);
if (FAILED(hr)) {
if (pErrorMsgs) {
MessageBoxA(nullptr, (char*)pErrorMsgs->GetBufferPointer(), "Vertex Shader Compile Error", MB_OK);
pErrorMsgs->Release();
}
return false;
}
// ピクセルシェーダーのコンパイル
hr = D3DXCompileShaderFromFile(
L"SimpleShader.hlsl",
nullptr,
nullptr,
"PSMain", // ピクセルシェーダーのエントリーポイント
"ps_2_0", // シェーダーモデル2.0
0,
&pPSCode,
&pErrorMsgs,
nullptr
);
if (FAILED(hr)) {
if (pErrorMsgs) {
MessageBoxA(nullptr, (char*)pErrorMsgs->GetBufferPointer(), "Pixel Shader Compile Error", MB_OK);
pErrorMsgs->Release();
}
if (pVSCode) pVSCode->Release();
return false;
}
// 頂点シェーダーオブジェクトの作成
hr = device->CreateVertexShader(
(DWORD*)pVSCode->GetBufferPointer(),
&g_pVertexShader
);
pVSCode->Release();
if (FAILED(hr)) {
pPSCode->Release();
return false;
}
// ピクセルシェーダーオブジェクトの作成
hr = device->CreatePixelShader(
(DWORD*)pPSCode->GetBufferPointer(),
&g_pPixelShader
);
pPSCode->Release();
if (FAILED(hr)) {
g_pVertexShader->Release();
g_pVertexShader = nullptr;
return false;
}
return true;
}
このコードは、SimpleShader.hlsl
ファイルから頂点シェーダーとピクセルシェーダーをそれぞれコンパイルし、シェーダーオブジェクトを作成しています。
コンパイルエラーがあればメッセージボックスで表示します。
作成したシェーダーは、描画時に以下のようにセットします。
device->SetVertexShader(g_pVertexShader);
device->SetPixelShader(g_pPixelShader);
定数レジスタ設定
行列やライトパラメータ転送
シェーダー内で使う行列やライトのパラメータは、定数レジスタに転送してGPUに渡します。
Direct3D9では、SetVertexShaderConstantF
やSetPixelShaderConstantF
関数を使ってfloat型の定数を設定します。
たとえば、WorldViewProj
行列を頂点シェーダーに渡す場合は以下のようにします。
void SetShaderConstants(LPDIRECT3DDEVICE9 device, const D3DXMATRIX& worldViewProj) {
// 行列は4x4のfloat配列として渡す
device->SetVertexShaderConstantF(0, (float*)&worldViewProj, 4);
}
ここで、0
は定数レジスタの開始番号、4
は4行分(16個のfloat)を意味します。
HLSL側のfloat4x4 WorldViewProj;
はこの定数レジスタ0~3に対応します。
ライトパラメータも同様に転送します。
例えば、単純なディレクショナルライトの方向と色を渡す場合は以下のようにします。
struct DirectionalLight {
D3DXVECTOR3 direction;
float pad; // アライメント用
D3DXVECTOR4 diffuseColor;
};
void SetLightConstants(LPDIRECT3DDEVICE9 device, const DirectionalLight& light) {
// ライト方向を定数レジスタ4にセット(xyz成分)
device->SetVertexShaderConstantF(4, (float*)&light.direction, 1);
// ライト色を定数レジスタ5にセット
device->SetVertexShaderConstantF(5, (float*)&light.diffuseColor, 1);
}
HLSL側では、対応する定数レジスタを以下のように指定します。
float3 LightDir : register(c4);
float4 LightDiffuse : register(c5);
このように、定数レジスタを明示的に指定することで、CPU側とシェーダー側のパラメータの対応を明確にできます。
これらの手順を踏むことで、シェーダーモデル2.0のシェーダーをDirectX9で正しく導入し、柔軟なグラフィックス表現が可能になります。
HLSLファイルの構成を理解し、コンパイルとハンドル取得を行い、定数レジスタに必要なパラメータを転送して描画に活用しましょう。
テクスチャとサンプリング
DirectX9でリアルな3D描画を実現するために、テクスチャの読み込みや管理、ミップマップやフィルタリングの設定、さらにはマルチテクスチャリングの活用が重要です。
ここではそれぞれのポイントを詳しく解説します。
テクスチャ読み込みと管理
テクスチャは画像データをGPUに転送し、モデルの表面に貼り付けるためのリソースです。
DirectX9では、D3DXCreateTextureFromFile
やD3DXCreateTextureFromFileEx
関数を使って簡単にテクスチャを読み込めます。
以下は、ファイルからテクスチャを読み込み、管理する基本的な例です。
#include <d3d9.h>
#include <d3dx9.h>
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
LPDIRECT3DTEXTURE9 g_pTexture = nullptr;
bool LoadTexture(LPDIRECT3DDEVICE9 device, const wchar_t* filename) {
HRESULT hr = D3DXCreateTextureFromFile(device, filename, &g_pTexture);
if (FAILED(hr)) {
MessageBox(nullptr, L"テクスチャの読み込みに失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
return false;
}
return true;
}
void ReleaseTexture() {
if (g_pTexture) {
g_pTexture->Release();
g_pTexture = nullptr;
}
}
このコードでは、LoadTexture
関数で指定したファイル名のテクスチャを読み込み、g_pTexture
に格納します。
失敗した場合はエラーメッセージを表示します。
使用後はReleaseTexture
で解放します。
テクスチャの管理は、複数のテクスチャを扱う場合は配列やマップで管理し、不要になったら適切に解放することが重要です。
メモリリークを防ぐため、Release
を忘れないようにしましょう。
ミップマップとフィルタリング
ミップマップは、テクスチャの縮小版を複数用意し、描画時の距離や角度に応じて適切な解像度のテクスチャを選択する技術です。
これにより、遠くのオブジェクトのテクスチャがぼやけたりちらついたりするのを防ぎ、描画品質とパフォーマンスを向上させます。
ミップマップ付きテクスチャを作成するには、D3DXCreateTextureFromFileEx
関数を使い、MipLevels
に0を指定すると自動生成されます。
HRESULT hr = D3DXCreateTextureFromFileEx(
device,
filename,
D3DX_DEFAULT, D3DX_DEFAULT,
0, // ミップマップレベル0で自動生成
0,
D3DFMT_UNKNOWN,
D3DPOOL_MANAGED,
D3DX_DEFAULT,
D3DX_DEFAULT,
0,
nullptr,
nullptr,
&g_pTexture
);
フィルタリングは、テクスチャの拡大縮小時にピクセルの色を補間する方法で、主に以下の3種類があります。
フィルタリング種類 | 説明 | DirectX定数 |
---|---|---|
ニアレストネイバー | 最も高速だがジャギーが目立つ | D3DTEXF_POINT |
バイリニア | 4ピクセルの線形補間 | D3DTEXF_LINEAR |
トリリニア | ミップマップ間も補間し滑らか | D3DTEXF_LINEAR (ミップマップ補間) |
フィルタリングの設定は、SetSamplerState
関数で行います。
例として、トリリニアフィルタリングを設定するコードを示します。
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
ここで、0
はサンプラー番号(テクスチャステージ)を示します。
MINFILTER
は縮小時、MAGFILTER
は拡大時、MIPFILTER
はミップマップ間の補間方法です。
マルチテクスチャリング
マルチテクスチャリングは、複数のテクスチャを同時に使って描画する技術です。
これにより、ベーステクスチャに加えてライトマップやノーマルマップ、ディテールテクスチャなどを重ねて表現できます。
DirectX9では、SetTexture
関数で複数のテクスチャを異なるテクスチャステージにセットします。
device->SetTexture(0, baseTexture); // ベーステクスチャ
device->SetTexture(1, lightMapTexture); // ライトマップ
シェーダーや固定機能パイプラインで、これらのテクスチャを組み合わせて描画します。
固定機能パイプラインの場合は、SetTextureStageState
で合成方法を指定します。
例として、2つのテクスチャを乗算合成する設定です。
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
device->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE);
device->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_CURRENT);
device->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_TEXTURE);
device->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
この設定では、ステージ0でベーステクスチャを取得し、ステージ1で現在の色とライトマップテクスチャを乗算しています。
マルチテクスチャリングは、シェーダーを使う場合はHLSL内で複数のテクスチャサンプラーを宣言し、自由に合成処理を記述できます。
テクスチャの読み込みからミップマップ生成、フィルタリング設定、そしてマルチテクスチャリングまでを適切に扱うことで、DirectX9の3D描画における表現力とパフォーマンスを大きく向上させられます。
これらの技術を活用して、リアルで美しいゲームグラフィックスを実現しましょう。
ライティングとマテリアル
3Dゲームのリアルな表現には、光の当たり方や物体の質感を正確に計算するライティングとマテリアルの設定が欠かせません。
DirectX9では、ディフューズやスペキュラーの計算、法線ベクトルやタンジェント空間の扱い、さらにピクセルシェーダによる動的ライティングが重要な技術となります。
ディフューズ・スペキュラー計算
ライティングの基本は、光源からの光が物体表面に当たったときの反射を計算することです。
代表的な反射モデルに「フォンモデル(Phongモデル)」があり、主にディフューズ反射とスペキュラー反射の2つの成分で構成されます。
- ディフューズ反射
光が表面に拡散的に反射する成分で、物体の基本的な色を決定します。
光源方向と法線ベクトルの内積で計算され、角度が浅いほど明るくなります。
- スペキュラー反射
鏡面反射成分で、光沢やハイライトを表現します。
視線方向と反射方向の角度に依存し、強い光沢感を生み出します。
フォンモデルの計算式は以下の通りです。
:最終的な光の強度(色) :環境光、拡散光、鏡面光の光源強度 :マテリアルの環境光、拡散光、鏡面光反射率 :光源方向ベクトル(正規化) :法線ベクトル(正規化) :反射ベクトル( ) :視線方向ベクトル(正規化) :鏡面反射のシャイネス(光沢度)
DirectX9の固定機能パイプラインでも、D3DLIGHT9
構造体とマテリアル設定でこれらの計算を自動的に行えますが、シェーダーを使う場合は自分で計算式を実装します。
法線ベクトルとタンジェント空間
ライティング計算の基礎となるのが法線ベクトルです。
法線はポリゴンの表面に垂直なベクトルで、光の当たり方を決定します。
正確なライティングには、各頂点やピクセルごとに正しい法線が必要です。
さらに、ノーマルマップなどの高度なテクスチャを使う場合は「タンジェント空間(Tangent Space)」の概念が重要です。
タンジェント空間は、法線ベクトルに加えて「タンジェントベクトル」と「ビットンジェントベクトル(バイナリーマップ)」を使って局所座標系を作り、ノーマルマップの法線を正しく解釈します。
- タンジェントベクトル
頂点のU方向(テクスチャ座標の横方向)に沿った接線ベクトル。
- ビットンジェントベクトル
頂点のV方向(テクスチャ座標の縦方向)に沿った接線ベクトル。
これら3つのベクトル(法線、タンジェント、ビットンジェント)は直交座標系を形成し、ノーマルマップのRGB値をこの空間に変換して正しいライティング計算を行います。
タンジェント空間の計算は、モデルの頂点データ作成時に行うか、ツールで生成します。
シェーダー内では、これらのベクトルを使ってライト方向や視線方向をタンジェント空間に変換し、ノーマルマップの法線と比較します。
ピクセルシェーダによる動的ライティング
固定機能パイプラインでは頂点単位のライティングが主ですが、ピクセルシェーダを使うとピクセル単位でライティング計算が可能になり、よりリアルで細かな光の表現ができます。
ピクセルシェーダでの動的ライティングの基本的な流れは以下の通りです。
- 頂点シェーダで頂点の位置や法線、タンジェント空間の情報を計算し、ピクセルシェーダに渡します。
- ピクセルシェーダでノーマルマップから法線を取得し、ライト方向や視線方向と計算。
- ディフューズやスペキュラーの光の強度をピクセル単位で計算。
- 最終的な色を出力。
以下は、シンプルなピクセルシェーダの例(HLSL)です。
struct PS_INPUT {
float4 pos : POSITION;
float3 normal : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
float4 PSMain(PS_INPUT input) : COLOR {
float3 N = normalize(input.normal);
float3 L = normalize(input.lightDir);
float3 V = normalize(input.viewDir);
// ディフューズ成分
float diff = max(dot(N, L), 0.0);
// 反射ベクトル
float3 R = reflect(-L, N);
// スペキュラー成分(シャイネスは16)
float spec = pow(max(dot(R, V), 0.0), 16);
float4 diffuseColor = float4(1, 1, 1, 1) * diff;
float4 specularColor = float4(1, 1, 1, 1) * spec;
return diffuseColor + specularColor;
}
このシェーダーは、ピクセルごとに法線と光源・視線方向の内積を計算し、ディフューズとスペキュラーの光を合成しています。
動的に変化する光源や視点に対応できるため、リアルなライティング表現が可能です。
これらの技術を組み合わせることで、DirectX9のゲーム開発において高品質なライティングとマテリアル表現が実現できます。
ディフューズ・スペキュラー計算の理解、法線ベクトルとタンジェント空間の適切な扱い、そしてピクセルシェーダによる動的ライティングを活用して、リアルで魅力的な3Dグラフィックスを作りましょう。
カメラとビュー行列
3Dゲームにおけるカメラ制御は、プレイヤーの視点を決定し、シーンの見え方を大きく左右します。
DirectX9では左手座標系を採用しており、ビュー行列や投影行列を適切に設定することで、FPS視点やTPS視点など多様なカメラ表現が可能です。
左手座標系でのカメラ制御
DirectX9の座標系は左手系(Left-Handed Coordinate System)で、X軸が右方向、Y軸が上方向、Z軸が奥方向を指します。
これにより、カメラの前方向ベクトルはZ軸の正方向を向きます。
カメラ制御の基本は、ビュー行列(View Matrix)を作成することです。
ビュー行列は、ワールド空間の座標をカメラ空間に変換し、カメラの位置や向きを反映します。
DirectX9では、D3DXMatrixLookAtLH
関数を使って簡単にビュー行列を作成できます。
#include <d3dx9math.h>
// カメラ位置、注視点、上方向ベクトルを指定してビュー行列を作成
D3DXVECTOR3 eyePos(0.0f, 0.0f, -5.0f); // カメラ位置
D3DXVECTOR3 targetPos(0.0f, 0.0f, 0.0f); // 注視点
D3DXVECTOR3 upDir(0.0f, 1.0f, 0.0f); // 上方向
D3DXMATRIX viewMatrix;
D3DXMatrixLookAtLH(&viewMatrix, &eyePos, &targetPos, &upDir);
この関数は、カメラの位置eyePos
から注視点targetPos
を見て、upDir
を上方向とするビュー行列を生成します。
左手系のため、Z軸はカメラの前方向に向かいます。
カメラの向きを変更するには、注視点やカメラ位置を動的に更新し、再度ビュー行列を計算します。
FPS視点とTPS視点の実装
ゲームの視点には主にFPS(First Person Shooter)視点とTPS(Third Person Shooter)視点があります。
どちらもビュー行列の計算方法が異なります。
FPS視点
FPS視点はプレイヤーの目線そのもので、カメラはプレイヤーの位置に固定され、視線の向きだけが変わります。
カメラ位置はプレイヤーの座標とほぼ同じで、注視点は視線方向に設定します。
D3DXVECTOR3 playerPos; // プレイヤーの位置
float yaw, pitch; // 回転角度(ラジアン)
// 視線方向ベクトルを計算
D3DXVECTOR3 lookDir(
cosf(pitch) * sinf(yaw),
sinf(pitch),
cosf(pitch) * cosf(yaw)
);
D3DXVECTOR3 targetPos = playerPos + lookDir;
D3DXVECTOR3 upDir(0.0f, 1.0f, 0.0f);
D3DXMATRIX viewMatrix;
D3DXMatrixLookAtLH(&viewMatrix, &playerPos, &targetPos, &upDir);
このコードでは、yaw
(水平回転)とpitch
(垂直回転)から視線方向を計算し、カメラの注視点を決定しています。
マウス操作などでyaw
とpitch
を更新し、リアルタイムに視点を変えられます。
TPS視点
TPS視点はプレイヤーの背後や斜め上からキャラクターを映す視点です。
カメラはプレイヤーから一定距離離れた位置に配置され、プレイヤーを注視点とします。
D3DXVECTOR3 playerPos; // プレイヤーの位置
float yaw, pitch; // 回転角度(ラジアン)
float distance = 5.0f; // カメラとプレイヤーの距離
// カメラ位置を計算(プレイヤーの背後に配置)
D3DXVECTOR3 offset(
distance * cosf(pitch) * sinf(yaw),
distance * sinf(pitch),
distance * cosf(pitch) * cosf(yaw)
);
D3DXVECTOR3 cameraPos = playerPos - offset;
D3DXVECTOR3 upDir(0.0f, 1.0f, 0.0f);
D3DXMATRIX viewMatrix;
D3DXMatrixLookAtLH(&viewMatrix, &cameraPos, &playerPos, &upDir);
この例では、カメラ位置をプレイヤーの位置からoffset
分だけ離し、プレイヤーを注視点にしています。
yaw
やpitch
を変えることでカメラの回転を制御し、自由な視点操作が可能です。
投影行列と画角調整
ビュー行列でカメラの位置と向きを決めたら、次に投影行列(Projection Matrix)を設定します。
投影行列は3D空間の座標をスクリーン座標に変換し、遠近感を表現します。
DirectX9では、透視投影行列をD3DXMatrixPerspectiveFovLH
関数で作成します。
float fovY = D3DXToRadian(45.0f); // 垂直方向の画角(45度)
float aspectRatio = static_cast<float>(screenWidth) / screenHeight; // アスペクト比
float nearZ = 0.1f; // ニアクリップ距離
float farZ = 1000.0f; // ファークリップ距離
D3DXMATRIX projMatrix;
D3DXMatrixPerspectiveFovLH(&projMatrix, fovY, aspectRatio, nearZ, farZ);
fovY
は垂直方向の視野角で、一般的に30~90度の範囲で設定します。画角が広いほど視野が広がり、狭いほどズームしたように見えますaspectRatio
は画面の横幅と高さの比率で、画面の歪みを防ぐために正確に設定しますnearZ
とfarZ
は描画可能な最小・最大距離で、これらの範囲外のオブジェクトは描画されません
投影行列は、ビュー行列と組み合わせてワールドビュー投影行列(WorldViewProjection)を作成し、頂点変換に使います。
これらのカメラ制御技術を活用することで、DirectX9の左手座標系に適したFPS視点やTPS視点を実装でき、投影行列の画角調整で視覚的な表現を自在にコントロールできます。
リアルで操作性の高いカメラを作り、ゲーム体験を向上させましょう。
衝突判定の組み込み
ゲーム開発において、オブジェクト同士の衝突判定は重要な要素です。
DirectX9での3D描画に加え、衝突判定を組み込むことで、物理的なインタラクションやゲームロジックを実現できます。
ここでは、基本的なバウンディングボックスによる判定、レイキャストとピッキングの手法、そして物理エンジンとの連携について詳しく説明します。
バウンディングボックス
バウンディングボックスは、オブジェクトを囲む最小の直方体(AABB: Axis-Aligned Bounding Box)や回転可能な直方体(OBB: Oriented Bounding Box)を使って衝突判定を簡略化する手法です。
計算コストが低く、広範囲の衝突検出に適しています。
AABBの定義と判定
AABBは座標軸に平行な箱で、最小点(Min)と最大点(Max)の2つの3Dベクトルで表現します。
struct AABB {
D3DXVECTOR3 min; // 最小座標
D3DXVECTOR3 max; // 最大座標
};
2つのAABBが衝突しているか判定するには、各軸で重なりがあるかをチェックします。
bool CheckAABBCollision(const AABB& a, const AABB& b) {
if (a.max.x < b.min.x || a.min.x > b.max.x) return false;
if (a.max.y < b.min.y || a.min.y > b.max.y) return false;
if (a.max.z < b.min.z || a.min.z > b.max.z) return false;
return true;
}
この関数は、X、Y、Z軸それぞれで重なりがなければ衝突していないと判定し、すべての軸で重なりがあれば衝突と判断します。
OBBの利用
OBBはオブジェクトの回転を考慮したバウンディングボックスで、より正確な判定が可能ですが計算が複雑です。
OBB同士の衝突判定には「分離軸定理(Separating Axis Theorem)」を用います。
実装は難易度が高いため、必要に応じて物理エンジンの利用を検討します。
レイキャストとピッキング
レイキャストは、ある点から特定の方向に伸びる直線(レイ)とオブジェクトの衝突を判定する技術です。
主にマウスクリックによるオブジェクト選択(ピッキング)や射線判定に使われます。
レイの生成
スクリーン座標のマウス位置からワールド空間のレイを生成するには、逆投影行列を使います。
DirectX9のD3DXVec3Unproject
関数が便利です。
D3DXVECTOR3 GetRayFromMouse(LPDIRECT3DDEVICE9 device, int mouseX, int mouseY, int screenWidth, int screenHeight,
const D3DXMATRIX& view, const D3DXMATRIX& proj) {
D3DVIEWPORT9 viewport;
device->GetViewport(&viewport);
// 近クリップ面の座標
D3DXVECTOR3 nearPoint(mouseX, mouseY, 0.0f);
// 遠クリップ面の座標
D3DXVECTOR3 farPoint(mouseX, mouseY, 1.0f);
D3DXVECTOR3 nearPos, farPos;
D3DXVec3Unproject(&nearPos, &nearPoint, &viewport, &proj, &view, &D3DXMATRIX());
D3DXVec3Unproject(&farPos, &farPoint, &viewport, &proj, &view, &D3DXMATRIX());
D3DXVECTOR3 rayDir = farPos - nearPos;
D3DXVec3Normalize(&rayDir, &rayDir);
return rayDir; // レイの方向ベクトル
}
レイとバウンディングボックスの交差判定
レイとAABBの交差判定は、スラブ法(Slab Method)などのアルゴリズムで行います。
以下は簡易的な例です。
bool RayIntersectsAABB(const D3DXVECTOR3& rayOrigin, const D3DXVECTOR3& rayDir, const AABB& box, float& t) {
float tmin = (box.min.x - rayOrigin.x) / rayDir.x;
float tmax = (box.max.x - rayOrigin.x) / rayDir.x;
if (tmin > tmax) std::swap(tmin, tmax);
float tymin = (box.min.y - rayOrigin.y) / rayDir.y;
float tymax = (box.max.y - rayOrigin.y) / rayDir.y;
if (tymin > tymax) std::swap(tymin, tymax);
if ((tmin > tymax) || (tymin > tmax)) return false;
if (tymin > tmin) tmin = tymin;
if (tymax < tmax) tmax = tymax;
float tzmin = (box.min.z - rayOrigin.z) / rayDir.z;
float tzmax = (box.max.z - rayOrigin.z) / rayDir.z;
if (tzmin > tzmax) std::swap(tzmin, tzmax);
if ((tmin > tzmax) || (tzmin > tmax)) return false;
if (tzmin > tmin) tmin = tzmin;
if (tzmax < tmax) tmax = tzmax;
t = tmin;
return t >= 0;
}
この関数は、レイの始点rayOrigin
と方向rayDir
、判定対象のAABBを受け取り、交差していれば交差距離t
を返します。
物理エンジン連携
より高度な衝突判定や物理挙動を実現するには、物理エンジンとの連携が効果的です。
DirectX9は描画APIであり、物理計算は別途物理エンジンに任せるのが一般的です。
代表的な物理エンジンには以下があります。
エンジン名 | 特徴 | ライセンス |
---|---|---|
Bullet Physics | オープンソースで高機能な3D物理エンジン | zlibライセンス |
PhysX | NVIDIA製、GPUアクセラレーション対応 | 無償版あり |
Havok | 商用ゲームで広く使われる高性能エンジン | 商用ライセンス |
物理エンジンとDirectX9を連携する際は、物理エンジンで計算したオブジェクトの位置や回転をDirectXのワールド行列に反映し、描画と物理挙動を同期させます。
// 物理エンジンから取得した位置と回転
D3DXVECTOR3 physPos = ...;
D3DXQUATERNION physRot = ...;
// ワールド行列を作成
D3DXMATRIX matTrans, matRot, worldMatrix;
D3DXMatrixTranslation(&matTrans, physPos.x, physPos.y, physPos.z);
D3DXMatrixRotationQuaternion(&matRot, &physRot);
worldMatrix = matRot * matTrans;
// 描画時にworldMatrixをセットしてモデルを描画
device->SetTransform(D3DTS_WORLD, &worldMatrix);
物理エンジンの導入により、衝突判定だけでなく、重力や摩擦、剛体の挙動などリアルな物理表現が可能になります。
これらの技術を組み合わせて衝突判定を実装することで、DirectX9のゲーム開発においてインタラクティブでリアルな動作を実現できます。
バウンディングボックスによる高速判定、レイキャストを使った選択処理、そして物理エンジンとの連携で高度な物理表現を目指しましょう。
入力デバイス制御
ゲーム開発において、ユーザーからの入力を正確かつ快適に受け取ることは非常に重要です。
DirectX9環境では、キーボードやマウス、ゲームパッドなど複数の入力デバイスを制御できます。
ここでは、キーボードの状態取得、マウスルック処理、そしてXInputを使ったゲームパッド対応について詳しく解説します。
キーボード状態取得
キーボードの入力状態を取得する方法として、Windows APIのGetAsyncKeyState
やDirectInputを使う方法があります。
ここではシンプルにGetAsyncKeyState
を使った例を紹介します。
GetAsyncKeyState
は指定したキーの押下状態をリアルタイムに取得でき、キーが押されているかどうかを判定できます。
#include <windows.h>
bool IsKeyPressed(int vKey) {
// 最上位ビットがセットされていればキーが押されている
return (GetAsyncKeyState(vKey) & 0x8000) != 0;
}
int main() {
while (true) {
if (IsKeyPressed(VK_UP)) {
// 上矢印キーが押されている場合の処理
OutputDebugStringA("Up arrow key pressed\n");
}
Sleep(10); // CPU負荷軽減のため少し待機
}
return 0;
}
このコードでは、VK_UP
(上矢印キー)が押されているかを判定し、押されていればデバッグ出力しています。
GetAsyncKeyState
は非同期にキー状態を取得できるため、ゲームループ内での連続チェックに適しています。
より高度な入力管理が必要な場合は、DirectInputを使うことで複数キーの同時押しやデバイスの詳細情報を取得可能です。
マウスルック処理
3Dゲームでの視点操作に欠かせないのがマウスルック処理です。
マウスの移動量を取得し、カメラの回転に反映させることで、プレイヤーが自由に視点を動かせます。
Windows APIのGetCursorPos
やSetCursorPos
を使い、マウスの相対移動量を計算する方法が一般的です。
#include <windows.h>
POINT lastMousePos;
bool firstFrame = true;
void UpdateMouseLook(float& yaw, float& pitch) {
POINT currentPos;
GetCursorPos(¤tPos);
if (firstFrame) {
lastMousePos = currentPos;
firstFrame = false;
}
int deltaX = currentPos.x - lastMousePos.x;
int deltaY = currentPos.y - lastMousePos.y;
// マウス感度調整
float sensitivity = 0.005f;
yaw += deltaX * sensitivity;
pitch += deltaY * sensitivity;
// ピッチの制限(上下の回転制限)
if (pitch > 1.5f) pitch = 1.5f;
if (pitch < -1.5f) pitch = -1.5f;
// マウスカーソルを画面中央に戻す
RECT rc;
GetClientRect(GetForegroundWindow(), &rc);
POINT center = { (rc.right - rc.left) / 2, (rc.bottom - rc.top) / 2 };
ClientToScreen(GetForegroundWindow(), ¢er);
SetCursorPos(center.x, center.y);
lastMousePos = center;
}
この関数は、マウスの現在位置と前回位置の差分を計算し、yaw
(水平回転)とpitch
(垂直回転)に加算しています。
上下の回転は制限を設けて不自然な視点移動を防止します。
最後にカーソルを画面中央に戻すことで、無限に回転できるようにしています。
ゲームループ内でこの関数を呼び出し、カメラの回転角度を更新することでマウスルックを実現します。
XInputによるゲームパッド対応
ゲームパッドの入力は、MicrosoftのXInput APIを使うのが標準的です。
XInputはXboxコントローラーなどのデバイスを簡単に扱え、振動機能やボタン・スティックの状態取得が可能です。
以下は、XInputを使ってゲームパッドの状態を取得する基本的な例です。
#include <windows.h>
#include <Xinput.h>
#pragma comment(lib, "Xinput.lib")
void UpdateGamepad() {
XINPUT_STATE state;
ZeroMemory(&state, sizeof(XINPUT_STATE));
// コントローラー0の状態を取得
DWORD dwResult = XInputGetState(0, &state);
if (dwResult == ERROR_SUCCESS) {
// コントローラーが接続されている場合
// 左スティックの値(-32768~32767)
SHORT lx = state.Gamepad.sThumbLX;
SHORT ly = state.Gamepad.sThumbLY;
// スティックのデッドゾーン処理
const SHORT DEADZONE = 7849;
float normLX = 0.0f;
float normLY = 0.0f;
if (abs(lx) > DEADZONE) normLX = lx / 32767.0f;
if (abs(ly) > DEADZONE) normLY = ly / 32767.0f;
// ボタンの状態チェック
bool aButtonPressed = (state.Gamepad.wButtons & XINPUT_GAMEPAD_A) != 0;
// 入力に応じた処理
if (aButtonPressed) {
OutputDebugStringA("Aボタンが押されました\n");
}
// スティックの値を使って移動や視点操作などを行う
}
else {
// コントローラーが接続されていない場合
}
}
このコードは、コントローラー0番の状態を取得し、左スティックの入力値を正規化してデッドゾーンを考慮しています。
Aボタンの押下も判定し、押された場合にデバッグ出力しています。
XInputは最大4台のコントローラーをサポートし、XInputGetState
の引数を変えることで複数のデバイスを扱えます。
また、XInputSetState
で振動モーターを制御することも可能です。
これらの入力デバイス制御技術を組み合わせることで、DirectX9のゲームにおいて多様な操作方法を実装できます。
キーボードのリアルタイム状態取得、マウスによる視点操作、そしてXInputを使ったゲームパッド対応を適切に行い、快適なユーザーインターフェースを提供しましょう。
サウンドの統合
ゲームにおけるサウンドは、没入感や演出効果を高める重要な要素です。
DirectX9環境では、DirectSoundを利用して効果音やBGMの再生、3Dサウンドの実装が可能です。
ここでは、DirectSoundの基本的な呼び出し方法、ループ再生や3Dサウンドの実装、そしてサウンドバッファの最適化について詳しく解説します。
DirectSound基本呼び出し
DirectSoundはWindowsの低レイテンシなサウンドAPIで、サウンドバッファの作成や再生制御を行います。
まずはDirectSoundオブジェクトの作成と初期化、サウンドバッファの生成、再生までの基本的な流れを示します。
#include <windows.h>
#include <dsound.h>
#pragma comment(lib, "dsound.lib")
#pragma comment(lib, "dxguid.lib")
LPDIRECTSOUND8 g_pDS = nullptr;
LPDIRECTSOUNDBUFFER g_pPrimaryBuffer = nullptr;
LPDIRECTSOUNDBUFFER g_pSecondaryBuffer = nullptr;
bool InitDirectSound(HWND hWnd) {
// DirectSoundオブジェクトの作成
if (FAILED(DirectSoundCreate8(nullptr, &g_pDS, nullptr))) {
MessageBox(hWnd, L"DirectSoundの作成に失敗しました。", L"エラー", MB_OK);
return false;
}
// プライマリバッファの設定
if (FAILED(g_pDS->SetCooperativeLevel(hWnd, DSSCL_PRIORITY))) {
MessageBox(hWnd, L"CooperativeLevelの設定に失敗しました。", L"エラー", MB_OK);
return false;
}
// プライマリバッファの作成
DSBUFFERDESC primaryDesc = {};
primaryDesc.dwSize = sizeof(DSBUFFERDESC);
primaryDesc.dwFlags = DSBCAPS_PRIMARYBUFFER;
if (FAILED(g_pDS->CreateSoundBuffer(&primaryDesc, &g_pPrimaryBuffer, nullptr))) {
MessageBox(hWnd, L"プライマリバッファの作成に失敗しました。", L"エラー", MB_OK);
return false;
}
// セカンダリバッファの作成(WAVEファイルなどの音声データ用)
// ここでは例として16bit 44.1kHz ステレオPCMを想定
WAVEFORMATEX waveFormat = {};
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nChannels = 2;
waveFormat.nSamplesPerSec = 44100;
waveFormat.wBitsPerSample = 16;
waveFormat.nBlockAlign = (waveFormat.nChannels * waveFormat.wBitsPerSample) / 8;
waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
DSBUFFERDESC secondaryDesc = {};
secondaryDesc.dwSize = sizeof(DSBUFFERDESC);
secondaryDesc.dwFlags = DSBCAPS_CTRLVOLUME | DSBCAPS_GLOBALFOCUS;
secondaryDesc.dwBufferBytes = waveFormat.nAvgBytesPerSec * 2; // 2秒分のバッファ
secondaryDesc.lpwfxFormat = &waveFormat;
if (FAILED(g_pDS->CreateSoundBuffer(&secondaryDesc, &g_pSecondaryBuffer, nullptr))) {
MessageBox(hWnd, L"セカンダリバッファの作成に失敗しました。", L"エラー", MB_OK);
return false;
}
return true;
}
void CleanupDirectSound() {
if (g_pSecondaryBuffer) {
g_pSecondaryBuffer->Release();
g_pSecondaryBuffer = nullptr;
}
if (g_pPrimaryBuffer) {
g_pPrimaryBuffer->Release();
g_pPrimaryBuffer = nullptr;
}
if (g_pDS) {
g_pDS->Release();
g_pDS = nullptr;
}
}
このコードは、DirectSoundオブジェクトの作成、プライマリバッファの設定、セカンダリバッファの作成までを行っています。
セカンダリバッファは実際に音声データをロードして再生するためのバッファです。
音声データのロードや再生は別途行いますが、基本的な初期化はこのように行います。
ループ再生と3Dサウンド
ループ再生
BGMや環境音などを繰り返し再生するには、セカンダリバッファのPlay
関数にDSBPLAY_LOOPING
フラグを指定します。
if (g_pSecondaryBuffer) {
g_pSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);
}
これにより、バッファ内の音声がループ再生されます。
停止する場合はStop
を呼び出します。
3Dサウンド
DirectSoundは3Dサウンド機能も提供しており、音源の位置やリスナーの位置を設定して空間的な音響効果を実現できます。
3Dサウンドを使うには、IDirectSound3DBuffer
インターフェースを取得し、音源の位置や速度を設定します。
LPDIRECTSOUND3DBUFFER p3dBuffer = nullptr;
if (SUCCEEDED(g_pSecondaryBuffer->QueryInterface(IID_IDirectSound3DBuffer, (void**)&p3dBuffer))) {
// 音源の位置を設定
D3DVECTOR position = { 0.0f, 0.0f, 0.0f };
p3dBuffer->SetPosition(position.x, position.y, position.z, DS3D_DEFERRED);
// 音源の速度を設定(移動音源の場合)
D3DVECTOR velocity = { 0.0f, 0.0f, 0.0f };
p3dBuffer->SetVelocity(velocity.x, velocity.y, velocity.z, DS3D_DEFERRED);
p3dBuffer->CommitDeferredSettings();
p3dBuffer->Release();
}
リスナーの位置や向きはIDirectSound3DListener
インターフェースで設定します。
LPDIRECTSOUND3DLISTENER pListener = nullptr;
if (SUCCEEDED(g_pDS->QueryInterface(IID_IDirectSound3DListener, (void**)&pListener))) {
D3DVECTOR listenerPos = { 0.0f, 0.0f, 0.0f };
D3DVECTOR listenerFront = { 0.0f, 0.0f, 1.0f };
D3DVECTOR listenerUp = { 0.0f, 1.0f, 0.0f };
pListener->SetPosition(listenerPos.x, listenerPos.y, listenerPos.z, DS3D_DEFERRED);
pListener->SetOrientation(listenerFront.x, listenerFront.y, listenerFront.z,
listenerUp.x, listenerUp.y, listenerUp.z, DS3D_DEFERRED);
pListener->CommitDeferredSettings();
pListener->Release();
}
これにより、3D空間内での音源とリスナーの相対位置に応じた音量やパンニングが自動的に調整されます。
サウンドバッファ最適化
サウンドバッファの管理はパフォーマンスに影響するため、適切な最適化が必要です。
- バッファサイズの調整
バッファが大きすぎると遅延が増え、小さすぎると途切れやノイズが発生します。
一般的には数百ミリ秒から数秒程度のバッファサイズが適切です。
- バッファのロック範囲を最小化
音声データの更新時は、必要な範囲だけをロックして書き込み、CPU負荷を抑えます。
- ダブルバッファリング
バッファを2つ用意し、片方を再生中にもう片方を更新することで途切れのない再生を実現します。
- ストリーミング再生
大容量の音声ファイルは一度に全て読み込まず、必要な部分だけを逐次読み込むストリーミング再生を行うことでメモリ使用量を抑えられます。
以下は、サウンドバッファのロックと書き込みの例です。
void FillSoundBuffer(LPDIRECTSOUNDBUFFER buffer, BYTE* data, DWORD dataSize) {
void* pData1 = nullptr;
void* pData2 = nullptr;
DWORD size1 = 0;
DWORD size2 = 0;
if (SUCCEEDED(buffer->Lock(0, dataSize, &pData1, &size1, &pData2, &size2, 0))) {
memcpy(pData1, data, size1);
if (pData2) {
memcpy(pData2, data + size1, size2);
}
buffer->Unlock(pData1, size1, pData2, size2);
}
}
この関数は、サウンドバッファ全体をロックして音声データを書き込みます。
バッファの境界で分割される場合に備え、2つのポインタとサイズを受け取って処理しています。
DirectSoundを活用したサウンド統合は、ゲームの臨場感を大きく向上させます。
基本的な初期化と再生から、ループ再生や3Dサウンドの実装、そしてバッファ管理の最適化までを適切に行い、快適で高品質な音響体験を提供しましょう。
エフェクト実装例
ゲームのビジュアル表現を豊かにするために、さまざまなエフェクトが用いられます。
DirectX9環境でよく使われるエフェクトとして、パーティクルシステム、ポストプロセスブラー、HDR風表現があります。
ここではそれぞれの実装例を詳しく解説します。
パーティクルシステム
パーティクルシステムは、火花、煙、炎、爆発などの複雑な自然現象を多数の小さな粒子で表現する技術です。
DirectX9では頂点バッファを使い、多数のパーティクルを効率的に描画します。
基本構造
パーティクルは位置、速度、寿命、色などの属性を持ち、時間経過で更新されます。
以下はパーティクルの構造体例です。
struct Particle {
D3DXVECTOR3 position;
D3DXVECTOR3 velocity;
float life; // 残り寿命(秒)
D3DCOLOR color;
};
更新処理
ゲームループ内でパーティクルの位置を速度に基づいて更新し、寿命を減らします。
寿命が0以下になったパーティクルは再利用または削除します。
void UpdateParticles(Particle* particles, int count, float deltaTime) {
for (int i = 0; i < count; ++i) {
if (particles[i].life > 0.0f) {
particles[i].position += particles[i].velocity * deltaTime;
particles[i].life -= deltaTime;
// 例:寿命に応じて色をフェードアウト
float alpha = max(particles[i].life / MAX_LIFE, 0.0f);
particles[i].color = D3DCOLOR_ARGB(static_cast<int>(alpha * 255), 255, 255, 255);
}
}
}
描画
パーティクルは通常、点や四角形(スプライト)として描画します。
頂点バッファにパーティクルの位置と色をセットし、DrawPrimitive
で描画します。
device->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE);
device->SetStreamSource(0, vertexBuffer, 0, sizeof(Vertex));
device->DrawPrimitive(D3DPT_POINTLIST, 0, particleCount);
スプライト描画の場合は、DrawPrimitiveUP
で四角形を描画し、テクスチャを貼り付けることも多いです。
ポストプロセスブラー
ポストプロセスブラーは、画面全体にぼかし効果をかける技術で、動きの表現や光のにじみを演出します。
DirectX9ではレンダーテクスチャを使い、シェーダーでブラー処理を行います。
実装手順
- シーンをレンダーテクスチャに描画
通常の描画をバックバッファではなく、テクスチャに行います。
- ブラーシェーダーで処理
レンダーテクスチャを入力として、水平・垂直方向にぼかしをかけるシェーダーを適用します。
- 結果を画面に描画
ブラー処理後のテクスチャを画面に描画します。
シェーダー例(HLSL)
sampler2D tex : register(s0);
float2 texelSize; // 1.0 / テクスチャサイズ
float4 PSBlur(float2 texCoord : TEXCOORD) : COLOR {
float4 color = float4(0,0,0,0);
color += tex2D(tex, texCoord + float2(-2, 0) * texelSize) * 0.1;
color += tex2D(tex, texCoord + float2(-1, 0) * texelSize) * 0.2;
color += tex2D(tex, texCoord) * 0.4;
color += tex2D(tex, texCoord + float2(1, 0) * texelSize) * 0.2;
color += tex2D(tex, texCoord + float2(2, 0) * texelSize) * 0.1;
return color;
}
この例は水平ブラーの一部で、垂直方向も同様に処理します。
2回のパスで水平・垂直ブラーをかけるのが一般的です。
HDR風表現
HDR(High Dynamic Range)風表現は、明るい部分の輝度を強調し、よりリアルで鮮やかな映像を作る技術です。
DirectX9では、レンダーテクスチャとシェーダーを組み合わせて実装します。
実装の流れ
- シーンを高輝度レンダーテクスチャに描画
通常のレンダリングとは別に、輝度の高い部分だけを抽出してテクスチャに描画します。
- 輝度抽出とブラー
輝度抽出シェーダーで明るい部分を抽出し、ブラーをかけて光のにじみを作ります。
- 合成
元のシーンとブラー処理した輝度テクスチャを加算合成し、HDR風の効果を実現します。
輝度抽出シェーダー例(HLSL)
sampler2D sceneTex : register(s0);
float4 PSExtractBright(float2 texCoord : TEXCOORD) : COLOR {
float4 color = tex2D(sceneTex, texCoord);
float brightness = dot(color.rgb, float3(0.299, 0.587, 0.114)); // 輝度計算
if (brightness > 0.8) {
return color;
} else {
return float4(0,0,0,0);
}
}
このシェーダーは輝度が0.8以上のピクセルだけを抽出し、それ以外は黒にします。
これらのエフェクトを組み合わせることで、DirectX9のゲームにおいて視覚的に魅力的な表現が可能になります。
パーティクルシステムで動的な効果を、ポストプロセスブラーで滑らかな映像を、HDR風表現でリアルな光の輝きを演出しましょう。
パフォーマンス最適化
DirectX9を用いたゲーム開発では、高いフレームレートとスムーズな描画を実現するためにパフォーマンス最適化が不可欠です。
ここでは、描画呼び出し回数の削減、GPUプロファイリングの活用、そしてレベルオブディテール(LOD)とカリング技術について詳しく解説します。
描画呼び出し回数削減
描画呼び出し(Draw Call)は、CPUからGPUへ描画命令を送る回数を指し、多すぎるとCPU負荷が増大しパフォーマンス低下の原因となります。
描画呼び出し回数を削減することは、ゲームの高速化に直結します。
バッチングの活用
複数のオブジェクトをまとめて一度の描画呼び出しで処理する「バッチング」は基本的な手法です。
頂点バッファやインデックスバッファを結合し、同じマテリアルやテクスチャを使うオブジェクトをまとめて描画します。
// 複数オブジェクトの頂点データを一つの頂点バッファにまとめる例
device->SetStreamSource(0, combinedVertexBuffer, 0, sizeof(Vertex));
device->SetIndices(combinedIndexBuffer);
device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, totalVertices, 0, totalPrimitives);
状態変更の最小化
シェーダーやテクスチャ、レンダーステートの切り替えはコストが高いため、同じ状態のオブジェクトを連続して描画するように描画順序を工夫します。
これにより、状態変更回数を減らし描画効率を向上させます。
インスタンシングの利用
DirectX9では標準でインスタンシング機能はありませんが、拡張やシェーダーで同じメッシュを複数描画する際に頂点データを工夫して描画回数を減らすテクニックがあります。
GPUプロファイリング
GPUプロファイリングは、GPUの処理状況を解析し、ボトルネックを特定するための重要な手法です。
DirectX9環境では、MicrosoftのPIXツールなどを使って詳細なパフォーマンス解析が可能です。
PIXの活用
PIXはGPUの描画コマンドやシェーダーの実行状況、リソース使用状況を可視化します。
これにより、どの描画呼び出しが重いか、シェーダーのどの部分がボトルネックかを把握できます。
- Draw Call数の確認
過剰な描画呼び出しがないかチェック。
- シェーダー実行時間の測定
ピクセルシェーダーや頂点シェーダーの負荷を分析。
- リソース使用状況
テクスチャやバッファの使用量を確認し、無駄なリソースを削減。
プロファイリング結果の活用
解析結果をもとに、描画順序の最適化、シェーダーの簡素化、不要なリソースの削除などを行い、パフォーマンスを改善します。
LODとカリング
レベルオブディテール(LOD)
LODは、遠くのオブジェクトほどポリゴン数を減らした簡易モデルを使い、描画負荷を軽減する技術です。
距離に応じて複数のモデルを切り替えます。
float distance = D3DXVec3Length(&(cameraPos - objectPos));
if (distance < 50.0f) {
DrawHighDetailModel();
} else if (distance < 150.0f) {
DrawMediumDetailModel();
} else {
DrawLowDetailModel();
}
LODを適切に設定することで、遠景の描画負荷を大幅に削減しつつ、見た目の品質を保てます。
カリング
カリングは、視界に入っていないオブジェクトを描画しないことで無駄な処理を減らす技術です。
- フラスタムカリング
カメラの視錐台(フラスタム)外のオブジェクトを除外します。
視錐台はカメラの視野範囲を表す四角錐で、オブジェクトのバウンディングボックスと交差判定を行います。
- オクルージョンカリング
他のオブジェクトに隠れて見えないオブジェクトを描画しません。
高度な技術で、ハードウェアやソフトウェアで実装可能です。
フラスタムカリングの例
bool IsInViewFrustum(const BoundingBox& box, const Frustum& frustum) {
// 各平面に対してバウンディングボックスが外側にあるか判定
for (int i = 0; i < 6; ++i) {
if (frustum.planes[i].DistanceToBox(box) < 0) {
return false; // 視錐台外
}
}
return true; // 視錐台内
}
カリングを行うことで、描画対象を絞り込み、GPU負荷を軽減できます。
これらのパフォーマンス最適化技術を組み合わせることで、DirectX9のゲーム開発において高い描画効率と快適な動作を実現できます。
描画呼び出し回数の削減、GPUプロファイリングによるボトルネックの特定、そしてLODやカリングによる描画負荷の軽減を意識して開発を進めましょう。
デバッグとテスト
DirectX9を用いたゲーム開発では、描画や動作の問題を早期に発見し修正するためのデバッグとテストが欠かせません。
ここでは、MicrosoftのPIXツールを使った解析、ゲーム画面上に情報を表示するデバッグオーバーレイの実装、そしてエラー処理のベストプラクティスについて詳しく解説します。
PIXによる解析
PIX(Performance Investigator for Xbox)は、DirectXアプリケーションのパフォーマンス解析やデバッグに特化したツールです。
DirectX9環境でも利用でき、GPUの描画コマンドやシェーダーの実行状況、リソース使用状況を詳細に調査できます。
PIXの主な機能
- フレームキャプチャ
ゲームの1フレーム分の描画コマンドをキャプチャし、コマンドの順序や内容を確認可能です。
- GPUパフォーマンス分析
シェーダーの実行時間や描画呼び出しの負荷を可視化し、ボトルネックを特定。
- リソース管理の確認
テクスチャやバッファの使用状況、リークの有無をチェック。
PIXの使い方
- PIXを起動し、対象のDirectX9アプリケーションを指定して実行。
- 問題が発生するシーンでフレームキャプチャを取得。
- キャプチャしたフレームを解析し、描画コマンドやシェーダーの詳細を確認。
- 問題箇所を特定し、コード修正に活かす。
PIXはGPUの動作を詳細に追跡できるため、描画がおかしい、パフォーマンスが低下しているといった問題の原因究明に非常に有効です。
デバッグ表示オーバーレイ
ゲーム画面上にFPSやデバッグ情報を表示するオーバーレイは、動作確認やパフォーマンス監視に役立ちます。
DirectX9では、ID3DXFont
インターフェースを使ってテキスト描画が簡単に行えます。
ID3DXFontの初期化例
#include <d3dx9.h>
LPD3DXFONT g_pFont = nullptr;
bool InitFont(LPDIRECT3DDEVICE9 device) {
HRESULT hr = D3DXCreateFont(
device,
18, // フォントサイズ
0, // フォント幅(0は自動)
FW_NORMAL, // フォントの太さ
1, // ミップマップレベル
FALSE, // イタリック
DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
L"Arial", // フォント名
&g_pFont
);
return SUCCEEDED(hr);
}
デバッグテキスト描画例
void DrawDebugText(LPDIRECT3DDEVICE9 device, float fps) {
if (!g_pFont) return;
RECT rect = {10, 10, 300, 50};
wchar_t text[256];
swprintf_s(text, L"FPS: %.2f", fps);
g_pFont->DrawText(nullptr, text, -1, &rect, DT_NOCLIP, D3DCOLOR_ARGB(255, 255, 255, 0));
}
ゲームループ内でDrawDebugText
を呼び出し、FPSや変数の値をリアルタイムに表示できます。
色や位置は自由に調整可能です。
エラー処理のベスト実践
DirectX9のAPI呼び出しはHRESULT型の戻り値で成功・失敗を返します。
エラー処理を適切に行うことで、問題の早期発見や安定した動作が実現します。
HRESULTのチェック
API呼び出し後は必ずSUCCEEDED
やFAILED
マクロで結果を確認します。
HRESULT hr = device->CreateVertexBuffer(...);
if (FAILED(hr)) {
// エラー処理
MessageBox(nullptr, L"頂点バッファの作成に失敗しました。", L"エラー", MB_OK | MB_ICONERROR);
return false;
}
エラーメッセージの取得
DXGetErrorString9
やDXGetErrorDescription9
関数を使うと、HRESULTコードからわかりやすいエラーメッセージを取得できます。
#include <d3dx9.h>
void ShowErrorMessage(HRESULT hr) {
const char* errorStr = DXGetErrorString9(hr);
const char* errorDesc = DXGetErrorDescription9(hr);
char msg[512];
sprintf_s(msg, "Error: %s\nDescription: %s", errorStr, errorDesc);
MessageBoxA(nullptr, msg, "DirectX Error", MB_OK | MB_ICONERROR);
}
リソース解放の徹底
エラー発生時や終了時には、確実にCOMオブジェクトのRelease
を呼び、メモリリークを防ぎます。
デバッグビルドでの検証
デバッグビルドでは、DirectXのデバッグランタイムを利用し、APIの誤用やリソースリークを検出します。
Visual Studioの出力ウィンドウに詳細な警告が表示されるため、積極的に活用しましょう。
これらのデバッグとテスト手法を活用することで、DirectX9ゲーム開発の品質向上と安定稼働を実現できます。
PIXによる詳細解析、画面上のデバッグ表示、そして堅牢なエラー処理を組み合わせて効率的な開発を進めましょう。
リリースビルド調整
ゲーム開発の最終段階であるリリースビルドでは、動作環境の整備やユーザー体験の向上を目的とした調整が必要です。
DirectX9を利用したゲームでは、DirectXランタイムの依存性管理、設定ファイルやユーザーオプションの設計、そしてインストーラ作成時の注意点が重要なポイントとなります。
DirectXランタイム依存性
DirectX9を利用するゲームは、実行環境に対応するDirectXランタイムがインストールされている必要があります。
特に、DirectX9.0c以降のバージョンが推奨されますが、ユーザーの環境によっては古いバージョンしか入っていない場合もあります。
ランタイムの配布方法
- Webインストーラの利用
Microsoftが提供するWebインストーラ(DirectX End-User Runtime Web Installer)を利用し、必要なコンポーネントだけをダウンロード・インストールさせる方法です。
インストーラサイズが小さく、ユーザーの負担が軽減されます。
- フルパッケージの同梱
DirectXのフルランタイムパッケージをゲームのインストーラに同梱し、オフライン環境でもインストール可能にします。
ただし、パッケージサイズが大きくなるため注意が必要です。
バージョンチェック
ゲーム起動時にDirectXのバージョンや機能サポートをチェックし、必要なバージョンが満たされていない場合は警告を表示したり、ランタイムのインストールを促す処理を実装すると親切です。
#include <d3d9.h>
bool CheckDirectXVersion() {
LPDIRECT3D9 d3d = Direct3DCreate9(D3D_SDK_VERSION);
if (!d3d) return false;
D3DADAPTER_IDENTIFIER9 adapterID;
HRESULT hr = d3d->GetAdapterIdentifier(D3DADAPTER_DEFAULT, 0, &adapterID);
d3d->Release();
return SUCCEEDED(hr);
}
このようにDirect3Dオブジェクトの作成が成功すれば、最低限のDirectX9ランタイムは存在すると判断できます。
設定ファイルとユーザーオプション
ユーザーがゲームの動作環境や操作性をカスタマイズできるように、設定ファイルやオプション画面を用意することが望ましいです。
設定ファイルの形式
- INIファイル
シンプルなテキスト形式で読み書きが容易。
Windows APIのGetPrivateProfileString
やWritePrivateProfileString
で扱えます。
- XML/JSONファイル
構造化されたデータを扱いやすく、拡張性が高いでしょう。
外部ライブラリを使って読み書きします。
設定項目例
項目名 | 内容 |
---|---|
画面解像度 | 例:800×600、1920×1080 |
フルスクリーン | ON/OFF |
音量 | BGMや効果音の音量調整 |
キー割り当て | 操作キーのカスタマイズ |
マウス感度 | 視点操作の感度調整 |
設定の読み込み・保存例(INIファイル)
#include <windows.h>
void LoadSettings() {
wchar_t buffer[256];
GetPrivateProfileString(L"Display", L"Resolution", L"800x600", buffer, 256, L"settings.ini");
// bufferに解像度文字列が格納される
}
void SaveSettings() {
WritePrivateProfileString(L"Display", L"Resolution", L"1920x1080", L"settings.ini");
}
設定ファイルはゲーム起動時に読み込み、終了時やオプション変更時に保存します。
インストーラ作成時の注意点
ゲームの配布にあたっては、インストーラの作成が必要です。
以下のポイントに注意しましょう。
必要なランタイムの同梱・チェック
- DirectXランタイムやVisual C++再頒布可能パッケージなど、ゲームが依存するランタイムを同梱またはインストールチェックを行い、ユーザー環境を整えます
ファイル配置とパス管理
- ゲームの実行ファイルやリソースファイルを適切なフォルダに配置し、相対パスでアクセスできるようにします
- 設定ファイルやログファイルの保存場所もユーザーのアクセス権を考慮し、
AppData
フォルダなど適切な場所を選びます
アンインストール対応
- インストーラにはアンインストール機能を組み込み、ユーザーが簡単にゲームを削除できるようにします
- レジストリや一時ファイルのクリーンアップも忘れずに行います
インストーラ作成ツールの選択
- Inno SetupやNSISなどの無料ツールが広く使われています。スクリプトで細かい制御が可能です
- 商用ゲームの場合は、InstallShieldやAdvanced Installerなどの有料ツールも検討します
リリースビルドの調整は、ユーザーが快適にゲームを楽しめる環境を整えるための重要な工程です。
DirectXランタイムの依存性を適切に管理し、柔軟な設定ファイルとユーザーオプションを用意し、信頼性の高いインストーラを作成することで、スムーズな配布と運用を実現しましょう。
よくあるトラブルシューティング
DirectX9を使ったゲーム開発では、さまざまなトラブルに遭遇することがあります。
ここでは特に多い「デバイスリセットエラー」「ドライバ非互換」「不正確なZバッファ精度」について原因と対策を詳しく解説します。
デバイスリセットエラー
Direct3D9のデバイスは、フルスクリーン切り替えや画面解像度変更、Alt+Tab操作などで「ロスト」状態になることがあります。
この状態では描画ができず、デバイスのリセットが必要です。
原因
- 他のアプリケーションがGPUリソースを奪った
- フルスクリーンとウィンドウモードの切り替え
- ドライバの再初期化やGPUの状態変化
対応方法
- TestCooperativeLevelの呼び出し
毎フレームIDirect3DDevice9::TestCooperativeLevel
を呼び、デバイス状態を確認します。
- D3DERR_DEVICELOSTの場合
デバイスはロスト状態で、まだリセットできません。
処理をスキップし、待機します。
- D3DERR_DEVICENOTRESETの場合
デバイスをReset
関数でリセットします。
リセット成功後は、失われたリソース(頂点バッファ、テクスチャなど)を再作成します。
HRESULT hr = device->TestCooperativeLevel();
if (hr == D3DERR_DEVICELOST) {
// デバイスロスト中、待機
Sleep(100);
} else if (hr == D3DERR_DEVICENOTRESET) {
hr = device->Reset(&d3dpp);
if (SUCCEEDED(hr)) {
// リソースの再作成処理
RestoreResources();
}
}
注意点
- リセット前に
IDirect3DResource9
のOnLostDevice
、リセット後にOnResetDevice
を呼ぶ設計が推奨されます - 動的リソースは
D3DPOOL_DEFAULT
、静的リソースはD3DPOOL_MANAGED
を使い分けることで管理が容易になります
ドライバ非互換
DirectX9は多くのGPUドライバで動作しますが、特定のドライババージョンやハードウェアで非互換やバグが発生することがあります。
症状例
- 描画が乱れる、テクスチャが正しく表示されない
- シェーダーが正しく動作しない
- アプリケーションがクラッシュする
対策
- ドライバの更新を促す
ユーザーに最新のGPUドライバをインストールしてもらうよう案内します。
- 互換性チェックの実装
起動時にGPU情報を取得し、既知の問題があるドライバを検出して警告を表示。
D3DADAPTER_IDENTIFIER9 adapterID;
device->GetAdapterIdentifier(D3DADAPTER_DEFAULT, 0, &adapterID);
OutputDebugStringA(adapterID.Driver);
- フォールバック処理
シェーダーモデルのバージョンを下げる、特定の機能を無効化するなど、問題回避のための処理を用意。
- バグレポートの収集
ユーザーからの報告を集め、問題の再現環境を特定しやすくします。
不正確なZバッファ精度
Zバッファ(深度バッファ)は3D空間の奥行きを管理し、正しい描画順序を実現しますが、精度不足や設定ミスで描画アーティファクトが発生することがあります。
症状
- ポリゴンの重なりが正しく描画されず、ちらつきやZファイティングが起こる
- 遠くのオブジェクトが近くのオブジェクトより手前に描画される
原因
- Zバッファフォーマットの選択ミス
16ビット深度バッファでは精度が不足しやすい。
24ビットや32ビットの深度バッファを使うことが望ましい。
- ニアクリップ距離が近すぎる
ニアクリップ面を極端に近く設定すると、Zバッファの分布が偏り、精度が低下します。
- 遠クリップ距離が遠すぎる
遠クリップ面が非常に遠いと、Zバッファの精度が薄くなります。
対策
- 深度バッファフォーマットは
D3DFMT_D24S8
など24ビット以上を選択します - ニアクリップ距離は可能な限り大きく設定し、遠クリップ距離は必要最小限に抑えます
d3dpp.EnableAutoDepthStencil = TRUE;
d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8;
- 投影行列の設定で、ニア・ファークリップ距離を適切に調整します
- 必要に応じて、Zバッファの精度を補う技術(例えば、浮動小数点深度バッファや逆Zバッファ)を検討します
これらのトラブルはDirectX9開発で頻繁に遭遇しますが、原因を理解し適切に対処することで安定した動作を実現できます。
デバイスリセットの管理、ドライバ互換性の確認、そしてZバッファの精度調整をしっかり行い、快適なゲーム体験を提供しましょう。
まとめ
本記事では、DirectX9を用いたC++ゲーム開発における基本から応用までの技術を幅広く解説しました。
Win32ウィンドウ生成やDirect3D初期化、描画フローの基礎から、シェーダーモデル2.0の導入、テクスチャ管理、ライティング、カメラ制御、衝突判定、入力デバイス制御、サウンド統合、エフェクト実装、パフォーマンス最適化、デバッグ手法、リリースビルド調整、そしてよくあるトラブル対策まで網羅しています。
これらを理解し実践することで、安定かつ高品質な3Dゲーム開発が可能となります。