Androidアプリ開発 OpenGL テクスチャマッピング

ええと、イベントを拾えるようになったので、タップすることで色を変えるとか、加速度と連動するようにするとか、ピンチイン、アウトで拡大縮小するとか、いろいろ面白いことが考えられそう。
それは各自でやってもらうとして、OpenGLの機能の紹介を進めていく。基本的な3DCGの機能はだいたい消化してきたが、重要なものが残っていた。それは「テクスチャマッピング」。

テクスチャマッピングは、2Dでの画像データを3Dの「面に貼り付ける」といったテクニック。テクスチャっていうのは、日本語でいうと「質感」。3DCGが始まったばかりの頃は、アンビエントやスペキュラーで「プラスチックっぽい」、とか「金属っぽい」といった表現しかできなかった。なんとか、「もっとザラついた表面にしたい」とか、「模様を付けたい」といったことから発展してきたのが、テクスチャマッピング。
しくみ的にはそう難しくない。画像データというのはピクセル単位の色データでしかない。jpgとか、pngとか、いろいろ形式はあるものの、それは圧縮とか変換が行われているから。ディスプレイに表示される時には、ピクセル単位の色データに変換されている。いわゆる「ビットマップ」形式のデータになる。
で、ポリゴンの色をシェーディングで計算する際、オブジェクトの元々の色と法線、光源の色、位置が必要であった。元々の色は、ディフューズで指定してきたのだが、テクスチャマッピングするとビットマップの色に置き換えられて計算される。
概要はわかった。実際にはどうやるのか。
まず、画像データを読み込んでビットマップを作らなくてはならない。OpenGLには、画像データの読み込み機能はない。androidにはあるので、BitmapFactoryクラスを使って行うことにする。
何か適当な画像データを用意しないといけないのだが、Eclipseでandroidプロジェクトを作成すると、デフォルトのアイコン画像がresフォルダに作成される。これを使うことにする。androidでは、あまり大きな画像データを読み込むことはできない。また、OpenGL ESでは、正方形の画像データしかテクスチャマッピングの元画像として受け入れてくれない。
アイコン画像は、正方形だし、大きさも小さいので妥当であろう。
どこで、読み込めばよいか。onSurfaceCreatedでやっておくか。
BitmapFactoryのdecodeResourceで、リソースファイルからビットマップを作成することができるのだが、第一引数にリソースのインスタンスを指定しなければならない。リソースは、ContextのgetResources()で取得できる。コンストラクタにContextが渡ってくるので、contextフィールドで記憶しておくことにする。

public class SampleGLSurfaceView extends GLSurfaceView implements OnGestureListener {
private Model model = new Model();
private float angle = 0.0f;
private float eyepos[] = new float[3];
private Context context;
// サーフェースビューのコンストラクタ
public SampleGLSurfaceView(Context context) {
super(context);
this.context = context;
setEGLConfigChooser(8, 8, 8, 8, 16, 0);
renderer = new OpenGLRenderer();
setRenderer(renderer);
}

ビットマップの生成は、以下のようにすればよい。

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
gl.glClearDepthf(1.0f);
eyepos[0] = 0;
eyepos[1] = 0;
eyepos[2] = 3;
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher);
}

BitmapやBitmapFactoryがエラーになると思うが、適宜クイックフィックスでインポートを行って欲しい。
次に、読み込んだビットマップデータをGPUの方に転送しないといけない。のではあるが、その前にテクスチャオブジェクトを作成してやる必要がある。
glGenTexturesでテクスチャオブジェクトを作成して、glBindTextureでバインドする。
GLUtils.texImage2Dでビットマップを割り当ててやればビットマップのロードは完了。

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
gl.glClearDepthf(1.0f);
eyepos[0] = 0;
eyepos[1] = 0;
eyepos[2] = 3;
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher);
// テクスチャを生成
int textures[] = new int[1];
gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
}

今回は、テクスチャがひとつだけなので、glGenTexturesの第一引数を1としている。textures配列の要素数も1。glGenTexturesに成功すると、textures配列にIDが収められて戻ってくる。複数のテクスチャを扱いたい場合はこれらを修正する。
一度に処理できるテクスチャオブジェクトがひとつだけなので、テクスチャのIDとGL_TEXTURE_2Dをバインドして、なんやらかんやら設定を行う。バインドされているテクスチャに設定が行われると思っていればよい。
さらに、マッピングしないといけないので、ポリゴンと画像の位置関係を座標値で示してやらないといけない。頂点データと同じ感じで、バッファを作ってやる。

public class Model {
private FloatBuffer buffer;			// 頂点用バッファ
private FloatBuffer normalBuffer;	// 法線用バッファ
private FloatBuffer textureBuffer;	// テクスチャ用バッファ
public Model() {
float vertex[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f,  1.0f, 0.0f,
};
ByteBuffer vb = ByteBuffer.allocateDirect(vertex.length * 4);
vb.order(ByteOrder.nativeOrder());
buffer = vb.asFloatBuffer();
buffer.put(vertex);
buffer.position(0);
float normal[] = {
1.0f, 0.0f, 1.0f,
-1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 1.0f,
};
ByteBuffer nb = ByteBuffer.allocateDirect(normal.length * 4);
nb.order(ByteOrder.nativeOrder());
normalBuffer = nb.asFloatBuffer();
normalBuffer.put(normal);
normalBuffer.position(0);
float texture[] = {
0.0f, 1.0f,
1.0f, 1.0f,
0.5f, 0.0f,
};
ByteBuffer tb = ByteBuffer.allocateDirect(texture.length * 4);
tb.order(ByteOrder.nativeOrder());
textureBuffer = tb.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);
}
public void draw(GL10 gl) {
gl.glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buffer);
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
gl.glNormalPointer(GL10.GL_FLOAT, 0, normalBuffer);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
}
}

このとき、テクスチャマッピング用の座標は、2次元となる。各頂点がビットマップのどの点に対応するものなのかを指定していく。座標値としては、0,0から1,1の範囲になる。実際の画像データの大きさにはならないので注意。
作成したバッファは、glEnableClientState(GL_TEXTURE_COORD_ARRAY)で機能を有効にした上で、glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer)でお知らせしてやる。2Dでのマッピングなので、最初の引数が2となっている点に注意。
最後に、描画時にテクスチャマッピングすることを宣言して描画する。

@Override
public void onDrawFrame(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// ライティングをON
gl.glEnable(GL10.GL_LIGHTING);
// 光源を有効にして位置を設定
gl.glEnable(GL10.GL_LIGHT0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightpos, 0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, red, 0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, blue, 0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, yellow, 0);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
// カメラ位置を設定
GLU.gluLookAt(gl, eyepos[0], eyepos[1], eyepos[2], 0, 0, 0, 0, 1, 0);
gl.glPushMatrix();		// マトリックス記憶
gl.glTranslatef(1, 0, 0);
gl.glRotatef(angle, 0, 1, 0);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glCullFace(GL10.GL_BACK);
// デプステスト
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);
gl.glDepthMask(true);
// テクスチャ
gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, gray, 0);
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 80f);
gl.glShadeModel(GL10.GL_FLAT);
model.draw(gl);
gl.glPopMatrix();		// マトリックスを戻す
// ふたつめの描画
gl.glPushMatrix();		// マトリックス記憶
gl.glTranslatef(-1, 0, 0);
gl.glRotatef(angle, 0, 1, 0);
gl.glCullFace(GL10.GL_BACK);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, gray, 0);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, gray, 0);
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 80f);
// スムースシェーディング
gl.glShadeModel(GL10.GL_SMOOTH);
model.draw(gl);
gl.glPopMatrix();		// マトリックスを戻す
//angle += 0.5;			// 回転角度は最後に計算
}

ふー。さすがにテクスチャマッピングとなるとやることが増えるなぁ。
複数のテクスチャをロードしておいて、貼りつけるテクスチャを切り替えたい場合は、テクスチャIDをフィールドで覚えておき、glBindTexture(GL10.GL_TEXTURE_2D, テクスチャID)としてバインドを変更しながら描画すればOK。

三角形に貼り付けたのでドロイド君の頭が欠けてしまった。
AREarthroidでの地球もNASAの衛星画像をテクスチャマッピングして表示している。
雲もテクスチャマッピングである。
地球全体日本を中心.png

かんたんAndroidアプリ作成入門 (プログラミングの教科書)

かんたんAndroidアプリ作成入門 (プログラミングの教科書)

  • 作者: 朝井 淳
  • 出版社/メーカー: 技術評論社
  • 発売日: 2013/04/16
  • メディア: 単行本(ソフトカバー)

投稿者プロフィール

asai
asai
システムエンジニア
喋れる言語:日本語、C言語、SQL、JavaScript