前提
『マルチプラットフォームのためのOpenGL ES入門 基礎編―Android/iOS対応グラフィックスプログラミング』
の抜粋メモです。9章の内容。
コードについては断片のみなので、本書を読んでください。
テクスチャ
OpenGL ES で扱うことの出来る画像を Texture と呼ぶ。
テクスチャ座標系とUV
OpenGL に読み込まれた画像は、その瞬間からテクスチャとして扱われ、テクスチャとしての制限を受ける。OpenGL にアップロードされたピクセル情報は Texel(テクセル) と呼ばれ、明確に区別される。
テクスチャでは画像のX軸に相当する横軸を U軸、縦軸を V軸 と呼ぶ。
OpenGL ES で2Dの座標を扱うときは左下が原点」
OpenGL ES は、画像をVRAMにアップロードした際に、 上下を逆さにして 格納する。
座標は 0.0〜1.0 に丸められる。
「左下原点」「上下が逆」のため、結果として左上原点で 0.0〜1.0 の座標系となる。
- U座標 = Xピクセル座標 / width
- V座標 = Yピクセル座標 / height
※ OpenGL ES1.1 以前では、一辺のテクセル数が 2^n という制限があったが、2.0以降では撤廃されている。ただし、高速な処理のために依然として 2^n のスクエアが推奨ではある。
テクスチャの作成
typedef struct {
// レンダリング用シェーダープログラム
GLuint shader_program;
// 位置情報属性
GLint attr_pos;
// UV情報属性
GLint attr_uv;
// テクスチャのUniform
GLint unif_texture;
// 読み込んだテクスチャ
GLuint texture_id;
} Extension_LoadTexture;
/**
* アプリの初期化を行う
*/
void sample_LoadTexture_initialize(GLApplication *app) {
// サンプルアプリ用のメモリを確保する
app->extension = (Extension_LoadTexture*) malloc(sizeof(Extension_LoadTexture));
// サンプルアプリ用データを取り出す
Extension_LoadTexture *extension = (Extension_LoadTexture*) app->extension;
// 頂点シェーダーを用意する
{
const GLchar *vertex_shader_source =
//
"attribute mediump vec4 attr_pos;"
"attribute mediump vec2 attr_uv;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_Position = attr_pos;"
" vary_uv = attr_uv;"
"}";
const GLchar *fragment_shader_source =
//
"uniform sampler2D unif_texture;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_FragColor = texture2D(unif_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_texture = glGetUniformLocation(extension->shader_program, "unif_texture");
assert(extension->unif_texture >= 0);
}
// テクスチャの生成を行う
{
glGenTextures(1, &extension->texture_id);
assert(extension->texture_id != 0);
assert(glGetError() == GL_NO_ERROR);
// 現状ではおまじない扱い
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
assert(glGetError() == GL_NO_ERROR);
glBindTexture(GL_TEXTURE_2D, extension->texture_id);
assert(glGetError() == GL_NO_ERROR);
// 画像ピクセルを読み込む
{
RawPixelImage *image = RawPixelImage_load(app, "texture_rgb_asymmetry.png", TEXTURE_RAW_RGBA8);
// 正常に読み込まれたかをチェック
assert(image != NULL);
// VRAMへピクセル情報をコピーする
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->pixel_data);
assert(glGetError() == GL_NO_ERROR);
// コピー後は不要になるため、ピクセル画素を解放する
RawPixelImage_free(app, image);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
assert(glGetError() == GL_NO_ERROR);
}
// シェーダーの利用を開始する
glUseProgram(extension->shader_program);
assert(glGetError() == GL_NO_ERROR);
}
/**
* レンダリングエリアが変更された
*/
void sample_LoadTexture_resized(GLApplication *app) {
// 描画領域を設定する
glViewport(0, 0, app->surface_width, app->surface_height);
}
/**
* アプリのレンダリングを行う
* 毎秒60回前後呼び出される。
*/
void sample_LoadTexture_rendering(GLApplication *app) {
// サンプルアプリ用データを取り出す
Extension_LoadTexture *extension = (Extension_LoadTexture*) 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);
// unif_textureへテクスチャを設定する
glUniform1i(extension->unif_texture, 0);
// このブロックはカリングを含めて正しい順番で頂点が定義されていることをチェックします
#if 0 /* カリングチェック */
{
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
#endif /* カリングチェック */
// 四角形描画
{
const GLfloat position[] = {
// v0(left top)
-0.75f, 0.75f,
// v1(left bottom)
-0.75f, -0.75f,
// v2(right top)
0.75f, 0.75f,
// v3(right bottom)
0.75f, -0.75f, };
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);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
}
UV情報の設定
ポリゴンに貼り付けるテクスチャを「どのように貼り付けるか」という指定は、頂点ごとに対応するテクスチャのUV座標を指定することで行う。
そこで、前章で色情報を追加したように、頂点に「UV成分」を追加する。
"attribute mediump vec4 attr_pos;"
"attribute mediump vec2 attr_uv;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_Position = attr_pos;"
" vary_uv = attr_uv;"
"}";
attribute変数の attr_uv と、varying変数の vary_uv が追加。vary_uv に対して attr_uv を代入している。
頂点シェーダは「頂点とテクスチャのどこの座標が関連づいているか」を指定する。しかし、実際に色を決めるのはフラグメントシェーダが行うため、フラグメントシェーダに対して varying 変数として UV 座標を渡す必要がある。
GLSL文法:sampler2D キーワード
"uniform sampler2D unif_texture;"
"varying mediump vec2 vary_uv;"
"void main() {"
" gl_FragColor = texture2D(unif_texture, vary_uv);"
"}";
フラグメントシェーダには、uniform 変数の unif_texture が定義されている。
sampler2D は、2次元テクスチャを示す型。精度を指定することはできない。
sampler2D 型 uniform 変数はその他の uniform 変数と同じく、glGetUniformLocation コマンドで location を取得することができる。
GLSL文法:texture2D 関数
varying 変数 vary_uv として、頂点シェーダから UV 座標を受け取る。varying の「頂点間で値が補完される」という特性は、UV 座標に関しても同様。なので、ピクセルごとの正確な UV 座標をフラグメントシェーダが把握できるようになる。
ピクセル単位の UV 座標が分かったら、その UV 座標を元にテクスチャからテクセル情報=色を取り出す。そのための GLSL 組み込み関数が texture2D
vec4 texture2D (sampler2D, vec2)
- sampler2D : 色を取り出したいテクスチャ
- vec2 : テクセルを取り出したい UV 座標
サンプルでは vary_uv の値を元に unif_texture からテクセル色を vec4 型で取り出し、ポリゴンの色として表示している。
gl_FragColor = texture2D(unif_texture, vary_uv);
glGenTextures
OpenGL のテクスチャは GPU がアクセス可能な専用領域に保持される。そのため、C言語の構造体や Java クラスのような直接的なオブジェクトは取得できず、ID で管理が行われる。(シェーダオブジェクト、プログラムオブジェクトと同じ)
GLuint 型で生成されるテクスチャの ID を、テクスチャオブジェクト と呼ぶ。テクスチャオブジェクトは glGenTextures コマンドで生成する。
void glGenTextures (GLsizei n, GLuint* textures);
- n : 生成するテクスチャオブジェクト数
- texutures : テクスチャオブジェクトの格納先ポインタ
テクスチャオブジェクトは必ず 0 以外の整数が利用される。0 は予約で「無効なテクスチャオブジェクト」を示す。
glPixelStorei
画像を VRAM へアップロードするために、このコマンドが必要な場合がある。
glPixelStorei コマンドは、画像の1行(X方向のピクセル)が、何バイト境界で整列されているかを示す。
サンプルでは単純化のため常に1、デフォルトは4。
速度的な効率のためには適宜設定することが望ましい。
void glPixelStorei (GLenum pname, GLint param)
- pname : GL_UNPACK_ALLIGNMENT または GL_PACK_ALIGNMENT
- param : 何バイト境界でピクセル情報が並べられているか(2のn乗のみ)
UNPACK はテクスチャへピクセルをアップロードするとき、PACK は逆にテクスチャからテクセルをダウンロードするときに使用する。
glBindTexture
glGenTextures で生成されたテクスチャオブジェクトを操作するためには、このコマンドでテクスチャを OpenGL ES にバインド(関連付け)する必要がある。
OpenGL ES のテクスチャ系コマンドは、「最後にバインドされたテクスチャオブジェクト」に対して操作を行う。
void glBindTexture (GLenum target, GLuint texture)
- target : テクスチャの種類(GL_TEXTURE_2D または GL_TEXTURE_CUBE_MAP)
- texture : バインド対象のテクスチャID
生成されたテクスチャオブジェクトは、glBindTexture を行うことで初めてメモリを確保される。(ただし、ここでのメモリはあくまでテクスチャオブジェクトの管理用であり、ピクセル情報メモリはまた別途確保の必要がある)
補助関数:RawPixelImage_load
OpenGL ES は、JPG, PNG, GIF のような「汎用画像ファイル」を扱うための API を用意されていない。
扱えるのは、 非圧縮のピクセル情報 もしくは GPU専用画像形式 のみ。
PNG ファイルは画像の容量削減のため、画素情報が圧縮されている。また、幅や高さなどのメタ情報も含む非常に複雑なファイルフォーマットである。Android の Bitmap クラスや iOS の UIImage クラスが「PNG ファイルを読み込む」とき、それらが行うことは、 圧縮状態のピクセル情報を非圧縮のRawピクセル情報=RGBAの色情報配列に変換 することである。
RawPixelImage_load 関数は サンプルが用意する汎用画像読み出し(RAWピクセル情報に変換)機能を提供する。
対応フォーマットは RGBA8, RGB8, RGBA5551, RGB65。
サンプルでは
RawPixelImage *image = RawPixelImage_load(app, "texture_rgb_asymmetry.png", TEXTURE_RAW_RGBA8);
glTexImage2D
読み込んだ RAW ピクセル情報を、VRAM へアップロードする。
void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels)
- target : テクスチャの利用法(2D or 各種 CUBE_MAP)
- level : 転送先のミップマップレベルを指定
- internalformat : テクスチャフォーマット(RGBに加え、深度、輝度、アルファなどを持つなら指定)
- width : テクスチャの幅
- height : テクスチャの高さ
- border : テクスチャの境界ピクセル数(後方互換のための引数。OpenGL ES では 0 のみ)
- format : アップロードするピクセル情報フォーマット(internalformat と必ず同じ)
- type : アップロードするピクセル情報の型
- pixels : アップロードするピクセル情報へのポインタ
補助関数:RawPixelImage_free
VRAM は CPU が利用できるメモリとは論理的・物理的に切り離されている場合がある。そのため、OpenGL ES は glTexImage2D コマンドが発行されると、ピクセル情報を GPU のアクセスが用意な領域へコピーする。
コピーが行われると画像の上下は反転され、ピクセルデータは「テクセル」として GPU での利用が可能になる。コピーによって VRAM にアップロードされると同時に RAM 側にあるピクセル情報は不要になるので、それらをこの補助関数によって解放する。
サンプルでは
RawPixelImage_free(app, image);
glTexParameteri
OpenGL ES のテクスチャはいくつかのオプション項目があり、それらを変更することで描画時の見た目を変更できる。そのためのコマンド。
void glTexParameteri (GLenum target, GLenum pname, GLint param)
- target : テクスチャの利用方法(2D or CUBE_MAP)
- pname : 変更するパラメータ名
- param : 変更する値
現時点ではサンプルの記述を「おまじない」と受け入れる。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
sampler2D 型 uniform 変数の利用
テクスチャの読み込みが終わったら、テクスチャ付きポリゴンのレンダリングを行う。
レンダリングを行うのはプログラムオブジェクト(=シェーダ)であり、シェーダにはテクスチャにアクセスするための sampler2D 型 uniform 変数 が定義されている。
テクスチャを利用するためには、 テクスチャユニットの番号 を指定する必要がある。
テクスチャユニットは GPU がテクスチャへ直接アクセスするためのハードウェアであり、OpenGL ES は最大で 32 のテクスチャユニットを持つ。(実際には GPU ごとに固有)
「どのテクスチャユニットに目的のテクスチャがバインドされているか」をシェーダに伝えることで、シェーダからテクスチャへのアクセスが可能になる。 OpenGL ES がデフォルトで利用しているユニットは「0番ユニット」。
サンプルでは glUniform1i コマンドを利用し、シェーダへ「0番ユニットを利用する」ことを明示している。
// unif_textureへテクスチャを設定する
glUniform1i(extension->unif_texture, 0);
テクスチャ付きポリゴンのレンダリング
シェーダを記述する際、UV情報として attribute 変数 attr_uv を定義してあるので、これにアップロードするための頂点成分を作成する。
const GLfloat position[] = {
// v0(left top)
-0.75f, 0.75f,
// v1(left bottom)
-0.75f, -0.75f,
// v2(right top)
0.75f, 0.75f,
// v3(right bottom)
0.75f, -0.75f, };
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);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
頂点の座標はそれぞれ「左上」「左下」「右上」「右下」の順に用意されているので、UV座標もテクスチャの左上(0, 0)、左下(0, 1)、右上(1, 0)、右下(1, 1) にそれぞれセットしている。
glDeleteTextures
生成したテクスチャの解放を行う。
void glDeleteTextures (GLsizei n, const GLuint* textures)
- n : 解放するテクスチャ数
- textures : 解放するテクスチャの先頭ポインタ
サンプルでは、まずテクスチャ ID を0番(無効なテクスチャ)にバインドしてから、解放を行っている。
// テクスチャを廃棄する
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &extension->texture_id);
assert(glGetError() == GL_NO_ERROR);
解放前の Unbind
実際には、glDeleteTextures を呼び出した時点で解放対象のテクスチャはすべてのテクスチャユニットからアンバインドされ、0番の ID がセットされる。
シェーダの場合は「解放フラグ」が設定されるだけで解放そのものは行われなかったが、テクスチャは強制的にアンバインドされるという挙動の違いがある。