Programs / 最終更新時間:2004年08月19日 18時26分01秒

RayPickの考え方

RayPickネタです。3Dシーン上を特定位置からの直線(or線分)で検索して、交差したものを探したりする奴です。

かつてのDirect3D-RMにもあります。で、今はSDKサンプルにも入っています。それはそれとして、昔、自分で実装するときにはこんな感じでしたというのを紹介します。

目次

手順

それでは手順を考えてみます。

RayPickの手順

1.Pickを行う初期位置(RayStart)と光線ベクトルの向き(RayDir)を決める。このベクトルはワールド座標で。

2.RayStartとRayDirをメッシュのモデル座標に変換したベクトルを計算。メッシュ内のポリゴンと、直線(or線分)と三角形の当たり判定(交点判定)を行う。

3.メッシュの数だけ2を繰り返す。

これだけ。でも、それだけだとどれか特定のものを選択(Pick)できないので……

一番近いものは?

RayStartから一番近いメッシュやポリゴンをPickしたいときなどのために、それが選べるよう、データを保持してやります。

具体的には、

光線が交差した順のリストの作成

1.「あるメッシュ内で光線と交差したポリゴン(とその交点)のリスト」を作る。交差したポリゴンがなければ4へ。

2.交点がRayStart(のこのメッシュでのモデル座標)に一番近いものを選ぶ。

3.このメッシュへのポインタと、一番近いポリゴン(とその交点)のデータを「光線と交差したポリゴンを持つ全メッシュリスト」に追加。

(「あるメッシュ内で光線と交差したポリゴン(とその交点)のリスト」はこの時点で破棄できる)

4.メッシュの数だけ1〜3を繰り返す。

5.「光線と交差したポリゴンを持つ全メッシュリスト」を交点の近い順にソート。

あとは、リストの中から一番近いメッシュやポリゴンを選ぶなりなんなりすればよい、と。

でも、馬鹿正直にやっていたのでは処理が重いです。

なので、高速化する方法など考えてみます。

高速化するには

  • メッシュのポリゴンとの交差判定をするまえに、メッシュの境界箱や境界球と(光線の)当たり判定をして、はずれならポリゴンとの交差判定は省く。
  • 手前のメッシュのみが知りたい等という場合には、境界箱/球との当たり判定を先にまとめて行い、光線の当たったメッシュのリストを近い順にソート。で、そのメッシュ順にポリゴンの交差判定。メッシュ内でポリゴンに当たらなければ、次のメッシュへ。当たれば、当たったポリゴンの中で一番近いものを選択(Pick)し、以降の判定は行わない。ただし、境界箱/球同士が交差している場合は、両方で判定して、近い方のメッシュ/ポリゴンを選ぶ必要がある。
  • ポリゴンとの厳密な当たり判定が必要ない場合には、ローポリ化された当たり判定用の簡易モデルを判定に使用する。

他にもあるかも。

当たり判定

しかし、それはともかく当たり判定(/交点計算)は、具体的にどうすんの?

「三角形と直線/線分の交点を得る関数」が必要です。もし、境界箱/球を使うのならば、「箱/球と直線/線分の交点を得る関数」とか、「箱/球同士の交差判定をする関数」などが必要になります。

となると、基本的に数学の問題なので、それを理解する必要があります。でも、すでに実装されたソースやら何やらの「解説」や「答え」があれば、理解もはやまるかも。Googleで調べてみましょう。

スクリーン座標をクリックして選択

ところで、RayPickの使い道として僕が一番望んでいたのは、画面をクリックしてそこにあるオブジェクトを選択する、とかそういう用途です。

それについても語らねばなるまい。

以下、その手順です。[1]

  • [1]なお、3Dライブラリの中には3D空間<-->2D空間のベクトル変換関数が用意されていることもあり、それを使うと楽ができます。DirectX(D3DX)ではD3DXVec3ProjectとD3DXVec3Unproject、OpenGLではgluProjectとgluUnProjectという関数があります。また、OpenGLの場合は、glRenderMode(GL_SELECT) を使う手もあります。

画面をクリックしてそこにあるオブジェクトを選択するには

1.ベクトル clickPos をスクリーン座標で設定

クリックしたスクリーン座標 clickX, clickY とした場合、

clickPos.x = clickX;
clickPos.y = clickY;
clickPos.z = 0.0f; // 0=最も手前, 1=最も奥 の場合

2.ワールド座標を画面座標に変換する4x4行列を作成

matrix worldToScreenMatrix = viewMatrix * projMatrix * viewportMatrix; 

viewMatrixとprojMatrixは、IDirect3DDevice7::GetTransformのD3DTRANSFORMSTATE_VIEWやD3DTRANSFORMSTATE_PROJECTIONで得ることができます。

viewportMatrixは、IDirect3DDevice7::GetViewportでD3DVIEWPORT7 viewportなどに値を得て、

float w, h;
w = (float)viewport.dwWidth*0.5f;
h = (float)viewport.dwHeight*0.5f;

matrix viewportMatrix;
viewportMatrix._11 = w; // width
viewportMatrix._22 = -h; // height
viewportMatrix._41 = (float)viewport.dwX + w; // x
viewportMatrix._42 = (float)viewport.dwY + h; // y

と、このようにして作成します(他の行列の持ち方などによって、微妙に変わってくるかも)。clipMatrixはここでは必要ないので省略。

3.clickPos(スクリーン座標)をワールド座標に変換

worldToScreenMatrix の逆行列が、スクリーン座標からワールド座標に変換を行える行列です。

matrix ScreenToWorld = InverseMatrix(worldToScreenMatrix);

この行列でclickPosの座標系を変換してやります。

worldClickPos = ScreenToWorld * clickPos;

4.Picking用の光線を用意

カメラのワールド座標上での位置(=光線の始点)をeyeとすると、Picking用の光線の向きベクトルは、

worldClickDir = worldClickPos - eye;

をNormalize(正規化)したものです。

5.RayPick

あとは、「4」で用意した光線のデータを使って、各メッシュをPickしてやればOK。
スクリーン上のZ値を使えば、不必要な交差判定を省くこともできるでしょう。