iPhone で OpenGL ES は簡単だ!!_1 (2Dだけど) | ゲームプログラマ志望が福岡で叫ぶ 『絶望』

ゲームプログラマ志望が福岡で叫ぶ 『絶望』

プログラマーになりたい!!!!! あ、風のうわさで聞いた最近若者で流行っているトゥイッターなるものを始めてみました (・ト・) @toshi_desu_yo

はい!
真面目に更新して行こうとぉ・・・・、、思います!


iPhoneでアプリを作成する時に画像の描画をしますが、
一度に画面に出す画像の枚数が多くなってくると、思った速度が出ず
に悩んだりすることが出てきます。(多分)


そういう時にGPUをフルに使ってくれる OpenGL ES を使用するのですが、



「 むずい 」 「 めんどくさい 」 「 もういやだ!」
の 3M が襲いかかってきます。



 OpenGL ES の敷居が高い!!(半年前のわたくし)
と思われがちですが、実際そうでもないです。  ( 2Dの場合は )


一個ずつ見ていくと初めての方でも意外に簡単だったりするものなので、
時間をかけてゆるりと初期化部分を見ていくことにします 







現在 IOS6 では 初期化がすこぶる簡単になったそうなので、
今更こんな旧式の初期化は必要ないかと思いますが、
僕は IOS4 時代の初期化方法を調べた時、OpenGL ESが内部で何を行なっているか「なるへそ」と理解し、
感動した気持ちで胸が昂ぶり震えたので多用しています。

なので、旧式のOpenGL ES の初期化方法( 2D )を記述していきます。

今回は xib も使えるよう OpenGL ESテンプレートから作成していきます!







まずはプロジェクトの作成


⦿ [XCode] → [新規プロジェクト]

[OpenGL Game] で作成


ココでは名前を 「OpenGLES_Test1」 とする


ストーリーボードはよく知らないのでチェック外す。


IOS6 では ViewController で全て初期化から描画までやっている模様!


ViewController.h


では、まず継承元を UIViewController に変更

これで このViewController で勝手にOpenGL ESの初期化をさせない(ΦωΦ)フフフ…

 


.mを見てみると




    (((( ;゚д゚))))  お・おう・・・

みたいな感じになったので、全部削除削除削除削除!


ViewController.m  ViewDidLoad のみにする!!


/** ViewController.m の中身 **/
#import "ViewController.h"

@implementation
ViewController


/**
 * MainGLView が呼び出された。
 */
- (void)viewDidLoad
{
    [super viewDidLoad];
}
@end



そして新しく、

 UIView クラスを継承した MainGLViewクラスを作成し、OpenGL ESの初期化をそこに記述します。



描画は全部 View に行われるので.. 
OpenGL ES も例外ではないです。


名前を MainGLView で作成....






ViewController.xib
を開いて、

ViewControllerView に...


MainGLView と設定。




これで 実行時に呼び出されるViewController の 初期View が MainGLView に設定され、

勝手に MainGLView が呼び出され、ViewControllerに設定されるようになりました。






そして次に  MainGLViewでOpenGL ESの描画準備をします。




取り敢えず、QuartzCore.frameworkプロジェクトに追加します。

これがないと CAEAGLLayer とやらを使用する時に
よくわからないエラーが出ます。  (゚Д゚)




CAEAGLLayer とは OpenGL ES専用のレイヤです。

レイヤとは描画先みたいな感じです。 


OpenGL ES はこのレイヤを持っているUIView に 描画しないと 画面に現れてくれません。
というかエラーになります。


-----
UIViewは常にレイヤを持っています。
 「UIViewに描画 = UIViewが持っているレイヤに描画」 

CoreGraphicsでもなんでも結局は全てレイヤに描画されるわけです。
今回はOpenGL ES 専用のレイヤを使用します。
-----



なので、 MainGLView.m  でこいつを使用するために関数を追加します。


上のほうで...

/**
 * この関数を書くことで OpenGL ESを描画できるレイヤーを自動的にセットする
 */
+ ( Class )layerClass
{
return [ CAEAGLLayer class ];
}


を記述。


これで自動的に MainGLViewは、

 CAEAGLLayer 使用される事になりました。


※これを書かないと通常の レイヤが MainGLView に自動的にセットされます。






つぎに、ヘッダーに変数と関数を追加。

#import
<UIKit/UIKit.h>

@interface
MainGLView : UIView
{
    /** OpenGL ESの描画設定を保持する物 **/
    EAGLContext* mpGLContext;

    /**
フレームバッファとレンダバッファ **/
    GLuint mFrameBuffer;
    GLuint mColorBuffer;    // カラーレンダバッファ
}

/**
 * 描画準備
 */
- ( void ) BeginScene;

/**
 * 描画終了
 */
- ( void ) EndScene;

@end


ヘッダーにEAGLContext という、
OpenGL ES の描画設定等を保存する入れ物を記述。


こいつを描画前に OpenGL に指定してやることで OpenGL はこいつの中にある設定情報を使い、
それにあった描画をするんです。



描画に必要なものが全て入る素晴らしい入れ物だと思っておきます(`・ω・´)


OpenGLを使うためにもまずはこのコンテキストを作成しなければなりません。



*****************************

ここからは


MainGLView.m の

//- (id)initWithFrame:(CGRect)frame
- ( id ) initWithCoder:(NSCoder *)aDecoder
{
    self = [ super 
initWithCoder:aDecoder
 ];
}


に書いていきます。


ちなみにこの記事の一番下に全部記述したソース、あります。




なぜ initWithCoder に書き換えたのかというと、

xib で、ViewContollerに オリジナルのView を設定し、自動的にオリジナルViewを生成するようにした場合
そのViewの initWithFrame は呼び出されません。



なので 初期化をするためには initWithCoder にする必要がある。

*****************************



話を戻し、
 OpenGL ES 用のContext ( 設定情報を保持する物 )

EAGLContext の initWithAPI 関数で作成できます。


[ [EAGLContext allocinitWithAPI: /**  引数 **/ ];



"引数" には
 ● kEAGLRenderingAPIOpenGLES1
 ● kEAGLRenderingAPIOpenGLES2

が指定でき、

 kEAGLRenderingAPIOpenGLES1
        の方は固定機能を使用したOpenGL ES 1.1

 kEAGLRenderingAPIOpenGLES2
        の方は主に自分で機能を全て記述するタイプの OpenGL ES 2.0 を使用します。



2Dを使用する場合は OpenGL ES に必要な機能
( 固定機能 )
自動的に提供してくれる OpenGL 1.1 の方で十分です。


3Dでも固定機能で十分な場合がありますが、上手く利用したい場合 2.0 の方を採用するといいと思います。




こういう感じで作成~


/** 全てのプラットフォームに対応させるため今回はOpenGL ES 1.1を採用する **/
mpGLContext = [ [EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1 ];




後2つの変数は フレームバッファと(カラー)レンダバッファです。

------
● フレームバッファ
 レンダバッファ複数持ってくれる領域。

● レンダバッファ
 描画に直接関係する領域
------


バッファとは記憶領域的な感じだと思っています。


レンダ = 描画なので

描画した情報を保持してくれる領域 = レンダバッファ だと思います。




今回宣言しているレンダバッファ
画面のピクセル色情報が入る領域にします。 だから mColorBuffer という名前
 


他にもレンダバッファは、

3Dで奥行きを利用する時に使う
【ピクセルごとの Z座標 の値を持ってくれる深度レンダバッファ】などがあります。



レンダバッファはピクセルごとの情報を入れることから、
描画画面( iPhoneのデフォルトでは 横:320pix, 縦:480pix で十分 )同じ大きさを取ります。
 

2Dでは基本色情報以外いらないので、
カラーレンダバッファのみ設定すればいいと思います。
 


で、
 作成したカラーレンダバッファをフレームバッファという入れ物に格納します。


さらに、そのフレームバッファをコンテキストに格納します。





そしたら、、、

コンテキストをOpenGLにセットするだけで描画の準備が整います。


色以外にも奥行きとか様々なレンダバッファが作成できるので


纏め役としてフレームバッファがいる。

今回は色のみ。



フレームバッファレンダバッファの作成と設定方法は良く似ています。



glGenFramebuffers( 1, &mFrameBuffer );  // フレームバッファの作成
glGenRenderbuffers1, &mColorBuffer );  // レンダバッファの作成


今回だと、
”1 つ"  フレームバッファ と レンダバッファ を作成して

2番目の引数( 今回は、mFrameBuffer と mColorBuffer )
 "作成した領域の『識別子』” を渡す。 



という感じです。
GLuint型( ただの4バイト変数 )なので、 mFameBuffer, mColorBuffer に領域が直接入っているわけではない。
領域を示す識別子が入る。



そして作成したフレームバッファとレンダバッファを
作成済みのコンテキストにバインド( 設定 )する。

glBindFramebuffer( GL_FRAMEBUFFER, mFrameBuffer );
glBindRenderbuffer( GL_RENDERBUFFER, mColorBuffer ); 




で、コンテキストの中で  レンダバッファカラーレンダバッファとして
フレームバッファに設定します。

これは一度 フレームバッファとレンダバッファを コンテキストに格納してから設定します。


glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, mColorBuffer );


GL_COLOR_ATTACHMENT0 で、
カラーレンダバッファの0番目としてフレームバッファに登録しているわけです。



基本は 
GL_COLOR_ATTACHMENT0 のみで良い!




これで OpenGL ES で描画を行うと
 
コンテキストに設定されているフレームバッファのカラーレンダバッファ 、
つまり mColorBuffer の指定したバッファ(領域)に描画されることになります。




そしてフレームバッファが正しく使用出来る状態になったかどうかチェックする
glCheckFramebufferStatus 関数があります。


/** フレームバッファが正しく設定されたかチェックする **/
if ( glCheckFramebufferStatus( GL_FRAMEBUFFER ) != GL_FRAMEBUFFER_COMPLETE )
{
    /** 正しく設定されていない! **/
}


この関数を通ればもう大丈夫です( ゚д゚ )






しかし、カラーレンダバッファに描画した所で
画面に出てくるわけではありませんね。



iPhone では画面に色を出させるためには、画面に出てくるUIViewの
レイヤに描画しなければなりません。 上で作成したOpenGL ES用のレイヤに。



今回はOpenGL ES初期化処理をView( MainGLView )に書いているので、
自分自身のレイヤに描画するわけですね。

そしてMainGLViewはVIewControllerに xib でセットされているので
これでiPhoneの画面に レイヤの内容 = 描画物 が現れる仕掛けになっている。




なので、

カラーレンダバッファに描画した内容

描画するレイヤ(今回は自分) にコピーてあげます。



まずはレイヤをレンダバッファに設定。

/** これでレンダバッファの内容を自分のレイヤーに移すよう設定 **/
[ mpGLContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:( CAEAGLLayer* )self.layer ];


設定後、
OpenGLESの様々な描画関数でレンダバッファに描画して、
 "presentRenderbuffer"
 という関数
を呼び出すことで、


【レンダバッファの中身が、レンダバッファに設定したレイヤにコピーされる。】

= 画面に出てくる。 というわけです。


ですから 
レンダバッファの大きさは画面(レイヤ)の大きさと一緒にしとかないといけません。
これによりごっそりとそのままコピーできます。




描画の前に注意点。


ゲームは常にループしています。


ループごとに次々とカラーレンダバッファに描画していき、レイヤにぽいぽいコピーすると

前回の描画情報と今回の描画情報が入ったままレイヤにコピーされるので


画面に前回の描画分までもが出てくるという、嫌な感じになります。



毎回、ループのはじめで
レンダバッファの中身を綺麗サッパリ消してやります。


/** 指定した色でクリアするよう設定 **/
glClearColor( 0.8f, 0.85f, 0.9f, 1.0f );

/** 消す対象はカラーバッファ **/
glClear( GL_COLOR_BUFFER_BIT );

------- 描画処理 ---------

// 描画終了後 ↓ を呼び出す

/** この関数でコンテキストが持っているレンダバッファの内容を設定されたレイヤにコピーする **/
mpGLContext presentRenderbuffer:GL_RENDERBUFFER ];



という感じになります。


これでカラーレンダバッファの中身はつねに清潔に保たれます。





初期化の最後OpenGL ES を 2Dで簡単に使用できるような設定をします。


そのための関数が


/** X 0.0f ~ 320.0 に、 Y 0.0 ~ 480.0f にする **/
// X, X, Y, Y, 手前Z, Z
glOrthof( 0.0f, 320.0f, 480.0f, 0.0f, 0.5f, -0.5f );



これになります。



これは座標系を変更する OpenGL ES 1.1 専用の関数です。


OpenGL ES は3Dを扱う為に、座標系が特殊です。



左上が ( -1.0, 1.0 ) で、 右下の座標が ( 1.0, -1.0 ) となります。



中心に ( 0.0, 0.0 ) が来るのは3Dではとても扱いやすいのですが、 



2D では煩わしいこと言語道断です(# ゚Д゚)


なので、 glOrthof 関数

こんな風に 
左上が ( 0.0, 0.0 )、 右下が ( 320.0, 480.0 ) になるようにすれば


一々 -1.0 ~ 1.0 の間に直す手間が省けてとても使いやすくなります。




最後に

glViewport( 0, 0, 320, 480 );

というもので どこからどこまでの範囲に描画するか決定します。

今回はあいぽんの画面全体なので 320, 480 です。




初期化はこれで終わりです。 + その他もろもろは適当に覚えておいたら良い。


その一連の流れを記述した MainGLView.m のコードが ↓


#import "MainGLView.h"
#import <GLKit/GLKit.h>
#import <QuartzCore/QuartzCore.h>

@implementation MainGLView

/**
 * このクラスメソッドをオーバーライドすることで OpenGL ESを描画できるレイヤーをセットする
 */
+ ( Class )layerClass
{
    return [ CAEAGLLayer class ];
}


//- (id)initWithFrame:(CGRect)frame   ← xibではこれは呼び出されない
- ( id ) initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];

    if (self)
    {
        /** 設定されたレイヤの取得 **/
        CAEAGLLayer* pGLLayer = ( CAEAGLLayer* )self.layer;

        // 不透明にすることで処理速度が上がる
        pGLLayer.opaque = YES;


        /** 描画の設定を行う **/
        // 辞書登録をする。
        // 順番として 値 → キー
        pGLLayer.drawableProperties = [ NSDictionary dictionaryWithObjectsAndKeys:
                                      /** 描画後レンダバッファの内容を保持しない。 **/
                                      [ NSNumber numberWithBool:FALSE ],
                                     kEAGLDrawablePropertyRetainedBacking,
                                     /** カラーレンダバッファの1ピクセルあたりRGBAを8bitずつ保持する **/
                                      kEAGLColorFormatRGBA8,
                                     kEAGLDrawablePropertyColorFormat,
                                     /** 終了 **/
                                     nil ];


        /** 全てのプラットフォームに対応させるため今回はOpenGL ES 1.1を採用する **/
        mpGLContext = [ [EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1 ];

        /** 現在のコンテキストにレンダリングコンテキストを設定する **/
        [ EAGLContext setCurrentContext:mpGLContext ];


        /** フレームバッファを作成する **/
        // Gen で作成 → Bind で現在のコンテキストに格納。 の流れ
        glGenFramebuffers( 1, &mFrameBuffer );               // かぶらない識別子を渡す
        glBindFramebuffer( GL_FRAMEBUFFER, mFrameBuffer );   // コンテキストに与えられた識別子をもつフレームバッファを作成

  
        /** カラーレンダバッファを作成する **/
        glGenRenderbuffers( 1, &mColorBuffer );
        glBindRenderbuffer( GL_RENDERBUFFER, mColorBuffer );

        // 先ほどのレンダバッファオブジェクトに描画するために必要なストレージを割り当てる。
        //      fromDrawable : レンダバッファにバインドするストレージ
        // ストレージをレイヤに割り当てることで、バッファに書き込んだらレイヤに書き込まれる!
        [ mpGLContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:( CAEAGLLayer* )self.layer ];

        // フレームバッファとレンダバッファを結びつける
        glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, mColorBuffer );

        /** フレームバッファが正しく設定されたかチェックする **/
        if ( glCheckFramebufferStatus( GL_FRAMEBUFFER ) != GL_FRAMEBUFFER_COMPLETE )
            NSLog( @"フレームバッファが正しくありません! %x", glCheckFramebufferStatus( GL_FRAMEBUFFER ) );
        /** X を 0.0f ~ 320.0 に、 Y を 0.0 ~ 480.0f にする **/
        // 左X, 右X, 下Y, 上Y, 手前Z, 奥Z
        glOrthof( 0.0f, 320.0f, 480.0f, 0.0f, 0.5f, -0.5f );

        /**
ビューポートの作成 **/
        glViewport( 0, 0, 320, 480 );
    }

    return self;
}



/**
 * 描画準備
 */
- ( void ) BeginScene
{
    /** 画面を指定した色でクリアする **/
    glClearColor( 0.8f, 0.85f, 0.9f, 1.0f );


    /** クリアする対象のバッファを指定 **/
    //      色を指定
    glClear( GL_COLOR_BUFFER_BIT );
}



/**
 * 描画終了
 */
- ( void ) EndScene
{
    // 画面上( ビューの持つレイヤ )にレンダバッファのコンテンツを表示する
    [ mpGLContext presentRenderbuffer:GL_RENDERBUFFER ];
}


/**
 * 終了
 */
- ( void )dealloc
{
    glDeleteFramebuffers( 1, &mFrameBuffer );
    glDeleteRenderbuffers( 1, &mColorBuffer );
}

@end
これで MainGLView に描画の準備が整いました。



BeginScene関数呼び出し と EndScene関数呼び出し の間 に OpenGL の描画処理を書くと描画されます。


実際に使ってみましょう。



・・ なんか文字数の制限で書けなくなったので、、、

(URL)続く!!!