前提
『マルチプラットフォームのためのOpenGL ES入門 基礎編―Android/iOS対応グラフィックスプログラミング』
の抜粋メモです。8章の内容。
コードについては断片のみなので、本書を読んでください。
シェーダの基礎
塗りつぶしの色を変更する
const GLchar *fragment_shader_source =
"void main() {"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
"}";
※ 「1.0f」とは書けないので注意
これを、「任意の色で塗る」シェーダに変更する。
typedef struct {
// レンダリング用シェーダープログラム
GLint shader_program;
// 位置情報属性
GLint attr_pos;
// フラグメントシェーダの描画色
GLint unif_color;
} Extension_ShaderUniformColor;
/**
* アプリの初期化を行う
*/
void sample_ShaderUniformColor_initialize(GLApplication *app) {
// サンプルアプリ用のメモリを確保する
app->extension = (Extension_ShaderUniformColor*) malloc(sizeof(Extension_ShaderUniformColor));
// サンプルアプリ用データを取り出す
Extension_ShaderUniformColor *extension = (Extension_ShaderUniformColor*) app->extension;
// 頂点シェーダーを用意する
{
const GLchar *vertex_shader_source =
"attribute mediump vec4 attr_pos;"
"void main() {"
" gl_Position = attr_pos;"
"}";
const GLchar *fragment_shader_source =
"uniform lowp vec4 unif_color;"
"void main() {"
" gl_FragColor = unif_color;"
"}";
// コンパイルとリンクを行う
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);
}
// uniform変数のlocationを取得する
{
extension->unif_color = glGetUniformLocation(extension->shader_program, "unif_color");
assert(extension->unif_color >= 0);
}
// シェーダーの利用を開始する
glUseProgram(extension->shader_program);
assert(glGetError() == GL_NO_ERROR);
// シェーダーで使用可能なベクトル数を取得する
{
GLint vert_vectors = 0;
GLint frag_vectors = 0;
glGetIntegerv(GL_MAX_VERTEX_UNIFORM_VECTORS, &vert_vectors);
glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_VECTORS, &frag_vectors);
__logf("Max Uniform Vectors / Vert(%d) Frag(%d)", vert_vectors, frag_vectors);
}
}
/**
* レンダリングエリアが変更された
*/
void sample_ShaderUniformColor_resized(GLApplication *app) {
// 描画領域を設定する
glViewport(0, 0, app->surface_width, app->surface_height);
}
/**
* アプリのレンダリングを行う
* 毎秒60回前後呼び出される。
*/
void sample_ShaderUniformColor_rendering(GLApplication *app) {
// サンプルアプリ用データを取り出す
Extension_ShaderUniformColor *extension = (Extension_ShaderUniformColor*) 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[] = {
// v0(left top)
-0.75f, 0.75f,
// v1(left bottom)
-0.75f, 0.25f,
// v2(right top)
-0.25f, 0.75f,
// v3(right bottom)
-0.25f, 0.25f, };
// 描画色を指定する
{
// R, G, B, A
glUniform4f(extension->unif_color, 1.0f, 0.0f, 1.0f, 1.0f);
}
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// 右上へ四角形描画
{
const GLfloat position[] = {
// v4(left top)
0.25f, 0.75f,
// v5(left bottom)
0.25f, 0.25f,
// v6(right top)
0.75f, 0.75f,
// v7(right bottom)
0.75f, 0.25f, };
// 描画色を配列指定する
{
const GLfloat poly_color[] = {
// R
1.0f,
// G
1.0f,
// B
0.0f,
// A
1.0f, };
glUniform4fv(extension->unif_color, 1, poly_color);
}
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
}
GLSL文法:uniform キーワード
attribute変数は「頂点から情報を受け取る」方法でシェーダに情報を伝えていたが、フラグメントシェーダは attribute 変数を受け取ることはできない。
フラグメントシェーダは、 uniform変数 を利用することで、アプリから情報を受け取ることができる。
const GLchar *fragment_shader_source =
"uniform lowp vec4 unif_color;"
"void main() {"
" gl_FragColor = unif_color;"
"}";
attribute変数が「頂点ごとの情報」であるのに対し、uniform変数は アプリが設定した値から一切変更されない という特徴がある。
ピクセル色は低精度でもモバイルディスプレイの色情報を表現できるので、lowp で宣言している。
glGetUniformLocation
uniform変数のアクセスには、attributeと同じく Location を取得する必要がある。
GLint glGetUniformLocation(GLuint program, const GLchar* name)
- program : location を取得するプログラムオブジェクト
- name : location を取得する uniform 変数名
正常に取得できたら 0 以上、失敗した場合は -1 が返る。
glUniform4f
取得した location を利用して uniform 変数を書き換える方法は複数提供されている。ここでは、GLfloat型のデータを送信(アップロード)するコマンドを示す。
void glUniform4f(GLint location, GLfloat x, GLfloat y, GLfloat z, GLfloat w)
GLfloat型変数を vec4 型 uniform 変数へアップロードする。アップロード対象は、最後に glUseProgram コマンドによって使用中となっているプログラムオブジェクトになる。
xyzw それぞれの引数は、RGBA の色情報として扱われる。
glUniform4fv
配列を介してXYZWの情報をアップロードする。vec4 の配列として定義したuniform変数へのアップロードにも利用できる。
void glUniform4fv(GLint location, GLsizei count, const GLfloat* v)
- location : アップロード対象の uniform 変数 location
- count : アップロードするベクトル数、vの配列長は count * 4 以上が必要
- v : アップロードするベクトル配列
シェーダスペックについて
定義できる uniform 変数の数は端末ごとに限界がある。
glGetIntegerv コマンドに GL_MAX_VERTEX_UNIFORM_VECTORS, GL_MAX_FRAGMENT_UNIFORM_VECTORS を指定することで、それぞれの uniform ベクトル個数が取得できる。
GLint vert_vectors = 0;
GLint frag_vectors = 0;
glGetIntegerv(GL_MAX_VERTEX_UNIFORM_VECTORS, &vert_vectors);
glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_VECTORS, &frag_vectors);
たとえば Nexus5 は頂点シェーダで 256 個、フラグメントシェーダで 224 個といった制限がある。
あくまでこの数字は「vec4型で換算して」という値であり、例えば mat4 という 4 x 4の行列であれば消費量は 4倍になる。
GPU はベクトル同士の演算に特化しているので、基本となる vec4 単位で管理がされている。
頂点を動かす
uniform 変数は頂点シェーダにも渡せる。
例えば「四角形を描画ごとに移動させる」という処理を行う場合、glVertexAttribPointer() での操作では、毎度毎度「全頂点位置に加減算処理」を行い OpenGL への転送を行う必要がある。
CPU では操作記述は簡単でも、速度的に優れているとは言えない。GPU はそのような単純作業、「ここの独立性が高く、並列化が可能な処理」を高速に処理するための機構を多く備えている。
以下では、頂点シェーダに「移動情報」を渡して移動アニメーションを行う。
void sample_ShaderUniformPos_initialize(GLApplication *app) {
// サンプルアプリ用のメモリを確保する
app->extension = (Extension_ShaderUniformPos*) malloc(sizeof(Extension_ShaderUniformPos));
// サンプルアプリ用データを取り出す
Extension_ShaderUniformPos *extension = (Extension_ShaderUniformPos*) app->extension;
// 頂点シェーダーを用意する
{
const GLchar *vertex_shader_source = "attribute mediump vec4 attr_pos;"
// プログラムから設定する変数
// この変数の値だけ、頂点位置が動く
"uniform mediump vec2 unif_move_pos;"
"void main() {"
" gl_Position = attr_pos;"
// unif_move_posの値を更に加算する
" gl_Position.xy += unif_move_pos;"
"}";
const GLchar *fragment_shader_source = "void main() {"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);"
"}";
// コンパイルとリンクを行う
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);
}
// TODO 解説
// uniform変数のlocationを取得する
{
extension->unif_move_pos = glGetUniformLocation(extension->shader_program, "unif_move_pos");
assert(extension->unif_move_pos >= 0);
}
// シェーダーの利用を開始する
glUseProgram(extension->shader_program);
assert(glGetError() == GL_NO_ERROR);
{
// XY座標初期化
extension->pos.x = 0;
extension->pos.y = 0;
}
}
/**
* レンダリングエリアが変更された
*/
void sample_ShaderUniformPos_resized(GLApplication *app) {
// 描画領域を設定する
glViewport(0, 0, app->surface_width, app->surface_height);
}
/**
* アプリのレンダリングを行う
* 毎秒60回前後呼び出される。
*/
void sample_ShaderUniformPos_rendering(GLApplication *app) {
// サンプルアプリ用データを取り出す
Extension_ShaderUniformPos *extension = (Extension_ShaderUniformPos*) 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[] = {
// v4(left top)
0.0, 0.5f,
// v5(left bottom)
0.0f, 0.0f,
// v6(right top)
0.5f, 0.5f,
// v7(right bottom)
0.5f, 0.0f, };
// TODO 解説
// 頂点の設定は一度だけでいい
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
{
// XYの位置に適当に加算を行う
extension->pos.x += 0.01f;
extension->pos.y += 0.02f;
// 一定を超えたら値をリセットする
if (extension->pos.x > 1.0f) {
extension->pos.x = -1;
}
if (extension->pos.y > 1.0f) {
extension->pos.y = -1;
}
// uniformへXYの位置を転送する
glUniform2f(extension->unif_move_pos, extension->pos.x, extension->pos.y);
// glUniform2fv(extension->unif_move_pos, 1, (GLfloat*) &extension->pos);
}
// 四角形の描画を行う
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
}
GLSLの変数アクセス構文
上記の頂点シェーダは、
const GLchar *vertex_shader_source = "attribute mediump vec4 attr_pos;"
// プログラムから設定する変数
// この変数の値だけ、頂点位置が動く
"uniform mediump vec2 unif_move_pos;"
"void main() {"
" gl_Position = attr_pos;"
// unif_move_posの値を更に加算する
" gl_Position.xy += unif_move_pos;"
"}";
のようになっている。main の最後の文
"gl_Position.xy += unif_move_pos;"
では、gl_Position の X要素とY要素への加算を行っている。
GLSL ES では、ベクトル演算が多いという特性を反映し、XY要素(ベクトル内の特定要素)に対して同時に代入を行うような構文が許されている。C言語的に書くと、
gl_Position.x += unif_move_pos.x
gl_Position.y += unif_move_pos.y
と同一。
左辺が gl_Position(vec4) で、右辺が unif_move_pos(vec2) だが、gl_Position.xy と書くことで、vec2 として代入が可能になる。
gl_Position.xyz とすれば3次元だし、gl_Position.zy のように順番を入れ替えることも可能。
glUniform2f, glUniform2fv
vec2型 uniform 変数へのアップロードはこれらのコマンドで行う。考え方は色情報を 4f, 4fv で与えたときと同じ。
void glUniform2f(GLint location, GLfloat x, GLfloat y)
void glUniform2fv(GLint location, GLsizei count, const GLfloat* v)
頂点に位置情報以外の情報を付与する
OpenGLの頂点データは、「位置情報」以外にも「法線」「UV」「色」という情報を持ち、それらを 頂点の成分 と呼ぶ。また、OpenGLは頂点ごとの値を 補完 して出力するための機構を備えている(赤の頂点と青の頂点の間のピクセルをグラデーションさせるように)。
以下は3頂点に「色」情報を持たせるときのシェーダのサンプル
void sample_VertexColor_initialize(GLApplication *app) {
// サンプルアプリ用のメモリを確保する
app->extension = (Extension_VertexColor*) malloc(sizeof(Extension_VertexColor));
// サンプルアプリ用データを取り出す
Extension_VertexColor *extension = (Extension_VertexColor*) app->extension;
// 頂点シェーダーを用意する
{
const GLchar *vertex_shader_source =
//
"attribute mediump vec4 attr_pos;"
// 頂点カラー
"attribute lowp vec4 attr_color;"
"varying lowp vec4 vary_color;"
"void main() {"
" gl_Position = attr_pos;"
// 頂点カラー出力
" vary_color = attr_color;"
"}";
const GLchar *fragment_shader_source =
//
"varying lowp vec4 vary_color;"
"void main() {"
" gl_FragColor = vary_color;"
"}";
// コンパイルとリンクを行う
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_color = glGetAttribLocation(extension->shader_program, "attr_color");
assert(extension->attr_color >= 0);
}
// シェーダーの利用を開始する
glUseProgram(extension->shader_program);
assert(glGetError() == GL_NO_ERROR);
// シェーダーで使用可能なベクトル数を取得する
{
GLint vary_vectors = 0;
glGetIntegerv(GL_MAX_VARYING_VECTORS, &vary_vectors);
__logf("Max Varying Vectors / %d", vary_vectors);
}
}
void sample_VertexColor_rendering(GLApplication *app) {
// サンプルアプリ用データを取り出す
Extension_VertexColor *extension = (Extension_VertexColor*) app->extension;
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// attribute変数を有効にする
glEnableVertexAttribArray(extension->attr_pos);
glEnableVertexAttribArray(extension->attr_color);
// 画面中央へ描画する
const GLfloat position[] = {
// v0
0.0f, 1.0f,
// v1
1.0f, -1.0f,
// v2
-1.0f, -1.0f };
// 頂点カラーを設定する
const GLubyte color[] = {
// v0 rgb
255, 0, 0,
// v1 rgb
0, 255, 0,
// v2 rgb
0, 0, 255,
};
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glVertexAttribPointer(extension->attr_color, 3, GL_UNSIGNED_BYTE, GL_TRUE, 0, color);
glDrawArrays(GL_TRIANGLES, 0, 3);
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
}
GLSL文法:varying キーワード
上記の2つのシェーダは共に下記の変数を定義している。
"varying lowp vec4 vary_color;"
varying 変数は、頂点シェーダからフラグメントシェーダへ値を渡す際に用いられる。頂点シェーダが書き込んだ varying 変数はある特定の処理を施されてフラグメントシェーダに渡される。療法で同じ型・同じ変数名で定義しなければならない。
gl_Position などの組み込み変数は特定用途でしか利用できないが、varying変数は汎用的に値を受け渡すことができる。
varyingという名の通り、渡される際に値が変化する。
varying 変数の制限
varying変数は定義できる変数の個数が非常に少ない。
Nexus5 で、16 とか。glGetIntegerv(GL_MAX_VARYING_VECTORS, &num) で取得できる。超えたらコンパイル失敗。
頂点に複数の情報を与える
サンプルの描画部分を見ると、頂点座標と頂点カラーの2つの情報を設定している。(それぞれの型は同一である必要はない)
とくに attribute 変数の enable を忘れがちなので注意
最後に、glVertexAttribPointer で複数の頂点情報= 頂点ストリーム を集約し、attribute変数として頂点シェーダに渡す。
このとき、第4引数(normalized)に注目すると、位置は GL_FALSE, カラーは GL_TRUE となっている。これは、color 配列が 0〜255 で用意されているため、正規化が必要だから。
正規化の利用シーンは「0.0〜1.0(-1.0〜1.0)」を入力したいけど、GLfloat ほどの分解能を必要としない」という場合。色成分はその典型(1byteで十分)。