Model View Projection 行列

永井 忠一 2025.1.19


GLM

ライブラリのインストール(私の環境では、Linux apt)

Linux apt
$ sudo apt install libglm-dev

OpenGL 1.1 の頃にあった API の代わりに GLM を利用

ドキュメント


MVP 行列

頂点シェーダー(Vertex shader)によって行われる、座標変換\[ \begin{align} \text{gl_Position} &= \text{Projection}*\text{View}*\text{Model}*\text{vec4(position, 1.0f)} \\ & = \text{MVP}*\text{vec4(position, 1.0f)} \end{align} \]

ここで、「vec4(position, 1.0f)」は、(位置ベクトルの)同次座標系への変換

MVP 行列を掛けたあとの gl_Position は、クリップ座標系で表現されている(w で割って正規化する前)

(そのあと、w で割って正規化を行い NDC へ変換するのは、レンダリングパイプライン側。NDC は、正規化デバイス座標系(Normalized Device Coordinates)

Model 行列

scale、回転(rotate)、そして並進(translate)からなる(scale → rotate → translateの順)\[ \text{Model} = \text{translate}*\text{rotate}*\text{scale} \]

何も作用しない Model 行列の例(単位行列。つまり、ローカル座標系とグローバル座標系とが等しい)

C++ GLM (OpenGL Mathematics)
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 scale = glm::scale(glm::mat4(1.f), glm::vec3(1.f, 1.f, 1.f));
	const glm::mat4 rotate = glm::rotate(glm::mat4(1.f), 0.f, glm::vec3(1.f, 0.f, 0.f));
	const glm::mat4 translate = glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, 0.f));
	const glm::mat4 Model = translate*rotate*scale;
	show(Model);
}

$ g++ -Wall model.cpp && ./a.out
1.000000 0.000000 0.000000 0.000000
0.000000 1.000000 0.000000 0.000000
0.000000 0.000000 1.000000 0.000000
0.000000 0.000000 0.000000 1.000000

スケーリング行列による、右手系と左手系の変換。Z 軸を反転\[ \text{scale} = \begin{bmatrix} S_x & 0 & 0 & 0 \\ 0 & S_y & 0 & 0 \\ 0 & 0 & -S_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

(拡大も縮小もしない場合は、\( \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \) となる)

C++ GLM
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 scale = glm::scale(glm::mat4(1.f), glm::vec3(1.f, 1.f, -1.f));
	show(scale);
}

$ g++ -Wall right-hand_left-hand.cpp && ./a.out
1.000000 0.000000 -0.000000 0.000000
0.000000 1.000000 -0.000000 0.000000
0.000000 0.000000 -1.000000 0.000000
0.000000 0.000000 -0.000000 1.000000

(表示の「-0.000000」は、浮動小数点数の演算誤差)

View 行列

OpenGL は右手系。(View 行列が単行列となる)カメラの初期姿勢では、カメラは -z の方向を向いている(カメラの初期位置はワールド座標系の原点、up ベクトルは上)

C++ GLM
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 View = glm::lookAt(glm::vec3(0.f, 0.f, 0.f), glm::vec3(0.f, 0.f, -1.f), glm::vec3(0.f, 1.f, 0.f));
	show(View);
}

$ g++ -Wall view.cpp && ./a.out
1.000000 -0.000000 0.000000 -0.000000
0.000000 1.000000 0.000000 -0.000000
-0.000000 -0.000000 1.000000 0.000000
0.000000 0.000000 0.000000 1.000000

不正な引数を lookAt() 関数に与えると、View 行列の値が nan になる。注視点とカメラ位置が同じになってしまっている例(カメラの方向が決定できない)

C++ GLM
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 View = glm::lookAt(glm::vec3(0.f, 0.f, 0.f), glm::vec3(0.f, 0.f, 0.f), glm::vec3(0.f, 1.f, 0.f));
	show(View);
}

$ g++ -Wall view_ill.cpp && ./a.out
-nan -nan -nan nan
-nan -nan -nan nan
nan nan nan -nan
0.000000 0.000000 0.000000 1.000000

カメラが真下を向いている状態で、up ベクトルにワールド座標系の Y 軸方向を設定してしまった例

C++ GLM
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 View = glm::lookAt(glm::vec3(0.f, 0.f, 0.f), glm::vec3(0.f, -1.f, 0.f), glm::vec3(0.f, 1.f, 0.f));
	show(View);
}

$ g++ -Wall view_singular.cpp && ./a.out
-nan -nan -nan nan
-nan -nan -nan nan
-0.000000 1.000000 -0.000000 0.000000
0.000000 0.000000 0.000000 1.000000

また、カメラの位置姿勢を表現するのに、9パラメタ(vec3 の引数が3個)あるのは冗長なため、結果、同じ View 行列となるものはいくつもある

C++ GLM
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 View = glm::lookAt(glm::vec3(0.f, 0.f, 0.f), glm::vec3(0.f, 0.f, -100.f), glm::vec3(0.f, 1.f, 0.f));
	show(View);
}

$ g++ -Wall view_redundant.cpp && ./a.out
1.000000 -0.000000 0.000000 -0.000000
0.000000 1.000000 0.000000 -0.000000
-0.000000 -0.000000 1.000000 0.000000
0.000000 0.000000 0.000000 1.000000

(この場合も、View 行列は、単位行列になっている)

Projection 行列

正射影(orthogonal projection)で、Projection 行列が単位行列になるように引数を指定

C++ GLM
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 Projection = glm::ortho(-1.f, 1.f, -1.f, 1.f, 1.f, -1.f);
	show(Projection);
}

$ g++ -Wall projection.cpp && ./a.out
1.000000 0.000000 0.000000 -0.000000
0.000000 1.000000 0.000000 -0.000000
0.000000 0.000000 1.000000 0.000000
0.000000 0.000000 0.000000 1.000000

(※Projection 行列が単位行列になるのは、zNear と zFar とが逆転しているときなので、注意

最後に、(それぞれの単位行列を掛け合わせた結果として)MVP 行列が単位行列となっている例

C++ GLM
#include <stdio.h>
#include <glm/gtc/matrix_transform.hpp>

static void show(const glm::mat4 &a) {
	printf("%f %f %f %f\n", a[0][0], a[1][0], a[2][0], a[3][0]);
	printf("%f %f %f %f\n", a[0][1], a[1][1], a[2][1], a[3][1]);
	printf("%f %f %f %f\n", a[0][2], a[1][2], a[2][2], a[3][2]);
	printf("%f %f %f %f\n", a[0][3], a[1][3], a[2][3], a[3][3]);
}

int main(int argc, char *argv[]) {
	const glm::mat4 scale = glm::scale(glm::mat4(1.f), glm::vec3(1.f, 1.f, 1.f));
	const glm::mat4 rotate = glm::rotate(glm::mat4(1.f), 0.f, glm::vec3(1.f, 0.f, 0.f));
	const glm::mat4 translate = glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, 0.f));
	const glm::mat4 Model = translate*rotate*scale;
	const glm::mat4 View = glm::lookAt(glm::vec3(0.f, 0.f, 0.f), glm::vec3(0.f, 0.f, -1.f), glm::vec3(0.f, 1.f, 0.f));
	const glm::mat4 Projection = glm::ortho(-1.f, 1.f, -1.f, 1.f, 1.f, -1.f);
	const glm::mat4 MVP = Projection*View*Model;
	show(MVP);
}

$ g++ -Wall mvp.cpp && ./a.out 
1.000000 0.000000 0.000000 0.000000
0.000000 1.000000 0.000000 0.000000
0.000000 0.000000 1.000000 0.000000
0.000000 0.000000 0.000000 1.000000

(当然、単位行列同士を掛けているだけなので、結果は単位行列になる)


© 2025 Tadakazu Nagai