Model View Projection 行列
永井 忠一 2025.1.19
GLM
ライブラリのインストール(私の環境では、Linux apt)
Linux apt |
$ sudo apt install libglm-dev
|
OpenGL 1.1 の頃にあった API の代わりに GLM を利用
- glLoadIdentity(), glMultMatrix*()
- glScale*(), glRotate*(), glTranslate*()
- gluLookAt()
- glOrtho()
ドキュメント
MVP 行列
頂点シェーダーによって行われる、座標変換\[ \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 は、正規化デバイス座標系)
Model 行列
scale、回転、そして並進からなる(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
|
行列とベクトルの積は、ベクトルになる(線形変換)\[ \text{Model}*\text{vec4(position, 1.0f)} = \text{translate}*(\text{rotate}*(\text{scale}*\text{vec4(position, 1.0f)})) \]
∵ 行列の積には、結合法則が成り立つ\[ \begin{align}
\text{translate}*(\text{rotate}*(\text{scale}*\text{vec4(position, 1.0f)})) &= (\text{translate}*\text{rotate}*\text{scale})*\text{vec4(position, 1.0f)} \\
&= \text{Model}*\text{vec4(position, 1.0f)}
\end{align} \]
「typeid 演算子」で型を確認
C++ GLM |
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <typeinfo>
#include <glm/mat4x4.hpp>
#include <glm/vec4.hpp>
int main(int argc, char *argv[]) {
using namespace glm;
printf("%s\n", typeid(mat4x4).name());
printf("%s\n", typeid(vec4).name());
mat4x4 A;
assert(strcmp(typeid(mat4x4).name(), typeid(A).name()) == 0);
vec4 u;
assert(strcmp(typeid(vec4).name(), typeid(u).name()) == 0);
printf("%s\n", typeid(A*u).name());
}
$ g++ -Wall mat_vec.cpp && ./a.out
N3glm3matILi4ELi4EfLNS_9qualifierE0EEE
N3glm3vecILi4EfLNS_9qualifierE0EEE
N3glm3vecILi4EfLNS_9qualifierE0EEE
|
(名前が name mangling されている。結果は、ベクトル)
スケーリング行列による、右手系と左手系の変換。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 行列
正射影で、以下の引数を指定して 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[2][2] が -1」となり、Projection 行列が単位行列にはなっていないことに、注意)
(zNear が -1 なのは感覚に合わないので、)zNear を 0 とすると
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, 0.f/* intuitive */, 1.f);
show(Projection);
}
$ g++ -Wall projection_int.cpp && ./a.out
1.000000 0.000000 0.000000 -0.000000
0.000000 1.000000 0.000000 -0.000000
0.000000 0.000000 -2.000000 -1.000000
0.000000 0.000000 0.000000 1.000000
|
(視錐台の場合には、glFrustum() や gluPerspective() を使う。透視投影変換)
最後に、それぞれの行列を掛け合わせたものが、MVP 行列\[ \text{MVP} = \mathrm{Projection}*\mathrm{View}*\mathrm{Model} \]
(単位行列の MV 行列と P 行列を掛け合わせた結果として)MVP 行列が 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 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, 0.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 -2.000000 -1.000000
0.000000 0.000000 0.000000 1.000000
|
(当然、単位行列と P 行列を掛けているだけなので、結果は P 行列と等しい)
⦅以前のページ⦆
参照させていただいた資料
© 2025 Tadakazu Nagai