GameProgrammar's Night

ゲームプログラム系の覚え書き

DirectX3D 9 でスクリーンショットを撮る方法

 制作しているゲームのスクショを撮れると何かと便利です。開発中のデバッグから、ユーザーサポートの際に添付してもらったりなど。いろいろ使い出があります。
 D3D9でスクショを撮る方法をいくつかメモしておきます。

D3DXSaveSurfaceToFile関数を使い出力する

 一番お手軽な方法です。バックバッファを取得し、D3DXSaveSurfaceToFile関数に渡すだけでスクリーンショットが撮ることができます。

//pDeviceは、使用可能なLPDIRECT3DDEVICE9とします

// 1フレームの描画が終わった後

// バックバファの取得
LPDIRECT3DSURFACE9 pBackBuf;
pDevice->GetRenderTarget(0, &pBackBuf);

// スクショ出力
D3DXSaveSurfaceToFile("screenshot.bmp", D3DXIFF_BMP, pBackBuf, NULL, NULL);

// Get系で取得したサーフェイスはAddRefが呼ばれているので忘れずに解放する
pBackBuf->Release();

 ただし、D3DXSaveSurfaceToFile関数は、BMPかDDSでしか出力ができません。

バックバッファから自前で出力する

 D3DXSaveSurfaceToFile関数は、BMPかDDSでしか出力ができません。BMPはファイルサイズが大きいし、DDSは閲覧できるビューアーがあまりありません。そこで、JPGやPNGなどの形式で出力したいと考えると思います。
 そのためには、バックバッファのデータを取得して自前(libjpgやlibpngを使って)で欲しい形式に変換しつつ、ファイルに出力するしかありません。

ロックしてデータを取り出す

 基本的なやり方としては、バックバッファをロックし直接データを取得します。
 通常、バックバッファはロックできませんが、DIRECT3DDEVICE9を作る際にロック可能フラグ(D3DPRESENTFLAG_LOCKABLE_BACKBUFFER)を立てることでロックできるようになります。ロックすればデータを直接取り出すことができますので、データを取り出したあとは、煮るなり焼くなり好きな方式に変換してファイルに出力すれば良いです。

D3DPRESENT_PARAMETERS d3dParam; // CreateDeviceに渡す初期化のための構造体
d3dParam = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER; // ロック可能フラグを設定

のようにして、Direct3DDevice9を初期化

//pDeviceは、使用可能なLPDIRECT3DDEVICE9とします

// バックバファの取得
LPDIRECT3DSURFACE9 pBackBuf;
pDevice->GetRenderTarget(0, &pBackBuf);

BYTE* pBackBufData = new BYTE[/*バックバッファのサイズ*/];

D3DLOCKED_RECT rect;
pBackBuf->LockRect(&rect, NULL, D3DLOCK_READONLY);
 memcopy(pBackBufData, rect.pBits, /*バックバッファのサイズ*/);
pBackBuf->UnlockRect();
pBackBuf->Release();

// pBackBufDataを使ってJPGでもPNGにでも好きにする

// ファイル出力したら忘れずにdelete
delete[] pBackBufData;

もちろん、pBackBufDataはアプリ起動時に確保して、アプリ終了時に解放するでも可

IDirect3DDevice9::GetRenderTargetData メソッドを使う

 バックバッファがロック可能になっていると、ロック不可能よりはパフォーマンスが落ちるらしいです。外部書き込みを想定する場合としない場合ではプログラムが変わるのは容易に想像できることですので、ありうることだと思います。
 たまにしかスクショを撮らない(ほとんどの場合そうだと思いますが)のであれば、たまにしか動作しない機能のために常にパフォーマンスを犠牲にするのはちょっともったいないですよね。
 そこで、ロック不可能な状態でもバックバッファのデータを取る方法を紹介します。ロック不可能なバックバッファから、ロック可能なサーフェイスにデータを転送し、ロック可能なサーフェイスからデータを取得します。
 ロック不可能なバックバッファからロック可能なサーフェイスにデータを転送するメソッドが、GetRenderTargetDataです。

//pDeviceは、使用可能なLPDIRECT3DDEVICE9とします

// ロック可能なサーフェイスを作成
LPDIRECTSURFACE9 pSurface;
pDevice->CreateOffscreenPlainSurface(/*バックバッファの幅*/,
                                     /*バックバッファの高さ*/,
                                     /*バックバッファのフォーマット*/,
                                     D3DPOOL_SYSTEMMEM,
                                     &pSurface, NULL);

// バックバファの取得
LPDIRECT3DSURFACE9 pBackBuf;
pDevice->GetRenderTarget(0, &pBackBuf);

// バックバッファデータ転送!
pDevice->GetRenderTargetData(pBackBuf, pSurface);

pBackBuf->Release();

// pSurfaceはロック可能なので煮るなり(略

 注意点は、転送先サーフェイス(pSurface)はシステムメモリ上のサーフェイスである必要があります。サーフェイス作成時に、D3DPOOL_SYSTEMMEMで作成します。

 D3DXSaveSurfaceToFile関数は、バックバッファがロック可能でもロック不可能でもファイルを出力してくれるので、この方法を使ってファイルを出力してるのだと思われます。

D3DXSaveSurfaceToFile関数出力したファイルを形式変換する

 横着な方法としては、D3DXSaveSurfaceToFile関数でBMPファイルを出力したあと、それを読み直して変換するというやり方もあります。
 スクショの形式変換を開発中のデバッグ用途と割り切れば、PCにImageMagickあたりをインストールして、system関数やCreateProcessAPIを使ってコマンドラインを叩いて変換するというさらなる横着も可能です。