ホーム < ゲームつくろー! < プログラマブルシェーダ編 < スクリーン座標にあるオブジェクトをマウスで指す方法


シェーダ編

その2 スクリーン座標にあるオブジェクトをマウスで指す方法



 2Dのゲームにおいて、スクリーンにあるキャラクタをカーソルで指すのはとても簡単な作業です。カーソル位置にキャラクタが含まれているかどうかを単純に比較します。しかし、オブジェクトが3Dで記述されている場合、同じ作業は非常に面倒な事になってしまいます。それはカーソルが存在するスクリーン座標とオブジェクトが存在するワールド座標がものすごく異なっているからです。まともに判定しようと思うと、カーソルかオブジェクトのどちらかを一方の座標空間に変換するしかありません。どちらにするかは好みかもしれませんが、スクリーンを見ればオブジェクトがあるわけで、どうせならばスクリーン座標で解決したいものです。ですから3Dオブジェクトをスクリーン座標に変換し、その占める領域を見つける必要があるわけですが、3次元の空間には「重なり」があるため、非常に複雑な領域になってしまいます。まともにやると駄目なのです。

 そこでこの章では、ピクセルシェーダを用いてオブジェクトのスクリーン座標に占める領域を色分けして描画してみます。これにより、スクリーンに描画された色からオブジェクトを逆に検索する事ができるようになるわけです!



@ 色分けのアイデアは深度バッファの機能の利用

 最終的にスクリーンに描画されたオブジェクトは、少なくとも目線方向に対して一番前に存在しています。この前後関係は「深度バッファ」によって描画時に判定されています。もし描画しようするピクセルのZ値が深度バッファの値よりも前にあるならば、そのピクセルへオブジェクトの色が書き込まれます。これは「深度テスト」と呼ばれています。

 通常オブジェクトの色は頂点からの線形補間によって自動計算されます。しかし、最終的に何色にするかを決めるのは「ピクセルシェーダ」です。ピクセルシェーダを通った色は、その後「描くか描かないか」というテストを受けるだけです。ここで、描くオブジェクトに固有の色を与えて、それをピクセルシェーダに伝え、その色でオブジェクトを塗りつぶすと、オブジェクトのベタ塗りシルエットができます。しかも、深度テストをパスしなかった色は塗られません。これにより、最終的にスクリーンには各オブジェクトについてカメラから見える部分だけが塗られることになります。今回はこれを実装する方法を考えるわけです。



A オブジェクト判定サーフェイス

 ベタ塗りシルエットを描画バッファ(描画サーフェイス)に直接描いても仕方ありません。描画バッファにはやっぱり通常の3Dオブジェクトを描画します。ベタ塗りするのは、自前で用意するもう1つのサーフェイスに対してです。つまり、今回の作業には2つのサーフェイスが必要になります。ベタ塗りするサーフェイスの事を「オブジェクト判定サーフェイス」と呼ぶことにしましょう。

 オブジェクト判定サーフェイスにレンダリングするには、デバイスが持つ描画サーフェイスを一度取り外し、オブジェクト判定サーフェイスを代わりに差し込む必要があります。描画サーフェイスを取り替えるには、IDirect3DDevice9::SetRenderTarget関数を用います。

IDirect3DDevice9::SetRenderTarget関数
HRESULT SetRenderTarget(      
    DWORD RenderTargetIndex,
    IDirect3DSurface9 *pRenderTarget
);

RenderTargetIndexにはレンダリングターゲットの番号を指定します。デバイスは複数のサーフェイスを持つことができ、それを番号で管理しています。今回はデフォルトレンダリングターゲットである0番を使用することにします。
pRenderTargetにはレンダリングターゲットとなるサーフェイスへのポインタを渡します。もし指定の番号にすでにサーフェイスが定義されていたら、そのポインタは置き換えられてしまいます。

 このようにサーフェイスを切り替えることで、デバイスが持つ描画バッファ以外にもレンダリングが出来るようになります。ただし、pRenderTargetに渡すサーフェイスにはD3DUSAGE_RENDERTARGETが指定されていなければなりません。

 とは言うものの、実はレンダリングターゲットとなるサーフェイスは、IDirect3DDevice9::CreateRenderTarget関数を用いることで、割と簡単に作成できてしまいます。

IDirect3DDevice9::CreateRenderTarget関数
HRESULT CreateRenderTarget(
    UINT Width,
    UINT Height,
    D3DFORMAT Format,
    D3DMULTISAMPLE_TYPE MultiSample,
    DWORD MultisampleQuality,
    BOOL Lockable,
    IDirect3DSurface9** ppSurface,
    HANDLE* pHandle
);

WidthHeightは作成するサーフェイスの幅と高さを指定します。今回はデバイスの描画サーフェイスと全く同じ幅と高さにします。描画サーフェイスの属性を取得する方法は後述します。
Formatはレンダリングターゲットとなるサーフェイスのフォーマットを指定します。これはデバイスと同じである必要は無いのですが、よほどでない限りD3DFOMAT_A8R8B8G8になるでしょう。詳しくはD3DFORMATをご覧下さい。
MultiSampleにはマルチサンプリングバッファタイプという値を指定します。これはアンチエイリアシングのサンプリング回数を表す列挙型を指定します。デバイスと同じものにするのが普通です。
MultisampeQualityにはアンチエイリアシングの精度を指定します。ここもデバイスと一緒にします。
Lockableはそのサーフェイスをロックするかどうかを指定します。サーフェイスに描いたピクセルを操作する必要がないのであればfalseにしますが、今回はサーフェイスの色情報をピクセル単位で取得したいためtrueとします。
ppSurfaceに新しく作成されたレンダリングターゲット用のサーフェイスへのポインタが返ります。
pHandleは予約なのでNULLにしておいてください。

 デバイスが持つ描画サーフェイスの能力を取得するには、まず描画サーフェイス自体をデバイスから得ます。これはGetRenderTarget関数を用います。取得した描画サーフェイスから、さらにIDirect3DSurface9::GetDesc関数で能力(幅、高さ、フォーマットなど)を得ることが出来ます。これら関数の定義及び使い方の紹介は簡単ですから、ここでは割愛致します。

オブジェクト判定サーフェイス作成例
// デバイスの描画バッファを取得
g_pD3DDev->GetRenderTarget(0, &pDevSurface);

// バックバッファサーフェイスの属性を取得
D3DSURFACE_DESC DevSufDesc;
pDevSurface->GetDesc( &DevSufDesc );

// オブジェクト判定サーフェイスの作成
g_pD3DDev->CreateRenderTarget( DevSufDesc.Width, DevSufDesc.Height, DevSufDesc.Format, DevSufDesc.MultiSampleType, 0, true, &pColorSurface, NULL);

 これで、2つのサーフェイスの用意が整いました。



B ピクセルシェーダプログラム

 ピクセルシェーダの役目は1つ。オブジェクトごとに定義された一意の色でベタ塗りすることです。そのピクセルシェーダプログラムを次に作成します。入力されてきた色情報を「無視」して、ユーザが設定したオブジェクトの色に置き換えてしまいます。これはとても簡単でして、次のようになります。

ベタ塗りピクセルシェーダプログラム
const char PxShader[] =
   "ps_1_1 \n"
   "mov r0, c0";       // 色をc0に決定

1行です。何をしているかと言いますと、c0というレジスタ(r,g,b,aの色情報がある)の値をr0(出力レジスタ)にコピー(mov)しているだけです。c0がベタ塗りする色で、外部から与えますc0レジスタに値を登録するにはIDirect3DDevice9インターフェイスの持つSetPixelShaderConstantF関数を用います。例えば赤色(0xff0000ff)に塗りたいのであれば、次のように実装します。

IDirect3DDevice9::SetPixelShaderConstantF関数
// ピクセルシェーダに渡すオブジェクトカラー
FLOAT ObjectColor[4] = {1.0f, 0.0f, 0.0f, 1.0f};          // ARGB

g_pD3DDev->SetPixelShaderConstantF(0, ObjectColor, 1);

SetPixelShaderConstantF関数の第1引数には浮動小数点レジスタ(c0〜)のレジスタ番号を指定します。第2引数にはレジスタに格納するFLOAT×4の配列、第3引数には書き込むレジスタの数を指定します。指定数はFLOAT×4で1つと数えます。



C レンダリングプロセス

 ここまでで必要な用意は終わりました。次にレンダリングのプロセスを見てみましょう。今回はレンダリングを2度行います。1回目は描画バッファに対する通常のレンダリング、2回目はオブジェクト判定サーフェイスに対するベタ塗りレンダリングです。そのプロセスは次のようになります。

 同じオブジェクトに対してこの2回のレンダリングを個別に行います。描画サーフェイスにはいつもと同じように3Dのオブジェクトが描画されます。一方オブジェクト判定サーフェイスには、そのオブジェクトに指定した色がシルエットとしてベタ塗りされます。しかも、深度テストを通しているので、最前面にあるオブジェクトの色だけが描画されます。ここまでくれば、もうできたようなものです(^-^)



D カーソル位置にあるサーフェイスの色の取得

 オブジェクト判定サーフェイスはロック可能なように作成しました。これによりスクリーン座標上の指定の位置にあるピクセルカラーを取得できます。カーソルの位置pointに対して、指定の色を取得するには次のように実装します。

カーソル位置にあるサーフェイスカラーを取得
// カーソルの位置(クライアント座標)を取得
POINT point;
GetCursorPos( &point );   // スクリーン座標で取得
ScreenToClient( hWnd, &point);   // クライアント座標に変換
RECT LockR = {point.x, point.y, point.x+1, point.y+1}; // カーソル位置のみをロック
D3DLOCKED_RECT D3DLockRect;

// オブジェクト判定サーフェイスをロック
if(SUCCEEDED( pColorSurface->LockRect( &D3DLockRect, &LockR, D3DLOCK_READONLY)))
{
   DWORD *color = (DWORD*)D3DLockRect.pBits;   // 色情報をDWORD型として取得

   // サーフェイスアンロック
   pColorSurface->->UnlockRect();
}

オブジェクト判定サーフェイスを読み込み専用で1ピクセルだけロックします。ロックが成功すれば、colorにはオブジェクトを判別するための一意の色が代入されます。後は、この色からオブジェクトを逆に検索するだけです。 



E 実際の画面はこうなります

 ここまで説明してきた内容で実際にテストプログラムを作りました。描画サーフェイス及びオブジェクト判定サーフェイスのレンダリング結果は次のようになります。

描画サーフェイス オブジェクト判定サーフェイス

左が描画結果で、右側がベタ塗りの結果です。緑色の立方体はちゃんと背後に隠れてベタ塗りされていますよね。この全リストはサンプルプログラムとして公開します。

 簡単なシェーダプログラムでしたが、意外と使える技だと思います。これを利用すればオブジェクトの輪郭を描くこともできます。こういう小さなプログラムでシェーダーと遊ぶことがとても大切ですよね。



F 謝辞

 この題材を提供していただいたgood_jobさんに感謝いたします。