macOSでOpenGLプログラミング (3-9. モデル行列でモデルを動かす)


macOSでOpenGLプログラミングの目次に戻る

はじめに

前回は、LookAt()関数を使ってビュー行列を作る方法を説明しました。

これまでの解説で、プロジェクション行列によって画角やアスペクト比といったカメラの特性を、ビュー行列によってカメラの位置と回転を変えられることを理解していただけたかと思います。

ここでもう1つ、モデル行列という行列を導入しましょう。

モデル行列は、描画対象となるモデル(ポリゴンデータ、あるいはポリゴンメッシュと言ってもいいでしょう)の位置・回転・スケールを調整するための行列です。

VBOとVAO、そして頂点のインデックス・リストで用意した3Dデータは、多くの場合、複数回描画されます。RPGゲームの宝箱や樽などのプロップ(小道具)、パズルゲームのアイテムなど、いろんなゲームを思い出してみると、その事が理解していただけるでしょう。同じモデルが、位置を変えて、いろんな角度で、いろんなスケールで描画されるわけです。これを実現するのがモデル行列です

モデル行列を使った3Dデータの描画の流れは、次のようになります。

まずGameクラスのコンストラクタで、描画する頂点データを準備します。

  • 準備1. VBOを作って頂点データを登録する。
  • 準備2. 頂点のインデックス・リストを作って頂点インデックスを登録する。
  • 準備3. VAOを作って頂点データのデータ構造を登録する。

次に、準備した3DモデルのデータをGame::Render()関数で描画します。

  • 描画1. glClear()関数で画面をクリアする。
  • 描画2. 利用するシェーダをセットする(program->Use())。
  • 描画3. プロジェクション行列を作成する。
  • 描画4. ビュー行列を作成する。
  • 描画5. 描画対象となる3DモデルのVAOをバインドする。
  • 描画6. モデルの位置・回転・スケールを表すモデル行列を作成する。
  • 描画7. プロジェクション行列・ビュー行列・モデル行列を掛け合わせたものをシェーダのuniform変数にセットする。
  • 描画8. glDrawElements()関数でモデルの描画を実行する。
  • 描画9. 描画対象のモデルが残っていれば手順「描画5」に戻る。

それでは、実際のコード例を見ていきましょう。

この章では頂点データの値を直接書いて指定していますが、実際のゲームを作る場合には、MayaやBlenderといったDCC (Digital Content Creation) ツールから書き出した、.objファイルや.fbxファイルといった3Dモデルのファイルから頂点データを読み込むことになります。これらの3Dモデルファイルからのデータ読み込みは、4章以降で解説していきます。

1. 頂点データを1セット用意する

まずは三角ポリゴン1個のための頂点データを1セット、3頂点分のデータを用意します。デフォルトのカメラの位置を、少し手前の上の方に移動しておきましょう。

Game.cpp(一部)
Game::Game()
{
    glEnable(GL_DEPTH_TEST);

    program = new ShaderProgram("myshader.vsh", "myshader.fsh");

    data.push_back({ {  -1.0f, -1.0f, 0.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {   1.0f, -1.0f, 0.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    data.push_back({ {   0.0f,  1.0f, 0.0f }, { 0.0f, 0.75f, 1.0f, 1.0f } });
    /* 以下省略 */
    cameraPos = GLKVector3Make(0.0f, 2.0f, 5.5f);
}

z=0.0 のX-Y平面に三角形が描かれていますので、これを実行すると、次のようになります。

 

 ここまでのプロジェクト:MyGLGame_step3-9a.zip

2. モデル行列を使って複数回描画する

モデル行列は、個々のオブジェクトの位置・回転・スケールを表すものですので、描画する度に変わりますが、画角とアスペクト比を表すプロジェクション行列とカメラの位置・回転を表すビュー行列の内容は変わりません。そこで、プロジェクション行列とビュー行列を掛け合わせた行列をあらかじめprojViewMat変数に計算して保存しておきます。

Game.cpp(一部)
void Game::Render()
{
    /* 一部省略 */
    GLKMatrix4 projMat = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(60.0f), 640.0f / 480.0f, 0.001f, 50.0f);
    GLKMatrix4 viewMat = GLKMatrix4MakeLookAt(cameraPos.x, cameraPos.y, cameraPos.z,
                                              0.0f, 0.0f, 0.0f,
                                              0.0f, 1.0f, 0.0f);
    GLKMatrix4 projViewMat = GLKMatrix4Multiply(projMat, viewMat);

あとはこのprojViewMat行列にモデル行列を掛け合わせれば、プロジェクション行列・ビュー行列・モデル行列が結び付いた変換が行われた上で各頂点データが描画されます。

VAOをバインドし、VBOのデータから0番と1番のデータを有効化してから、各オブジェクトごとのモデル行列を計算して、セットして、描画していきます。ここでは、各オブジェクトを八角形に並べてみましょう。

Game.cpp(一部)
    glBindVertexArray(vao);
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    for (int i = 0; i < 8; i++) {
        float x = cosf(M_PI / 4 * i) * 2.414f;
        float z = -sinf(M_PI / 4 * i) * 2.414f;
        GLKMatrix4 modelMat = GLKMatrix4Identity;
        modelMat = GLKMatrix4Translate(modelMat, x, 0.0f, z);
        modelMat = GLKMatrix4RotateY(modelMat, M_PI / 4 * i + M_PI / 2);
        GLKMatrix4 pvmMat = GLKMatrix4Multiply(projViewMat, modelMat);
        program->SetUniform("mat", pvmMat);
        glDrawElements(GL_TRIANGLES, (GLsizei)data.size(), GL_UNSIGNED_SHORT, (void *)0);
    }
}

まず、次のようにモデル行列を単位行列として用意して、移動・回転・スケーリングがない状態を表しておきます。

GLKMatrix4 modelMat = GLKMatrix4Identity;

ここに、移動 (Translation) → 回転 (Rotation) → スケーリング (Scaling) の順番に変形を適用してきます。この順番は変えられませんので、こういうものだと丸暗記してしまってください(興味がある方は、順番を入れ替えてみて、なぜこの順番なのかを確かめてみると理解が深まるでしょう。ぜひやってみてください!)。モデル行列には通常この順番で変形が適用されるので、TRS行列とも呼ばれます。

for文を使って変数iで0から7までカウントし、360°/8 = π/4ラジアンずつ角度をずらしながら、コサインとサインでX座標とZ座標を計算していきます。三角形の1辺の長さが2ですので、八角形に内接する円の半径はおよそ2.414になります(1 / tan(45°/2)を計算すると出てきます)。コサインとサインの値にこの半径を掛けて、各モデルの位置を計算します。

float x = cosf(M_PI / 4 * i) * 2.414f;
float z = -sinf(M_PI / 4 * i) * 2.414f;
modelMat = GLKMatrix4Translate(modelMat, x, 0.0f, z);

Z座標を計算するサインの値をマイナス倍しているのは、OpenGLの奥行き方向がマイナスになっているためです。X-Z平面を上から見た時に、数学でよく使う座標と同じように考えたいので、マイナス倍してサインを使っています。

そして 90° = π/2ラジアンから始めて、やはり π/4 ラジアンずつモデルの角度を変えて回転させていきます。

modelMat = GLKMatrix4RotateY(modelMat, M_PI / 2 + M_PI / 4 * i);

こうして出来上がったモデル行列を、プロジェクション行列とビュー行列をあらかじめ掛け合わせておいたprojViewMat変数に掛け合わせて、シェーダのuniform変数にセットすれば、透視変換に必要な行列がセット完了となります。あとはモデル行列ごとに描画のためのglDrawElements()関数を呼び出します。

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

 

 ここまでのプロジェクト:MyGLGame_step3-9b.zip

3. まとめ

今回は、モデル行列の使い方を説明しました。ここまでで、

  • プロジェクション行列
  • ビュー行列(位置・回転の直接指定と、LookAt()関数を使う方法)
  • モデル行列

の3種類の行列を使って、ほとんどの描画ができるようになりました。あとは組み合わせ方次第で、いろんなゲームが作れます。

次回は、パズルゲームなどに利用しやすい、正射影のプロジェクション行列の作り方と使い方を見てみたいと思います。


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