OpenGL ES 2.0のテクスチャ設定
AndroidのOpenGL ES 2.0で図形を表示できるようになったので、続いてテクスチャを貼ってみます。
OpenGL ES2.0のテクスチャ処理
OpenGL ES 2.0では、「画面に出力するピクセルの色」を決定するのはフラグメントシェーダーのプログラム(GLSL ESのコード)でした。テクスチャを使用する場合は、このフラグメントシェーダーで「ピクセルに対応するテクスチャ画素(テクセル)の色情報」を取得し、任意の処理(そのまま、あるいは合成・加工)を行って最終的なピクセルの色に反映させることになります。
流れとしては
- テクスチャ用の画像(Bitmap)を作成
- OpenGL ESのテクスチャを作成し画像を転送
- 描画する図形の頂点データを作成
- 図形に貼り付けるテクスチャの座標データを作成
- 図形の頂点データとテクスチャの座標データをOpenGL ESに設定
- glDrawArrays()で図形を描画
- シェーダーコードでテクスチャのテクセルを処理し出力
といった感じになります。
画像とOpenGL ES 2.0のテクスチャを作成する
まず、テクスチャ用の画像として32*32ピクセルのBitmapを作成することにしましょう。以下のように、適当なグラデーションを描画しておきます。
ビットマップのピクセルが画面上にどのような形で反映されているか確認しやすいように、内側がx/y方向、外側がy方向に変化していく(大きな色差の「境界」を持つ)グラデーション画像にしてみました。
// テクスチャ用Bitmap作成
Bitmap bmp = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
// テクスチャ用の画像を描画
for (int y = 0;y < 32;y++) {
for (int x = 0;x < 32;x++) {
if (x == 0 || x == 31 || y == 0 || y ==31) {
bmp.setPixel(x, y, Color.argb(255, 0, y * 8, 255));
} else {
bmp.setPixel(x, y, Color.argb(255, 255 - x * 8, y * 4, 0));
}
}
}
作成したBitmapをOpenGL ES(シェーダーコード)で利用するには、glGenTextures()でテクスチャを作成して画像を設定(OpenGL側に転送)する必要があります。
OpenGL ES 2.0では複数のテクスチャを使えますが、とりあえず今回は一つ(0番)のテクスチャを作成しそこに画像を転送してみました。
// テクスチャ0を有効化
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
int[] textures = new int[1];
// テクスチャ作成しidをtextures[0]に保存
GLES20.glGenTextures(1, textures, 0);
// テクスチャ0にtextures[0]をバインド
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
// 縮小時の補間設定
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
// 拡大時の補間設定
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
// bmpをテクスチャ0に設定
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bmp, 0);
これでOpenGL ES側にテクスチャが作成されたので、GLSL ESのuniform変数にテクスチャ番号を保存し、図形やテクスチャ関連の座標データをバッファとして作成します。
テクスチャ座標と図形の頂点座標をバッファに保存
テクスチャ座標は、対応させる図形の頂点順にテクスチャ上の座標を(0, 0)-(1, 1)の範囲で設定します。OpenGLの座標系が-1から1の範囲なのに対し、テクスチャの座標は0から1の範囲になっているので注意が必要ですね。
テクスチャの座標系もOpenGLの座標系と同じくBitmapとは「上下逆」になる点に注意しましょう。
// gl変数texture0に0を設定
GLES20.glUniform1i(_gl_vars.get("texture0"), 0);
// 「全体」を覆うテクスチャ座標配列を作成
float[] texture_uvs = {0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f};
ByteBuffer tex_bb = ByteBuffer.allocateDirect(texture_uvs.length * 4);
tex_bb.order(ByteOrder.nativeOrder());
_texture_buffer = tex_bb.asFloatBuffer();
_texture_buffer.put(texture_uvs);
_texture_buffer.position(0);
// (-0,9, -0.9)-(0.9, 0.9)の長方形頂点データ作成
float[] vertex_array = {-0.9f, -0.9f, -0.9f, 0.9f, 0.9f, -0.9f, 0.9f, 0.9f};
// 頂点データをバッファに格納
ByteBuffer vt_bb = ByteBuffer.allocateDirect(vertex_array.length * 4);
vt_bb.order(ByteOrder.nativeOrder());
_vertex_buffer = vt_bb.asFloatBuffer();
_vertex_buffer.put(vertex_array);
_vertex_buffer.position(0);
以上で、OpenGL ES(シェーダーコード)で使用する各種バッファやテクスチャができました。今回は、(-0.9, -0.9)-(0.9, 0.9)の範囲、つまり画面上の縦横それぞれ9割の領域に長方形を描き、その図形全面にテクスチャを貼るわけですね。
バーテックスシェーダーとフラグメントシェーダーのコード
テクスチャ関連の処理はほとんどOpenGL ESが自動的にやってくれるので、GLSL ESのコードとしては単純に図形を描くのと大差ありません。
バーテックスシェーダー
バーテックスシェーダーでは、図形の座標(A_position)とテクスチャの座標(A_texture_uv)をフラグメントシェーダーに(出力位置に応じて)渡すだけです。
attribute vec2 A_position;
attribute vec2 A_texture_uv;
varying vec2 V_texture_uv;
void main(void){
gl_Position = vec4(A_position, 0.0, 1.0);
V_texture_uv = A_texture_uv;
}
フラグメントシェーダー
バーテックスシェーダーから渡されたテクスチャ座標の位置にあるテクセルをtexture2D()で取得し、gl_Positionの位置にそのまま出力します。
precision mediump float;
uniform sampler2D texture0;
varying vec2 V_texture_uv;
void main() {
gl_FragColor = texture2D(texture0, V_texture_uv);
}
画面に描画する
描画を行うonDrawFrame()では、最初に全面を描画回数に応じてグレースケールでクリアしてから図形を描画します。
実行してみると、画面の大半をテクスチャが貼られた長方形が覆い、画面端に見える背景がゆっくりと黒→白→黒・・・と変化していきます。
実行すると左側の画像のように境界部分がぼけて表示されますが、これはテクスチャの拡大補完を以下のように周辺ピクセルで補完する「GL_LINEAR」にしているためです。
// 拡大時の補間設定
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
以下のように最も近いピクセルを取得する「GL_NEAREST」にすると、低解像度のテクスチャを引き延ばして表示する場合もテクスチャ画像のドットがそのまま表示されるようになります。
// 拡大時の補間設定
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
プログラムソース(Activity)
プログラムソース(GLSurfaceView)