これはrogy Advent Calendar 2015の16日目の記事です.

こんにちは.15のアーク(@_Ark_0 )と言います. rogyブログに記事を書くのは初めてです.
一応ゲーム作ったり,競プロに参加したりしています.最近はD言語がマイブームです. D言語くんかわいい

今回は,マウス操作によってカメラの移動を直感的に動かせるようにするためのクラスを作ってみました. (3Dグラフィックでの話です.実在のカメラではないです><)

概要

何らかの式を3D空間に視覚化させたり,実験的に何かを3D描画させたりすることがあると思います.そのときに困るのがカメラの視点.ある一方向からの視点からだと全体像がよくわからなかったり,カメラの位置を数値で設定するのが面倒だったりします.そこで,マウス操作によって簡単にカメラを動かせたらいいなと思い,実装してみました.
具体的には次のマウス操作ができるように作りました.
  • 左ボタンをドラッグ → 回転
  • 中ボタンをドラッグ → 平行移動
  • 右ボタンをクリック → 初期化
  • マウスホイールを動かす → 拡大縮小
demo_MouseCamera
(Three.jsのTrackballControlsみたいな挙動を目指しました)

環境

Processingを用いました.(Processingはグラフィック系に特化した言語or統合開発環境です.ちょっとしたものの可視化などにおすすめです)
他の環境でもだいたい同じような手段で実装できると思います.

アフィン変換


点$P(x, y, z)$を点$Q(X, Y, Z)$に移動させるときにアフィン変換を用いると便利です. \[ \left ( \begin{array}{ccc} X \\ Y \\ Z \\ 1 \end{array} \right ) = \left ( \begin{array}{ccc} a_{11} & a_{12} & a_{13} & b_1 \\ a_{21} & a_{22} & a_{23} & b_2 \\ a_{31} & a_{32} & a_{33} & b_3 \\ 0 & 0 & 0 & 1 \end{array} \right ) \left ( \begin{array}{ccc} x \\ y \\ z \\ 1 \end{array} \right ) \] このようにして4次正方行列によって点$P$から点$Q$への写像先を定めます.
例えば,$x$軸正方向に$5$だけ平行移動するような写像を定める行列を$A$,$z$軸周りに$\pi / 3$だけ回転する写像を定める行列を$B$としたとき,$x$軸正方向に$5$だけ平行移動してから$z$軸周りに$\pi / 3$だけ回転する写像を定める行列は$BA$という行列の積で表せます.行列で変換を表せることの良さはこの辺りに出てきます.
具体的な行列は次節以降に書きます.

Processingには,この変換行列の成分をそのまま引数にとって座標変換をしてくれる組み込み関数applyMatrix()があるので,それを使いました.プログラムの大筋の流れとしては次のようにしました.
  • 最初,変換行列は単位行列にしておく
  • マウスの入力を受けるたびにそれに対応する行列を変換行列に左からかけていく
  • 毎フレームで「カメラを初期位置に戻し,applyMatrix()で累積した変換行列を適用する」という処理をする

平行移動

$\vec{t} = \left ( \begin{array}{ccc} t_x \\ t_y \\ t_z \end{array} \right )$だけ平行移動させるようなアフィン変換は次のようになります. \[ \left ( \begin{array}{ccc} X \\ Y \\ Z \\ 1 \end{array} \right ) = \left ( \begin{array}{ccc} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{array} \right ) \left ( \begin{array}{ccc} x \\ y \\ z \\ 1 \end{array} \right ) \] よって中ボタンをドラッグしているときの平行移動を表す行列は,始点の$xy$座標を$(x_{pre}, y_{pre})$,終点の$xy$座標を$(x_{now}, y_{now})$(ただし,原点は画面の中心)として次のようにしました. \[ \left ( \begin{array}{ccc} X \\ Y \\ Z \\ 1 \end{array} \right ) = \left ( \begin{array}{ccc} 1 & 0 & 0 & x_{now} - x_{pre} \\ 0 & 1 & 0 & y_{now} - y_{pre} \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right ) \left ( \begin{array}{ccc} x \\ y \\ z \\ 1 \end{array} \right ) \]

拡大縮小

原点との距離を$s$倍にするようなアフィン変換は次のようになります. \[ \left ( \begin{array}{ccc} X \\ Y \\ Z \\ 1 \end{array} \right ) = \left ( \begin{array}{ccc} s & 0 & 0 & 0 \\ 0 & s & 0 & 0 \\ 0 & 0 & s & 0 \\ 0 & 0 & 0 & 1 \end{array} \right ) \left ( \begin{array}{ccc} x \\ y \\ z \\ 1 \end{array} \right ) \] よってマウスホイールを動かしているときののときの拡大縮小を表す行列は,ホイールの回転量を$c$として次のようにしました. \[ \left ( \begin{array}{ccc} X \\ Y \\ Z \\ 1 \end{array} \right ) = \left ( \begin{array}{ccc} e^c & 0 & 0 & 0 \\ 0 & e^c & 0 & 0 \\ 0 & 0 & e^c & 0 \\ 0 & 0 & 0 & 1 \end{array} \right ) \left ( \begin{array}{ccc} x \\ y \\ z \\ 1 \end{array} \right ) \] 冪乗を取ることによって倍率を正の数にすることを保証し,また回転量の正負で拡大と縮小が切り替わるようにしました.(もっといい方法があるのかもしれない)

回転

回転の行列は$x$軸周りの回転とかなら楽だが任意の回転軸における回転をしたかったので,ロドリゲスの回転公式を用いました.(ロドリゲスの回転公式についてはこちらの説明と証明が分かりやすかったです)
正規化したベクトル$\vec{n} = \left ( \begin{array}{ccc} n_x \\ n_y \\ n_z \end{array} \right )$を回転軸として$\theta$回転をするようなアフィン変換は次のようになります. \[ \left ( \begin{array}{ccc} X \\ Y \\ Z \\ 1 \end{array} \right ) = \left ( \begin{array}{ccc} \cos \theta + n_x^2 (1-\cos \theta) & n_x n_y (1-\cos \theta) - n_z \sin \theta & n_x n_z (1-\cos \theta) + n_y \sin \theta & 0 \\ n_y n_x (1-\cos \theta) + n_z \sin \theta & \cos \theta + n_y^2 (1-\cos \theta) & n_y n_z (1-\cos \theta) - n_x \sin \theta & 0 \\ n_z n_x (1-\cos \theta) - n_y \sin \theta & n_z n_y (1-\cos \theta) + n_x \sin \theta & \cos \theta + n_z^2 (1-\cos \theta) & 0 \\ 0 & 0 & 0 & 1 \end{array} \right ) \left ( \begin{array}{ccc} x \\ y \\ z \\ 1 \end{array} \right ) \] これで,任意の回転を表せるようになりましたが,実際にマウスのドラッグと回転をどのようにして結びつけるかは結構悩みました.最終的に決めた方法は次のようにしました.
  1. クラスの生成時に仮想的な球の半径$radius$を引数に指定する.
  2. 左ボタンのドラッグをしたときに,マウスのxy座標$(x, y)$ (ただし,原点は画面の中心)を取得し,$x' = x/radius$,$y' = y/radius$,$size = \sqrt{x'^2 + y'^2}$という数を定める.
  3. ベクトル$\vec{v} = \left ( \begin{array}{ccc} v_x \\ v_y \\ v_z \end{array} \right )$を次式で定義する.
  4. \[ \left ( \begin{array}{ccc} v_x \\ v_y \\ v_z \end{array} \right ) = \begin{cases} \left ( \begin{array}{ccc} \frac{x'}{size} \\ \frac{y'}{size} \\ 0 \end{array} \right ) & (size \ge 1) \\ \left ( \begin{array}{ccc} x' \\ y' \\ \sqrt{1- x'^2 - y'^2} \end{array} \right ) & (size < 1) \end{cases} \]
  5. マウスのドラッグにおいて,始点の$xy$座標$(x_{pre}, y_{pre})$,終点の$xy$座標$(x_{now}, y_{now})$に対応したベクトル$\vec{v}$をそれぞれ$\overrightarrow{v_{pre}}$,$\overrightarrow{v_{now}}$とする.
  6. $\overrightarrow{v_{pre}}$と$\overrightarrow{v_{now}}$のなす角を$\theta$とすると,$\cos \theta = \overrightarrow{v_{pre}} \cdot \overrightarrow{v_{now}}$,$\sin \theta = |\overrightarrow{v_{pre}} \times \overrightarrow{v_{now}}|$となる.($\overrightarrow{v_{pre}}$,$\overrightarrow{v_{now}}$がすでに正規化されていることに注意)
  7. ベクトル$\vec{n}$を$\vec{n} = \overrightarrow{v_{pre}} \times \overrightarrow{v_{now}}$で定義する.
  8. 以上で定めた$\vec{n}$の各成分と$\cos \theta$,$\sin \theta$を上に書いたロドリゲスの回転公式に代入して回転行列を定める.
最終的に定められた回転行列は,回転軸$\vec{n}$まわりに$\theta$回転する回転行列,つまり$\overrightarrow{v_{pre}}$から$\overrightarrow{v_{now}}$に向かう回転を示すこととなります.$\overrightarrow{v_{pre}}$と$\overrightarrow{v_{now}}$はマウスの座標を射影したときの球面上の位置ベクトルを示します.

初期化

右クリックしたときに,変換行列を単位行列にするようにしました.そうすることで変換行列が定める写像は恒等写像になり初期のカメラ位置に戻すことができます.

ソースコード

実際に作成したプログラムのソースコードは次のとおりです. 実際の使用例があったほうが使い方がわかりやすいと思うので,このクラスを用いた,球とトーラスを描画するプログラムを貼っておきます.