前提
『マルチプラットフォームのためのOpenGL ES入門 基礎編―Android/iOS対応グラフィックスプログラミング』
の抜粋メモです。5章、6章あたり。
1〜4 はありません。
コードについては断片のみなので、本書を読んでください。
5章
glClearColor
void glClearColor (GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha)
clamp は 0.0f - 1.0fの範囲にクランプされるという意味
glClear
void glClear (GLbitfield mask)
maskは塗りつぶすバッファの種類
- GL_COLOR_BUFFER_BIT
- GL_DEPTH_BUFFER_BIT
- GL_STENCIL_BUFFER_BIT
OpenGL ES は、用途の違う3枚のバッファを組み合わせて描画を行う。それぞれ保存する内容やメモリの使用量が異なる。
カラーバッファは、ディスプレイのひとつひとつのピクセル情報の集合を保存するただのメモリの塊。
GLbitfield mask の引数には、複数のバッファを OR で組み合わせて、「1回のコマンド呼び出しで複数のバッファを同時に塗りつぶす」ことができるようになっている。
補助関数:ES20_postFrontBuffer
本誌サンプル独自のメソッド。画面を更新する。これを呼び出さない限り、描画結果がディスプレイに反映されることはない。中身は各プラットフォームに依存した処理で、レンダーバッファのバインドやレンダリングコンテキストへの命令が含まれるが、「描画結果をディスプレイに反映させる」処理はプラットフォームへの依存性が大きいので、本来の OpenGL ES の機能には含まれていない。
OpenGL ES は、 サーフェイスに対して描画するのであって、画面に対して描画するわけではない。
※OS と Surface の連携を担当するのが、EGL(Android)であり、EAGL(iOS)となる。
OpenGL ES における「ダブルバッファリング」は、サーフェイスに対する描画バッファ( バックバッファ )とディスプレイに対する描画バッファ( フロントバッファ )を言う。
※Android4.1以降では、さらに「予備のバッファ」を儲けた「トリプルバッファリング」の構造になっている。
描画フレーム
Android/iOSともに、Viewは基本的に「更新が必要なタイミング」だけを描画している。それに対し、OpenGL ES は、
- Viewと同じく必要なタイミングのみ描画
- 決まったfpsで常に描画
という2種類の使い方がされる。
OpenGL ES のパラメータ保存先
glClearColorで指定された「塗りつぶしの色」は、どこに保存されているか?
OpenGL ESのコマンドが実行されると、OpenGL ES が管理している Context と呼ばれる領域に値が保持される。OpenGL ES は Context に設定されている情報に基づいて処理を実行する ステートマシン になっている。
Context には、OpenGL ES の状態(ステート)、画像やシェーダー等の管理・保持領域も含まれる。基本的に集中管理されている。
「OpenGL ES が管理するグローバル変数に保存される」というイメージでOK
6章
頂点(vertex)
OpenGL ES は、n個の頂点を空間上に浮かべ(座標データを用意し)、これらを結んで塗りつぶすことで図形を描く。
OpenGL ES が描画可能なプリミティブ(図形)は、点/線/三角形の3種類。
シェーダー
OpenGL ES 1.x 世代には存在しなかったプログラム。新しい描画能力と柔軟性を手に入れた変わりに、「とっつきやすさ」が失われた。つまり、「シェーダーを記述しなければ何もできない」という世界観になった。コード量も増えた。
シェーダーは「特殊なプログラム」であり、Cに似た専用の言語で記述する。
sample_rendaring_triangle.c
共通構造
- initialize で extention 構造体用メモリを確保して、必要な箇所でキャストして使用
- サンプルで必要な値はすべて"Extension_サンプル名"で管理、extensionメンバーに対し、OpenGL ES が生成した値やアプリが必要な変数等を保存・アクセスする
- それらは destroy で廃棄される
頂点シェーダの生成
シェーダは、描画処理の前半を担う 頂点シェーダ と後半部分の フラグメントシェーダ に分けられる。
「頂点の最終的な座標を決める」ことが頂点シェーダの役割。
サンプルでは、頂点シェーダを GLchar* のポインタに文字列(のような型)として格納している。
const GLchar *vertex_shader_source =
"attribute mediump vec4 attr_pos;"
"void main() {"
" gl_Position = attr_pos;"
"}";
見てわかるように、厳密な C言語とは異なる書式になっている。この言語を GLSL ES(OpenGL Shader Language ES) と呼ぶ。
GLSL文法:attributeキーワード
[attribute] [mediump vec4] [attr_pos];
[キーワード宣言] [変数型] [変数名]
attribute キーワードは、頂点の属性を宣言する。attribute 変数は読込み専用で、頂点シェーダが実行されるときに OpenGL が自動的に値を代入する。(代入するとコンパイルエラーになる)
vec4 はおなじみの
typedef struct vec4 {
GLfloat x;
GLfloat y;
GLfloat z;
GLfloat w;
}
2D を扱う限りは、「とりあえず w には 1.0 を入れておく」で、まずは OK。
GLSL文法:mediumpキーワード
float は 4byte だが、毎秒数百万という頂点数を処理することにもなる OpenGL では、ほんの僅かな計算量の増加がクリティカルになる。
そこで、GLSL ES では、処理精度の指定を強制している。
識別子 | 精度 | スピード | 目安 |
---|---|---|---|
highp | 高 | 低速 | 絶対値が1.0以上 |
mediump | 中 | 中速 | 絶対値が1.0以下 |
lowp | 低 | 高速 | 絶対値が1.0以下で、分解能が低くても問題無い |
ただし環境にもよるので、コンパイルエラー時はこの設定も疑ってみること。
GLSL文法:main
GLSLは、"void main()"関数から開始される。(エントリーポイント)
main() は引数を受け取らず、欲しい情報は attribute 等によって受け取る。
サンプルでは attr_pos で受け取った頂点の位置情報を gl_Position に代入している。これは組込み変数で、頂点シェーダから位置情報を受け取るという役割を持つ。
頂点シェーダから受け取った gl_Position は、OpenGL に規定された処理が加えられ、後半のフラグメントシェーダの処理が始まる。
頂点シェーダは頂点の数だけ実行される。また gl_Position は main() が呼び出されるたびに初期化される。なので、前回の描画結果が次の描画に影響を与えるということは一切ない。そして、呼び出しはGPUの複数コア・複数スレッドによって 並列的に呼び出される 。GLSL ES の独特な記法と制約は、並列実行を効率よく行うためでもある。
GLSLのコンパイル
// シェーダオブジェクトの作成
extension->vert_shader = glCleateShader(GL_VERTEX_SHADER);
assert(glGetError() == GL_NO_ERROR);
assert(extension->vert_shader != 0);
glShaderSource(extension->vert_shader, 1, &vertex_shader_source, NULL);
glCompileShader(extension->vert_shader);
// コンパイルエラーをチェックする
{
GLint compileSuccess = 0;
glGetShaderiv(extension->vert_shader, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
// エラーが発生した
GLint infoLen = 0;
// エラーメッセージを取得
glGetShaderiv(extension->vert_shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
GLchar *message = (GLchar*) calloc(infoLen, sizeof(GLchar));
glGetShaderInfoLog(extension->vert_shader, infoLen, NULL, message);
__log(message);
free((void*) message);
} else {
__log("comple error not info...");
}
}
assert(compileSuccess == GL_TRUE);
}
// ....
glCreateShader
GLuint glCreateShader(GLenum type)
typeは作成するシェーダの種類。戻り値はシェーダオブジェクト(管理用のID)。成功すると 0 以外が返る。
glGetError
GLenum glGetError ()
エラー取り出し命令。注意点は3つ:
- glGetErrorコマンドを呼び出した時点で、保持しているエラーは GL_NO_ERROR に上書きされる
- 「どのコマンドのエラーか」を管理していない。また既にエラーが発声している状態ではエラー内容の上書きも行わない。
- 呼び出しのコスト(時間)は高い
なので、assert でデバッグビルド時のみチェックするのがおすすめ。
glShaderSource
シェーダオブジェクトにソースコードを渡す。
void glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length);
- shader:シェーダオブジェクト
- count: シェーダソースコードの文字列配列の数(ここでは1)
- string: ソースコードの文字列配列(予約語じゃないのかこれ。。
- length: ソースコードの文字列長(NULLを渡すと、\0で終わる文字列と見なす
glCompileShader
void glCompileShader(GLuint shader);
渡されたソースコードはこのタイミングで初めてコンパイルされる。
glGetShaederiv
実際にコンパイルが成功したかどうかは判別できない(戻り値はvoid)。なのでこのコマンドによって情報を問い合わせる。
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
引数は本誌 p.88 参照。サンプルでは COMPILE_STATUS を取得し、GL_TRUE / GL_FALSE をチェックしている。
フラグメントシェーダの記述
フラグメントシェーダは、ピクセルシェーダとも呼ばれ、最終的な「ピクセルの色」を確定させる。
サンプルでは単色の赤を出力するシンプルなシェーダを記述している。
const GLchar *fragment_shader_source =
"void main() {"
" gl_FlagColor = vec4(1.0, 0.0, 0.0, 1.0);"
"}";
※GLSL ES では、浮動小数を記述する際に、末尾に"f"を付けるとコンパイルエラーになるので注意。
頂点シェーダから渡された頂点は GPU によって三角形に結ばれ、中身の塗りつぶしが始まる。そのときにピクセルを塗る色をピクセルシェーダによって判断する。
gl_FlagColor も OpenGL の組込み変数であり、GLSL ES は、そこに書かれた色を「最終的に出力したい色」だと判断する。
頂点自身の情報である attribute 変数は、ピクセルシェーダには宣言できない。
フラグメントシェーダは最終的に塗りつぶされるピクセルの数だけ呼び出される。FullHDサイズでは2,073,600回の呼び出しが行われ、その 60fps を維持するためには、毎秒1億2400万回以上の呼び出しに応えなくてはならない。
フラグメントシェーダの読込みとコンパイル
基本的には頂点シェーダと同じ流れ。glCreateShaderの引数に注意。
// シェーダーオブジェクトを作成する
extension->frag_shader = glCreateShader(GL_FRAGMENT_SHADER);
assert(extension->frag_shader != 0);
glShaderSource(extension->frag_shader, 1, &fragment_shader_source, NULL);
glCompileShader(extension->frag_shader);
// コンパイルエラーをチェックする
{
GLint compileSuccess = 0;
glGetShaderiv(extension->frag_shader, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
// エラーが発生した
GLint infoLen = 0;
// エラーメッセージを取得
glGetShaderiv(extension->frag_shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
GLchar *message = (GLchar*) calloc(infoLen, sizeof(GLchar));
glGetShaderInfoLog(extension->frag_shader, infoLen, NULL, message);
__log(message);
free((void*) message);
} else {
__log("comple error not info...");
}
}
assert(compileSuccess == GL_TRUE);
}
プログラムオブジェクトの生成
2つのシェーダを用意できたところで、それらをコンパイルしてリンクすることで初めてシェーダプログラムとして機能するようになる。OpenGLES では、頂点シェーダとフラグメントシェーダをリンクさせたオブジェクトを プログラムオブジェクト と呼ぶ。
glCreateProgram
GLenum glCreateProgram
シェーダオブジェクトと同じく専用のメモリ領域に保存・管理され、GLuint 型の ID が割り振られる。
サンプルでの生成とエラーチェック:
extension->shader_program = glCreateProgram();
assert(extension->shader_program != 0);
生成に成功した場合、0以外が返る。
glAttachShader
プログラムオブジェクトを生成したら、リンクしたいシェーダを接続(アタッチ)する。
void glAttachShader(GLuint program, GLuint shader)
glLinkProgram
2つのシェーダオブジェクトを関連付けたら、それらをリンクさせて使用可能な状態になる。
void glLinkProgram(GLuint program)
サンプルでは:
//
// 頂点シェーダーとプログラムを関連付ける
glAttachShader(extension->shader_program, extension->vert_shader);
assert(glGetError() == GL_NO_ERROR);
// フラグメントシェーダーとプログラムを関連付ける
glAttachShader(extension->shader_program, extension->frag_shader);
assert(glGetError() == GL_NO_ERROR);
// リンク処理を行う
glLinkProgram(extension->shader_program);
// リンクエラーをチェックする
{
GLint linkSuccess = 0;
glGetProgramiv(extension->shader_program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
// エラーが発生したため、状態をチェックする
GLint infoLen = 0;
// エラーメッセージを取得
glGetProgramiv(extension->shader_program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
GLchar *message = (GLchar*) calloc(infoLen, sizeof(GLchar));
glGetProgramInfoLog(extension->shader_program, infoLen, NULL, message);
__log(message);
free((void*) message);
}
}
// GL_NO_ERRORであることを検証する
assert(linkSuccess == GL_TRUE);
}
glGetProgramiv
void glGetProgramiv(GLuint program, GLenum pname, GLint* param)
シェーダオブジェクトと同様にリンクエラーのログ情報を取得
glGetProgramInfoLog
void glGetProgramInfoLog(GLuint program, GLsizei bufsize, GLsize* length, GLchar* infolog)
プログラムのエラーログを取得する。
glGetAttribLocation
シェーダ自体の準備が終わったら、シェーダにデータを渡す準備をする。頂点シェーダに記述したattribute変数(attr_pos)はアプリ側から操作を行う際に、「変数名」を直接利用せず、 attribute location という ID を利用する。取得には以下のコマンドを利用:
GLint glGetAttribLocation(GLuint program, const GLchar* name)
attribute location は基本的に変更されず、プログラムオブジェクトがリンクした際に確定される(ので、Extension 構造体に保存している)。
extension->attr_pos = glGetAttribLocation(extension->shader_program, *attr_pos);
assert(extension->attr_pos >= 0);
失敗したときは -1
glUseProgram
最後に「このシェーダで描画します」という宣言を行う。glUserProgram() で、描画に使用するシェーダを切り替える。
void glUseProgram(GLuint program)
通常はシェーダは複数利用するが、glUseProgram() は描画ごとではなく、「シェーダを切り替えたいタイミング」のみ呼び出すことが推奨される。
描画を行う
void sample_RenderingTriangle_rendering(GLApplication *app) {
// サンプルデータを取り出す
Extension_RenderingTriangle *extension = (Extension_RenderingTriangle*) app->extension;
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// attr_pos を有効にする
glEnableVertexAttribArray(extension->attr_pos);
// 画面中央へ描画
const GLfloat position[] = {
0.0f, 1.0f, // v0
1.0f, -1.0f, // v1
-1.0f, -1.0f // v2
};
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*)position);
glDrawArrays(GL_TRIANGLES, 0, 3);
// バックバッファをフロントバッファへ転送(プラットフォームごとに独自の処理)
ES20_postFrontBuffer(app);
}
glEnableVertexAttribArray
attribute変数には有効・無効状態があり、有効でなければ頂点データにアクセスすることができない。
void glEnableVertexAttribArray(GLuint index)
「どのプログラムオブジェクトのattribute変数か」を指定する必要はなく、最後に glUseProgram コマンドで指定したオブジェクトの attribute 変数に対して効果を与える(ので、複数のプログラムオブジェクトがある場合は注意が必要)。
glVertexAttribPointer
頂点データは単なるメモリの塊であり、専用の型や構造はない。なので、1つの頂点にどのようなデータがあって、どんな型で格納されているかを、下記で伝える。
void glVertexAttribPointer(GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
- index : 頂点データを関連付ける attribute location
- size : 頂点データの要素数
- type : 頂点データの型。GL_BYTE, GL_FLOAT など
- normalized : 正規化して渡す場合は GL_TRUE, そのまま渡す場合は GL_FALSE
- stride : 頂点の先頭位置ごとのオフセット、0指定可能
- ptr : 関連付ける頂点の先頭ポインタ
OpenGLES 2.0 では、「頂点データ」と「シェーダに渡されるデータ」は分離して考える。GPUは頂点データをソースとして、シェーダが指定する都合のいい型に変換して頂点シェーダに渡している。
glDrawArrays
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
- mode : 描画モードを指定、GL_LINES, GL_TRIANGLES,,, など
- first : 開始する頂点のインデックスを指定
- count : 描画する頂点数を指定
ここで、サンプルの position[] のサイズを first + count が超えると当然悲惨なことになるので注意すべし。
Viewport
リサイズ時に呼び出される関数:
void sample_RenderingTriangle_resized(GLApplication *app) {
glViewport(0, 0, app->surface_width, app->surface_height);
}
2つの座標系
フラグメントシェーダは、X軸Y軸がピクセル単位で管理される ウィンドウ座標系 で処理が行われる。
Viewportで指定するのはこのウィンドウ座標系。
ウィンドウ座標系は 左下 を原点とする。(一般的な 2D座標系は左上が基準になる)
ウィンドウ座標の対角、右上の座標はサーフェイスの大きさとリンクしている。それ以外の「画面外」のピクセル単位は物理的には存在しない。
しかし、
const GLfloat position[] = {
0.0f, 1.0f, // v0
1.0f, -1.0f, // v1
-1.0f, -1.0f // v2
};
は、0-1の浮動小数である。
OpenGLES が頂点の演算に利用する座標系は、X軸、Y軸、Z軸を持つ 正規化されたデバイス座標系 である。
座標系の左下手前が(-1, -1, -1)であり、右上奥が(1, 1, 1)となる。
頂点シェーダによって gl_Position に書き込まれた頂点座標は、常に正規化されたデバイス座標系の座標として扱われる。
正規化デバイス座標系の浮動小数空間と、ウィンドウ座標系のピクセル単位空間を結びつけるのが Viewport の設定。
Viewport はウィンドウ座標系で座標を指定し、多くの場合サーフェイスの大きさと同じ設定にする。
OpenGLES は正規化デバイス座標系を Viewport の値にしたがってウィンドウ座標系に変換する。
Viewport の左下が正規化座標系のXY(-1, -1)に対応し、右上がXY(1, 1)になるように全体が引き伸ばされたり縮められたりする。
glViewport
Viewportを変更する。
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height)
- x : 画面左下から数えたXピクセル座標
- y : 画面左下から数えたYピクセル座標
- width : Viewport の幅ピクセル数
- height : Viewport の高さピクセル数
リサイズ時には必ず glViewport を呼び出すのがお約束(強く推奨
※Androidのサーフェイスがリサイズされるタイミング
Android の Activity のデフォルトの挙動では、縦横が切り替わると Activity を廃棄・再生成するようになっている。イコール View が廃棄される、イコール OpenGLES自体が完全廃棄されるということ。
なので実際にこのタイミングでは、
- - 画像やシェーダ等のリソースを再読み込みする
- アプリの状態を保持しておく
などのコードが必要になるが、サンプルでは縦横を切り替えても Activity が廃棄されないように改造されている。Manifestに以下のように書けばおk
<activity
android:name="com.xxx..."
android:configChanges="orientation|screenSize"
android:xxx=....
....
</activity>
後処理
glDeleteProgram
シェーダプログラムの削除。glCreateProgram との対になる。
void glDeleteProgram(GLuint program)
サンプルでは、
// シェーダの利用を終了する
glUseProgram(0);
asset(glGetError() == GL_NO_ERROR);
// シェーダプログラムを廃棄する
glDeleteProgram(extension->shader_program);
asset(glGetError() == GL_NO_ERROR);
// プログラムオブジェクトでないことを確認する
asset(glIsProgram(extension->shader_program) == GL_FALSE);
と、UseProgram に 0 を入れて使用中のシェーダがない状態に戻してから、プログラムを削除している。
glIsProgram() の意味や使い方はサンプルにある通り。
glDeleteShader
プログラムオブジェクトと同様に、シェーダオブジェクトの削除。
void glDelteShader(GLuint shader)