macOSでOpenGLプログラミング (3-10. 正射影のプロジェクション行列を作る)

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


# はじめに

[前回](https://qiita.com/sazameki/items/33a726c1de8679937438)までで、プロジェクション行列、ビュー行列、モデル行列を使った描画の基本については説明を終えました。今回は、少し特殊なプロジェクション行列として、正射影のプロジェクション行列を紹介したいと思います。


# 1. 確認用のデータを用意する

今回の記事の内容が理解しやすいように、立方体の頂点データを用意しましょう。

```cpp:Game.cpp（一部）
Game::Game()
{
    /* 一部省略 */
    // 手前の面
    data.push_back({ { -1.0f, -1.0f,  1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {  1.0f, -1.0f,  1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f,  1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f,  1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ { -1.0f,  1.0f,  1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ { -1.0f, -1.0f,  1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });

    // 奥の面
    data.push_back({ { -1.0f, -1.0f, -1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {  1.0f, -1.0f, -1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f, -1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f, -1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ { -1.0f,  1.0f, -1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ { -1.0f, -1.0f, -1.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });

    // 上の面
    data.push_back({ { -1.0f,  1.0f, -1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f, -1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f,  1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f,  1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ { -1.0f,  1.0f,  1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ { -1.0f,  1.0f, -1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });

    // 下の面
    data.push_back({ { -1.0f, -1.0f, -1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ {  1.0f, -1.0f, -1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ {  1.0f, -1.0f,  1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ {  1.0f, -1.0f,  1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ { -1.0f, -1.0f,  1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });
    data.push_back({ { -1.0f, -1.0f, -1.0f }, { 1.0f, 0.0f, 0.75f, 1.0f } });

    // 左の面
    data.push_back({ { -1.0f, -1.0f, -1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ { -1.0f, -1.0f,  1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ { -1.0f,  1.0f,  1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ { -1.0f,  1.0f,  1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ { -1.0f,  1.0f, -1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ { -1.0f, -1.0f, -1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });

    // 右の面
    data.push_back({ {  1.0f, -1.0f, -1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ {  1.0f, -1.0f,  1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f,  1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f,  1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ {  1.0f,  1.0f, -1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });
    data.push_back({ {  1.0f, -1.0f, -1.0f }, { 1.0f, 0.75f, 0.0f, 1.0f } });

    /* 以下省略 */

    cameraPos = GLKVector3Make(2.5f, 2.0f, 5.0f);
}
```

このデータを描画する `Game::Render()` 関数の実装を次のように書き換えて、立方体が前後に3つ、交互にずれた状態で表示されるようにします。

```cpp:Game.cpp（一部）
void Game::Render()
{
    /* 一部省略 */

    glBindVertexArray(vao);
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    GLKMatrix4 modelMat = GLKMatrix4Identity;
    GLKMatrix4 pvmMat = GLKMatrix4Multiply(projViewMat, modelMat);
    program->SetUniform("mat", pvmMat);
    glDrawElements(GL_TRIANGLES, (GLsizei)data.size(), GL_UNSIGNED_SHORT, (void *)0);

    modelMat = GLKMatrix4Identity;
    modelMat = GLKMatrix4Translate(modelMat, 2.0f, 0.0f, -2.0f);
    pvmMat = GLKMatrix4Multiply(projViewMat, modelMat);
    program->SetUniform("mat", pvmMat);
    glDrawElements(GL_TRIANGLES, (GLsizei)data.size(), GL_UNSIGNED_SHORT, (void *)0);

    modelMat = GLKMatrix4Identity;
    modelMat = GLKMatrix4Translate(modelMat, -2.0f, 0.0f, -2.0f);
    pvmMat = GLKMatrix4Multiply(projViewMat, modelMat);
    program->SetUniform("mat", pvmMat);
    glDrawElements(GL_TRIANGLES, (GLsizei)data.size(), GL_UNSIGNED_SHORT, (void *)0);
}
```

実行すると、これまで通り、奥の方の立方体は小さく、手前の立方体は大きく、パースがついた状態で描画されているのが分かると思います。

　<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/166309/90a8739d-54a4-e33d-7493-5be76bb85d07.png">

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


# 2. 正射影のプロジェクション行列に替える

それではここで、プロジェクション行列を作っている `GLKMatrix4MakePerspective()` 関数を、正射影を行うための `GLKMatrix4MakeOrtho()` 関数に置き換えてみましょう。この関数の引数は、次のとおりです。

```cpp
GLKMatrix4MakeOrtho(画面左端の座標, 画面右端の座標,
                    画面下端の座標, 画面上端の座標,
                    z-near, z-far);
```

アスペクト比を指定する方法はありませんので、左端と右端の距離と、下端と上端の距離を、4:3の画面のアスペクト比に合わせて指定します。たとえば次のように指定します。

```cpp:Game.cpp（一部）
//GLKMatrix4 projMat = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(60.0f), 640.0f / 480.0f, 0.001f, 50.0f);
GLKMatrix4 projMat = GLKMatrix4MakeOrtho(-8.0f, 8.0f, -6.0f, 6.0f, 0.001f, 50.0f);
```

これを実行すると、次のようになります。手前の立方体も奥の立方体も同じ大きさで描かれるようになっていることが分かります。

　<img width="400" src="https://qiita-image-store.s3.amazonaws.com/0/166309/acffb2ee-da92-95e2-cd8b-3c5f18da67f0.png">

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

> DirectXで使う [D3DXMatrixOrthoLH()](https://msdn.microsoft.com/ja-jp/library/cc372881.aspx)関数では、横と縦のサイズを指定するようになっていますが、原理は同じです。

# 3. まとめ

今回は、正射影と呼ばれる方法で3D表現を行うためのプロジェクション行列の作り方について説明しました。もう一度、両方のプロジェクション行列の描画結果を並べてみて、その違いを実感しておきましょう。

　<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/90a8739d-54a4-e33d-7493-5be76bb85d07.png"> <img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/166309/acffb2ee-da92-95e2-cd8b-3c5f18da67f0.png">


正射影を使うと、**キャビネット図、等角図などと呼ばれる図と同じような描き方**になり、**すべての立方体の大きさが一様に表示されます**。**正射影は手前の物体も奥の物体も見渡しやすく、パズルゲームなどに向いた表現**であると言えるでしょう。

この先の章では、これまで通り、主に透視投影のプロジェクション行列を使って描画を行っていきますが、正射影の方が向いているのではと思った場合は、いつでもプロジェクション行列を正射影に替えて試してみてください。

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