Android で OpenGL ES 2.0 事始め 〜Beginning〜

夏休みでちょっと時間もできたし、せっかくなので AndroidOpenGL ES 2.0 にトライしてみました。
以下の Beginning サンプルでは超入門として ApiDemos よりシンプルなサンプルを目指してみました。
なので、ビットマップを読み込んでテクスチャとしてそのまま表示するだけの物となっています。
ソースは

  • AndroidManifest.xml
  • Activity
  • Renderer
  • OpenGL ES 2.0 用の簡易ユーティリティ

だけです。
エミュレータ(Android OS 4.0.3 以上)で実行する場合は、デバイスのハードウェアプロパティで GPU emulation を yes にして下さい。

AndroidManifest.xml

Android Developers の OpenGL に記載されてるままですが、OpenGL ES 2.0 を使用するので最低の API レベル に 8 を指定しています。そして uses-feature で OpenGL ES 2.0 を使用可能な端末に制限します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.orangesignal.android.example.gles20"
	android:versionCode="1"
	android:versionName="1.0"
	android:installLocation="auto">

	<!-- OpenGL ES 2.0 を使用するので API レベル 8 以上とします。 -->
	<uses-sdk android:minSdkVersion="8" />

	<!-- Google プレイ (旧 Android マーケット) からダウンロード可能な端末を OpenGL ES 2.0 を使用可能な端末に制限します。 -->
	<uses-feature android:glEsVersion="0x00020000" />

	<application
		android:label="@string/app_name"
		android:icon="@drawable/ic_launcher">

		<activity android:name=".ui.MainActivity">
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>

	</application>

</manifest>

Activity

ApiDemos より短いです。ApiDemos の GLES20Activity#detectOpenGLES20 はエミュレータでは正しい値が帰ってこないので、OpenGL ES 2.0 しかサポートしない場合は、setEGLContextClientVersion(2) として例外をキャッチしてごにょごにょする方が良いのではないかと。

/*
 * Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
 */

package com.orangesignal.android.example.gles20.ui;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;

import com.orangesignal.android.example.gles20.gl.SimpleRenderer;

/**
 * このアプリケーションの主たるアクティビティを提供します。
 * 
 * @author 杉澤 浩二
 */
public final class MainActivity extends Activity {

	private GLSurfaceView mGLSurfaceView;

	//////////////////////////////////////////////////////////////////////////
	// ライフサイクル

	@Override
	public void onCreate(final Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		mGLSurfaceView = new GLSurfaceView(this);
		mGLSurfaceView.setEGLContextClientVersion(2);	// OpenGL ES 2.0 を使用するように構成します。
		mGLSurfaceView.setRenderer(new SimpleRenderer(getApplicationContext()));
		setContentView(mGLSurfaceView);

	}

	@Override
	public void onResume() {
		super.onResume();
		mGLSurfaceView.onResume();
	}

	@Override
	public void onPause() {
		super.onPause();
		mGLSurfaceView.onPause();
	}

}

GLSurfaceView.Renderer

このサンプルの肝心なところですね。ソースにいっぱいコメント書いてみたのでご覧ください。あとしつこいくらいにエラーチェックも入れてみました。
サンプル中の R.drawable.sample は res/drawable-nodpi に何か適当な表示させたい画像を置いて下さい。

/*
 * Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
 */

package com.orangesignal.android.example.gles20.gl;

import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;

import com.orangesignal.android.example.gles20.R;
import com.orangesignal.android.opengl.GLES20Utils;

/**
 * ビットマップをテクスチャとして読み込んで {@link GLSurfaceView} の描画領域いっぱいに描画するだけのシンプルな {@link GLSurfaceView.Renderer} を提供します。
 * 
 * @author 杉澤 浩二
 */
public final class SimpleRenderer implements GLSurfaceView.Renderer {

	/**
	 * コンテキストを保持します。
	 */
	private final Context mContext;

	/**
	 * 頂点データです。
	 */
	private static final float VERTEXS[] = {
		-1.0f,  1.0f, 0.0f,	// 左上
		-1.0f, -1.0f, 0.0f,	// 左下
		 1.0f,  1.0f, 0.0f,	// 右上
		 1.0f, -1.0f, 0.0f	// 右下
	};

	/**
	 * テクスチャ (UV マッピング) データです。
	 */
	private static final float TEXCOORDS[] = {
		0.0f, 0.0f,	// 左上
		0.0f, 1.0f,	// 左下
		1.0f, 0.0f,	// 右上
		1.0f, 1.0f	// 右下
	};

	/**
	 * 頂点バッファを保持します。
	 */
	private final FloatBuffer mVertexBuffer   = GLES20Utils.createBuffer(VERTEXS);

	/**
	 * テクスチャ (UV マッピング) バッファを保持します。
	 */
	private final FloatBuffer mTexcoordBuffer = GLES20Utils.createBuffer(TEXCOORDS);

	private int mProgram;
	private int mPosition;
	private int mTexcoord;
	private int mTexture;

	private int mTextureId;

	//////////////////////////////////////////////////////////////////////////
	// コンストラクタ

	/**
	 * コンストラクタです。
	 * 
	 * @param context コンテキスト
	 */
	public SimpleRenderer(final Context context) {
		mContext = context;
	}

	//////////////////////////////////////////////////////////////////////////
	// オーバーライド メソッド

	/**
	 * ポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコード
	 */
	private static final String VERTEX_SHADER =
		"attribute vec4 position;" +
		"attribute vec2 texcoord;" +
		"varying vec2 texcoordVarying;" +
		"void main() {" +
			"gl_Position = position;" +
			"texcoordVarying = texcoord;" +
		"}";

	/**
	 * 色描画用のピクセル/フラグメントシェーダのソースコード
	 */
	private static final String FRAGMENT_SHADER =
		"precision mediump float;" +
		"varying vec2 texcoordVarying;" +
		"uniform sampler2D texture;" +
		"void main() {" +
			"gl_FragColor = texture2D(texture, texcoordVarying);" +
		"}";

	@Override
	public void onSurfaceCreated(final GL10 gl, final EGLConfig config) {
		// OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。

		// プログラムを生成して使用可能にします。
		mProgram = GLES20Utils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
		if (mProgram == 0) {
			throw new IllegalStateException();
		}
		GLES20.glUseProgram(mProgram);
		GLES20Utils.checkGlError("glUseProgram");

		// シェーダで使用する変数のハンドルを取得し使用可能にします。

		mPosition = GLES20.glGetAttribLocation(mProgram, "position");
		GLES20Utils.checkGlError("glGetAttribLocation position");
		if (mPosition == -1) {
			throw new IllegalStateException("Could not get attrib location for position");
		}
		GLES20.glEnableVertexAttribArray(mPosition);

		mTexcoord = GLES20.glGetAttribLocation(mProgram, "texcoord");
		GLES20Utils.checkGlError("glGetAttribLocation texcoord");
		if (mPosition == -1) {
			throw new IllegalStateException("Could not get attrib location for texcoord");
		}
		GLES20.glEnableVertexAttribArray(mTexcoord);

		mTexture = GLES20.glGetUniformLocation(mProgram, "texture");
		GLES20Utils.checkGlError("glGetUniformLocation texture");
		if (mTexture == -1) {
			throw new IllegalStateException("Could not get uniform location for texture");
		}

		// テクスチャを作成します。(サーフェスが作成される度にこれを行う必要があります)
		final Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.sample);
		mTextureId = GLES20Utils.loadTexture(bitmap);
		bitmap.recycle();
	}

	@Override
	public void onSurfaceChanged(final GL10 gl, final int width, final int height) {
		// OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。

		// ビューポートを設定します。
		GLES20.glViewport(0, 0, width, height);
		GLES20Utils.checkGlError("glViewport");
	}

	@Override
	public void onDrawFrame(final GL10 gl) {
		// OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。

		// XXX - このサンプルではテクスチャの簡単な描画だけなので深さ関連の有効/無効や指定は一切していません。

		// 背景色を指定して背景を描画します。
		GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

		// 背景とのブレンド方法を設定します。
		GLES20.glEnable(GLES20.GL_TEXTURE_2D);
		GLES20.glEnable(GLES20.GL_BLEND);
		GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);	// 単純なアルファブレンド

		// テクスチャの指定
		GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
		GLES20.glUniform1i(mTexture, 0);
		GLES20.glVertexAttribPointer(mTexcoord, 2, GLES20.GL_FLOAT, false, 0, mTexcoordBuffer);
		GLES20.glVertexAttribPointer(mPosition, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

		GLES20.glDisable(GLES20.GL_BLEND);
		GLES20.glDisable(GLES20.GL_TEXTURE_2D);
	}

}

OpenGL ES 2.0 簡易ユーティリティ

/*
 * Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
 */

package com.orangesignal.android.opengl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLException;
import android.opengl.GLUtils;
import android.util.Log;

/**
 * OpenGL ES 2.0 に関するユーティリティを提供します。
 * 
 * @author 杉澤 浩二
 */
public final class GLES20Utils {

	/**
	 * ログ出力用のタグです。
	 */
	private static String TAG = "GLES20Utils";

	/**
	 * オブジェクトが無効であることを表します。<p>
	 * 
	 * @see {@link #createProgram(String, String)}
	 * @see {@link #loadShader(int, String)}
	 */
	public static final int INVALID = 0;

	/**
	 * インスタンス化できない事を強制します。
	 */
	private GLES20Utils() {}

	/**
	 * 最初の要素の位置です。
	 */
	private static final int FIRST_INDEX = 0;

	private static final int DEFAULT_OFFSET = 0;

	private static final int FLOAT_SIZE_BYTES = 4;

	/**
	 * 指定されたプリミティブ型配列のデータを {@link FloatBuffer} へ変換して返します。
	 * 
	 * @param array バッファデータ
	 * @return 変換されたバッファデータ
	 * @see {@link GLES20#glVertexAttribPointer(int, int, int, boolean, int, java.nio.Buffer)}
	 */
	public static FloatBuffer createBuffer(float[] array) {
		final FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
		buffer.put(array).position(FIRST_INDEX);
		return buffer;
	}

	/**
	 * 指定されたバーテックスシェーダとフラグメントシェーダを使用してプログラムを生成します。
	 * 
	 * @param vertexSource ポリゴン描画用バーテックスシェーダのソースコード
	 * @param fragmentSource 色描画用のフラグメントシェーダのソースコード
	 * @return プログラムハンドラまたは {@link #INVALID}
	 * @throws GLException OpenGL API の操作に失敗した場合
	 */
	public static int createProgram(final String vertexSource, final String fragmentSource) throws GLException {
		// バーテックスシェーダをコンパイルします。
		final int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
		if (vertexShader == INVALID) {
			return INVALID;
		}

		// フラグメントシェーダをコンパイルします。
		final int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
		if (pixelShader == INVALID) {
			return INVALID;
		}

		// プログラムを生成して、プログラムへバーテックスシェーダとフラグメントシェーダを関連付けます。
		int program = GLES20.glCreateProgram();
		if (program != INVALID) {
			// プログラムへバーテックスシェーダを関連付けます。
			GLES20.glAttachShader(program, vertexShader);
			checkGlError("glAttachShader");
			// プログラムへフラグメントシェーダを関連付けます。
			GLES20.glAttachShader(program, pixelShader);
			checkGlError("glAttachShader");

			GLES20.glLinkProgram(program);
			final int[] linkStatus = new int[1];
			GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, DEFAULT_OFFSET);
			if (linkStatus[FIRST_INDEX] != GLES20.GL_TRUE) {
				Log.e(TAG, "Could not link program: ");
				Log.e(TAG, GLES20.glGetProgramInfoLog(program));
				GLES20.glDeleteProgram(program);
				program = INVALID;
			}
		}

		return program;
	}

	/**
	 * 指定されたシェーダのソースコードをコンパイルします。
	 * 
	 * @param shaderType シェーダの種類
	 * @param source シェーダのソースコード
	 * @return シェーダハンドラまたは {@link #INVALID}
	 * @see {@link GLES20#GL_VERTEX_SHADER}
	 * @see {@link GLES20.GL_FRAGMENT_SHADER}
	 */
	public static int loadShader(final int shaderType, final String source) {
		int shader = GLES20.glCreateShader(shaderType);
		if (shader != INVALID) {
			GLES20.glShaderSource(shader, source);
			GLES20.glCompileShader(shader);
			final int[] compiled = new int[1];
			GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, DEFAULT_OFFSET);
			if (compiled[FIRST_INDEX] == INVALID) {
				Log.e(TAG, "Could not compile shader " + shaderType + ":");
				Log.e(TAG, GLES20.glGetShaderInfoLog(shader));
				GLES20.glDeleteShader(shader);
				shader = INVALID;
			}
		}
		return shader;
	}

	/**
	 * 指定された直前の OpenGL API 操作についてエラーが発生しているかどうか検証します。
	 * 
	 * @param op 検証する直前に操作した OpenGL API 名
	 * @throws GLException 直前の OpenGL API 操作でエラーが発生している場合
	 */
	public static void checkGlError(final String op) throws GLException {
		int error;
		while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
			Log.e(TAG, op + ": glError " + error);
			throw new GLException(error, op + ": glError " + error);
		}
	}

	/**
	 * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。
	 * 
	 * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報
	 * @return テクスチャ ID
	 */
	public static int loadTexture(final Bitmap bitmap) {
		return loadTexture(bitmap, GLES20.GL_NEAREST, GLES20.GL_LINEAR);
	}

	/**
	 * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。<p>
	 * この実装は簡易なテクスチャの初期化のみで繰り返しの指定をサポートしません。
	 * 
	 * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報
	 * @param min テクスチャを縮小するときの補完方法
	 * @param mag テクスチャを拡大するときの補完方法
	 * @return テクスチャ ID
	 * @see {@link GLES20#GL_TEXTURE_MIN_FILTER}
	 * @see {@link GLES20#GL_TEXTURE_MAG_FILTER}
	 */
	public static int loadTexture(final Bitmap bitmap, final int min, final int mag) {
		final int[] textures = new int[1];
		GLES20.glGenTextures(1, textures, DEFAULT_OFFSET);

		final int texture = textures[FIRST_INDEX];
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
		GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

		// テクスチャを拡大/縮小する方法を設定します。
		GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, min);
		GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, mag);

		return texture;
	}

}