芸軌社株式会社
RSS 2.0

芸軌社公式ブログ

2013年3月3日(日)19:54(+0900)

OpenGL 2.0/OpenGL ES 2.0のPC/Android対応サンプルを作ってみた

このエントリーをはてなブックマークに追加
2013年3月4日修正

自分自身のメモ用も兼ねて、OpenGL ES 2.0のAndroid用サンプルコードをPC(Ubuntu Linux)用のOpenGL 2.0コードにしてみました。何かの参考になれば幸いです。

コンパイルオプションをつけることにより、同じソースでAndroidでも動きます。また、GLSLによるプログラマブルシェーダを使わないOpenGL 1.xにも無駄に対応しています(なお、Androidでプログラマブルシェーダ使用版/不使用版を動かすには、それぞれJava側でOpenGL ESのバージョンを2または1にしてやる必要があります)。

ソースコードは以下のとおり。android-ndkのサンプルコード「hello-gl2」のgl_code.cppを元に、言語をC++からC言語に変えています。Androidで動かす場合は、gl_code.cppの代わりに以下のファイルを使い、Android.mkを少し書き換える(後述)ことで動きます。PCで動かす場合はこのままコンパイルできます(Ubuntuの場合、freeglut3-devやlibglew-dev等をインストールしておく必要があります。Windowsでは動作未確認です)。

/*
 * Modified by Geikisha, Inc. ( http://www.geikisha.com/ )
 *      3 March 2013
 */
/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef ANDENB
	#include <jni.h>
	#include <android/log.h>
	#ifdef GL1x
	#include <GLES/gl.h>
	#else
	#include <GLES2/gl2.h>
	#include <GLES2/gl2ext.h>
	#endif
	#define  LOG_TAG    "libgl2jni"
	#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
	#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#else
	#include <GL/glew.h>
	#include <GL/glut.h>
	#define  LOGI(...)  fprintf(stdout,__VA_ARGS__)
	#define  LOGE(...)  fprintf(stderr,__VA_ARGS__)
#endif

GLuint gProgram;
GLuint gvPositionHandle;

static const char gVertexShader[] =
	"attribute vec4 vPosition;\n"
	"void main() {\n"
	"  gl_Position = vPosition;\n"
	"}\n";

static const char gFragmentShader[] =
#ifdef ANDENB
	"precision mediump float;\n"
#endif
	"void main() {\n"
	"  gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);\n"
	"}\n";

const GLfloat gTriangleVertices[] = { 0.0f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f};

void renderFrame(void) {
	static float grey = 0.0f;
	grey += 0.01f;
	if (grey > 1.0f) grey = 0.0f;

	glClearColor(grey, grey, grey, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

#ifdef GL1x
	glColor4f(0.0f, 1.0f, 0.0f, 1.0f);
	glEnableClientState(GL_VERTEX_ARRAY);
	glVertexPointer(2, GL_FLOAT, 0, gTriangleVertices);
#else
	glUseProgram(gProgram);
	glEnableVertexAttribArray(gvPositionHandle);
	glVertexAttribPointer(gvPositionHandle, 2, GL_FLOAT, GL_FALSE,
		0, gTriangleVertices);
#endif

	glDrawArrays(GL_TRIANGLES, 0, 3);
}

#ifdef GL1x
#else
static void printGLString(const char *name, GLenum s) {
	const char *v = (const char *) glGetString(s);
	LOGI("GL %s = %s\n", name, v);
}

static void checkGlError(const char* op) {
	for (GLint error = glGetError(); error; error
			= glGetError()) {
		LOGI("after %s() glError (0x%x)\n", op, error);
	}
}

static void printGLSLLog(const char* tag, GLuint id) {
	void (*getInfo)(GLuint id, GLenum pname, GLint* params);
	void (*getInfoLog)(GLuint id, GLsizei maxLength, GLsizei* length,
		GLchar* infoLog);
	if(strcmp(tag,"shader")==0) {
		getInfo = glGetShaderiv;
		getInfoLog = glGetShaderInfoLog;
	}
	if(strcmp(tag,"program")==0) {
		getInfo = glGetProgramiv;
		getInfoLog = glGetProgramInfoLog;
	}

	GLint infoLen = 0;
	getInfo(id, GL_INFO_LOG_LENGTH, &infoLen);
	if(infoLen) {
		char* buf = (char*) malloc(infoLen);
		if(buf) {
			getInfoLog(id, infoLen, NULL, buf);
			LOGE("%s\n", buf);
			free(buf);
		}
	}
}

GLuint loadShader(GLenum shaderType, const char* pSource) {
	GLuint shader = glCreateShader(shaderType);
	if (shader) {
		glShaderSource(shader, 1, &pSource, NULL);
		glCompileShader(shader);
		GLint compiled = 0;
		glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
		if (!compiled) {
			if(shaderType==GL_VERTEX_SHADER)
				LOGE("Could not compile vertex shader\n");
			if(shaderType==GL_FRAGMENT_SHADER)
				LOGE("Could not compile fragment shader\n");
			printGLSLLog("shader", shader);
			glDeleteShader(shader);
			shader = 0;
		}
	}
	return shader;
}

GLuint createProgram(const char* pVertexSource, const char* pFragmentSource) {
	GLuint vertexShader = loadShader(GL_VERTEX_SHADER, pVertexSource);
	if(!vertexShader) return 0;
	GLuint fragmentShader
		= loadShader(GL_FRAGMENT_SHADER, pFragmentSource);
	if(!fragmentShader) return 0;

	GLuint program = glCreateProgram();
	if (program) {
		glAttachShader(program, vertexShader);
		checkGlError("glAttachShader");
		glAttachShader(program, fragmentShader);
		checkGlError("glAttachShader");
		glLinkProgram(program);
		GLint linkStatus = GL_FALSE;
		glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
		if(linkStatus != GL_TRUE) {
			LOGE("Could not link program\n");
			printGLSLLog("program", program);
			glDeleteProgram(program);
			program = 0;
		}
	}
	return program;
}

void setupShader() {
	printGLString("Version", GL_VERSION);
	printGLString("Vendor", GL_VENDOR);
	printGLString("Renderer", GL_RENDERER);
	printGLString("Extensions", GL_EXTENSIONS);

#ifdef ANDENB
#else
	glewInit();
	if (!glewIsSupported("GL_VERSION_2_0")) {
		LOGE("Can't Use GLSL\n");
		return;
	}
#endif

	gProgram = createProgram(gVertexShader, gFragmentShader);
	if(!gProgram) {
		LOGE("Could not create program.\n");
		return;
	}
	gvPositionHandle = glGetAttribLocation(gProgram, "vPosition");
	checkGlError("glGetAttribLocation");
	LOGI("glGetAttribLocation(\"vPosition\") = %d\n", gvPositionHandle);
}
#endif

#ifdef ANDENB

void setupGraphics(int w, int h) {
	glViewport(0, 0, w, h);
#ifdef GL1x
#else
	setupShader();
#endif
}

JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_init(
	JNIEnv * env, jobject obj,  jint width, jint height) {
	setupGraphics(width, height);
}

JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_step(
	JNIEnv * env, jobject obj) {
	renderFrame();
}

#else

void init(void) {
}

void reshape(int w, int h) {
	glViewport(0, 0, w, h);
}

void display(void) {
	renderFrame();
	glutSwapBuffers();
}

void idle(void) {
	glutPostRedisplay();
}

int main(int argc, char** argv) {
	int w = 320, h = 480;

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
	glutInitWindowSize(w, h);
	glutCreateWindow("hogehoge");

	glutReshapeFunc(reshape);
	glutDisplayFunc(display);
	glutIdleFunc(idle);

	init();

#ifdef GL1x
#else
	setupShader();
#endif

	glutMainLoop();
}

#endif

コンパイルは、Ubuntuでは例えば以下のようにします。

clang gl2.c -lglut -lGLEW -Wall #-DGL1x

最後のコメントアウトしている-DGL1xを外すと、プログラマブルシェーダを使用しない実行ファイルになります。

Android版は、Android.mkのLOCAL_SRC_FILESを上記ソースコードのファイル名(ここではgl2.c)に変更し、LOCAL_CFLAGSに-DANDENB等を追加します。

#
# Modified by Geikisha, Inc. ( http://www.geikisha.com/ )
#      3 May 2013
#
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := libgl2jni
LOCAL_CFLAGS    := -std=c99 -Werror -DANDENB #-DGL1x
LOCAL_SRC_FILES := gl2.c
LOCAL_LDLIBS    := -llog -lGLESv2 #-lGLESv1_CM

include $(BUILD_SHARED_LIBRARY)

このAndroid.mkも、最後の-DGL1x等のコメントアウトを外すとプログラマブルシェーダ不使用版(OpenGL ES 1.x)になります。ただし、サンプルのJavaソースそのままではColor関係のところで「Called unimplemented OpenGL ES API」というエラーが発生してしまい、的確な描写を得るためには最初に述べたようにJava側でOpenGL ESのバージョンを1に設定してやる必要があります(GL2JNIView.javaの前半にあるattrib_list内のEGL_CONTEXT_CLIENT_VERSIONの次にある2を1に変えてやればOKです)。

なお、「Android NDKネイティブプログラミング」の初版本の時点ではAndroid Virtual Device(AVD)ではOpenGL ES 2.0は動かせなかったようですが、現在のAVDはAndroid 4.0.3以上を用いて設定(Android Virtual Device ManagerのEdit)で「Use Host GPU」にチェックを入れてやることにより、AVD上でもOpenGL ES 2.0が動くようになっています。

解説

プログラムはマクロにより、#ifdef ANDENBの部分でAndroid用のコードとPC用のコードを、#ifdef GL1xでプログラマブルシェーダ不使用コードと使用コードを分けています。

シェーダと描写関数

最初のほうに出てくるgProgramがシェーダプログラム、gvPositionHandleが頂点シェーダでの頂点、gVertexShader[]が頂点シェーダ、gFragmentShader[]がフラグメントシェーダになります。フラグメントシェーダの精度設定"precision mediump float;\n"はOpenGL ES専用のようで、普通のOpenGLに入っているとエラーになるのでマクロでAndroid専用としています。

次のrenderFrame()が実際に描写を行う関数です(明るさの変化する灰色の背景に黄緑の三角形を描写します)。プログラマブルシェーダ使用版では色は先ほどのgFragmentShader[]内に書かれていますが、不使用版ではここでは色をglColor4fで直に設定しています。

シェーダプログラムの作成

次の#ifdef GL1x #else ~ #endifはシェーダのコンパイル・リンク等を行う部分になります。

最初のprintGLString(...)、checkGlError(...)、printGLSLLog(...)はログ関連の関数です。printGLSLLog(...)は、元のプログラムでのシェーダのロード・リンク部分の見通しを良くするために、エラーログ表示部分を関数として抜き出したものです。

次のloadShader(...)がシェーダをロードする関数、createProgram(...)がロードしたシェーダをアタッチしリンクする関数です。

loadShader(...)とcreateProgram(...)は若干ごちゃごちゃした関数になっていますが、半分以上の行はエラー処理関係で、本質的な部分は少しだけです。エラー処理関係の部分を取り除くと以下のようになります。

GLuint loadShader(GLenum shaderType, const char* pSource) {
	GLuint shader = glCreateShader(shaderType);
	glShaderSource(shader, 1, &pSource, NULL);
	glCompileShader(shader);
	return shader;
}

GLuint createProgram(const char* pVertexSource, const char* pFragmentSource) {
	GLuint vertexShader = loadShader(GL_VERTEX_SHADER, pVertexSource);
	GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, pFragmentSource);

	GLuint program = glCreateProgram();
	glAttachShader(program, vertexShader);
	glAttachShader(program, fragmentShader);
	glLinkProgram(program);

	return program;
}

最後のsetupShader()が実際にシェーダをセットアップする時に呼び出す関数です。次項で見るように、setupShader()はプログラマブルシェーダ使用版でのみ呼び出すようにしています。

setupShader()内のglewInit()は、プログラマブルシェーダ等を使うのに必要な設定を行っているようです。Android版のOpenGL ES関連の設定は「hello-gl2」ではJavaでEGLで行っているので、このプログラムソース内では設定関係のことはしていません。

その他

最後の#ifdef ANDENB ~ #else ~ #endifは、実際にAndroidまたはPC上にグラフィックを表示するための手続き的な部分になります。

Android版では、今回用いた「hello-gl2」はJavaからJNIでOpenGL ES 2.0部分を呼び出すようになっているので、グラフィックの初期設定をするsetupGraphics()と実際に描写するrenderFrame()をJavaから呼び出せるようにしています。なお、setupGraphics()ではシェーダの設定の他、ビューポートの設定も行っています。

PC版は、シェーダをロードする部分以外はプログラマブルシェーダを使わないOpenGLのテンプレートどおりです。