前提
『マルチプラットフォームのためのOpenGL ES入門 基礎編―Android/iOS対応グラフィックスプログラミング』
の抜粋メモです。13章の内容。
コードについて詳細は↑を参照してください。
シェーダと行列
Matrix
OpenGL で頻出するのは 4 x 4 の行列。
行列を OpenGL ES の用途に限って言い表せば、 ベクトルに対する計算内容を保存したもの と言える。GPU とはベクトルと行列に対する演算を得意とするハードウェアであり、行列とベクトルの演算は専用の演算装置によって行われるため、非常に高速に処理できる。
ここまで頂点シェーダで記述してきた vec4 型も 1 行 4 列の行列と考えることができる。XYZW の4要素を持つベクトルに行列を適用(乗算)することにより、XYZW それぞれの要素に対して任意の計算を行うことができる。
単位行列(Identity)
[ 1, 0, 0, 0 ]
[ 0, 1, 0, 0 ]
[ 0, 0, 1, 0 ]
[ 0, 0, 0, 1 ]
的なやつ。
「乗算」処理を行ってもベクトルに対して一切の効果を持たない。
OpenGL や DirectX 等の 3D ライブラリは単位行列を Identity と呼び、多くの数学系ライブラリは単位行列を簡単に作成する方法をサポートしている。
移動行列(Translate)
[ 1, 0, 0, 0 ]
[ 0, 1, 0, 0 ]
[ 0, 0, 1, 0 ]
[ X, Y, Z, 1 ]
ベクトルに対して「X方向に◯, Y方向に◯, Z方向に◯だけ移動しろ」という命令を保存したもの。
たとえばこれを (5, 6, 7, 1) のベクトルと乗算すると、(5+X, 6+Y, 7+Z, 1) が得られる。
OpenGL ES が「W要素」を持つ理由として、行列演算を行えるというメリットが有る。3要素のベクトルでは 4x4 行列と演算できないため。
拡大縮小行列(Scale)
[ X, 0, 0, 0 ]
[ 0, Y, 0, 0 ]
[ 0, 0, Z, 0 ]
[ 0, 0, 0, 1 ]
ベクトルに対して「X方向に◯倍、Y方向に◯倍、Z方向に◯倍」という命令を保存したもの。
回転行列(Rotate)
2D の回転は「時計回り」or「反時計回り」のみだが、3D 空間には無数の「回転軸」が存在する。
傾いた回転軸に対応するため、3DCG の回転とは「回転軸の向きベクトル」と「回転角」の2つの要素によって書き表される。X軸で回転させたければ、向きベクトルは (1, 0, 0) になり、Y軸回転なら (0, 1, 0)、Z軸回転なら (0, 0, 1) となる。
任意の回転軸ベクトル(X, Y, Z) に対して θ 度回転させる行列は次のように表される。
[ XX(1.0 - cosθ) + cosθ, XY(1.0 - cosθ) - Zsinθ, XZ(1.0 - cosθ) + Ysinθ, 0.0 ]
[ XY(1.0 - cosθ) + Zsinθ, YY(1.0 - cosθ) + cosθ, YZ(1.0 - cosθ) - Xsinθ, 0.0 ]
[ XZ(1.0 - cosθ) + Ysinθ, ZY(1.0 - cosθ) + Xsinθ, ZZ(1.0 - cosθ) + cosθ, 0.0 ]
[ 0.0, 0.0, 0.0, 1.0 ]
行列の乗算
例えば、1つのベクトルに対して「移動」「拡大縮小」「回転」をすべて適用したい場合、プログラムはベクトル×行列の計算を3回、vec4型のメモリを 4x4=16個分保持することになる。
そこで事前に「行列の乗算」を行うことで3つの行列演算を1つにまとめることができる。
行列演算に交換法則は成り立たない。行列による計算はすべてが 原点を中心に行われる。 ので、拡大縮小や回転と移動などは適用する順番で結果が変わってくる(p.306)。
行列を使ったシンプルなシェーダ処理
行列とベクトルに関わる演算は、OpenGL ES2.0 の API としては用意されない。GLSL ES の仕様としては含まれており、シェーダ内では簡単に記述することができる。
// 頂点シェーダーを用意する
const GLchar *vertex_shader_source =
//
"uniform mediump mat4 unif_matrix;"
"attribute mediump vec4 attr_pos;"
"attribute mediump vec2 attr_uv;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_Position = unif_matrix * attr_pos;"
" vary_uv = attr_uv;"
"}";
const GLchar *fragment_shader_source =
//
"uniform sampler2D texture;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_FragColor = texture2D(texture, vary_uv);"
"}";
GLSL文法:mat4キーワード
mat4 は、 4x4 の行列を表す組み込み変数。
gl_Position に対する出力として、unif_matrix と attr_pos の乗算が行われている(順番に注意)。このシェーダでは「行列×ベクトル」が正しい順番になる。
OpenGL のシェーダは大量の行列演算を扱う必要があり、「行列×行列」や「行列×ベクトル」のような特殊な乗算処理も自然に記述できるように仕様が策定されている。(C++ でいう演算子オーバーロードの考え方)
// サンプルアプリ用データを取り出す
Extension_ShaderMatrixTranslate *extension = (Extension_ShaderMatrixTranslate*) app->extension;
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// attr_posを有効にする
glEnableVertexAttribArray(extension->attr_pos);
glEnableVertexAttribArray(extension->attr_uv);
// 四角形描画
{
// TODO 解説
// この章のサンプルではpositionとUVの位置を変更しない
const GLfloat position[] = {
// v0(left top)
-0.5f, 0.5f,
// v1(left bottom)
-0.5f, -0.5f,
// v2(right top)
0.5f, 0.5f,
// v3(right bottom)
0.5f, -0.5f, };
//
const GLfloat uv[] = {
// v0(left top)
0, 0,
// v1(left bottom)
0, 1,
// v2(right top)
1, 0,
// v3(right bottom)
1, 1, };
// 頂点情報を関連付ける
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glVertexAttribPointer(extension->attr_uv, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) uv);
// TODO 解説
// 行列をアップロードする
{
// 行列を生成する
mat4 matrix = mat4_identity();
// 移動行列を作成する
matrix.m[3][0] = 0.1f; // X方向
matrix.m[3][1] = 0.5f; // Y方向
glUniformMatrix4fv(extension->unif_matrix, 1, GL_FALSE, (GLfloat*) matrix.m);
}
glBindTexture(GL_TEXTURE_2D, extension->texture->id);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
補助関数:mat4_identity
行列のアップロードのところで、mat4 型が宣言されているのは OpenGL ではなくサポート用の構造体。OpenGL ES2.0 では、行列はすべて GLfloat 型の配列として用意する。4x4 行列の場合は 16個以上の要素を持った GLfloat 配列を用意する必要がある。
/**
* 行列を保持する構造体
*/
typedef struct mat4 {
GLfloat m[4][4];
} mat4;
mat4_identity() は、そのまま 4x4 の単位行列 mat4 を返す。
glUniformMatrix4fv
行列を GPU へアップロードする。
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)
- location : アップロード先の location
- count : アップロードする行列の個数
- transpose : 行列を転置する場合は GL_TRUE
- value : 行列を格納した GLfloat 変数へのポインタ。最低でも x16 以上の長さが必要
サンプルでは:
glUniformMatrix4fv(extension->unif_matrix, 1, GL_FALSE, (GLfloat*) matrix.m);
最後に glDrawArrays コマンドで、任意の X, Y だけ移動した四角形が描画される。
補助関数:mat4_translate
移動行列を作成するための補助関数
mat4 mat4_translate(const GLfloat x, const GLfloat y, const GLfloat z)
ポリゴンの拡大縮小
サンプル。上との違いは行列の作成部分のみ。
// 行列をアップロードする
{
#if 0
// 行列を生成する
mat4 matrix = mat4_identity();
// スケール行列を作成する
matrix.m[0][0] = 1.5f; // X方向
matrix.m[1][1] = 2.0f; // Y方向
matrix.m[2][2] = 0.0f; // Z方向
#else
// 補助関数で行列を生成する
mat4 matrix = mat4_scale(1.5f, 2.0f, 0.0f);
#endif
glUniformMatrix4fv(extension->unif_matrix, 1, GL_FALSE, (GLfloat*) matrix.m);
}
glBindTexture(GL_TEXTURE_2D, extension->texture->id);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
拡大縮小行列とは頂点の座標に対して掛け算をする行列であり、「拡大縮小の原点」は常に画面に中心になる。原点にある頂点は、何を掛け算しても不動。
補助関数:mat4_scale
みたまま。
mat4 mat4_scale(const GLfloat x, const GLfloat y, const GLfloat z)
ポリゴンの回転
まずは、数学的な理解よりも「この行列を適用すると回転する」という理解から。
typedef struct {
// レンダリング用シェーダープログラム
GLuint shader_program;
// 位置情報属性
GLint attr_pos;
// UV情報属性
GLint attr_uv;
// シェーダーで適用する行列
GLuint unif_matrix;
/**
* 回転角
*/
GLfloat rotate;
// 描画対象のテクスチャ
Texture *texture;
} Extension_ShaderMatrixRotate;
void sample_ShaderMatrixRotate_initialize(GLApplication *app) {
// サンプルアプリ用のメモリを確保する
app->extension = (Extension_ShaderMatrixRotate*) malloc(sizeof(Extension_ShaderMatrixRotate));
// サンプルアプリ用データを取り出す
Extension_ShaderMatrixRotate *extension = (Extension_ShaderMatrixRotate*) app->extension;
{
// 初期化
extension->rotate = 0;
}
// 頂点シェーダーを用意する
{
const GLchar *vertex_shader_source =
//
"uniform mediump mat4 unif_matrix;"
"attribute mediump vec4 attr_pos;"
"attribute mediump vec2 attr_uv;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_Position = unif_matrix * attr_pos;"
" vary_uv = attr_uv;"
"}";
const GLchar *fragment_shader_source =
//
"uniform sampler2D texture;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_FragColor = texture2D(texture, vary_uv);"
"}";
// コンパイルとリンクを行う
extension->shader_program = Shader_createProgramFromSource(vertex_shader_source, fragment_shader_source);
}
// attributeを取り出す
{
extension->attr_pos = glGetAttribLocation(extension->shader_program, "attr_pos");
assert(extension->attr_pos >= 0);
extension->attr_uv = glGetAttribLocation(extension->shader_program, "attr_uv");
assert(extension->attr_uv >= 0);
}
// uniformを取り出す
{
extension->unif_matrix = glGetUniformLocation(extension->shader_program, "unif_matrix");
assert(extension->unif_matrix >= 0);
}
// Textureを読み込む
{
extension->texture = Texture_load(app, "texture_rgb_asymmetry.png", TEXTURE_RAW_RGBA8);
assert(extension->texture != NULL);
}
// シェーダーの利用を開始する
glUseProgram(extension->shader_program);
assert(glGetError() == GL_NO_ERROR);
}
// サンプルアプリ用データを取り出す
Extension_ShaderMatrixRotate *extension = (Extension_ShaderMatrixRotate*) app->extension;
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// attr_posを有効にする
glEnableVertexAttribArray(extension->attr_pos);
glEnableVertexAttribArray(extension->attr_uv);
// 四角形描画
{
// この章のサンプルではpositionとUVの位置を変更しない
const GLfloat position[] = {
// v0(left top)
-0.5f, 0.5f,
// v1(left bottom)
-0.5f, -0.5f,
// v2(right top)
0.5f, 0.5f,
// v3(right bottom)
0.5f, -0.5f, };
//
const GLfloat uv[] = {
// v0(left top)
0, 0,
// v1(left bottom)
0, 1,
// v2(right top)
1, 0,
// v3(right bottom)
1, 1, };
// 頂点情報を関連付ける
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glVertexAttribPointer(extension->attr_uv, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) uv);
// 行列をアップロードする
{
#if 1
// 行列を生成する
// Z軸で回転を行わせる
mat4 matrix = mat4_identity();
// 回転軸のベクトルを生成する
vec3 axis = vec3_createNormalized(0.0f, 0.0f, 1.0f);
// 回転軸のベクトルを設定する
const GLfloat axis_x = axis.x;
const GLfloat axis_y = axis.y;
const GLfloat axis_z = axis.z;
const GLfloat c = cos(degree2radian(extension->rotate));
const GLfloat s = sin(degree2radian(extension->rotate));
{
matrix.m[0][0] = (axis_x * axis_x) * (1.0f - c) + c;
matrix.m[0][1] = (axis_x * axis_y) * (1.0f - c) - axis_z * s;
matrix.m[0][2] = (axis_x * axis_z) * (1.0f - c) + axis_y * s;
matrix.m[0][3] = 0;
}
{
matrix.m[1][0] = (axis_y * axis_x) * (1.0f - c) + axis_z * s;
matrix.m[1][1] = (axis_y * axis_y) * (1.0f - c) + c;
matrix.m[1][2] = (axis_y * axis_z) * (1.0f - c) - axis_x * s;
matrix.m[1][3] = 0;
}
{
matrix.m[2][0] = (axis_z * axis_x) * (1.0f - c) - axis_y * s;
matrix.m[2][1] = (axis_z * axis_y) * (1.0f - c) + axis_x * s;
matrix.m[2][2] = (axis_z * axis_z) * (1.0f - c) + c;
matrix.m[2][3] = 0;
}
{
matrix.m[3][0] = 0;
matrix.m[3][1] = 0;
matrix.m[3][2] = 0;
matrix.m[3][3] = 1;
}
#else
mat4 matrix = mat4_rotate(vec3_createNormalized(0, 0, 1), extension->rotate);
#endif
glUniformMatrix4fv(extension->unif_matrix, 1, GL_FALSE, (GLfloat*) matrix.m);
// 回転角を増やす
extension->rotate += 1.0f;
}
glBindTexture(GL_TEXTURE_2D, extension->texture->id);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
毎フレーム1度ずつ回転角を増やす。行列作成の部分がややこしいが、それ以外は移動・拡大と同じ。補助関数で行列が作れるところも同じ。
補助関数:vec3_createNormalized
正規化した向きベクトルを作成する。サンプルではZ方向の回転軸、(0, 0, 1)というベクトルを作成している。正規化とは「ベクトルの長さで各要素を割ったもの」であり、ベクトルの長さとは「各要素の二乗を合計した数値の平方根」である。
実装はこんな感じ:
vec3 vec3_create(const GLFloat x, const GLfloat y, const GLfloat z) {
vec3 v = { x, y, z };
return v;
}
GLfloat vec3_length(const vec3 v) {
return (GLfloat) sqrt(((double) v.x * (double) v.x) +
((double) v.y * (double) v.y) +
((double) v.z * (double) v.z));
}
vec3 vec3_normalize(const vec3 v) {
const GLfloat len = vec3_length(v);
return vec3_create(v.x / len, v.y / len, v.z / len);
}
vec3 vec3_createNormalized(const GLfloat x, const GLfloat y, const GLfloat z) {
return vec3_normalized(vec3_create(x, y, z));
}
方向ベクトル
方向ベクトルとは、「ある座標Aから見て、、別な座標Bがどの方向にあるか」を指し示すベクトル。
原点から見て、(0, 1, 0), (0, 2, 0) は、どちらも「真上」にある。「ある座標への距離=ベクトルの長さ」は違っても、「ある座標への向き」は同じであるとき、ベクトルの向きだけに注目する場合は 正規化 というベクトルの長さを 1.0 に揃える処理を行う。
ベクトルの長さが 1.0 になると、純粋に方向だけを示すことになり、回転行列の「方向ベクトルの値の二乗」などの操作がシンプルになるメリットがある。
回転行列の作成
サンプルでは Extension 構造体の rotate 変数を Degree で示しているが、C言語標準の sin/cos の算出には Radian を引数にする必要があるので、以下のマクロを定義して使っている。
# define degree2radian(degree) ((degree * M_PI) / 180.0)
補助関数:mat4_rotate
mat4 mat4_rotate(const vec3 axis, const GLfloat rotate)
asix には正規化したベクトルを、rotate は degree を与える。
複数の行列を適用
const GLchar *vertex_shader_source =
//
"uniform mediump mat4 unif_translate;"
"uniform mediump mat4 unif_rotate;"
"uniform mediump mat4 unif_scale;"
"attribute mediump vec4 attr_pos;"
"attribute mediump vec2 attr_uv;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_Position = unif_translate * unif_rotate * unif_scale * attr_pos;"
" vary_uv = attr_uv;"
"}";
const GLchar *fragment_shader_source =
//
"uniform sampler2D texture;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_FragColor = texture2D(texture, vary_uv);"
"}";
// ....
// 行列をアップロードする
{
// 移動行列
const mat4 translate = mat4_translate(0.5f, 0.5f, 0.0f);
// 拡大縮小行列
const mat4 scale = mat4_scale(0.5f, 2.0f, 1.0f);
// 回転行列
const mat4 rotate = mat4_rotate(vec3_create(0.0f, 0.0f, 1.0f), extension->rotate);
// 各行列をアップロードする
glUniformMatrix4fv(extension->unif_translate, 1, GL_FALSE, (GLfloat*) translate.m);
glUniformMatrix4fv(extension->unif_rotate, 1, GL_FALSE, (GLfloat*) rotate.m);
glUniformMatrix4fv(extension->unif_scale, 1, GL_FALSE, (GLfloat*) scale.m);
// 回転角を増やす
extension->rotate += 1.0f;
}
glBindTexture(GL_TEXTURE_2D, extension->texture->id);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
左から順番に「移動」「回転」「拡大縮小」の順番で乗算を行う。
実行結果は、「中心を基点に X * 0.5, Y * 2.0 倍されたポリゴンが、X, Y = (0.5, 0.5) に移動し、ポリゴンの中心を軸に回転している」となる。これは、 シェーダに記述した掛け算の順番と逆順に 適用されていることになる。
2D/3D 処理では、多くの場合で 「拡大縮小・回転・移動」 の順番で行列を適用する(シェーダでの記述は逆順)。
回転アニメーション中心が画像の中心でないのは、「回転」→「移動」という処理を毎フレーム行っているため。
行列の転置
転置とは、行列の各値の「縦位置」と「横位置」を入れ替えること。
メモリの節約や処理効率化のため、わざと行列を転置させてアップロードさせることがある。なぜなら、移動・拡大・回転の行列は、4x4 のうち実際必要なデータは左側の 3x4 部分のみである。これを転置して 4x3 にすれば、vec4 型 3 つにメモリを節約できる、というわけ。
転置行列を使ってメモリ節約を行うテクニックは GLSL ES の組み込み関数が使えない制約から、非常に処理が複雑になる傾向にある(ので、本書では解説を行わない)。
乗算済みの行列を利用する
gl_Position = unif_translate * unif_rotate * unif_scale * attr_pos;
実のところこの制御方法は無駄が大きい。
uniform 変数である unif_xxx は、描画中は不変であることが保証される。つまり "unif_translate * unif_rotate * unif_scale" の結果も常に不変であると言える。
乗算によって複数の行列を1つにまとめる 合成 を事前に行い、GPU はその結果だけを利用するようにしてみる。
// 頂点シェーダーを用意する
{
const GLchar *vertex_shader_source =
//
"uniform mediump mat4 unif_matrix;"
"attribute mediump vec4 attr_pos;"
"attribute mediump vec2 attr_uv;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_Position = unif_matrix * attr_pos;"
" vary_uv = attr_uv;"
"}";
const GLchar *fragment_shader_source =
//
"uniform sampler2D texture;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_FragColor = texture2D(texture, vary_uv);"
"}";
// ...
// 行列をアップロードする
{
// 移動行列
const mat4 translate = mat4_translate(0.5f, 0.5f, 0.0f);
// 拡大縮小行列
const mat4 scale = mat4_scale(0.5f, 2.0f, 1.0f);
// 回転行列
const mat4 rotate = mat4_rotate(vec3_create(0.0f, 0.0f, 1.0f), extension->rotate);
// シェーダーに渡す前に行列を計算済みにしておく
mat4 matrix = mat4_multiply(translate, rotate);
matrix = mat4_multiply(matrix, scale);
glUniformMatrix4fv(extension->unif_matrix, 1, GL_FALSE, (GLfloat*) matrix.m);
// 回転角を増やす
extension->rotate += 1.0f;
}
補助関数: mat4_multiply
mat4 mat4_multiply(const mat4 a, const mat4 b)
みたまま。ベクトル×行列の公式を行っているだけ。