OpenGL ES2 でテクスチャ 〜iOS版〜

久しぶりの投稿だわ。加えて久しぶりのプログラミングネタ、というより、なーんか苦労したんで忘れないようにメモ。
iOSプログラミングでOpenGL ES2 のテクスチャ関連を調べてみると意外と情報がまとまっていない。見つかったと思ったら、よくわからないのライブラリ使っていたり、コードが中途半端に抜けていたり、C++コードが混ざっていてそのままじゃ使えなかったりと、なんでこう使えないのが多いのかな?と思うほど。Android向けの方が結構まとまっている感がある。


iPhone 3GS からだったか、OpenGL ES2 が使えるようになったのは。困ったことにOpenGL ES1.xとは互換がないと言っていいほど違う。特に描画。ES2からシェーダなるものが導入され、これを使わないといけないらしい。ただ、シェーダもプログラミング言語で書かれるので、いろいろできそうではある。これは今後の課題かな。うまくいけば弾幕計算をGPGPUでやることも可能かと。シェーダの細かいことはおいといて、テクスチャうまくいったので、それを書いておく。シェーダはGLSLなる言語で書かれているらしい。


2DのゲームでOpenGLを使うとなると、作成した画像はテクスチャで貼ることになる。なので、テクスチャは結構肝なのだな。
さてさて、実際にコードを記していこう。
まず、Xcodeにて新規プロジェクトから・・・あれ?なんか画面かわってる・・・???あれ?作成されるテンプレートがES2オンリーになっている。ipod touch 2nd系のES1.xしか使えない端末(3Gもかな?)が切り捨てられたか。さすがApple
フルコードは作り直すとして、手順だけ書いておこう。


まずは、vertex shader。ここに、テクスチャに関する情報を設定する。といっても、座標とかはメインのプログラムから渡されてくるので、代入や演算処理だけになる。

attribute vec4 position;
attribute vec2 texcoord;
varying vec2 texcoordVarying;

void main()
{
    gl_Position = position;
    texcoordVarying = texcoord;
}

attribute で宣言したものはメインプログラムから渡ってくるもの、varying はfragment shaderに渡すもの、gl_PositionはOpenGL ES2ライブラリが内部的に持っている変数で、この変数に値を設定するとその場所に描画してくれる。今回は、描画位置positionと用意した画像に対するクリップ範囲texcoordをそのままセットする。


次にfragment shader。

precision mediump float;
varying vec2 texcoordVarying;
uniform sampler2D texture;

void main()
{
    gl_FragColor = texture2D(texture, texcoordVarying);
}        

precision で演算精度を決められるらしい。
varying で宣言されている texcoordVarying はvertex shaderから渡ってくる。
uniform で宣言された変数はメインプログラムとやり取りできる。なのでtextureもメインプログラムから渡ってくる。変数の型(vec2 とか vec4とか sampler2Dとか)は割愛。調べてね。
gl_FragColorは表示物の色用変数で、gl_Positionと同様に、OpenGLライブラリが内部的に持っている変数のようです。この変数に表示色を設定します。今回はテクスチャで画像表示が目的なので、texture2D()を使って、テクスチャの色をそのまま出すように設定しています。


いよいよ、メインのプログラムです。
まず変数定義しておきましょう。列挙型です。これは、シェーダの変数にアクセスする(値を渡す)ために必要です。あ、なくてもできますが、可読性が悪くなります。

// Uniform index.
enum {
    UNIFORM_TEXTURE,
    NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];

// Attribute index.
enum {
    ATTRIB_VERTEX,
    ATTRIB_TEXCOORD,
    NUM_ATTRIBUTES
};

シェーダでuniformで宣言された変数に対するものと、attributeで宣言された変数に対するものですね。
どうやら、メインプログラムからシェーダ変数へはIDのような数値でアクセするようで、しかもそのIDは宣言順に0, 1, 2・・・と割り当てられているようですね。


次に初期化。OpenGLの初期化のところに(ビューの初期化時でも良いと思う)、以下を追加します。

glEnableVertexAttribArray(ATTRIB_VERTEX);
glEnableVertexAttribArray(ATTRIB_TEXCOORD);
glEnable(GL_TEXTURE_2D);

mTexture = [self loadTexture:@"back.png"];
    
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

vertexとtexcoordを使えるようにglEnableVertexAttribArray()をしておきます。テクスチャを使うので、ATTRIB_TEXCOORDを忘れないように。また、テクスチャを使うので、glEnable()でGL_TEXTURE_2Dを使用可能にしておきます。
次にテクスチャを作成して(loadTextureは自分で用意する。これはOpenGL ES1.xでも同じ)、ブレンドするので、ブレンドの設定をしておきます。今回は余意味がありませんが。。。


次は、シェーダのロード。テクスチャを使うためにシェーダないで使用するattributeの変数を変更しているので、メインプログラムに対応関係を教える必要があります。今回、VERTEX<->position、TEXCOORD<->texcoordなので、lglLinkProgram()の前あたりに

glBindAttribLocation(program, ATTRIB_VERTEX, "position");
glBindAttribLocation(program, ATTRIB_TEXCOORD, "texcoord" );

をしておきます。

最後に、実際の描画です。
以前のテンプレートだと drawFrameメソッドの中身・・・ですが、まぁ、適宜合わせてください。
vertex、texcoordの座標を設定してそれらをシェーダに渡します。
とりあえずサンプルなので決めうちで設定してシェーダに渡します。

float vertexs[] = {
    -1.0f, -1.0f, 0.0f,  //left top
    -1.0f,  1.0f, 0.0f,   //left bottom
      1.0f, -1.0f, 0.0f,   //right top
      1.0f,  1.0f, 0.0f,   //right bottom
};

float texcoords[] = {
    0.0f, 0.0f,//left top
    0.0f, 1.0f,//left bottom
    1.0f, 0.0f,//right top
    1.0f, 1.0f,//right bottom
};

glUseProgram(program);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mTexture);
glUniform1i(uniforms[UNIFORM_TEXTURE], 0);

glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, false, 0, texcoords);                
glVertexAttribPointer(ATTRIB_VERTEX, 3, GL_FLOAT, false, 0, vertexs);

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

テクスチャは複数用意できるのでglActiveTexture()でアクティブにするテクスチャを選択できます。今回は1つなのでテクスチャ0をアクティブに。
glBindTexture()で作成したテクスチャを渡します。
glUniform1i()でシェーダにuniformで宣言した変数に値を渡します。今回はtextureの値ですね。
glVertexAttribPointer()でattributeで宣言した変数に値を渡します。vertexとtexcoordですね。
最後に glDrawArrays()で描画となります。


ところで、今回は1画像を貼付けただけなので、これで良いのですが、弾幕シューティングのようにたくさんの弾が出てきたらどうするんでしょう。弾の種類分テクスチャを用意してテクスチャを切り替えて弾の数だけ glDrawArray()して表示・・・なんてやってたら、重くてゲームになりません。最近の端末なら多少いけるかもしれませんが、少なくとも ipod touch 2ndでは全然だめでした。そもそもglDrawArrays()は描画処理なので結構重いです。なので、これを減らす工夫が必要になります。glDrawArrays()を1回にする方法がありますが、それはまた、まとまってからということで。ES1.1のがそのままいければすぐにまとまると思いますが。。。
今回はここまで。


あー、そうそう、Androidも同じような感じでできるので、できたらメモるかも。