はじめに2013/11/01

ブログなんてものを始めるにあたって、
好きなときに、好きなことを、好き勝手に書いていこうとは思うけど、
すぐにネタが尽きてしまうのもなんなので、
何かしら芯となるものを持とうと、Android NDKのプログラミングについて書いていくことにしました。

巷にはすでにNDKによるサンプルもあふれていて今更なんですが、個々の情報が分かれている感じもしますので、そのへんをまとめて1つのプロジェクトにしていこうかと思います。のんびりと。

まずはメインループ2013/11/02

前回、テーマを「Android NDKのプログラミング」としましたが、これではちょっと曖昧なのでとりあえず目標を「2Dゲームの作成」として、必要なものを順番に作っていくことにします。

そこで一番初めの今回は、
・初期化
・一定間隔でメインの処理
という全体の流れ(だけ)を作成します。

それでは、なにはなくともアクティビティ
BaseActivity.java
package sys;

import android.app.Activity;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

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


/********************
    アクティビティ
 ********************/
public class BaseActivity extends Activity
{
	static {
		System.loadLibrary("native");
    }


	private BaseView		surface_view;				// ビュー
	private BaseRenderer	renderer;					// レンダラー

	private ScheduledExecutorService	executor;		// 定期実行管理
	private ScheduledFuture<?>			future;


	public native void		initNative(int _w, int _h);			// native部初期化
	public native boolean	updateNative();						// native部稼働


	/**********
	    開始
	 **********/
	@Override
	protected void	onCreate(Bundle _savedInstanceState)
	{
		super.onCreate(_savedInstanceState);

		renderer		= new BaseRenderer();							// レンダラー作成
		surface_view	= new BaseView(getApplication(), renderer);		// ビュー作成
		setContentView(surface_view);

		executor = Executors.newSingleThreadScheduledExecutor();		// 定期実行管理
	}

	/**********
	    終了
	 **********/
	@Override
	protected void	onDestroy()
	{
		super.onDestroy();
		executor.shutdown();
	}

	/**************
	    一時停止
	 **************/
	@Override
	protected void	onPause()
	{
		super.onPause();
		if ( future != null ) {					// 定期実行停止
			future.cancel(false);
			future = null;
		}
		surface_view.onPause();
    }

	/**********
	    再開
	 **********/
	@Override
	protected void	onResume()
	{
		super.onResume();
		surface_view.onResume();
	}


	/************
	    ビュー
	 ************/
	class BaseView extends GLSurfaceView
	{
		public BaseView(Context _context, BaseRenderer _renderer)
		{
			super(_context);
			setEGLContextClientVersion(2);					// OpenGL ES 2.0使用
			setEGLConfigChooser(new ConfigChooser());
			setRenderer(_renderer);
			setRenderMode(RENDERMODE_WHEN_DIRTY);
		}

		class ConfigChooser implements GLSurfaceView.EGLConfigChooser
		{
			@Override
			public EGLConfig	chooseConfig(EGL10 egl, EGLDisplay display)
			{
				final
				int[]	configAttribs =
				{
					EGL10.EGL_RED_SIZE,		8,
					EGL10.EGL_GREEN_SIZE,	8,
					EGL10.EGL_BLUE_SIZE,	8,
					EGL10.EGL_ALPHA_SIZE,	8,
					EGL10.EGL_NONE
				};

				int[]	num_config = new int[1];

				egl.eglChooseConfig(display, configAttribs, null, 0, num_config);

				EGLConfig[]		configs = new EGLConfig[num_config[0]];

				egl.eglChooseConfig(display, configAttribs, configs, num_config[0], num_config);

				return	configs[0];
			}
		}
	}


	/****************
	    レンダラー
	 ****************/
	class BaseRenderer implements GLSurfaceView.Renderer
	{
		public void	onSurfaceCreated(GL10 _gl, EGLConfig _config) {}

		public void	onSurfaceChanged(GL10 gl, int width, int height)
		{
			initNative(width, height);						// native部初期化

			if ( future == null ) {							// 定期実行開始
				future = executor.scheduleAtFixedRate(new Runnable()
				{
					@Override
					public void	run()
					{
						surface_view.requestRender();		// 描画リクエスト
					}
				},
				0, 1000/30, TimeUnit.MILLISECONDS);
			}
		}

		public void	onDrawFrame(GL10 gl)
		{
			if ( !updateNative() ) {						// native部稼働
				finish();
			}
		}
	}
}

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

アクティビティのonCreateでサーフェスビューとレンダラーを作成するとか、System.loadLibraryでnative側のプログラムを読み込むとかいろいろありますが、この辺はNDKのプロラムでは共通事項なので説明は省きます。シンプルに作っているので、見れば大体わかると思います。

さて、プログラムの流れを追っていくと、
BaseActivityの onCreate → onResume
BaseViewの onResume
BaseRendererの onSurfaceCreated → onSurfaceChanged
と進みます。
ここでまず、nativeの関数 initNativeを呼んでnative側を初期化しておきます。
次に、「一定間隔でメインの処理」を行うための設定をします。

ScheduledExecutorServiceのメソッド
ScheduledFuture<?> scheduleAtFixedRate(
              Runnable command,
              long initialDelay,
              long period,
              TimeUnit unit)
時間単位 unitで、initialDelay後から delay毎にタスク commandを実行します。

このプログラムでは、
・1000/30ms毎に
・surface_view.requestRender()を実行する
となります。
BaseViewのコンストラクタで setRenderMode(RENDERMODE_WHEN_DIRTY)としているので、surface_viewの requestRender()により rendererの onDrawFrame()が呼ばれます。
よって、1000/ms毎に、nativeの関数 updateNativeが呼ばれることになります。

それでは、native側のプログラム
native.cpp

#include <jni.h>
#include <android/log.h>


static int	cnt = 0;


#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,  "INFO",  __VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, "ERROR", __VA_ARGS__)

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

/************
    初期化
 ************/
JNIEXPORT void JNICALL	Java_sys_BaseActivity_initNative(JNIEnv*, jobject, jint _width, jint _height)
{
	LOGI("initNative (%d, %d)", _width, _height);
}

/**********
    稼働
 **********/
JNIEXPORT jboolean JNICALL	Java_sys_BaseActivity_updateNative(JNIEnv*, jobject)
{
	if ( cnt % 60 == 0 ) {
		LOGI("updateNative %4d", cnt/60);
	}
	cnt++;
    return	JNI_TRUE;
}

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

今回はログを表示するだけで、何もしません。

なお、初期化処理といいつつ initNativeは起動時だけではなくサスペンドからのレジューム時などにも呼ばれますので注意してください。

プロジェクト一式は、こちら

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を指定して三角形を描画します。

スクリーンショット

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

食品偽装だらけ2013/11/04

阪急阪神ホテルズのレストランでの食品偽装が発覚して以来、全国各地のホテルで次々に同じような偽装が明るみになっているようです。
これだけあると、最初に批判された阪急阪神ホテルズでは「どこもやってることだ」と言いたかったんじゃないでしょうか?
むしろ便乗ではなく、自分のところで問題を発覚させたという点では他のホテルよりマシなのではないかとも思ってしまいます。

ただ種類とか産地とか明らかな間違いはともかく、「フレッシュ」みたいな曖昧な表現は偽装の判断が難しいですけどね。
「一口ひれかつ」なんか一口で食べられたためしがないですし。

この前もラーメン屋で注文した「具だくさんつけ麺」が全然具だくさんに見えませんでしたが、これはどうも間違って普通のつけ麺を出していたらしい。

assetsファイル読み込み2013/11/04

三角ポリゴンの描画ができたので、次はテクスチャの描画に移ります。
テクスチャのデータは assetsフォルダに置いておきます。res/rawに置くという手もありますが、assetsの方がいろいろ融通がききそうなので。
ただ、assetsフォルダからの読み込みにもいくつか手順がありますので、今回はその辺りをやってみます。

assetsの読み込みには幾通りかのやり方があるようなのですが、Android OS 2.3以降で使える AssetManagerを使う方法にしてみます。

BaseActivity.java
		public void	onSurfaceChanged(GL10 gl, int width, int height)
		{
			int		period = initNative(width, height, getAssets());			// native部初期化

			if ( future == null ) {							// 定期実行開始
				future = executor.scheduleAtFixedRate(new Runnable()
				{
					@Override
					public void	run()
					{
						surface_view.requestRender();		// 描画リクエスト
					}
				},
				0, period, TimeUnit.MILLISECONDS);
			}
		}

AssetManagerは java側で取得できるので、initNativeで native側に送ります。

SysMain.cpp
	asset_manager = AAssetManager_fromJava(env, mgr);		// asset読み込みマネージャー
	assert(asset_manager != NULL);

それを native側で受け取って、AAssetManagerに変換します。

namespace sys
{

AAssetManager*	asset_manager;			// asset読み込み用

/****************************************
    assetファイル読み込み
			引数	name = ファイル名
			戻り値	データ
					size:データサイズ
 ****************************************/
void*	load_asset(const char* name, u32* size)
{
	AAsset*	_asset = AAssetManager_open(asset_manager, name, AASSET_MODE_BUFFER);

	assert(_asset);

	size_t	_size = AAsset_getLength(_asset);		// ファイルサイズ
	void*	_buf = malloc(_size);					// データバッファ

	AAsset_read(_asset, _buf, _size);				// データ読み込み
	AAsset_close(_asset);
	if ( size ) {
		*size = (u32)_size;
	}
	return	_buf;
}

}

ファイル読み込み用の関数です。
データバッファを mallocで取っているので、使い終わったら freeで解放してください。

ファイル読み込みのサンプルです。
assetsフォルダには、test.txtというテキスト(終端に'\0'を追加)を置いておきます。

AppMain.cpp
/************
    初期化
 ************/
void	init_app(void)
{
	u32		_size;
	char*	_str = (char*)sys::load_asset("test.txt", &_size);		// テスト文字列

	LOGI("[%s] size:%d", _str, (int)_size);
	free(_str);
}

load_assetで文字列データを読み込んで、ログに出力しています。


最後についでなんですが、アプリの定義は native側に持っていこうと、1フレームの間隔は initNativeの戻り値で native側から取得するように変更しました。

プロジェクト一式はこちらから。