エイバースの中の人

株、不動産、機械学習など。

カテゴリ: iOS

OpenGLにはフォント描画命令が無いので、テクスチャに文字を描き、そのテクスチャをポリゴンに貼って描画することで、文字列の描画を実現します。

テクスチャに直接文字を書くことはできないので、CPU側に確保したメモリ領域に文字を描画し、そのメモリ領域をVRAMに転送することで、テクスチャに文字を書きます。

メモリ領域に文字を書くには、NSString*のdrawInRectを使います。メモリ領域からテクスチャへの転送には、glTexImage2Dを使います。

ただし、テクスチャを毎回glTexImage2Dで新規確保すると重いので、最初に一度だけglTexImage2Dでテクスチャを確保して、以降はglTexSubImage2Dで転送だけを行います。

以降の説明では、以下の構造体を使っています。


//確保したテクスチャを格納
struct DynamicTextureStruct{
GLuint id;
int width;
int height;
int original_width;
int original_height;
};

//文字列の描画先のメモリを格納
struct FontTextureMipmap{
struct DynamicTextureStruct texture; //テクスチャ情報構造体
CGContextRef _context; //コンテキスト
GLubyte* data; //テクスチャのRGBA実データ
};

//文字列描画用のフォント
UIFont *m_font; //文字の描画に使うフォント


データの流れは次のようになります。


初期化
(1)文字列描画用のメモリ領域を確保してglTexImage2Dでテクスチャを確保
(2)GLubyte* dataへの文字列描画コンテキストCGContextRef _contextとフォントUIFont *m_fontを確保
ゲームループ
(3)NSString*のdrawInRectでGLubyte* dataに文字を描画
(4)glTexSubImage2Dでテクスチャに転送


(1)VRAMにテクスチャを生成します。


//テクスチャサイズを定義する
int s=FONT_TEXTURE_MIPMAP_SIZE[i];
m_p_font_texture->texture.width=s;
m_p_font_texture->texture.height=s;

// テクスチャを作成する
glGenTextures(1, &(m_p_font_texture->texture.id));

// テクスチャをバインドする
glBindTexture(GL_TEXTURE_2D, m_p_font_texture->texture.id);

// テクスチャの設定を行う
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glEnable(GL_TEXTURE_2D);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);

//テクスチャのRGBAデータの配列を確保する
m_p_font_texture->data = (GLubyte *)malloc(m_p_font_texture->texture.width * m_p_font_texture->texture.height * 4);

//テクスチャデータをVRAM上に転送し領域を確保する
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_p_font_texture->texture.width,m_p_font_texture->texture.height,0, GL_BGRA, GL_UNSIGNED_BYTE, m_p_font_texture->data);


(2)文字描画用のコンテキストを確保します


//文字描画用のコンテキストを作成する
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
m_p_font_texture->_context = CGBitmapContextCreate(m_p_font_texture->data, m_p_font_texture->texture.width, m_p_font_texture->texture.height, 8, m_p_font_texture->texture.width * 4, colorSpace, kCGImageAlphaPremultipliedLast);

//フォントを確保する
m_font = [UIFont systemFontOfSize:14];


(3)文字列を描画します


//実際の描画サイズを取得
CGSize size=[text sizeWithFont:m_font constrainedToSize:CGSizeMake(sx,512) lineBreakMode:UILineBreakModeWordWrap];
m_p_font_texture->texture.original_width=size.width;
m_p_font_texture->texture.original_height=size.height;

//文字画像は上下反転しているので描画時にUV反転する
//また、クリッピングエリアも反転するので注意

// 文字を描画する
memset(m_p_font_texture->data,0,m_p_font_texture->texture.width*m_p_font_texture->texture.height*4);
UIGraphicsPushContext(m_p_font_texture->_context);
UIColor *color=[UIColor colorWithRed:r/255.0f green:g/255.0f blue: b/255.0f alpha:1.0f];
[color set];
[text drawInRect:CGRectMake(0,m_p_font_texture->texture.height-m_p_font_texture->texture.original_height,sx,m_p_font_texture->texture.original_height) withFont:m_font lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];
UIGraphicsPopContext();


(4)文字をテクスチャに転送します


// テクスチャをバインドする
glBindTexture(GL_TEXTURE_2D, m_p_font_texture->texture.id);

// テクスチャを更新する
glTexSubImage2D(GL_TEXTURE_2D, 0, 0,m_p_font_texture->texture.height-m_p_font_texture->texture.original_height, m_p_font_texture->texture.width,m_p_font_texture->texture.original_height, GL_RGBA, GL_UNSIGNED_BYTE, m_p_font_texture->data);
}


後はこのテクスチャを描画すればOKです。

尚、glTexSubImage2Dで転送する画像の横幅は、最初に確保したテクスチャ領域の横幅になります。CPU->VRAMへの転送は結構遅いので、複数のサイズの文字描画用テクスチャを用意しておいて、文字サイズに応じて使うテクスチャを切り替えるとよいかと思います。

----------------------------------------
2011/4/19追記
続編がiPhoneのOpenGLで文字を書く場合の高速化法にあります。

また、ソースコードを
http://www.abars.biz/blog/FontTexture.h
http://www.abars.biz/blog/FontTexture.mm
に置きましたのでどうぞ。

9fa0b0c3.jpgしばらくPhonegap上でメトセラを動かしていたんですが、やはりゲームに使うとなると描画速度がネックとなることが分かりました。また、HTTP REQUESTでネイティブアプリに命令を出すよりも、NSString *test=[m_web_view stringByEvaluatingJavaScriptFromString:@"getDisplayList();"];のようにstringByEvaluatingJavaScriptFromStringを使って文字列で命令リストを取った方が高速なことも分かりました。

ということで、方針としては、Phonegapを使うのは止めて、Phonegapっぽいフレームワークを自分で作ることにしました。Phonegapとの違いは、

・HTTP REQUESTによるコマンド送信は行わず、stringByEvaluatingJavaScriptFromStringによる命令リストの形で、JavaScriptとネイティブアプリ連携をする
・WebviewはJavascriptエンジンの実行にしか使わず、描画は全てネイティブアプリ側で行う
・描画はOpenGLで行う

の三点です。これで、ゲームエンジンはJavaScriptで動かしつつも、OpenGLの高速な描画の恩恵が受けられます。ということでここ四日間ぐらいフレームワークを作っていまして、さきほどようやく文字が出るようになりました。

速度はすばらしく出ていまして、Phonegap、というかWebkit上で描画すると5FPSだったのが、普通に20FPSで動くようになりました。さらに、色変換もかけられるため、アバターの色設定もできます。

プログラミング的には、初OpenGLだったので、基本的なことは下記書籍が大変役に立ちました。



また、OpenGLの文字関係は、http://gamesfromwithin.com/remixing-opengl-and-uikitが詳しかったです。

ということで、iTunesStore販売用完全オフラインディレクターズカット版メトセラを作ってますです。230円ぐらいの予定。詳細はまたまとまったらご連絡します。

JavaScriptからiPhoneへのコマンド出力は、phonegap.jsで行われています。PhoneGap.execでコマンドをキューに登録、PhoneGap.run_commandで順次処理をしているようです。PhoneGap.run_commandでは、コマンドを"gap://〜"という形式にして、document.location = url;でページ遷移コマンドを発生させます。

ObjectiveCではページ遷移する直前にWebViewのshouldStartLoadWithRequestが呼ばれるので、PhoneGapDelegate.mにおいて[[url scheme] isEqualToString:@"gap"]の場合にコマンドを処理し、[theWebView stringByEvaluatingJavaScriptFromString:@"PhoneGap.queue.ready = true;"];で処理の終了をJavaScriptに通知、return NO;で実際のページ遷移を禁止しているようです。

尚、ajaxのXmlHttpRequestではshouldStartLoadWithRequestが呼ばれないようなので、コマンドの連続動作をサポートする場合は、urlに複数のコマンドをパックするように改造するしかないかなぁと思っています。

せっかくの三連休ということでPhonegapとJavascriptでネイティブiPhoneアプリを作ってみているのでメモ。

Phonegapはマルチプラットフォームに対応した中間レイヤです。基本はHTML5で開発して、HTML5で出来ない部分をPhonegapのAPIを通じて補います。例えばiPhoneのSafariですと、サウンドの制御や電話帳へのアクセスができませんが、Phonegapを通すことで、これらのネイティブAPIを呼び出すことができます。

仕組みとしては、全画面でブラウザが表示され、そこでJavascriptが走ります。ネイティブAPIはJavaScriptのAPIとして定義されており、ブラウザからそのAPIが呼び出されると、特殊なURLのHTTP REQUESTが発生し、ネイティブアプリ部分でそのURLをフェッチし、URLに合わせてiPhoneのAPIをcallする形になります。

まずはインストールですが、GettingStarted(iPhone)の手順通りにインストールします。インストールしたphonegap-iphoneフォルダにPhoneGapTutorialがあるので、これを編集して開発するのが一番楽です。iPhoneSDKが4の場合はBaseSDKMissingになりますので、Xcodeのプロジェクト設定から編集します。

ここまで来たら、Tutorialフォルダのwwwフォルダ以下を編集するだけでOKです。ここにサブフォルダを作って画像を置いても大丈夫、自動的にパッケージングされます。APIリファレンスはPhonegap Docsにあります。

ゲームを作る人のキラーAPIはサウンドだと思います。ただ、PhonegapはAVAudioPlayerクラスでサウンドを制御しているため、効果音を重ねられないという問題があります。

ここで、Phonegapのオープンソースという強みが生きてきます。リンクするスタティックライブラリは普通にXcodeでBuildされるため、Phonegap自体を自由に書き換えることができます。phonegap-iphoneと同じ階層にあるPhoneGapLibがコードで、ここを書き換えると、TutorialをBuildする時に一緒にコンパイルされます。

ということで、さくさくとSound.hに

@interface AudioFile : NSObject
{
@public
NSString* successCallback;
NSString* errorCallback;
NSString* downloadCompleteCallback;
NSString* resourcePath;
NSURL* resourceURL;
AVAudioPlayer* player;
SystemSoundID system_sound_id;
#ifdef __IPHONE_3_0
AVAudioRecorder* recorder;
#endif
}

@property (nonatomic) SystemSoundID system_sound_id;

とSystemSoundID system_sound_id;を追加します。

次に、Sound.mの
 audioFile.player = [[ AVAudioPlayer alloc ] initWithContentsOfURL:resourceURL error:&error];
の後に、
 AudioServicesCreateSystemSoundID(resourceURL, &(audioFile->system_sound_id));
を加えます。三カ所あります。

で、play関数でループ再生指定がされていない場合は効果音と考えて
 if(numberOfLoops!=0){
  [audioFile.player play];
 }else{
  AudioServicesPlaySystemSound(audioFile->system_sound_id);
 }
とすればOK。

効果音はAudioServicesPlaySystemSoundで再生されるので、ゲームでも実用的な重ね合わせが実現できます。尚、audioPlayerはmp3が使えるのですが、AudioServicesPlaySystemSoundはwavしか使えないので注意です。

現状、なぜかSDK=iPhoneOS3.2+DebugBuildでしか動いていませんが、まぁほとんどHTML5で動くからDebugBuildでもいいかということで全く問題ありません。WEBアプリを移植するのはかなり簡単な印象ですね。

ということで8/Eに向けて少し作ってます。230円ぐらいで売れるといいなぁ。

↑このページのトップヘ