OpenGLによる描画2013/11/03

正確にいうと、OpenGL ES 2.0になりますか。
よくあるサンプルですが、三角形ポリゴンを表示してみることにします。

と、その前に
・描画全体の初期化など、共通の処理
・アプリ毎の処理
を区別するために、今のうちにソースを分けておきます。

jni
 ├ sys
 │ ├ common.h         いろいろ共通事項のヘッダ
 │ ├ SysMain.cpp       native側のメイン処理
 │ ├ Renderer.h        描画管理クラスヘッダ
 │ ├ Renderer.cpp     描画管理クラスソース
 ├ def.h                     アプリの定義
 └ AppMain.cpp          アプリのメイン処理

SysMain.cpp

#include "common.h"
#include "Renderer.h"


using namespace sys;


extern "C"
{
JNIEXPORT void JNICALL		Java_sys_BaseActivity_initNative(JNIEnv*, jobject, jint, jint);
JNIEXPORT jboolean JNICALL	Java_sys_BaseActivity_updateNative(JNIEnv*, jobject);
}

void	init_app(void);			// メイン初期化
Bool	update_app(void);		// メイン稼働

/************
    初期化
 ************/
JNIEXPORT void JNICALL	Java_sys_BaseActivity_initNative(JNIEnv* env, jobject, jint width, jint height)
{
	LOGI("initNative (%d, %d)", width, height);

	Renderer::init(width, height);				// 描画管理初期化

	init_app();									// アプリメイン初期化
}

/**********
    稼働
 **********/
JNIEXPORT jboolean JNICALL	Java_sys_BaseActivity_updateNative(JNIEnv*, jobject)
{
	Renderer::update();							// 描画管理稼働

	if ( update_app() ) {						// アプリメイン稼働
		return	JNI_TRUE;
	}
	return	JNI_FALSE;							// アプリ終了
}

/**************** End of File *************************************************/

native側のメイン処理、java側から呼ばれる initNativeや updateNativeは、ここに移しました。
見ての通り初期化とフレーム毎の稼働で、描画管理とアプリの処理をそれぞれ呼んでいるだけです。

Renderer.h
#ifndef	___RENDERER_H___
#define	___RENDERER_H___

#include "common.h"

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>


namespace sys
{

/************************
    シェーダプログラム
 ************************/
struct ShaderProgram
{
	GLuint	program;			// プログラムオブジェクト
	GLint	position;			// 座標
	GLint	color;				// カラー
	GLuint	projection;			// 透視変換

		ShaderProgram(void)					// コンストラクタ
		{
			program = 0;
		}
		~ShaderProgram()					// デストラクタ
		{
			quit();
		}
	void	init(GLuint, GLuint);			// 初期化
	void	use(const GLfloat*);			// 使用
	void	quit(void);						// 終了
};

/**************
    描画管理
 **************/
class Renderer
{
private :

	static void		initShader(void);					// シェーダ初期化
	static GLuint	loadShader(GLenum, const char*);	// シェーダ作成

public :

	static ShaderProgram*	shader;						// シェーダプログラム
	static float			mat_projection[4*4];		// 透視変換行列

	static void		init(int, int);						// 初期化
	static void		quit(void);							// 終了
	static void		update(void);						// 稼働
	static ShaderProgram*	use_shader(void);			// シェーダ使用
};

}

#endif
/********************* End of File ********************************/

Renderer.cpp
/***************************

		描画管理

 ***************************/

#include "Renderer.h"


namespace sys
{

ShaderProgram*	Renderer::shader;							// シェーダプログラム
float			Renderer::mat_projection[4*4];				// 透視変換行列


/************************************************
    初期化
		引数	width, height = 端末画面サイズ
 ************************************************/
void	Renderer::init(int width, int height)
{
	initShader();								// シェーダ初期化

	for (int i = 0; i < 4*4; i++) {				// 透視変換行列初期化
		mat_projection[i] = 0.0f;
	}
	mat_projection[0]  =  2.0f/SCREEN_WIDTH;
	mat_projection[5]  = -2.0f/SCREEN_HEIGHT;
	mat_projection[10] =  1.0f;
	mat_projection[15] =  1.0f;

	int		_w, _h;

	if ( width*SCREEN_HEIGHT < height*SCREEN_WIDTH ) {		// 横長(上下カット)
		_w = width;
		_h = width*SCREEN_HEIGHT/SCREEN_WIDTH;
	}
	else {													// 縦長(左右カット)
		_w = height*SCREEN_WIDTH/SCREEN_HEIGHT;
		_h = height;
	}
	glViewport((width - _w)/2, (height - _h)/2, _w, _h);	// ビューポート設定
}

/********************
    シェーダ初期化
 ********************/
void	Renderer::initShader(void)
{
	shader = new ShaderProgram();

	GLuint	vert_shader, frag_shader;

	static const
	char	gVertexShader[] = 					// 頂点シェーダプログラム
				"attribute vec4 position;"
				"attribute vec4 color;"
				"varying vec4 vColor;"
				"uniform mat4 projection;"
				"void main() {"
					"gl_Position = projection*position;"
					"vColor = color;"
				"}";

	static const
	char	gFragmentShader[] = 				// フラグメントシェーダプログラム
				"precision mediump float;"
				"varying vec4 vColor;"
				"void main() {"
					"gl_FragColor = vColor;"
				"}";

	vert_shader = loadShader(GL_VERTEX_SHADER, gVertexShader);			// 頂点シェーダ
	frag_shader = loadShader(GL_FRAGMENT_SHADER, gFragmentShader);		// フラグメントシェーダ

	shader->init(vert_shader, frag_shader);								// シェーダプログラム作成
	glDeleteShader(vert_shader);
	glDeleteShader(frag_shader);
}

/*******************************************
    シェーダ作成
		引数	type   = シェーダ種類
				source = プログラムソース
		戻り値	シェーダオブジェクト
 *******************************************/
GLuint	Renderer::loadShader(GLenum type, const char* source)
{
    GLuint	_shader = glCreateShader(type);						// シェーダオブジェクト作成
    GLint	_compiled = 0;

	assert(_shader != 0);
	glShaderSource(_shader, 1, &source, NULL);					// プログラムソース設定
	glCompileShader(_shader);									// コンパイル
	glGetShaderiv(_shader, GL_COMPILE_STATUS, &_compiled);		// コンパイル結果取得
	if ( !_compiled ) {											// コンパイル失敗
		GLchar*	_buf;
		GLint	_len;

		glGetShaderiv(_shader, GL_INFO_LOG_LENGTH, &_len);
		if ( (_len > 0) && (_buf = new GLchar[_len]) ) {
			glGetShaderInfoLog(_shader, _len, NULL, _buf);		// エラーログ取得
			LOGE("Could not compile shader %d:\n%s\n", type, _buf);
			delete[]	_buf;
		}
		glDeleteShader(_shader);
		return	0;
	}
	return	_shader;
}

/*************************************************
    シェーダプログラム作成
		引数	v_shader = 頂点シェーダ
				f_shader = フラグメントシェーダ
 *************************************************/
void	ShaderProgram::init(GLuint v_shader, GLuint f_shader)
{
	program = glCreateProgram();								// プログラムオブジェクト作成
	assert(program != 0);
	glAttachShader(program, v_shader);							// 頂点シェーダアタッチ
	glAttachShader(program, f_shader);							// フラグメントシェーダアタッチ

    GLint	_linked = GL_FALSE;

	glLinkProgram(program);										// リンク
	glGetProgramiv(program, GL_LINK_STATUS, &_linked);			// リンク結果取得
	if ( !_linked ) {											// リンク失敗
		GLchar*	_buf;
		GLint	_len;

		glGetProgramiv(program, GL_INFO_LOG_LENGTH, &_len);
		if ( (_len > 0) && (_buf = new GLchar[_len]) ) {
			glGetProgramInfoLog(program, _len, NULL, _buf);		// エラーログ取得
			LOGE("Could not link program:\n%s\n", _buf);
			delete[]	_buf;
		}
		glDeleteProgram(program);
		program = 0;
		return;
	}

	position	= glGetAttribLocation(program, "position");			// 座標
	color		= glGetAttribLocation(program, "color");			// カラー
	projection	= glGetUniformLocation(program, "projection");		// 透視変換
}


/**********
    終了
 **********/
void	Renderer::quit(void)
{
	delete	shader;									// シェーダ削除
}

void	ShaderProgram::quit(void)
{
	if ( program ) {
		glDeleteProgram(program);
	}
}


/**********
    稼働
 **********/
void	Renderer::update(void)
{
	glEnable(GL_BLEND);					// αブレンド初期化
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

/**************************
    シェーダ使用
		戻り値	シェーダ
 **************************/
ShaderProgram*	Renderer::use_shader(void)
{
	shader->use(mat_projection);
	return	shader;
}

void	ShaderProgram::use(const GLfloat* mat_projection)
{
	glUseProgram(program);
	glUniformMatrix4fv(projection, 1, GL_FALSE, mat_projection);
	glEnableVertexAttribArray(position);
	glEnableVertexAttribArray(color);
}

}

/**************** End of File ******************************************/

描画全体の管理をするクラス sys::Rendererです。
初期化処理は以下の通り。

・シェーダの初期化
OpenGL ES 2.0はプログラマブルシェーダなので、シェーダの設定が必要です。
今回は、
・頂点シェーダは、行列projectionによる座標変換
・フラグメントシェーダは、カラーcolorを設定
という、ごく単純なものになっています。

・座標変換行列の設定
座標はそのままだと左上(-1.0, -1.0)-右下(1.0, 1.0)になりますが、2Dゲームでこの座標ではどうにも使いにくいです。
画面の解像度を決めて、座標を変換して使うようにします。画面解像度はアプリ毎に決めるので、def.hに定義しておきます。
#ifndef	___DEF_H___
#define	___DEF_H___

static const int	SCREEN_WIDTH  = 640,					// 画面サイズ
					SCREEN_HEIGHT = 960;

#endif
/********************** End of File ****************************************************/

行列mat_projectionによる変換で、画面上の座標は、
(-SCREEN_WIDTH/2, -SCREEN_HEIGHT/2) - (SCREEN_WIDTH/2, SCREEN_HEIGHT/2)
になります。
2Dの画面で座標は、
(0, 0) - (SCREEN_WIDTH, -SCREEN_HEIGHT)
というのが多いとは思いますが、後に出てくるスプライト等も中心を原点にしたいということもあり、(0, 0)を中心とするような形にしました。

・ビューポートの設定
Androidの端末は画面解像度もバラバラなら、縦横の比率もバラバラです。
画面の比率をSCREEN_WIDTH、SCREEN_HEIGHTに合わせるようにglViewportでビューポートを設定しています。

次にフレーム毎の処理ですが、今回はとりあえずαブレンドだけしておきます。


アプリの処理
AppMain.cpp

#include "common.h"
#include "Renderer.h"


/************
    初期化
 ************/
void	init_app(void)
{
}

/******************************
    稼働
		戻り値	アプリ続行か
 ******************************/
Bool	update_app(void)
{
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);					// 画面クリア

	static const
	GLfloat		vertex[] =
				{
					   0.0f, -480.0f,
					-320.0f,  480.0f,
					 320.0f,  480.0f
				};

	static const
	GLubyte		color[] =
				{
					255,   0,   0, 255,
					  0, 255,   0, 255,
					  0,   0, 255, 255,
				};

	sys::ShaderProgram*		_shader = sys::Renderer::use_shader();		// シェーダ使用

	glVertexAttribPointer(_shader->color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, color);		// カラー
	glVertexAttribPointer(_shader->position, 2, GL_FLOAT, GL_FALSE, 0, vertex);			// 頂点
	glDrawArrays(GL_TRIANGLES, 0, 3);									// 三角形描画

	return	TRUE;
}

/**************** End of File *************************************************/

初期化処理は、今回は単なる生ポリゴンなので何も無いです。

実際に三角形を描画する、フレーム毎の処理は以下の通りです。

・画面クリア
glClearで画面をクリアします。必ず必要な処理というわけではないので、sys::Rendererではなくアプリ側で行っています。デプスバッファでも使っていれば、共通処理でクリアしたほうが良いのでしょうが。

・シェーダプログラムの使用
今回のようにシェーダが一つしかないのであれば sys::Rendererで設定してしまっても良いのですが、後々シェーダの切り替えを行うことを考えてアプリ側で設定しています。

・三角形ポリゴンの描画
頂点座標・カラーを設定した後、glDrawArraysで GL_TRIANGLESを指定して三角形を描画します。

スクリーンショット

今回のプロジェクト一式は、こちらです。