glmを使った投影法と視点変換

連載一覧 "re:ゼロから始めるOpenGL 2.1"

サイトの更新がめんどくさくなってきたこの頃ですが、いつか人気サイトになることを信じて(多分)有益な情報をネットの海にぶん投げていこうと思います。

結構自分が躓いたところをまとめていってるので、きっと誰かの役に立っているでしょう!きっと!

そんなわけで(どんなわけで?)今回は視点関係のお話になります。

行列とは

ついに来てしまいました。

しかし、CGをやるうえで行列を避けて通ることはできません。

行列というと難しく聞こえてしまう、または高校の時にやった計算方法ということをイメージされるかもしれません。

無理もないですね。
高校でやっていたとしても、計算練習を永遠と反復するだけで全く何に役立つのか教えてくれませんから。
でも今日ついに役に立ちます。

行列とは、「移動」です。
以下に代表的な変換行列を挙げます。

平行移動行列

スケーリング行列

x軸を中心とした回転行列

y軸を中心とした回転行列

z軸を中心とした回転行列

数字を入れて計算してみれば、それぞれ頂点(x,y,z)を移動させた座標を手に入れられることがわかると思います。

零行列 どんなベクトルを掛けても零ベクトルになります

単位行列 どんなベクトルをかけても同じベクトルになります

これらも非常に基礎的な変換行列です。

投影法と視点移動

今まで作ったプログラムではある頂点を描画しようとしたときgl_Vertexに渡す座標はxyzのそれぞれが範囲(-1,1)を満たしている必要がありました。

しかし、表示したいデータをわざわざ(-1,1)の範囲に収めるのは非常に手間です。
また、遠近法が無視されています。

これでは3D空間を描画するのに非常に不便です。
それを解決するにはすべての頂点にModelViewProjection行列を掛ければ解決します。

これは文字通りModel変換行列とView変換行列とProjection変換行列とを掛け合わせたものになります。
3つの変換を適用しているわけです。

順に説明していきます。

まずModel変換行列ですが、これは物体を移動させるために用います。

特に物体を動かさないなら単位行列にしておきます。

次に投影法についてですが、投影法とは透視投影法と直交投影法の2つがあります。
ここでは透視投影法を主にみていきます。

透視投影法はgluPerspective、直交投影法はglOrthoという関数が用意されていますが、透視投影法についてはglm::Perspectiveを使うのが主流です。

透視投影法は下の図の様に視点からznearの距離にある面からzfarの距離にある面までの6面体に含まれる範囲にあるオブジェクトを表示します。

透視投影法

上の図で示した値をglm::perspective()の引数に与えるとProjection変換行列を返してくれます。

glm::mat4 glm::perspective(
glm::radians(Fovy), // ズームの度合い(通常90~30)
aspect,		// アスペクト比
znear,		// 近くのクリッピング平面
zfar		// 遠くのクリッピング平面
);

この行列を適用することで何が便利なのかというと一番は遠近法を適用できることです。

次に視点についてですが、これについてはglm::lookAt()を使うことで、View変換行列を得られます。


glm::mat4 glm::lookAt(
glm::vec3(x,y,z), // ワールド空間でのカメラの座標
glm::vec3(x,y,z), // 見ている位置の座標
glm::vec3(x,y,z)  // 上方向を示す。(0,1.0,0)に設定するとy軸が上になります
);

これを用いることで視点を移動させたり、見え方を変更することができます。

これらの関数は引数がすべてfloat系かdouble系のどちらかにそろえられてないと、「インスタンスが引数リストと一致しません」というエラーが出ます。

floatにそろえるなら数値0.5は0.5fの様に書きましょう。

実際にプログラムを見ていきます。

プログラム全体の前にUniform変数について説明しておきます。

これはGLSL側にデータを転送します。

/* C++側 */
// 初期化時
// uniform変数MVPのハンドルを取得
GLuint matrixID = glGetUniformLocation(shader, "MVP");
// ループ内(適時転送する値を変更することがあるためです)
// 現在バインドしているシェーダのuniform変数"MVP"に変換行列を送る
// 4つ目の引数は転送する行列の最初のアドレスを渡しています。
glUniformMatrix4fv(matrixID, 1, GL_FALSE, &mvpMat[0][0]);
/* GLSL側 */
// これで4×4の行列を受け取っています。
// 勿論ほかの型でも可能です。
uniform mat4 MVP;

プログラム全体。

#include "stdafx.h"
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <gl/glew.h>
#include <GLFW/glfw3.h>
// glmの使う機能をインクルード
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
//using namespace glm;でもいいけどここでは一部のみ「glm::」を省力できるようにする
using glm::vec3;
using glm::vec4;
using glm::mat4;
GLFWwindow* initGLFW(int width, int height)
{
/*以前の記事参照*/
}
int readShaderSource(GLuint shaderObj, std::string fileName)
{
/*以前の記事参照*/
}
GLint makeShader(std::string vertexFileName, std::string fragmentFileName)
{
/*以前の記事参照*/
}
int main()
{
GLint width = 640, height = 480;
GLFWwindow* window = initGLFW(width, height);
GLint shader = makeShader("shader.vert", "shader.frag");
GLuint matrixID = glGetUniformLocation(shader, "MVP");
// フレームループ
while (glfwWindowShouldClose(window) == GL_FALSE)
{
glUseProgram(shader);
glEnable(GL_DEPTH_TEST);
//glDepthFunc(GL_LESS);
glClearColor(0.2f, 0.2f, 0.2f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 宣言時には単位行列が入っている
mat4 modelMat, viewMat, projectionMat;
// View行列を計算
viewMat = glm::lookAt(
vec3(1.0, 2.0, 6.0), // ワールド空間でのカメラの座標
vec3(0.0, 0.0, 0.0), // 見ている位置の座標
vec3(0.0, 0.0, 1.0)  // 上方向を示す。(0,1.0,0)に設定するとy軸が上になります
);
// Projection行列を計算
projectionMat = glm::perspective(
glm::radians(45.0f), // ズームの度合い(通常90~30)
(GLfloat)width / (GLfloat)height,		// アスペクト比
0.1f,		// 近くのクリッピング平面
100.0f		// 遠くのクリッピング平面
);
// ModelViewProjection行列を計算
mat4 mvpMat = projectionMat * viewMat* modelMat;
// 現在バインドしているシェーダのuniform変数"MVP"に変換行列を送る
// 4つ目の引数は行列の最初のアドレスを渡しています。
glUniformMatrix4fv(matrixID, 1, GL_FALSE, &mvpMat[0][0]);
// 4枚のポリゴンから成る三角錐のデータを転送
vec3 position[4][3] = { 
{vec3( 0, 0, 1),vec3(-1,-1, 0),vec3( 1, 0, 0)},
{vec3( 0, 0, 1),vec3( 1, 0, 0),vec3( 0, 1, 0)},
{vec3( 0, 0, 1),vec3( 0, 1, 0),vec3(-1,-1, 0)},
{vec3(-1,-1, 0),vec3( 0, 1, 0),vec3( 1, 0, 0)} 
};
vec4 color[4] = { vec4(1,0,0,1), vec4(0,1,0,1), vec4(0,0,1,1), vec4(1,1,0,1)};
for (int i = 0; i < 4; ++i)
{
glColor4f(color[i].r, color[i].g, color[i].b, color[i].a);
glBegin(GL_TRIANGLES);
glVertex3f(position[i][0].x, position[i][0].y, position[i][0].z);
glVertex3f(position[i][1].x, position[i][1].y, position[i][1].z);
glVertex3f(position[i][2].x, position[i][2].y, position[i][2].z);
glEnd();
}
// ダブルバッファのスワップ
glfwSwapBuffers(window);
glfwPollEvents();
}
// GLFWの終了処理
glfwTerminate();
return 0;
}
#version 120
//
// shader.vert
//
uniform mat4 MVP;
void main(void)
{
gl_Position = MVP * gl_Vertex;
gl_FrontColor = gl_Color;
}

#version 120#version 120
//// shader.vert//
uniform mat4 MVP;
void main(void){ gl_Position = MVP * gl_Vertex; gl_FrontColor = gl_Color;}

視点を変えたり、物体の頂点を変えたり大分応用できるレベルになってきました。

難しいかもしれませんが頑張っていきましょう。

ちなみにgluLookAt()やgluPerspective()とかの関数と混乱してしまう人もいるかもしれません(主に僕)。
なぜならこれらの関数は特に返り値を処理する必要がないからです。

実はgluLookAt()やgluPerspective()を実行するとgl_ModelViewProjectionMatrixtという組み込み変数に値が設定されます。

この辺が互換性を持たせた進化によって複雑になってしまった1つですよね。

連載一覧 "re:ゼロから始めるOpenGL 2.1"

あわせて読みたい