タッチパネル入力 その12013/11/16

画面への出力がひと通り落ち着いたので、次はタッチパネルからの入力を行います。

タッチパネルの入力情報は java側で受け取ります。
まず一番上にあるビューに通知され、そこでスルーされると次のビュー、最後にはアクティビティに通知されるようになっています。

ここでは一つだけのビュー、サーフェスビューで処理します。
また、せっかくなのでマルチタッチに対応してみます。

BaseActivity.java

	private short[]		touch_status = new short[5*3];	// タッチパネル状態


サーフェスビュー BaseView
		/********************
			タッチイベント
		 ********************/
		public boolean	onTouchEvent(final MotionEvent event)
		{
			if ( future == null ) {
				return	false;
			}

			int		_action = event.getAction();
			int		_index, _id;

			switch ( _action & MotionEvent.ACTION_MASK ) {
			  case MotionEvent.ACTION_DOWN :
			  case MotionEvent.ACTION_POINTER_DOWN :
				_index	= (_action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
				_id		= event.getPointerId(_index)*3;
				touch_status[_id + 1] = (short)event.getX(_index);		// X座標
				touch_status[_id + 2] = (short)event.getY(_index);		// Y座標
				touch_status[_id + 0] = 1;								// タッチ中
				break;

			  case MotionEvent.ACTION_MOVE :
				for (_index = 0; _index < event.getPointerCount(); _index++) {
					_id = event.getPointerId(_index)*3;
					touch_status[_id + 1] = (short)event.getX(_index);	// X座標
					touch_status[_id + 2] = (short)event.getY(_index);	// Y座標
					touch_status[_id + 0] = 1;							// タッチ中
				}
				break;

			  case MotionEvent.ACTION_UP :
			  case MotionEvent.ACTION_POINTER_UP :
				_index	= (_action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
				_id		= event.getPointerId(_index)*3;
				touch_status[_id + 0] = 0;								// 非タッチ
				break;
			}
			return true;
		}

画面をタッチする、離す等のタッチイベントが起こると onTouchEventが呼ばれます。
Android SDKでダブルタップ等の動作を判定することもできるのですが、ここでは
・画面を押しているか
・押している場合、その位置
だけを取得して、後の処理は native側に任せることとします。

取得した状態は、どのみち nativeはフレーム単位で動いているのでイベントが起きる度ではなく、updateNativeのタイミングで送るようにします。

レンダラー BaseRenderer
		public void	onDrawFrame(GL10 gl)
		{
			synchronized (renderer)
			{
				if ( (future != null) && !updateNative(touch_status) ) {		// native部稼働
					finish();
				}
			}
		}

最新の状態だけを送るので、炎のコマで秒間200万回入力しても無視されます。

native側の処理は、また次回に。

ETC1圧縮テクスチャ2013/11/13

OpenGLの中でもAndroidで共通に使える圧縮テクスチャ、ETC1に対応します。

圧縮テクスチャは、画像を圧縮した状態(サイズが小さい)でVRAMに置いておくテクスチャです。
PNG等の圧縮画像フォーマットは展開して元のサイズに戻してVRAMに転送しますが、ETC等の圧縮テクスチャではVRAM上でも小さいままなので、
・VRAMを占有するサイズが小さくなる
・描画時、転送サイズが小さいので高速
というメリットがあります。

そのかわり、
・不可逆圧縮なので、画質が落ちる
・ETC1圧縮テクスチャではαに未対応で、不透明画像のみ
といったデメリットもあります。

Android SDKでは etc1tool({sdkのフォルダ}/tools/etc1tool.exe)というPNG画像をETC1に変換するツールがありますので、そのツールで出力されるPKMフォーマットに対応します。

Texture.cpp
/**********************************
    PKM読み込み
		引数	data = PKMデータ
 **********************************/
void	Texture::load_pkm(const u8* data)
{
	format	= FORMAT_ETC;
	width	= (u16)(((u16)data[8] << 8) | data[9]);
	height	= (u16)(((u16)data[10] << 8) | data[11]);
	make(data + 0x10);
}

読み込み部分です。
ヘッダから幅と高さを取得して、データをそのまま作成関数に送っています。
"圧縮テクスチャ"というフォーマットのデータであってデータ自体は圧縮されていないので、展開処理は必要ありません。逆にいうと、データ自体にはまだ圧縮の余地があるということになります。サンプルでは assetsのzip圧縮に任せちゃっていますが。

/*****************************************
    作成
		引数	data = テクスチャデータ
 *****************************************/
void	Texture::make(const u8* data)
{
	clear();

	glGenTextures(1, &texture);						// テクスチャオブジェクト作成
	glBindTexture(GL_TEXTURE_2D, texture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	switch ( format ) {
	  case FORMAT_RGBA :
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
		break;

	  case FORMAT_RGB :
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		break;

	  case FORMAT_ETC :
		glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_ETC1_RGB8_OES, width, height, 0, width*height/2, data);
		break;

	  default :
		assert(FALSE);
		break;
	}
}

テクスチャ作成部分です。
画像転送が glTexImage2Dから glCompressedTexImage2Dに代わり、フォーマットに GL_ETC1_RGB8_OESを指定しています。

あとは、読み込み部分でPNGとPKMに分けたり、キャッシュのサイズ計算でPKMに対応したりと細かな変更はありますが、ソースは省略します。

見た目はたいして変わらないので、スクリーンショットも省略していますが、
※ビー玉の画像は「かわいいフリー素材集 いらすとや」から使わせていただいています。どうもありがとうございます。

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

サスペンド…そしてレジューム2013/11/11

Androidでは電源ボタンを押すなどサスペンド時に、OpenGLで設定したものがクリアされてしまいます。そこでレジューム(再開)時に再設定しなければいけません。
今までは開始時も含めてレジューム時に全て初期化していましたが、メモリを解放しないまま確保し直すなど行儀の悪いことになっていました。
今回はきちんとサスペンド・レジュームの処理をやってみます。

久々に java側のソースです。
BaseActivity.java
	/**********
	    終了
	 **********/
	@Override
	protected void	onDestroy()
	{
		super.onDestroy();
		quitNative();							// native部終了
		executor.shutdown();
	}

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

	/**********
	    再開
	 **********/
	@Override
	protected void	onResume()
	{
		super.onResume();
		if ( ((KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE)).inKeyguardRestrictedInputMode() ) {
			registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_PRESENT));			// スクリーンロック解除待ち
		}
		else {
			surface_view.onResume();
		}
	}

	private UnLockReceiver	receiver = new UnLockReceiver();		// スクリーンロック解除検知用

	private class UnLockReceiver extends BroadcastReceiver
	{
		@Override
		public void		onReceive(Context context, Intent intent)
		{
			unregisterReceiver(receiver);							// レシーバー登録を解除
			surface_view.onResume();
		}
	}

終了・一時停止・再開時に native側の関数を呼んでいるだけですが、再開時だけ少し変えています。

Androidでは一旦電源ボタンを押してサスペンドし、もう一度ボタンを押すとレジュームで onResume()が呼ばれますが、このとき画面はロック状態になっています。
ゲームなどでは画面が見えないのに動いてしまっては都合が悪いので、ロック状態の場合は解除を待ってからアプリを再開させています。


native側のソースです.
SysMain.cpp
/************
    初期化
 ************/
JNIEXPORT int JNICALL	Java_sys_BaseActivity_initNative(JNIEnv* env, jobject, jint width, jint height, jobject mgr)
{
	LOGI("initNative (&d, &d)", width, height);

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

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

	if ( !init_flag ) {
		init_app();								// アプリメイン初期化
		init_flag = TRUE;
	}
	else {
		resume_app();							// アプリメイン再開
	}

	return	FRAME_PERIOD;
}

/**********
    終了
 **********/
JNIEXPORT void JNICALL	Java_sys_BaseActivity_quitNative(JNIEnv*, jobject)
{
	LOGI("quitNative");

	quit_app();									// アプリメイン終了
	init_flag = FALSE;
}

/**************
    一時停止
 **************/
JNIEXPORT void JNICALL	Java_sys_BaseActivity_pauseNative(JNIEnv*, jobject)
{
	LOGI("pauseNative");

	if ( init_flag ) {
		pause_app();							// アプリメイン一時停止
	}
	Renderer::quit();							// 描画管理終了
}

ポーズ時の処理で描画管理を終了させています。
テクスチャキャッシュをクリアしていますので、テクスチャのVRAMからの削除はシステムに任せられます。

アプリ側の処理は、
・init_app()         開始時
・quit_app()        終了時
・pause_app()     一時停止時
・resume_app()   再開時
を、それぞれのタイミングで呼んでいます。
サンプルではログを表示しているだけなので、ソースは省略します。


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

テクスチャキャッシュ2013/11/10

テクスチャを描画するときデータはVRAMに転送して使用しますが、VRAMの容量には限りがあります。アプリの規模がある程度大きくなり、一度に全てのテクスチャをVRAMに乗せられなくなると、場面によってVRAM上のテクスチャデータを入れ替える必要が出てきます。
場面がはっきりと分かれていて、一つ一つの場面で使用するテクスチャが十分小さい場合は自前でテクスチャを入れ替えても良いのですが、なかなかそうもいかなかったり、面倒だったりするのでテクスチャキャッシュという機構を使ってシステムにやらせてみます。

※"テクスチャキャッシュ"というと、VRAM上でキャッシュに乗せてGPUが高速にアクセス…みたいな話だったりしますが、ここではVRAMに乗せるテクスチャ自体をキャッシュするということになります。


テクスチャキャッシュ概要
・テクスチャが必要なとき(描画時)、キャッシュにそのテクスチャがあるか検索する。
・キャッシュにあった場合、すでにVRAMに読み込まれているのでそれを使用する。また、キャッシュ上での優先度を上げる。
・キャッシュに無かった場合、テクスチャをVRAMに読み込み、優先度を上げてキャッシュに登録する。このとき、登録されているテクスチャの総容量(枚数、またはサイズ)がキャッシュの容量を超えていた場合、優先度の低いテクスチャをVRAMとキャッシュから削除する。

Texture.h
/**************************
    テクスチャキャッシュ
 **************************/
class TexCache : public Texture
{
	static TexCache**	cache;				// キャッシュ
	static int			cache_num;			// キャッシュ数
	static u32			cache_mem_size;		// キャッシュ使用メモリサイズ

	static void		clear_cache(void);		// 最後尾のテクスチャを削除

public :

// リソース種類
enum
{
	RES_MEMORY	= 0,		// メモリ
	RES_ASSET,				// assetsファイル
};

	static void			init(void);							// 初期化
	static void			quit(void);							// 削除
	static Texture*		get_texture(short, const void*);	// テクスチャ取得

	short			type;			// リソース種類
	const void*		data;			// リソースデータ
	u32				mem_size;		// VRAM占有サイズ

		TexCache(short, const void*);		// コンストラクタ
};

Texture.cpp
TexCache**	TexCache::cache;				// キャッシュ
int			TexCache::cache_num;			// キャッシュ数
u32			TexCache::cache_mem_size;		// キャッシュ使用メモリサイズ

/**********************
    キャッシュ初期化
 **********************/
void	TexCache::init(void)
{
	cache			= new TexCache *[TEX_CACHE_NUM];		// キャッシュ作成
	cache_num		= 0;
	cache_mem_size	= 0;
}

/********************
    キャッシュ削除
 ********************/
void	TexCache::quit(void)
{
	for (int i = 0; i < cache_num; i++) {			// テクスチャ削除
		delete	cache[i];
	}
	delete[]	cache;
}

/****************************************
    テクスチャ取得
		引数	_type = リソース種類
				_data = リソースデータ
 ****************************************/
Texture*	TexCache::get_texture(short _type, const void* _data)
{
	int		i, j;

	for (i = 0; i < cache_num; i++) {
		if ( (_data == cache[i]->data) && (_type == cache[i]->type) ) {		// 登録済み
			if ( i > 0 ) {
				TexCache*	_tmp = cache[i];

				for (j = i; j > 0; j--) {
					cache[j] = cache[j - 1];
				}
				cache[0] = _tmp;					// 先頭に登録
			}
			return	(Texture*)cache[0];
		}
	}

	// 新規登録
	if ( cache_num == TEX_CACHE_NUM ) {				// 枚数オーバー
		clear_cache();
	}
	j = cache_num/2;
	for (i = cache_num; i > j; i--) {
		cache[i] = cache[i - 1];
	}
	cache[j] = new TexCache(_type, _data);			// リソース読み込み
	cache_num++;
	cache_mem_size += cache[j]->mem_size;
	while ( cache_mem_size > TEX_CACHE_MEM ) {		// メモリサイズオーバー
		clear_cache();
	}
	return	(Texture*)cache[j];
}

/******************************
    最後尾のテクスチャを削除
 ******************************/
void	TexCache::clear_cache(void)
{
	cache_num--;
	cache_mem_size -= cache[cache_num]->mem_size;
	delete	cache[cache_num];						// テクスチャ削除
}

/****************************************
    リソース読み込み
		引数	_type = リソース種類
				_data = リソースデータ
 ****************************************/
TexCache::TexCache(short _type, const void* _data)
{
	type = _type;					// リソース種類
	data = _data;					// リソースデータ

	switch ( _type ) {
	  case RES_MEMORY :				// メモリ
		load_png((const u8*)_data);
		break;

	  case RES_ASSET :				// assetsファイル
		{
			u8*	_p = (u8*)load_asset((const char*)_data);

			load_png(_p);
			free(_p);
		}
		break;

	  default :
		assert(FALSE);
		break;
	}
	switch ( format ) {				// VRAM占有サイズ計算
	  case FORMAT_RGBA :
		mem_size = width*height*4;
		break;

	  case FORMAT_RGB :
		mem_size = width*height*3;
		break;
	}
}


・テクスチャ取得
テクスチャリソースの指定は、メモリ(アドレス指定)または assetsのファイル(ファイル名指定)で行います。
まず同じリソースのテクスチャをキャッシュから検索します。このとき処理速度のことを考えて、ファイルの一致はファイル名文字列の中身の比較ではなく、アドレスを比較することにしています。
キャッシュから見つかった場合(179行目から)、優先度を上げて(他のテクスチャの優先度を下げて、先頭に登録する)そのテクスチャを返します。
見つからなかった場合(191行目から)、はじめにキャッシュ上の総数がオーバーしていたときは優先度最低の一枚を削除します。
次にキャッシュ上の総数の半分の優先度で登録します。優先度を最高ではなく半分にしているのは、今キャッシュに無いテクスチャは一時的なものかもしれないということによります。この値が最も効率が良いのかはわかりません。
最後に、キャッシュ上の総サイズがオーバーしていたときは、サイズを下回るまで優先度の低いテクスチャから削除します。テクスチャを登録してから削除しているので一時的にVRAM上でのサイズがオーバーしているのですが、流れをわかりやすくするためにこうしています。


スプライトをテクスチャキャッシュに対応させます。

Sprite.cpp
/******************************************
    設定(リソース指定)
		引数	_type   = リソース種類
				_data   = リソースデータ
				_coord  = UV座標
				_origin = 原点位置
 ******************************************/
void	Sprite::set(short _type, const void* _data, SRect const* _coord, int _origin)
{
	res_type = _type;					// リソース設定
	res_data = _data;
	status   = 1;						// UV座標未設定

	texcoord[0][X] = (GLfloat)_coord->x;			// 仮UV座標
	texcoord[0][Y] = (GLfloat)_coord->y;
	texcoord[1][X] = (GLfloat)(_coord->x + _coord->w);
	texcoord[2][Y] = (GLfloat)(_coord->y + _coord->h);
	width	= _coord->w;				// 幅
	height	= _coord->h;				// 高さ
	ox = _origin;
}

void	Sprite::set(short _type, const void* _data, int _origin)
{
	res_type = _type;					// リソース設定
	res_data = _data;
	status   = 2;						// 幅・高さ未設定

	texcoord[0][X] = texcoord[2][X] = 0.0f;			// UV座標
	texcoord[0][Y] = texcoord[1][Y] = 0.0f;
	texcoord[1][X] = texcoord[3][X] = 1.0f;
	texcoord[2][Y] = texcoord[3][Y] = 1.0f;
	ox = _origin;
}

リソース指定によるスプライトの設定部分です。
テクスチャを読み込まないと決定しない値があるので、状態を statusに覚えておいて読み込んだ時に設定するようにします。

void	Sprite::draw(GLfloat* _vertex)
{
	if ( res_type >= 0 ) {			// テクスチャキャッシュ
		Renderer::use_shader(Renderer::SHADER_TEXTURE);		// シェーダ
		texture = TexCache::get_texture(res_type, res_data);		// テクスチャ取得
		texture->bind();									// テクスチャ
		if ( status != 0 ) {
			switch ( status ) {
			  case 1 :				// UV座標設定
				texcoord[0][X] /= texture->width;  texcoord[2][X] = texcoord[0][X];
				texcoord[0][Y] /= texture->height; texcoord[1][Y] = texcoord[0][Y];
				texcoord[1][X] /= texture->width;  texcoord[3][X] = texcoord[1][X];
				texcoord[2][Y] /= texture->height; texcoord[3][Y] = texcoord[2][Y];;
				break;

			  case 2 :				// 幅・高さ設定
				width  = texture->width;
				height = texture->height;
				break;
			}
			set_origin(ox);
			status = 0;
		}
		Renderer::set_texcoord(&texcoord[0][0]);			// UV座標
	}
	else if ( texture ) {			// テクスチャ有り
		Renderer::use_shader(Renderer::SHADER_TEXTURE);		// シェーダ
		texture->bind();									// テクスチャ
		Renderer::set_texcoord(&texcoord[0][0]);			// UV座標
	}
	else {							// テクスチャ無し
		Renderer::use_shader(Renderer::SHADER_PLAIN);		// シェーダ
	}
	sys::Renderer::set_vertex(_vertex);						// 頂点
	Renderer::set_color((GLubyte*)spr_color);				// カラー

	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);					// 描画
}

描画部分です。
キャッシュからテクスチャを読み込んで、未設定の値を設定します。あとは通常の描画と同じです。


使用例です。
テクスチャキャッシュの容量を5枚(def.hの TEX_CACHE_NUMで定義)にして、11枚のテクスチャを扱っています。
キャッシュの枚数が5枚なのはあくまでテストのためであって、通常はもっと大きくするものです。少なくとも、1度に画面に出るテクスチャの枚数を超える必要があります。

AppMain.cpp

#include "common.h"
#include "Sprite.h"


// スプライト
enum
{
	SPR_PHOTO	= 0,			// 背景
	SPR_FISH,					// 魚
	SPR_MAX		= SPR_FISH + 10,
};

static sys::Sprite	sprite[SPR_MAX];			// スプライト

static int	cnt;			// カウンタ


/************
    初期化
 ************/
void	init_app(void)
{
	static const
	char*	tex_name[SPR_MAX] =
			{
				"photo.png",
				"animal_dolphin.png",
				"animal_hoojirozame.png",
				"fish_buri.png",
				"fish_ishidai.png",
				"fish_katsuo.png",
				"fish_kingyo.png",
				"fish_mola.png",
				"fish_tako.png",
				"fish_tatsunootoshigo.png",
				"sakana_mure.png",
			};

	for (int i = 0; i < SPR_MAX; i++) {			// スプライト初期化(リソース指定)
		sprite[i].set(sys::TexCache::RES_ASSET, tex_name[i]);
	}

	cnt = 0;			// カウンタ
}

/******************************
    稼働
		戻り値	アプリ続行か
 ******************************/
Bool	update_app(void)
{
	sprite[SPR_PHOTO].draw(0.0f, 0.0f);			// 背景

												// 魚
	sprite[SPR_FISH + (((cnt*4 + 640)/1280)*2 % 10)].draw(640 - ((cnt*4 + 640) % 1280), -128.0f);
	sprite[SPR_FISH + (((cnt*4/1280)*2 + 1) % 10)].draw(640 - (cnt*4 % 1280), -128.0f);
	cnt++;

	return	TRUE;
}

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

テクスチャは持たず、リソースを指定してスプライトを設定しています。
描画方法に変更はありません。


さかなの行進


※魚スプライトの画像は「かわいいフリー素材集 いらすとや」から使わせていただきました。どうもありがとうございます。

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

スプライトで描画2013/11/08

テクスチャで画像を描画できるようになりましたが、毎度OpenGLを直接叩くのでは手順が面倒です。
描画を管理するスプライトクラスを作って、手軽に描画できるようにしましょう。

・スプライトクラス(sys::Sprite)
スプライトはテクスチャと、描画する範囲(切り抜き)から成っています。つまり1枚のテクスチャには複数のスプライトの画像を収めることができます(描画時、テクスチャの切り替えにはコストがかかるので、なるべく少ないテクスチャにまとめておいた方が効率が良いです)。

Sprite.h
#ifndef	___SPRITE_H___
#define	___SPRITE_H___

#include	"common.h"
#include	"Texture.h"


namespace sys
{

/****************
    スプライト
 ****************/
class Sprite
{
protected :
	static u32 const*	spr_color;		// カラー

public :

// 原点位置
enum
{
	X_CENTER	= 0x00,			// センタリング
	X_LEFT		= 0x01,			// 左揃え
	X_RIGHT		= 0x02,			// 右揃え
	Y_CENTER	= 0x00,			// センタリング
	Y_TOP		= 0x10,			// 上揃え
	Y_BOTTOM	= 0x20,			// 下揃え
};

	static void		set_color(u32 _color = 0xffffffff);		// カラー設定
	static void		set_color(u8 _r, u8 _g, u8 _b, u8 _a = 0xff)
					{
						set_color(RGBA(_r, _g, _b, _a));
					}
	static void		set_color(u32 const*);


	Texture*	texture;			// テクスチャ
	GLfloat		texcoord[4][XY];	// UV座標
	short		width, height;		// 大きさ
	short		ox, oy;				// 原点

	void	set(Texture*, SRect const*, int _origin = X_CENTER | Y_CENTER);				// 設定
	void	set(Texture*, int _origin = X_CENTER | Y_CENTER);							// 設定(テクスチャに合わせた大きさ)
	void	set(short, short, int _origin = X_CENTER | Y_CENTER);						// 設定(テクスチャ無し)
	void	set(short, const void*, int _origin = X_CENTER | Y_CENTER);
	void	set_origin(int _origin = X_CENTER | Y_CENTER);								// 原点位置設定

	void	draw(GLfloat*);			// 描画
	void	draw(float, float);
	void	draw(float, float, float, float);
	void	draw(float _x, float _y, float _scl)
			{
				draw(_x, _y, _scl, _scl);
			}
	void	draw(float, float, float, float, float);
	void	draw(float const*);
};

}

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

Sprite.cpp
/*******************************

		スプライト

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

#include "Sprite.h"
#include "Renderer.h"


namespace sys
{

u32 const*	Sprite::spr_color;			// カラー


/**************************************
    設定
		引数	_tex    = テクスチャ
				_coord  = UV座標
				_origin = 原点位置
 **************************************/
void	Sprite::set(Texture* _tex, SRect const* _coord, int _origin)
{
	texture	= _tex;						// テクスチャ
	texcoord[0][X] = texcoord[2][X] = (GLfloat)_coord->x/_tex->width;		// UV座標
	texcoord[0][Y] = texcoord[1][Y] = (GLfloat)_coord->y/_tex->height;
	texcoord[1][X] = texcoord[3][X] = (GLfloat)(_coord->x + _coord->w)/_tex->width;
	texcoord[2][Y] = texcoord[3][Y] = (GLfloat)(_coord->y + _coord->h)/_tex->height;
	width	= _coord->w;				// 幅
	height	= _coord->h;				// 高さ
	set_origin(_origin);				// 原点位置設定
}

void	Sprite::set(Texture* _tex, int _origin)
{
	texture	= _tex;						// テクスチャ
	texcoord[0][X] = texcoord[2][X] = 0.0f;			// UV座標
	texcoord[0][Y] = texcoord[1][Y] = 0.0f;
	texcoord[1][X] = texcoord[3][X] = 1.0f;
	texcoord[2][Y] = texcoord[3][Y] = 1.0f;
	width	= _tex->width;				// 幅
	height	= _tex->height;				// 高さ
	set_origin(_origin);				// 原点位置設定
}

void	Sprite::set(short _width, short _height, int _origin)
{
	texture	= NULL;						// テクスチャ無し
	width	= _width;					// 幅
	height	= _height;					// 高さ
	set_origin(_origin);				// 原点位置設定
}

/************************************
    原点位置設定
		引数	_origin = 原点位置
 ************************************/
void	Sprite::set_origin(int _origin)
{
	switch ( _origin & 0x0f ) {
	  case X_CENTER :				// センタリング
		ox = width/2;
		break;

	  case X_LEFT :					// 左揃え
		ox = 0;
		break;

	  case X_RIGHT :				// 右揃え
		ox = width;
		break;
	}

	switch ( _origin & 0xf0 ) {
	  case Y_CENTER :				// センタリング
		oy = height/2;
		break;

	  case Y_TOP :					// 上揃え
		oy = 0;
		break;

	  case Y_BOTTOM :				// 下揃え
		oy = height;
		break;
	}
}


/***********************************
    カラー設定
		引数	_color = カラー値
 ***********************************/
void	Sprite::set_color(u32 _color)
{
	if ( _color == 0xffffffff ) {			// デフォルト
		static const
		u32		color_white[] =
				{
					0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
				};

		spr_color = color_white;
	}
	else if ( (_color != spr_color[0]) || (_color != spr_color[1]) || (_color != spr_color[2]) || (_color != spr_color[3]) ) {
		u32*	_p = (u32*)Renderer::get_prim_buffer(sizeof(u32)*4);		// カラーバッファ

		_p[0] = _color;
		_p[1] = _color;
		_p[2] = _color;
		_p[3] = _color;
		spr_color = _p;
	}
}

void	Sprite::set_color(u32 const* _color)
{
	u32*	_p = (u32*)Renderer::get_prim_buffer(sizeof(u32)*4);		// カラーバッファ

	_p[0] = _color[0];
	_p[1] = _color[1];
	_p[2] = _color[2];
	_p[3] = _color[3];
	spr_color = _p;
}


/***********************************
    描画
		引数	_x, _y   = 位置
				_sx, _sy = 拡大率
				_rot     = 回転角
 ***********************************/
void	Sprite::draw(GLfloat* _vertex)
{
	if ( texture ) {				// テクスチャ有り
		Renderer::use_shader(Renderer::SHADER_TEXTURE);		// シェーダ
		texture->bind();									// テクスチャ
		Renderer::set_texcoord(&texcoord[0][0]);			// UV座標
	}
	else {							// テクスチャ無し
		Renderer::use_shader(Renderer::SHADER_PLAIN);		// シェーダ
	}
	sys::Renderer::set_vertex(_vertex);						// 頂点
	Renderer::set_color((GLubyte*)spr_color);				// カラー

	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);					// 描画
}

void	Sprite::draw(float _x, float _y)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	_vertex[0*XY + X] = _vertex[2*XY + X] = _x - ox;
	_vertex[0*XY + Y] = _vertex[1*XY + Y] = _y - oy;
	_vertex[1*XY + X] = _vertex[3*XY + X] = _x + (width - ox);
	_vertex[2*XY + Y] = _vertex[3*XY + Y] = _y + (height - oy);
	draw(_vertex);
}

void	Sprite::draw(float _x, float _y, float _sx, float _sy)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	_vertex[0*XY + X] = _vertex[2*XY + X] = _x - _sx*ox;
	_vertex[0*XY + Y] = _vertex[1*XY + Y] = _y - _sy*oy;
	_vertex[1*XY + X] = _vertex[3*XY + X] = _x + _sx*(width - ox);
	_vertex[2*XY + Y] = _vertex[3*XY + Y] = _y + _sy*(height - oy);
	draw(_vertex);
}

void	Sprite::draw(float _x, float _y, float _sx, float _sy, float _rot)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	float	_cos = cosf(_rot), _sin = sinf(_rot),
			_cx0, _sx0, _cy0, _sy0, _cx1, _sx1, _cy1, _sy1;

	_cx0 = -_sx*ox; _sx0 = _sin*_cx0; _cx0 *= _cos;
	_cy0 = -_sy*oy; _sy0 = _sin*_cy0; _cy0 *= _cos;
	_cx1 = _sx*(width - ox); _sx1 = _sin*_cx1; _cx1 *= _cos;
	_cy1 = _sy*(height - oy); _sy1 = _sin*_cy1; _cy1 *= _cos;

	_vertex[0*XY + X] = _x + _cx0 - _sy0;
	_vertex[0*XY + Y] = _y + _cy0 + _sx0;
	_vertex[1*XY + X] = _x + _cx1 - _sy0;
	_vertex[1*XY + Y] = _y + _cy0 + _sx1;
	_vertex[2*XY + X] = _x + _cx0 - _sy1;
	_vertex[2*XY + Y] = _y + _cy1 + _sx0;
	_vertex[3*XY + X] = _x + _cx1 - _sy1;
	_vertex[3*XY + Y] = _y + _cy1 + _sx1;
	draw(_vertex);
}

void	Sprite::draw(float const* vertex)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	for (int i = 0; i < 4*XY; i++) {
		_vertex[i] = vertex[i];
	}
	draw(_vertex);
}

}

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

・初期化
void set(Texture* _tex, SRect const* _coord, int _origin)
void set(Texture* _tex, int _origin)
テクスチャ _tex、描画範囲座標 _coordでスプライトを初期化します。描画範囲を省略すると、テクスチャ自体と同じ大きさ(1枚のテクスチャ = 1枚のスプライト)になります。
_originはスプライトの原点を指定します。省略すると画像の中心が原点になります(sys::Sprite::X_CENTER | sys::Sprite::Y_CENTER)。

void set(short _width, short _height, int _origin)
テクスチャを指定しないと、幅 _width、高さ _heightの単色ポリゴンになります。

・描画
void draw(float _x, float _y)
座標(_x, _y)に描画します。

void draw(float _x, float _y, float _sx, float _sy)
縦と横の拡大率を指定して描画します。

void Sprite::draw(float _x, float _y, float _sx, float _sy, float _rot)
角度 _rot(単位:ラジアン)で回転して描画します。

void draw(float const* vertex)
直接4頂点(vertex[4][XY])を指定して描画します。


さて描画時に頂点座標をOpenGLに設定しますが、このデータをスプライト自体に持っておくと1度に1枚のスプライトしか描画できなくなります。よってRendererの方で汎用的なバッファを持っておいて、描画の度にそこから使用するようにします。
なお、UV座標はスプライトごとに固定なのでスプライト側で持っておきます。

Rendererの汎用的なプリミティブバッファの管理部分です。
def.hにバッファサイズ PRIM_BUF_SIZEを定義しておきます。

Renderer.cpp
	prim_buffer = (u8*)memalign(4, PRIM_BUF_SIZE);			// プリミティブ用汎用バッファ
	prim_p = 0;

/****************************************
    プリミティブバッファ取得
			引数	_size = 使用サイズ
			戻り値	バッファ
 ****************************************/
void*	Renderer::get_prim_buffer(u32 _size)
{
	if ( prim_p + _size > PRIM_BUF_SIZE ) {
		prim_p = 0;
	}

	void*	_ret = (void*)(prim_buffer + prim_p);

	prim_p += _size;
	return	_ret;
}


また、テクスチャのバインドなど変化していないのに設定し直すのは効率が悪いので、現在の値を覚えておくようにします。
ただし、頂点は無条件で設定しています。全く同じ座標に何枚もプリミティブを描画することは少ないと思われますので。
/**********************************************
    テクスチャ使用
		引数	tex = テクスチャオブジェクト
 **********************************************/
void	Renderer::bind_texture(GLuint tex)
{
	if ( tex != current_texture ) {
		glBindTexture(GL_TEXTURE_2D, tex);
		current_texture = tex;
	}
}

/********************************
    カラー設定
		引数	color = カラー
 ********************************/
void	Renderer::set_color(GLubyte const* color)
{
	if ( color != current_color ) {
		glVertexAttribPointer(current_shader->color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, color);
		current_color = color;
	}
}

/********************************
    テクスチャUV座標設定
		引数	coord = UV座標
 ********************************/
void	Renderer::set_texcoord(GLfloat const* coord)
{
	if ( coord != current_texcoord ) {
		glVertexAttribPointer(current_shader->texcoord, 2, GL_FLOAT, GL_FALSE, 0, coord);
		current_texcoord = coord;
	}
}

/***********************************
    頂点座標設定
		引数	vertex = 頂点座標
 ***********************************/
void	Renderer::set_vertex(GLfloat const* vertex)
{
	glVertexAttribPointer(current_shader->position, 2, GL_FLOAT, GL_FALSE, 0, vertex);
}


それでは、実際の使用例です。
AppMain.cpp

#include "common.h"
#include "Sprite.h"


// テクスチャ
enum
{
	TEX_PHOTO = 0,			// 背景
	TEX_BALL,				// ビー玉
	TEX_MAX,
};

// スプライト
enum
{
	SPR_PHOTO = 0,			// 背景
	SPR_BALL_BLUE,			// 青ビー玉
	SPR_BALL_GREEN,			// 緑ビー玉
	SPR_BALL_RED,			// 赤ビー玉
	SPR_BALL_YELLOW,		// 黄ビー玉
	SPR_MAX,
};

static sys::Texture		texture[TEX_MAX];			// テクスチャ
static sys::Sprite		sprite[SPR_MAX];			// スプライト

static int	cnt;			// カウンタ


/************
    初期化
 ************/
void	init_app(void)
{
	static const
	char*	tex_name[TEX_MAX] =
			{
				"photo.png",
				"biidama.png",
			};
	u8*		_buf;

	for (int i = 0; i < TEX_MAX; i++) {				// テクスチャ読み込み
		_buf = (u8*)sys::load_asset(tex_name[i]);
		texture[i].load_png(_buf);
		free(_buf);
	}


	struct SprDef
	{
		short	tex_num;			// テクスチャ番号
		SRect	coord;				// UV座標
	};

	static const
	SprDef	spr_def[SPR_MAX] =
			{
				{TEX_PHOTO,		{  0,   0, 640, 960}},			// 背景
				{TEX_BALL,		{  0,   0,  60,  60}},			// 青ビー玉
				{TEX_BALL,		{ 60,   0,  60,  60}},			// 緑ビー玉
				{TEX_BALL,		{  0,  60,  60,  60}},			// 赤ビー玉
				{TEX_BALL,		{ 60,  60,  60,  60}},			// 黄ビー玉
			};

	for (int i = 0; i < SPR_MAX; i++) {				// スプライト設定
		sprite[i].set(&texture[spr_def[i].tex_num], &spr_def[i].coord);
	}

	cnt = 0;			// カウンタ
}

/******************************
    稼働
		戻り値	アプリ続行か
 ******************************/
Bool	update_app(void)
{
	sprite[SPR_PHOTO].draw(0.0f, 0.0f);			// 背景
	for (int i = 0; i < 4; i++) {				// ビー玉
		int		t = ((cnt + i*15) % 60) - 30;

		sprite[SPR_BALL_BLUE + i].draw(i*128 - 192, 400 - (30*30 - t*t)/2);
	}
	cnt++;

	return	TRUE;
}

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

跳ねるビー玉

※ビー玉の画像は「かわいいフリー素材集 いらすとや」様から使わせていただきました。どうもありがとうございます。


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