その7 カーソルスワイプによるカメラ移動
〜 摘まんだ物とカーソル位置を常に一致させる 〜
@ 3D空間にある「物」を摘まんでカメラを動かす
ゲームで3D空間にある物をカーソルやタップで選択するシチュエーションは沢山あると思います。大抵はレイを飛ばして衝突したら選択と判断したりします。この選択した物をドラッグして移動させるというのも良くあります。この派生と言いますか逆と言いますか、選択した物は動かさずにカーソルの移動に合わせてカメラ側を動かす事がままあります。例えばシム系のゲームなどはフィールドの適当な所を摘まんで上下左右に動かせます。これは摘まんだ世界が動いているのではなくて、カメラが動いているんです。
このカーソルスワイプ(ドラッグ)によるカメラ移動のアルゴリズムを見ていく事にしましょう。
A 状況の整理
カメラがとあるフィールドを映し出しているとします:
青い建物的な辺りをカーソルで指定してクリックし、右上にすすすっとドラッグすると、
フィールドがカーソルにくっついて右上に移動したかのように見えます。が、これはカメラが逆方向に移動したのでそう見えているだけです。カメラの位置が移動して切り取る世界が変わったにもかかわらず、カーソルの位置には青い建物的な物がちゃんとありますね。これを作ろうという話です。
B カーソルの座標をワールドへ変換
まず前提として、移動前と移動後のカメラの姿勢(回転)は変わらないとします。また、カメラはワールドにある何らかの平面から一定の距離を保って移動するとします。上の例だとその平面は地面になります。カメラの画角(fov)も移動中は固定にしておきます。カーソルはスクリーン座標(xs, ys)で表しますが、画面の中心が(0,0)だとしておきます。
考えやすくするため、カメラの前に仮想のスクリーン座標があるとしましょう。模式的に書くと以下のような感じです:
カメラCはワールド内に配置されてとある方向を向いています。カメラの画角はfovになります。カメラの前にある青いラインが真横から見ている仮想的なスクリーンです。仮想スクリーンは画面中央が(0,0)な座標系とします。点Pはカーソルの位置に対応した座標です。下のラインが対象物P'のある平面で、法線NとP'の位置で表現されます。カメラはこの平面に平行に移動します。
カーソル位置に対応した点Pのワールド位置を求めるのがポイントの一つです。それには仮想スクリーンのカメラに対する距離を工夫してあげます。仮想スクリーンは実はカメラに対して真正面を向いていればどの距離にあっても構いません。仮に距離がカメラにうんと近ければ、カーソルを沢山動かしても変換後の位置Pは殆ど一緒になりますし、凄く遠ければカーソルのわずかな動きがワールド空間では大きな変動になります。という事は、この距離を上手く調節すれば、「点Pの移動量=ワールド空間での移動量」となる仮想スクリーンになってくれるはずです。
仮想スクリーンとカメラとの距離をDとします。カーソルが存在する実際の画面の解像度を(W,H)とすると、仮想スクリーンの高さHsとその解像度Hとが一致すればカーソルの移動量がワールドのそれと一致します。そうなるDが求める距離です:
上図のHとDの関係はtan関数を使って表せるので、以下のように距離Dを求められます:
この距離Dは画角fovや画面解像度が変わらない限り一定です。以後この仮想スクリーンは常にこの距離Dにあるとします。
距離Dに仮想スクリーンを置いた事でその座標値Pcはカメラ空間(カメラのローカル空間)の座標と一致するようになりました。カメラ空間にある座標Pcをワールド座標の位置Pへ変換するには、カメラの姿勢行列をPcに掛け算すれば良いんです。つまり:
(xs, ys)はカーソルのスクリーン座標(画面中央が(0,0))です。距離Dさえ求めておけば、スクリーン座標をワールドに変換する事自体は簡単ですね。
C カーソルワールド座標Pの先にある位置P'を求める
摘まむ位置P'は点Pの先にあります。これはカメラの位置CからP'へ飛ばしたレイと指定の平面との交点です。これは定番な解き方があります。カメラから点Pに向かうベクトルをvとすると、レイ上の座標P'は、
と表せます。aが0ならカメラの位置ですし、aが何か値を取ればそればカメラの位置から点Pを通るレイ上のどこかとなります。aが何らかの値の時、P'は平面上にもなるはずです。
P'は対象平面上にあるとしていますので、その平面上の一点P0からP'へ向かうベクトルと法線Nは直行します。つまり内積が0です。ここから式を展開すると:
とaを求める事が出来ます。これでスクリーン座標(xs,ys)を平面上のP'へ変換する道筋が出来ました。
D カーソル位置Qの先のQ'をP'にフィットさせる
さてここからが本番です。今クリックしたスクリーン座標位置をPとして、マウスや指を動かしてそれをQへ移動させたとします。Cまででスクリーン座標位置を平面位置へ変換する道筋は出来ているので、Qに対応したQ'は簡単に求められます:
このカーソル位置Qの先にクリックした位置にあったP'が来るようにカメラ自体を動かせば本章の目標を達成できます。上図の場合カメラから水平に伸びるベクトル方向へ動かせば良い訳です。これは、Q'をP'へ動かせばいいので、P'-Q'という事になりますね。
という事で、クリックしたときのカメラの位置C、その時の平面上の位置P'、そしてカーソルを動かした時の平面上の位置Q'があればすべての計算ができる事になります。P'やQ'を求めるには仮想スクリーンまでの距離Dが必要で、それには画面の高さH、カメラの画角fovを用います。クリック位置をワールド位置Pに変換するにはカメラの姿勢行列も必要です。動かす対象となる平面の情報として法線Nと平面上の一点P0もいりますね。必要パラメータと式を以下にまとめます:
上のD,P,P'はクリック時に計算して保持しておけば良い値です。ドラッグしてマウスが動く度にQ'を計算し、CmovePosをカメラに与えれば冒頭の平面に吸い付くような見た目になります。
上の式はカメラをズーム(前後へ移動)しても、クリックして動かしている間にパラメータを変更しなければもちろん成り立ちます。動かしている最中にパラメータを変更する場合はさらに考慮を深める必要がありますが、ひとまず上の式でも大分いい感じになりますのでお試しあれです(^-^)
E Unityでのカメラ空間 → ワールド空間の変換について
上のPを求める式で、Unityのように行列が使えない(表向きに扱えない)場合は、カメラの回転であるクォータニオンQtとカメラのワールド位置Posを利用し、
と計算します。