macOSでOpenGLプログラミング（3-8. LookAt()関数でビュー行列を作る）

[macOSでOpenGLプログラミングの目次に戻る](http://qiita.com/sazameki/items/d060f20f621c658046d9)


# はじめに

[前回](https://qiita.com/sazameki/items/9dc58829517d92643941)は、カメラの移動と回転を表すビュー行列の作り方を説明しました。手動でカメラを動かす場合、つまりカメラの位置と回転を直接のパラメータとして用意する場合はこれで十分です。

しかしゲームなどでは、特定のオブジェクトの方にカメラを向ける場面が多く出てきます。そのような場合、回転を計算するのはなかなか厄介な計算が必要です。Y軸中心の回転を考えるだけなら簡単ですが、Y軸方向に上下して見下ろしや見上げるアングルになった場合、X軸やZ軸を中心とした回転も考えなければいけないのです。

そのような場面で使えるのが`LookAt()`関数です。この関数は、カメラの位置と、カメラが向く対象のオブジェクトの中心位置を指定することで、位置と回転（そして逆行列の計算も）を合成したビュー行列を生成してくれます。


# 1. カメラを動かすコードを追加する

[前回](https://qiita.com/sazameki/items/9dc58829517d92643941)のコードに少し書き加えて、カメラをY軸方向にも移動できるようにしておきましょう。[A][D]キーでX軸方向の移動、[W][S]キーでZ軸方向の移動、[Q][E]キーでY軸方向の移動を行います。

```cpp:Game.cpp（一部）
void Game::Render()
{
    if (Input::GetKey(KeyCode::A)) {
        cameraPos.x -= 2.0f * Time::deltaTime;
    } else if (Input::GetKey(KeyCode::D)) {
        cameraPos.x += 2.0f * Time::deltaTime;
    } else if (Input::GetKey(KeyCode::W)) {
        cameraPos.z -= 2.0f * Time::deltaTime;
    } else if (Input::GetKey(KeyCode::S)) {
        cameraPos.z += 2.0f * Time::deltaTime;
    } else if (Input::GetKey(KeyCode::Q)) {
        cameraPos.y -= 2.0f * Time::deltaTime;
    } else if (Input::GetKey(KeyCode::E)) {
        cameraPos.y += 2.0f * Time::deltaTime;
    }
    /* 以下省略 */
}
```

カメラの回転を表す`cameraAngle`変数は不要になりますので、関係するコードを、Game.hppとGame.cppから削除しておいてください。


# 2. LookAt()関数でビュー行列を作る

GLKitでは、`LookAt()`関数は、`GLKMatrix4MakeLookAt()`という名前で用意されています。第1〜第3引数はカメラ位置の座標、第4〜第6引数はカメラを向けるオブジェクトの中心位置の座標、第7〜第9引数は回転がない場合のカメラの基本姿勢の方向ベクトルです。

```cpp:LookAt()関数
GLKMatrix4 mat = GLKMatrix4MakeLookAt(
    カメラ位置.x, カメラ位置.y, カメラ位置.z,
    オブジェクト.x, オブジェクト.y, オブジェクト.z,
    カメラ姿勢.x, カメラ姿勢.y, カメラ姿勢.z
);
```

なお、`LookAt()`関数は、OpenGL Utility Library (GLU)であれば[`gluLookAt()`](https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/gluLookAt.xml)関数、OpenGL Mathematics (GLM)であれば[`lookAt()`](https://glm.g-truc.net/0.9.2/api/a00245.html#ga2d6b6c381f047ea4d9ca4145fed9edd5)関数として用意されています。DirectXであれば[`LookAtLH()`](https://msdn.microsoft.com/ja-jp/library/aa514140.aspx)関数です。いずれも引数は同じですので、同じ使い方ができます。

カメラ位置とオブジェクトの位置は、それぞれのX座標, Y座標, Z座標を指定するだけなので、分かりやすいでしょう。

カメラ姿勢は、多くの場合、(0, 1, 0)のベクトルを指定することになります。X-Z平面を見渡すようにカメラを設置して、Y軸方向に上下に移動させながら撮影することが多いためです。

この関数を使って、ビュー行列を計算するコードを次のように書き換えましょう。

```cpp:Game.cpp（一部）
    GLKMatrix4 viewMat = GLKMatrix4MakeLookAt(cameraPos.x, cameraPos.y, cameraPos.z,
                                              0.0f, 0.0f, 0.0f,
                                              0.0f, 1.0f, 0.0f);
```

なお、`LookAt()`関数を使った計算はカメラのために最適化されているので（つまり初めからモデルの方を移動させる行列になっているので）、逆行列を計算する必要はありません。

[W][A][S][D][Q][E]のキーでカメラを移動させてみると、カメラが常に(0, 0, 0)の位置を向くようになったことが分かります。

　　<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/3b16c09a-5e49-b7d7-b1ec-f6d97b40071c.png"> <img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/a1c348ca-fcea-d5bc-83c2-f8370002d0ce.png">

　　ここまでのプロジェクト：[MyGLGame_step3-8.zip](http://sazameki.jp/temp/MyGLGame_step3-8.zip)



# 3. カメラ姿勢の注意点

カメラ姿勢のベクトルを(0, 1, 0)、すなわちY軸を基準として設定した場合、X軸およびZ軸を中心とした回転角度が -90度 < angle < 90度 となる範囲でしか正常に回転させられないということを覚えておいてください。

たとえば次の左図では、基本姿勢(0, 1, 0)で(0, 0, 0)の座標を狙っているカメラが(0, 4, 0.01)の位置（ほぼ真上）にいるのですが、Z座標が少し奥の方に動いて(0, 4, -0.01)になった右図では、Z方向の向きが上下逆になってしまっています。

　　<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/313a44d6-2058-bea2-190b-3443574f9cb3.png"> <img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/dd8f72f6-3fc8-cb48-66ad-94db3dba804a.png">

これは、カメラが(0.01, 4, 0)にいる状態から（左図）、(-0.01, 4, 0)に移動させた場合（右図）でも同じことです。回転角度が90度を超える一瞬で向きが変わってしまいます。

　　<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/5f9f0630-b7f6-2abe-a469-32f61978843e.png"> <img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/8f85f58b-c06d-6def-cc74-144232751dd8.png">

また、基本姿勢(0, 1, 0)のカメラを、ちょうど真上から見下ろすように、(0, 4, 0)から(0, 0, 0)を見させようとすると、真っ黒になってしまいます。ちょうど90度になるようにしてもいけません。

　　<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/166309/2e66e636-5393-1312-a6a5-c89ba6db7441.png">
X-Z平面を真上から見下ろす角度で撮りたいのであれば、たとえばZ方向が縦方向に来るようにカメラを設置するのであれば、基本姿勢のベクトルに(0, 0, -1)を指定します（OpenGLのZ軸は手前がプラスで奥がマイナスだということを思い出してください）。（ただしこの時は、X軸とY軸中心の回転角度が -90度 < angle < 90度 となるように注意します）

すると次のように、(0, 0, 0)の座標を向き続けているカメラのZ座標が+1.0（左図）から-0.5（右図）に変わっても、上下がひっくり返らずに連続して撮影できるようになったことが分かります。

　　<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/d04f7570-7f16-d969-a800-f7aab04ae598.png"> <img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/68e89bca-c4a4-ca67-5f87-f48698168df7.png">

また真上の(0, 4, 0)から(0, 0, 0)を見下ろした時にも、画面が暗くなることはありません。

　　<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/166309/18bae48c-16d5-4600-6dc0-e804e06d2b10.png">

カメラをX軸に沿って移動させるのであれば、基本姿勢のベクトルに(1, 0, 0)や(-1, 0, 0)を指定することになります。（この時は、Y軸とZ軸中心の回転角度が -90度 < angle < 90度 となるように注意します）


# 4. まとめ

今回は、`LookAt()`関数を使ってビュー行列を作る方法と、その注意点を解説しました。

まとめると、

- `LookAt()`関数には、カメラ位置、オブジェクト位置、カメラ姿勢の3つの引数を渡す。
- カメラ姿勢の方向ベクトルによって、いずれかの方向の回転が-90°〜90°の範囲に制限される。見回したい範囲に応じて方向ベクトルを選択する。

ということです。

次回は、モデルの方を動かす「モデル行列」という考え方について見てみたいと思います。

----
次の記事：[macOSでOpenGLプログラミング（3-9. LookAt行列でモデル行列でモデルを動かす）](https://qiita.com/sazameki/items/33a726c1de8679937438)