回転とズーム¶
クラス AppBase
が制御するウィンドウ上でのマウスイベントに応じて、シーンのズームや回転を動的に実現するためのクラス群の定義を行うモジュールを作る。本稿では、そのモジュールの実装について記す。
クラス AppBase
階層側から受け取ったマウスの動きを捕捉して、ズーム量や回転量を半ば無理矢理算出し、返送する。
方針¶
マウスモーションによるビューの変換を実装する。
マウスジェスチャーを 2 種類実装するという見方ができるので、一連のクラス階層を実装する。抽象基底クラスを
AbstractViewNavigation
とし、差し当たりその派生クラスを二つ、ViewRotate
とViewZoom
を定義する。クラス
AppBase
にそれらのクラスのオブジェクトを保持させて、マウスイベントを delegate させる。
モデルの回転に関して、次の点を考慮する:
とりあえず左クリックドラッグ時に動的にビューが回転するようにする。
マウスジェスチャーで回転を加算的に表現するのに便利な四元数オブジェクトを導入する。
カメラのズームに関して、次の点を考慮する:
とりあえず右クリックドラッグ時に、動的にカメラのズームを機能させる。
射影変換が透視変換なので、fovy の値を上下によりズーム率の変更とする。
クラス ViewRotate
¶
回転に関しては四元数を用いた計算をする。感覚的には、回転以外の同次座標系表現によるアフィン変換のコードより一手間多く手順がかかっている気がする。
メソッド capture_mouse
¶
"""Capture mouse."""
super(ViewRotate, self).capture_mouse(x, y)
self.last_quat_arg = trackball_space(x, y, self.width, self.height)
マウスドラッグ開始時のマウスポインターのウィンドウ座標的なものを、トラックボール(仮想半球)上の点の座標に読み替える。
メソッド update_mouse_position
¶
"""Handle mouse motion event."""
# Compute position on hemisphere.
cur_quat_arg = trackball_space(x, y, self.width, self.height)
# Compute the change in position the hemisphere.
diff = cur_quat_arg - self.last_quat_arg
if (abs(diff) < 1e-2).all():
return
last_quat = Quat(np.resize(self.last_quat_arg, 4))
cur_quat = Quat(np.resize(cur_quat_arg, 4))
self.last_quat_arg = cur_quat_arg
self.quat = self.quat * last_quat * cur_quat
self.app.update_rotation(self.quat)
GLUT.glutPostRedisplay()
マウスの動きが微小なときには、メソッドが
False
を返してウィンドウの再描画をさせない。直前のマウス位置、現在のマウス位置にそれぞれ対応する仮想半球上の変位の回転量を計算する。詳しくは後述する。
自作関数
trackball_space
の戻り値は三成分のnp.array
なので、四成分になるよう拡大する。それをそのまま
Quat
オブジェクト化する。
self.quat
を更新し、なおかつメソッドself.app.update_rotation
に渡す。呼び出し先のメソッドでは、OpenGL のコンテキストバージョンに応じた方式で、現在の変換行列を更新する。ウィンドウの再描画をさせる。
クラス ViewZoom
¶
メソッド update_mouse_position
¶
メソッド update_mouse_position
だけを解説する。
"""Handle mouse motion event."""
cur_pos = nds_coord(x, y, self.width, self.height)
factor = np.exp((cur_pos[1] - self.first_mouse_position[1]) * -0.25)
fovy = max(min(self.app.fovy * factor, 125), 25)
self.app.update_projection(fovy, self.width, self.height)
GLUT.glutPostRedisplay()
マウスがドラッグ開始地点から上方向に動いていればズームイン(拡大)、下方向ならばスームアウト(縮小)というふうに振る舞う。
個人的な好みにより、fovy 値を 25 度から 125 度に制限している。
更新後の
fovy
をメソッドself.app.update_projection
に渡す。呼び出し先のメソッドでは、OpenGL のコンテキストバージョンに応じた方式で、現在の変換行列を更新する。最後にシーンの再描画をさせる。
補助関数群を定義する¶
マウスカーソル位置のウィンドウ座標からある種の座標系に変換するための関数群を用意する。マウス位置、およびその変位をウィンドウの形状に依らずに取り扱いたいので、こういうものが要るのだ。
"""Convert SCS to NDS."""
return (2 * x - width) / width, (height - 2 * y) / height
def trackball_space(x, y, width, height):
"""Project orthographically the mouse cursor to a hemisphere."""
v = np.zeros(4)
v[0], v[1] = nds_coord(x, y, width, height)
d = np.dot(v, v) # length-squared
if d < 1:
v[2] = np.cos(d * np.pi / 2)
else:
v[2] = 0
return v / norm(v)
関数 nds_coord
¶
関数 nds_coord
はスクリーン座標から正規化座標系 \({[-1, 1]} \times
{[-1, 1]}\) への写像を求める。ただし y 座標は上方向を正とする。ウィンドウのサイズが 0 でない任意の大きさであっても、変換後は座標成分の絶対値が 1 以下になる。
関数 trackball_space
¶
関数 trackball_space
はスクリーン座標から仮想的な半球上への写像を求める。
OpenGL: A Primer Second Edition 読書ノート 4/4 等を参考。