タッチパネル入力 その1 ― 2013/11/16
画面への出力がひと通り落ち着いたので、次はタッチパネルからの入力を行います。
タッチパネルの入力情報は java側で受け取ります。
まず一番上にあるビューに通知され、そこでスルーされると次のビュー、最後にはアクティビティに通知されるようになっています。
ここでは一つだけのビュー、サーフェスビューで処理します。
また、せっかくなのでマルチタッチに対応してみます。
BaseActivity.java
サーフェスビュー BaseView内
画面をタッチする、離す等のタッチイベントが起こると onTouchEventが呼ばれます。
Android SDKでダブルタップ等の動作を判定することもできるのですが、ここでは
・画面を押しているか
・押している場合、その位置
だけを取得して、後の処理は native側に任せることとします。
取得した状態は、どのみち nativeはフレーム単位で動いているのでイベントが起きる度ではなく、updateNativeのタイミングで送るようにします。
レンダラー BaseRenderer内
最新の状態だけを送るので、炎のコマで秒間200万回入力しても無視されます。
native側の処理は、また次回に。
タッチパネルの入力情報は 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
読み込み部分です。
ヘッダから幅と高さを取得して、データをそのまま作成関数に送っています。
"圧縮テクスチャ"というフォーマットのデータであってデータ自体は圧縮されていないので、展開処理は必要ありません。逆にいうと、データ自体にはまだ圧縮の余地があるということになります。サンプルでは assetsのzip圧縮に任せちゃっていますが。
テクスチャ作成部分です。
画像転送が glTexImage2Dから glCompressedTexImage2Dに代わり、フォーマットに GL_ETC1_RGB8_OESを指定しています。
あとは、読み込み部分でPNGとPKMに分けたり、キャッシュのサイズ計算でPKMに対応したりと細かな変更はありますが、ソースは省略します。
見た目はたいして変わらないので、スクリーンショットも省略していますが、
※ビー玉の画像は「かわいいフリー素材集 いらすとや」から使わせていただいています。どうもありがとうございます。
プロジェクト一式は、こちらから。
圧縮テクスチャは、画像を圧縮した状態(サイズが小さい)で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
終了・一時停止・再開時に native側の関数を呼んでいるだけですが、再開時だけ少し変えています。
Androidでは一旦電源ボタンを押してサスペンドし、もう一度ボタンを押すとレジュームで onResume()が呼ばれますが、このとき画面はロック状態になっています。
ゲームなどでは画面が見えないのに動いてしまっては都合が悪いので、ロック状態の場合は解除を待ってからアプリを再開させています。
native側のソースです.
SysMain.cpp
ポーズ時の処理で描画管理を終了させています。
テクスチャキャッシュをクリアしていますので、テクスチャのVRAMからの削除はシステムに任せられます。
アプリ側の処理は、
・init_app() 開始時
・quit_app() 終了時
・pause_app() 一時停止時
・resume_app() 再開時
を、それぞれのタイミングで呼んでいます。
サンプルではログを表示しているだけなので、ソースは省略します。
プロジェクト一式は、こちらです。
今までは開始時も含めてレジューム時に全て初期化していましたが、メモリを解放しないまま確保し直すなど行儀の悪いことになっていました。
今回はきちんとサスペンド・レジュームの処理をやってみます。
久々に 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
Texture.cpp
・テクスチャ取得
テクスチャリソースの指定は、メモリ(アドレス指定)または assetsのファイル(ファイル名指定)で行います。
まず同じリソースのテクスチャをキャッシュから検索します。このとき処理速度のことを考えて、ファイルの一致はファイル名文字列の中身の比較ではなく、アドレスを比較することにしています。
キャッシュから見つかった場合(179行目から)、優先度を上げて(他のテクスチャの優先度を下げて、先頭に登録する)そのテクスチャを返します。
見つからなかった場合(191行目から)、はじめにキャッシュ上の総数がオーバーしていたときは優先度最低の一枚を削除します。
次にキャッシュ上の総数の半分の優先度で登録します。優先度を最高ではなく半分にしているのは、今キャッシュに無いテクスチャは一時的なものかもしれないということによります。この値が最も効率が良いのかはわかりません。
最後に、キャッシュ上の総サイズがオーバーしていたときは、サイズを下回るまで優先度の低いテクスチャから削除します。テクスチャを登録してから削除しているので一時的にVRAM上でのサイズがオーバーしているのですが、流れをわかりやすくするためにこうしています。
スプライトをテクスチャキャッシュに対応させます。
Sprite.cpp
リソース指定によるスプライトの設定部分です。
テクスチャを読み込まないと決定しない値があるので、状態を statusに覚えておいて読み込んだ時に設定するようにします。
描画部分です。
キャッシュからテクスチャを読み込んで、未設定の値を設定します。あとは通常の描画と同じです。
使用例です。
テクスチャキャッシュの容量を5枚(def.hの TEX_CACHE_NUMで定義)にして、11枚のテクスチャを扱っています。
キャッシュの枚数が5枚なのはあくまでテストのためであって、通常はもっと大きくするものです。少なくとも、1度に画面に出るテクスチャの枚数を超える必要があります。
AppMain.cpp
テクスチャは持たず、リソースを指定してスプライトを設定しています。
描画方法に変更はありません。
場面がはっきりと分かれていて、一つ一つの場面で使用するテクスチャが十分小さい場合は自前でテクスチャを入れ替えても良いのですが、なかなかそうもいかなかったり、面倒だったりするのでテクスチャキャッシュという機構を使ってシステムにやらせてみます。
※"テクスチャキャッシュ"というと、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
Sprite.cpp
・初期化
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
また、テクスチャのバインドなど変化していないのに設定し直すのは効率が悪いので、現在の値を覚えておくようにします。
ただし、頂点は無条件で設定しています。全く同じ座標に何枚もプリミティブを描画することは少ないと思われますので。
それでは、実際の使用例です。
AppMain.cpp
描画を管理するスプライトクラスを作って、手軽に描画できるようにしましょう。
・スプライトクラス(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 *************************************************/

最近のコメント