モデルビュー行列に関して,方針を示す.
実現したいこと
多分,このアプリケーションにおけるカメラ配置の制約というのは,以下のような話.
位置について:
ある点 P とカメラとの距離 r は常に一定に保たれている.すなわち,カメラは点 P を中心とする半径 r の球面上に位置する.
姿勢について:
カメラは常に点 P を真正面に捉える.
点 P の位置について:
定位置ではなく移動できる.
カメラは点 P と長さ r の透明な棒で繋がっているイメージ:P を動かせばカメラは勝手についてくる(=平行移動).
実装の方針
上記のような話なのであれば,例えば
を変数として 管理/更新 すればどうか.
ここでは話を分かりやすくするために,カメラ姿勢に関してはカメラ座標系基底ベクトルのうちの2つ(例えば{カメラの正面後ろ方向 CZ,カメラの右手方向 CX})を変数として持つことにする.
3つ目の基底ベクトル:カメラの上方向 CY については都度この2つから外積で求めればいい.
モデルビュー行列の算出に必要な登場人物は以下のようになる.
定数:
維持管理すべき変数:
- P (3次元ベクトル)
- CX, CZ (3次元ベクトル.もちろん単位ベクトル)
↑から都度求めればよい物:
- CY <= CZ と CX の外積
- カメラ位置 <= P + r * CZ
初期化
制約を満たすように初期値を与える.
- P <= (0,0,0)
- CX <= (1,0,0)
- CZ <= (0,0,1)
とかかな.この場合,
- CY <= (0,1,0)
- カメラ位置 <= (0,0, r)
になる感じ.
平行移動
(前述のように,カメラ位置は毎回 P を用いて計算されるのだから)フレーム間のマウス移動量 (dx, dy) に合わせて P を動かすだけの話.
動かす方向はカメラ座標系の上下左右に合わせたいのだろうから,CX, CY に沿う方向に動かしてやればよい.
- P += ( dx * CX - dy * CY ) //←pixel座標系と3次元座標系とでY方向が逆なので,dyは-1を乗じて使用
カメラの球面上での移動
フレーム間のマウス移動量(dx, dy) から,カメラ座標系における回転軸を (dx, -dy, 0) と直交するベクトルとして (dy, dx, 0) と定めて, CX, CZ をそれぞれ回転させる.
(※平行移動の箇所で前記したのと同様に,dy に -1 を乗じている)
(言うまでも無く回転量は (dx,dy) のノルムに比例した感じで適当に決める)
「任意軸周りの回転」とかでググって回転行列 R の算出を実装し,
- CX <= R * (1 0 0) を(回転前のカメラ姿勢を用いて)ワールド座標系の値に変換したもの
- CZ <= R * (0 0 1) を 同上
として更新すればいい.
モデルビュー行列
カメラの位置と姿勢があるのだから,そこから 回転R と 並進T を作って,
で.
R は CX, CY, CZ をしかるべき位置に並べればいい.
T も (- カメラ位置)の要素をしかるべき位置にならべればいい.
実装してみた
(※実装して確かめた結果明らかになった↑の記述の諸々の誤りも修正)
変えた部分を以下に示す.
Window.h
マウスカーソルのフレーム間移動量 (dx,dy) が欲しいのだけど,既存コードの実装のままでそういった情報を拾えるのか否か不明であったため,ここは適当に以下のようにやっつけコードを追加して対応した.
- それ用のメンバ変数を追加し
operator bool()
内の glfwPollEvents();
以降に変数の値を更新する処理を追加した
C++
1 //適当にこれらの変数を追加した
2 bool m_bMouseLDown = false;
3 bool m_bAltKeyDown = false;
4 GLfloat m_PrevMousePos[2] = { 0,0 };
5 GLfloat m_CurrMousePos[2] = { 0,0 };
6
7 //描画ループの継続判定
8 explicit operator bool()
9 {
10 double x, y;
11
12 //イベントを取り出す
13 glfwPollEvents();
14
15 //追加した変数群の値更新処理
16 m_bMouseLDown = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) != GLFW_RELEASE;
17 m_bAltKeyDown = glfwGetKey(window, GLFW_KEY_LEFT_ALT) != GLFW_RELEASE;
18 m_PrevMousePos[0] = m_CurrMousePos[0];
19 m_PrevMousePos[1] = m_CurrMousePos[1];
20 glfwGetCursorPos(window, &x, &y);
21 m_CurrMousePos[0] = GLfloat(x);
22 m_CurrMousePos[1] = GLfloat(y);
main.cpp
ベクトルや行列の演算のために,GLM をもってきた.
C++
1#include "glm.hpp" //GLMのヘッダをinclude
2
3//作業関数を追加.3次元ベクトルVに直行するベクトルを返す.(※中身が知りたい場合,ここteratailでの私の過去の質問を参照されたい)
4inline glm::vec3 GetPerpendicularOf( const glm::vec3 &V )
5{
6 if( fabs(V[0]) < fabs(V[1]) )
7 { return glm::vec3( 0, -V[2], V[1] ); }
8 else
9 { return glm::vec3( V[2], 0, -V[0] ); }
10}
で,モデルビュー行列そのものに関する実装は以下.
- メインループの
while
の手前に定数と変数の定義を追加
- 透視投影変換行列は適当に見やすい感じに変えてる
- モデルビュー行列の演算部分をまるごと差し替え(GLM はここの実装で使っている.変数
modelview
自体は元々の Matrix
型のままにしてある.)
modelview
に要素値を代入するために,Matrix
型に const ではない operator[] を追加(コードは省略)
C++
1//定数と変数を追加
2 constexpr GLfloat CamRotRadius = 20; //カメラ旋回半径 r
3 glm::vec3 P(0,0,0); //カメラ旋回中心点 P(ワールド座標系での値)
4 glm::vec3 CX(1,0,0); //カメラ右手方向単位ベクトル(ワールド座標系での値)
5 glm::vec3 CZ(0,0,1); //カメラ後ろ方向単位ベクトル(ワールド座標系での値)
6
7 //ウィンドウが開いている間繰り返す
8 while (window)
9 {
10 //フレームバッファを初期化する
11 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
12 //シェーダプログラム使用開始
13 glUseProgram(program);
14
15 //---------------------------------------
16 //透視投影変換行列
17 // ※見やすいように,適度な具合に変更
18 const Matrix projection = Matrix::frustum( -0.1f, 0.1f, -0.1f, 0.1f, 1.0f, 100.0f);
19
20 //---------------------------------------
21 //※元々存在した行列 view に関するコード
22 // view はモデルビュー行列に一切関わっていないので不要なのだが,
23 // なんか後ろの方で別の目的に使われている(?)ので残してある.
24 const GLfloat* location(window.getLocation(0));//マウスをドラッグしたときのカーソルの増加量を取得する
25 GLfloat pos[3] = { 0 };//カメラの球面座標上の座標
26 pos[0] = -4.0f * cos(location[0]) * cos(location[1]);//* -4.0fは素の増加量だと、見た目の変化が乏しいため
27 pos[1] = -4.0f * sin(location[1]);
28 pos[2] = -4.0f * sin(location[0]) * cos(location[1]);
29 Matrix view(Matrix::lookat(pos[0], pos[1], pos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f));//カメラを注視点を向いたまま、球体座標上に動かす
30
31 //---------------------------------------
32 //モデルビュー変換行列を求める
33 Matrix modelview = Matrix::identity();
34 {
35 glm::vec3 CY = glm::cross( CZ, CX ); //カメラ上方向単位ベクトル(ワールド座標系での値)
36
37 //マウスカーソル移動量 (dx,dy)[pixel]
38 auto dx = window.m_CurrMousePos[0] - window.m_PrevMousePos[0];
39 auto dy = window.m_CurrMousePos[1] - window.m_PrevMousePos[1];
40 auto dnorm = sqrt(dx*dx + dy*dy);
41
42 if( window.m_bMouseLDown && dnorm>1 )
43 {
44 if( window.m_bAltKeyDown )
45 {//並行移動
46 P += 0.01f * ( dx*CX - dy*CY );
47 }
48 else
49 {//P を中心とした旋回
50 glm::mat3x3 R;
51 {//任意軸周り回転行列の計算(カメラ座標系のものを回す用)
52 glm::vec3 Rot_Axis = glm::normalize( glm::vec3( dy, dx, 0 ) );
53 glm::vec3 OtherAxis1 = glm::normalize( GetPerpendicularOf(Rot_Axis) );
54 glm::vec3 OtherAxis2 = glm::cross( Rot_Axis, OtherAxis1 );
55 glm::mat3x3 M(
56 OtherAxis1[0], OtherAxis2[0], Rot_Axis[0],
57 OtherAxis1[1], OtherAxis2[1], Rot_Axis[1],
58 OtherAxis1[2], OtherAxis2[2], Rot_Axis[2]
59 );
60
61 double RotAngle = dnorm * 0.01;
62 glm::mat3x3 Roll( cos(RotAngle), sin(RotAngle), 0, -sin(RotAngle), cos(RotAngle), 0, 0,0,1 );
63 R = glm::transpose(M) * Roll * M;
64 }
65
66 glm::mat3x3 C2W( CX,CY,CZ ); //カメラ座標→ワールド座標 変換行列
67 CX = C2W * R * glm::vec3(1,0,0);
68 CZ = C2W * R * glm::vec3(0,0,1);
69 CY = glm::cross( CZ, CX );
70 }
71 }
72
73 //カメラ位置
74 glm::vec3 CamPos = P + CamRotRadius * CZ;
75
76 //モデルビュー行列の中身を設定
77 modelview[0]=CX[0]; modelview[4]=CX[1]; modelview[8]=CX[2]; modelview[12]=glm::dot( CX, -CamPos );
78 modelview[1]=CY[0]; modelview[5]=CY[1]; modelview[9]=CY[2]; modelview[13]=glm::dot( CY, -CamPos );
79 modelview[2]=CZ[0]; modelview[6]=CZ[1]; modelview[10]=CZ[2]; modelview[14]=glm::dot( CZ, -CamPos );
80 }
81
82 //※以降は既存コードのままなので省略
マウスをどっちに動かしたらどっちに動くのか? というのが逆の方がやりやすいとかはあるかも.
(マウスを動かした方に「カメラが」動く感じになっているので,絵的には逆に動いているように感じるかも)
並行移動後の旋回に関しては,多分点 P の位置に何かを描画するとか,何かが無いと動きが掴み難いと思う.
(Blender の動画だとグリッド平面みたいなのがあるよね)