「点群表示機能を自作してみる」 のその8です。
描画している点群をピックする機能を作ってみます。 企画時には実装できるか不安がありましたが、一応動いて見えるレベルにまでは作れたと思いますので記事にしてみます。
出来上がり図
GitHub の以下のリビジョンを参照してください。
Revision: 6797f39e8f15eecb40ba33a7a968a945b5d561c2
Message:
added code to draw a polyline from selected points.
単に点をピックするだけでは何をしているか見えにくいですので、ピックした点をつないだポリラインを描画させてみました。 普通に距離とか測ってもよかったのですが、きれいに見せるのが面倒だったので...
ピックした(/選択した)点は赤く表示するようにもしています。
背景
ピック機能とは
このページでは、画面上の位置を指定して、そこに表示されている描画対象を特定する機能をピック機能と呼びます。 通常マウスポインタで画面上の位置を指定します。 そこに表示されている図形や文字などの描画対象要素を特定する機能です。 多くのアプリケーションではその要素を「選択」しますので、ピックより選択機能と呼んだ方が分かりやすいかもしれません。 点群ビューアーであれば3次元の点を選択することになります。
ピック機能をとりあげる理由
今回作った点群表示機能で、点群上の2点を選択して距離を測るようなこともやりたいから、というのが直接的な理由になります。 ですが技術的な背景をもう少し補足します。
以下は私の知る限り、という話ですが。
OpenGL にはこのピック機能を実現するための機能がありますが、DirectX にはこの機能がありません。 おそらくこの違いは CAD・図形処理ソフトウェアでの使用を想定するか、ゲームでの使用を主に想定するか、という点にあるのだろうと思います。 CAD・図形処理ソフトウェアではピック機能は必須です。
一方ゲームではこのような機能はあまり必要にならず、多くの場合スクリーンは単なる描画先デバイスになります。 ピック機能は不要か、アプリケーションが自分で工夫して実装することになります。 Unity でもピックに相当する機能はレイキャストのような干渉チェックで実現しているように思います。またゲームで汎用ピック機能が必要とされないのは、レスポンスが特に重要だからかもしれません。レスポンスを保証するのは汎用機能では難しいでしょう。
で、OpenGL にある機能を Directx を使って実装してみようというのがこのページの趣旨になります。
OpenGL が(GPUを使って)ピック機能を実現できるなら、DirectX でもできるでしょ、というのりです。 ただなにかしら(HW/SW の)技術的な背景があって実現できないとかあるのかもしれません。そのあたりを具体的に確認することが目的となります。
説明
実装方針
ざっくり言うと以下のような実装になります。
- 要素毎に異なる色で描画する。
- 指定したスクリーン位置のピクセルの色を取得する。 → 色が特定の要素を指す。
こう書くと簡単ですが、大きな注意点が一つあります。色をIDとして使う以上、その色の値が描画により変化してしまっては困るということです。 ということはざくっと思いつくだけでも以下の注意点が発生します。
- ライティングをオフにすることが必須。 (今回はもともと未使用。)
- ポリゴン(三角形)の頂点は全て同じ色で描画する必要あり。
- 別の観点で言うと、IDの異なる三角形間で頂点を共有することはできません。
- 半透明処理もオフにすることが必要。
- そもそも半透明の場合のピック処理がどうあるべきかも自明でないですが。
- アンチエイリアシング機能もオフ必須。
- ドライバ、ハードウェアのレベルで画像処理をかけられるとアウト。
- アプリ毎に設定を変えられたりするでしょうし、最近のシェーダーを使う環境ではそのようなことをしにくい(すると嫌われる)かなという気もします。 実際上どの位問題になるは分かりません。
これまで画面に出力する際の色の書式に DXGI_FORMAT_R8G8B8A8_UNORM (参考)を使ってきましたが、これだとシェーダー中では float で値を持つことになります。 float だからと言って値が変化するわけではない(変化してもらっては困る)のですが、IDに整数(ポインタ値を含む)を使う限り、整数と float の間の変換で誤差を生じることが怖いです。
ということで色の書式には DXGI_FORMAT_R16G16B16A16_UINT を使います。(大規模点群を扱うということで、32bit越えを想定して16bit整数×4 を使用します。半透明機能は使わないのでαもID指定に使用します。)
実装個所
変更箇所は主に以下の3か所です。
D3D11_INPUT_ELEMENT_DESC aPointListElem[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{ "COLOR" , 0, DXGI_FORMAT_R16G16B16A16_UINT, 1, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
};
- 通常の描画用の色は無視して別途用意する64bit IDを色として描画したいです。 そのために Slot1 の VertexBuffer にそのIDの配列を設定して描画することにしました。
D3D11_TEXTURE2D_DESC hTexture2dDesc;
hTexture2dDesc.Format = DXGI_FORMAT_R16G16B16A16_UINT;
hr = m_pDevice->CreateTexture2D(&hTexture2dDesc, NULL, &pRenderTargetBuffer);
hr = m_pDevice->CreateRenderTargetView(pRenderTargetBuffer.Get(), NULL, &m_pRenderTargetView);
ID3D11RenderTargetView* apRtv[1] = { m_pRenderTargetView.Get() };
m_pDC->OMSetRenderTargets(1, apRtv, m_pDepthStencilView.Get());
struct VS_INPUT
{
float3 Pos : POSITION;
uint4 Col : COLOR;
};
struct PS_INPUT
{
float4 Pos : SV_POSITION;
uint4 Col : COLOR;
};
uint4 psMain(PS_INPUT input) : SV_TARGET
{
return input.Col;
}
- UNORM 系の書式では float4 型を使いますが、UINT にする場合は uint4 を指定します。
- なお SV_TARGET はピクセルの色を表す値に設定するセマンティクスです。
その他の変更
選択要素の描画
今回は選択した点を赤く描画することにし、かつ Depth Buffer を無視して一番手前に描画することにします。そのためにデプスチェックを無効化します。
D3D11_DEPTH_STENCIL_DESC depthStencilForSelectedEntityDesc = depthStencilDesc;
depthStencilForSelectedEntityDesc.DepthEnable = FALSE;
hr = m_pDevice->CreateDepthStencilState(&depthStencilForSelectedEntityDesc, &m_pDepthStencilStateForSelectedEntity);
所感
とりあえず動いたように見えますが、現時点では今回の用途ではあまり現実的でないように思われました。 もう少し実装方法や使い方に工夫、割り切りなど必要なようです。
- 選択するために可視な全点を描画する必要がある。
- 小さいモデルであれば問題はありませんが、100億点とか描画が終わるまでに時間がかかるようなモデルだと、待ちが許容できない。
- かといって描画する点を減らしてしまうと、描画してあるのに選択できない、といった事態も生じる。(もしかしたらそれでもいいかもしれませんが。)
参考資料
- DXGI_FORMAT列挙子 (Microsoft)