マウスピッキング、
つまり、画面上に描画された3Dオブジェクトを、
マウスポインタでピック(選択)する、というテクニックです。
これができるようになれば、
後は、選択したオブジェクトを
マウスでグリグリと移動させるなり、回転させるなり、
いろいろできるようになるかもしれません。
・・・が、まぁ、そこはあなたの実装次第です。
とにかく、
そんなインタラクティブなプログラムを実現するのに必要なのが
このマウスピッキングです。
OprnGLでは、マウスピッキングを実現するための
セレクションモードという機能が用意されているので、
これを利用します。
マウスピッキングに関しては、
特にC#でやろうとすると難しいようなところは、無いと思います。
全体の手順としては、次のようになります。
まずは、セレクションバッファというものを作成します。
これは、OpenGLがセレクションモードの実行結果を格納するためのバッファです。
uint 型の配列として用意します。
で、用意したバッファをOpenGLに渡しておきます。
//セレクションバッファを作成。
int selectionBufferLength = 100;
uint[] selectionBuff = new uint[selectionBufferLength];
//OpenGLに渡す。
gl.SelectBuffer( selectionBuff.Length, selectionBuff );
セレクションバッファ selectionBuff の長さは、
あまり短いと、実行結果が入りきらなくて途中で切れてしまうので、
ある程度の余裕を持たせておきます。
glSelectBuffer 関数で、OpenGLにこのセレクションバッファを渡します。
実行結果としてどんな値が格納されるかの説明は、後ほど。
OpenGLの描画モードをセレクションモードに設定します。
といっても、必要なのは次の1行だけですが。
gl.RenderMode( gl.SELECT );
このへんは通常のシーン描画と同じです。
普通に、glOrtho関数とか gluPerspective関数とかで
射影変換行列を設定します。
このタイミングで、マウスポインタの座標やマウスピッキングの選択範囲を指定するのですが、
これは、射影変換行列を変形することで行われます。
この範囲内に描画されるオブジェクトがヒットしたことになり、
セレクションモードの実行結果として返されます。
選択範囲は、選択点の座標と、選択範囲の幅・高さで指定します。
gluPickMatrix という関数を使用します。
//マウスポインタの座標(単位:pixel)
//(注:ビューポートなどと同様に、描画領域の左下が原点)
double mouseX, mouseY;
//マウスポインタを中心として、ヒットする範囲の幅と高さ(単位:pixel)
double width, height;
//ビューポートも指定する必要があります。
int[] viewport;
gluPickMatrix( mouseX, mouseY, width, height, viewport );
ここも、特別なことはないです。
普通にgluLookAt関数などでモデルビュー変換を行います。
描画オブジェクトに、名前(というか番号)を割り当てながら描画していきます。
この名前は、階層化が可能になっていて、
1, 2, 3, 4, 5, 6 .....
だけでなく、たとえば、
1,
2,
3-0,
3-1,
3-2-0,
3-2-1,
4,
5 .....
という具合に割り振ることが可能です。
この名前を割り当てるには、次の4つの関数を使用します。
・glInitNames()
名前を初期化します。
なにはともあれ、まずはこれを呼び出します。
・glPushName( uint name )
指定した名前をネームスタック(名前の階層)の先頭に積み上げます。
・glPopName()
ネームスタックの先頭を一つ削除します。
・glLoadName( uint name )
ネームスタックの先頭を指定した名前に置き換えます。
「ネームスタック」というのがここで登場してますが、
このスタックに積まれた名前(っていうか番号)が、
描画オブジェクトに割り振られる名前となります。
まずは、glInitNames で初期化を行い、
glLoadName, glPushName, glPopName でネームスタックを操作して名前を割り振る、
という手順です。
注意点としては、glInitNames を呼び出した後はネームスタックが空なので、
はじめに glPushName で0以上を指定しておかないと、エラーになります。
また、これら4つの関数は、セレクションモードでない場合は無視されます。
描画に関しては、通常通りです。
で、具体的にどうやるかというと、
以下のような感じです。
glInitName();
glPushName( 3 ); // 3 (ネームスタック = 指定された名前)
// オブジェクトA(以下B,C,D...と続く)を描画。
DrawObjectA();
glPushName( 1 ); // 3-1
DrawObjectB();
glPushName( 2 ); // 3-1-2
DrawObjectC();
glLoadName( 5 ); // 3-1-5
DrawObjectD();
glPushName( 4 ); // 3-1-5-4
DrawObjectE();
glPopName(); // 3-1-5
DrawObjectF();
glPopName(); // 3-1
glLoadName( 3 ); // 3-3
DrawObjectG();
glPopName(); // 3
glLoadName( 4 ) // 4
DrawObjectH();
一通り描画が終わったらセレクションモードを終了し、
今度はその結果、つまり、
どのオブジェクトがヒットしたかを取得します。
これは、先の手順でOpenGLに渡しておいたセレクションバッファに格納されています。
セレクションモードの終了は、
int hits = gl.RenderMode( gl.RENDER );
とするだけです。
このとき、戻り値として
ヒットしたオブジェクトの数が返されるので、
ちゃんと受け取っておくこと。
さて、
ヒットしたオブジェクトがいくつかあったとして、
次に、セレクションバッファを見て、
どのオブジェクトがヒットしたかを調べるわけですが、
セレクションバッファ自体は単なるuint型の配列なので、
ちゃんとそれを読んで解析する必要があります。
セレクションバッファに格納されるデータは、
次のようなルールで並んでいます。
・ネームスタックに積まれた名前の数
・選択範囲を横切るプリミティブの頂点のデプス値の最小値
・選択範囲を横切るプリミティブの頂点のデプス値の最大値
・ネームスタック(= オブジェクトの名前)
これらの項目が、ヒットしたオブジェクトの数だけ
セレクションバッファの中にずるずると並んでます。
もうちょっと詳しく。
1つ目、「ネームスタックに積まれた名前の数」
というのは、名前の階層の数の事です。
さっきの描画オブジェクトへの名前割り当ての例でいうと、
ObjectA(名前:3)なら1、
ObjectB(名前:3-1)なら2、
ObjectC(名前:3-1-2)は3、
ということ。
で、4つ目、「ネームスタック(= オブジェクトの名前)」。
ここに、その描画オブジェクトの名前がネームスタックの底(=親階層)から順に並びます。
同じく、さっきの例でいうと、
ObjectA(名前:3)なら、3
ObjectB(名前:3-1)なら、3, 1
ObjectC(名前:3-1-2)は、3, 1, 2
という具合です。
2つ目と3つ目の
「選択範囲を横切るプリミティブの頂点のデプス値の最小値」
これは、ヒットしたオブジェクトのうち、
選択領域にひっかかったプリミティブの頂点座標の
ウィンドウ座標系でのZ成分(=デプスバッファのデプス値)の最大値・最小値です。
・・・何か、いまいち説明がややこしい。
まぁ、詳しくは赤本などを参照されたし、ということで。
とにかく、
複数のオブジェクトがヒットした場合に、
このデプス値を比較すれば、
選択範囲のあたりでのオブジェクトの画面上での前後関係がわかる、というわけです。
あと、これに関しては、もう一つ。
この値はuint型の配列に格納されていますから、
多くの場合はdouble型などに変換する必要がありますが、
そんなときは、uint.MaxValueで割り算してやればOKです。
さて、
これで、セレクションモードでの描画から
セレクションバッファの解析まで、
マウスピッキングに必要なOpenGLに関連した手順は、おしまいです。
あとは、
この一連の手順をどうやってプログラムに実装するかとか、
ヒットしたオブジェクトをどう料理するのかとか、
そういった話になってきますので、
この解説は、これにて終了です。
えーと、
GLSharpに、マウスピッキングのためのクラスを作ってあるので、
(「Selection.cs」というソースファイルに入ってます。)
それで、サンプルプログラムの代わりということで。
セレクションバッファを解析するクラスとかもありますし、
セットになってるデモプログラムその2では、
実際にマウスピッキングとか、
オブジェクトをマウスでつかんで移動させたりとかやってます。