OpenGLプログラミング/Glescraft 4
はじめに
[編集]3Dシーンのビュー操作にはいくつかの方法があります。 コンピュータ支援設計(CAD)ソフトウェアの場合、通常はカメラのポイントが固定で、(3D)マウスやトラックボールを使用して関心のあるオブジェクトを回転または移動させます。多くのゲーム、特にファーストパーソンシューティング(FPS)では、ワールドが固定で、カメラをワールドのあちこちに移動させます。 グラフィックスカードは、これらの二つの方法を区別せず、あなたが用意する model-view-projection matrix がそのまま適用されます。 主な違いは、MVP行列をマウスおよび/またはキーボードで操作する方法です。 一人称視点のカメラでは、MVP行列を2つのベクトルから導出します。カメラの位置とビューの角度です。
glm::vec3 position;
glm::vec2 angles;
GLUTでマウスの動きをキャプチャ
[編集]GLUTには、マウスの動きをキャプチャするための2つのコールバックがあります。 ひとつは、少なくとも1つのマウスボタンが押されている間にマウスが移動すると呼び出されるもので、もうひとつはマウスボタンが押されていない場合に呼び出されるものです。 私たちの場合は、2つのケースを区別しなくてもよいので、両方のコールバックに同じ関数を登録することができます:
glutMotionFunc(motion);
glutPassiveMotionFunc(motion);
アプリケーションやゲームは一人称モードになっているときは、通常マウスカーソルを表示したくありません。 この方法でそれを無効にすることができます:
glutSetCursor(GLUT_CURSOR_NONE);
一人称モードになっていない場合、たとえば、メニューを表示しているときや、またはゲームが一時停止している場合、は glutSetCursor(GLUT_CURSOR_INHERIT)で再びデフォルトのマウスカーソルを表示することができます。
motion()コールバック関数は、ウィンドウの左上を基準とした(またはウィンドウがフルスクリーンモードにある場合、画面の左上を基準とした)、現在のマウス座標を取得します。 しかし、知りたいのは現在の座標ではなく、関心があるのはマウスがどのくらい移動されたかを知ることだけです。 もちろん、前の motion()呼び出しから現在のものへの座標を引き算することもできますが、マウスカーソルがウィンドウや画面の端にあると、うまく動作しません! GLUTでの解決策は、マウスが移動するたびに、 glutWarpPointer()関数を使用してマウスカーソルをウィンドウの中心に戻すことです。 ただし、この機能を使用すると、モーション·コールバックを再び呼び出すためにGLUTを起動するので、 motion()への毎秒の呼び出しを無視しなければなりません:
void motion(int x, int y) {
static bool wrap = false;
if(!wrap) {
int ww = glutGet(GLUT_WINDOW_WIDTH);
int wh = glutGet(GLUT_WINDOW_HEIGHT);
int dx = x - ww / 2;
int dy = y - wh / 2;
// Do something with dx and dy here
// move mouse pointer back to the center of the window
wrap = true;
glutWarpPointer(ww / 2, wh / 2);
} else {
wrap = false;
}
}
上記の関数では、 dx と dy変数が、マウスカーソルが移動した距離をピクセル単位で保持します。
View direction
[編集]人間は惑星の上で暮らしているので、水平面内で周りを見わたすことと、上下を見ることの間には大きな区別があります。 最も興味深いものが私たち自身がいる地上と同じレベルで起こるというのも理由ですが、それだけでなく(垂直)軸を中心に見回すのは簡単で望むままにできても、頭を上げたり下げたりは限られるという理由もあります。 FPSゲームでは、垂直軸周りの回転はいくらでも好きなだけできますが、上下には+/- 90度に制限されます 。 この制限とは別に、視野角の変化はマウスの移動量にスケーリング係数をかけたものに等しくなります:
const float mousespeed = 0.001;
angles.x += dx * mousespeed;
angles.y += dy * mousespeed;
if(angles.x < -M_PI)
angles.x += M_PI * 2;
else if(angles.x > M_PI)
angles.x -= M_PI * 2;
if(angles.y < -M_PI / 2)
angles.y = -M_PI / 2;
if(angles.y > M_PI / 2)
angles.y = M_PI / 2;
anglesから、単純な三角法を使用して見ている方向を計算することができます:
glm::vec3 lookat;
lookat.x = sinf(angles.x) * cosf(angles.y);
lookat.y = sinf(angles.y);
lookat.z = cosf(angles.x) * cosf(angles.y);
位置ベクトルと lookatが与えられると、ビュー行列を次のように作成することができます:
glm::vec3 position;
glm::mat4 view = glm::lookAt(position, position + lookat, glm::vec3(0, 1, 0));
エクササイズ:
- 多くのゲームでは、見下ろしは90度弱までに制限されています。 この理由を考えることができるでしょうか?
- アップ/ダウンの角度が90度を超えた場合に何が起こるでしょうか? シーンは逆さまに見えるでしょうか?
- lookatベクトルを計算したり glm::lookAt()関数を使用する代わりに、 glm::rotate()を使用してview行列を構築してみましょう。 これで同じ結果が得られているでしょうか? また、アップ/ダウンの角度が90度を超えたら?
Camera movement
[編集]マウスはすでに視点の角度の決定に使用しているので、カメラ位置の移動に残されているのはキーボードです。 コンピュータは、マウスをどこまで移動させたかを伝えることができますが、キーは押されているか押されていないかのどちらかだけです。 キーが押されるたびに一定量でカメラの位置を移動させるキーボードのコールバックは簡単に登録することができますが、この結果は、カメラの動きが非常にぎくしゃくしてしまいます。 位置を直接制御する代わりに、キーボードを使用して、私たちの速度を変更するようにしたいところです。 キーがまったく押されていない場合、速度がゼロになります。 上矢印キーが押された場合、前方向に一定の速度を持ちます。 下矢印キーが押された場合、前方向に対して負の速度を持ちます。 右矢印が押された場合、横方向に一定の速度を持ち、等々。 glutSpecialFunc()でコールバックを登録することができ、 これは "特別な"キー(例えばカーソルキーなど)が押されるたびに呼び出されます。 しかし、そのキーが再び解放されたかどうかも知る必要があります。 そのために、 glutSpecialUpFunc()コールバックを登録できます:
glutSpecialFunc(special);
glutSpecialUpFunc(specialup);
2つのコールバック関数は次のようになります:
const int left = 1;
const int right = 2;
const int forward = 4;
const int backward = 8;
const int up = 16;
const int down = 32;
int move = 0;
void special(int key, int x, int y) {
if(key == KEY_LEFT)
move |= left;
if(key == KEY_RIGHT)
move |= right;
if(key == KEY_UP)
move |= forward;
if(key == KEY_DOWN)
move |= backward;
if(key == KEY_PAGEUP)
move |= up;
if(key == KEY_PAGEDOWN)
move |= down;
}
void specialup(int key, int x, int y) {
if(key == KEY_LEFT)
move &= ~left;
if(key == KEY_RIGHT)
move &= ~right;
if(key == KEY_UP)
move &= ~forward;
if(key == KEY_DOWN)
move &= ~backward;
if(key == KEY_PAGEUP)
move &= ~up;
if(key == KEY_PAGEDOWN)
move &= ~down;
}
move変数はその後、現在押されている移動キーのビットマスクを収容します。 カメラの位置は、定期的に、好ましくはフレームごとに一回、更新する必要があります。 何もすることがないときに呼び出されるGLUT関数を登録することもできます:
glutIdle(idle);
その後、その関数内でカメラ位置を更新し、シーンを再描画するようGLUTに伝えます。 カメラ位置ベクトルを更新するために、どの方向が"前方", "右"等を知る必要があります。 ほとんどのゲームでは、 "前方"は私たちが見ている方向ですが、水平面に限られます。 通常、この理由はあなたが地面の上に立っているからで、たとえ歩き回っていて少し上下に目を向けても、床にとどまります。 "前"ベクトルから、 "右"ベクトルも導くことができ、そして"上"ベクトルは単に正のy方向を指します。 結果は次のようになります:
void idle() {
static int pt = 0;
const float movespeed = 10;
// Calculate time since last call to idle()
int t = glutGet(GLUT_ELAPSED_TIME);
float dt = (t - pt) * 1.0e-3;
pt = t;
// Calculate movement vectors
glm::vec3 forward_dir = vec3(sinf(angles.x), 0, cosf(angles.x));
glm::vec3 right_dir = vec3(-forward_dir.z, 0, forward_dir.x);
// Update camera position
if(move & left)
position -= right_dir * movespeed * dt;
if(move & right)
position += right_dir * movespeed * dt;
if(move & forward)
position += forward_dir * movespeed * dt;
if(move & backward)
position -= forward_dir * movespeed * dt;
if(move & up)
position.y += movespeed * dt;
if(move & down)
position.y -= movespeed * dt;
// Redraw the scene
glutPostRedisplay();
}
エクササイズ:
- 三角法を直接使用する代わりに、lookat と "上方向 "ベクターから、ベクトル代数を使用して forward_dir と right_dirを導き出してみてください。