iPhone で Render to Texture してみました!
本日は、 iPhone の OpenGL で Render to Texture を実現する方法をご紹介したいと思います。 Render to Texture とは、その名のとおりテクスチャに別のシーンをレンダリングして、それを本来のシーン内で再利用する技術です。ゲームなどで周りの風景が(動いているキャラクターなども含めて)物体に写り込んでいるのを見たことはないでしょうか。それらの表現はほとんどの場合この Render to Texture で実現されています。そのほか、シャドウマップや各種フィルターなど幅広い効果で利用される、リアルタイム 3DCG の基礎技術のひとつです。
この記事では Render to Texture のもっとも簡単な例として、「iPhone でテクスチャ圧縮 (PVRTC) を使う」のサンプルを改造してキューブの各面に回転する地球を表示するサンプルを作ります。
画像だと単に地球のテクスチャを貼っただけのように見えますが、この地球はそれ自体が別シーンとしてレンダリングされたもので、キューブとは独立して回転します。実際のゲームなどでは、地球の代わりに周りのシーンなどを貼り付けることで映り込みを表現したりするわけです。
検索しても iPhone に特化したわかりやすい解説があまり見当たらなかったので、それなりに情報価値もあるかなと思います。 iPhone で本格的な 3D シーンを表示したいと思っている方は、ぜひ参考にしてください。
レンダリング・ターゲットとなるテクスチャの作成
それでは、さっそく実装をはじめましょう。「iPhone でテクスチャ圧縮 (PVRTC) を使う」で基本的なシーンのレンダリングは実装できているので、それをベースに作業を進めます。また、実装済みのファイルは Subversion で以下のようにチェックアウトできますので、ご利用ください。
svn co http://webos-goodies.googlecode.com/svn/trunk/blog/articles/render_to_texture_in_iphone/
まずはレンダリング・ターゲットとなるテクスチャと FBO (FrameBuffer Object) 、それに深度バッファ用の RenderBuffer を作成します。深度バッファには通常画面用のものが流用できると良かったのですが、 iPhone はカラーバッファと深度バッファの解像度が完全に一致していないとダメなようですね。
ともあれ、実装は以下のようになります。 ES1Renderer.mm の drawScene メソッドの上に挿入するのが良いでしょう。
// Render to Texture 用のテクスチャとFBO static GLuint offscreenFBO; static GLuint offscreenTexture; static GLuint offscreenDepth; static void createOffscreenFBO() { // 以前のFBOを保存 GLint oldFBO; glGetIntegerv(GL_FRAMEBUFFER_BINDING_OES, &oldFBO); // FramebufferObjectを作成 glGenFramebuffersOES(1, &offscreenFBO); glBindFramebufferOES(GL_FRAMEBUFFER_OES, offscreenFBO); // テクスチャを生成 glGenTextures(1, &offscreenTexture); glBindTexture(GL_TEXTURE_2D, offscreenTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glBindTexture(GL_TEXTURE_2D, 0); glFramebufferTexture2DOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_TEXTURE_2D, offscreenTexture, 0); // 深度バッファを作成 glGenRenderbuffersOES(1, &offscreenDepth); glBindRenderbufferOES(GL_RENDERBUFFER_OES, offscreenDepth); glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES, 256, 256); glBindRenderbufferOES(GL_RENDERBUFFER_OES, 0); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, offscreenDepth); // FramebufferObjectの有効性チェック if (glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) != GL_FRAMEBUFFER_COMPLETE_OES) { NSLog(@"FBOの作成に失敗しました : %x", glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES)); } // FBOのbindを復元 glBindFramebufferOES(GL_FRAMEBUFFER_OES, oldFBO); }
上記の createOffscreenFBO 関数は、他の初期化処理と同じく -init メソッドの最後で呼び出します。
- (id) init { if (self = [super init]) { // 既存のコード ... createOffscreenFBO(); } return self; }
これで Render to Texture に必要なリソースが確保できました。
シーンのレンダリング
あとは、作成した FBO を使ってテクスチャ化したいシーンを描画し、次に表示画面用の FBO に切り替えて実際のシーンをレンダリングすることになります。そのために、新たに drawScene2 という関数を用意し、以下の処理を行います。
- Render to Texture 用の FBO を bind する。
- 従来の drawScene を呼び出し、地球をテクスチャにレンダリングする。
- 表示画面用の FBO を bind する。
- 各種設定を調整した後、作成したテクスチャを利用してキューブを描画する。
実装は以下のようになります。 drawScene 関数の下に挿入するのが良いでしょう。
// キューブの頂点データ(1面分) static GLfloat faceVertices[8 * 4] = { +1, +1, +1, 0, 0, 1, 1, 1, -1, +1, +1, 0, 0, 1, 0, 1, +1, -1, +1, 0, 0, 1, 1, 0, -1, -1, +1, 0, 0, 1, 0, 0 }; static void drawScene2(GLfloat screenWidth, GLfloat screenHeight) { // テクスチャにレンダリング GLint oldFBO; glGetIntegerv(GL_FRAMEBUFFER_BINDING_OES, &oldFBO); glBindFramebufferOES(GL_FRAMEBUFFER_OES, offscreenFBO); drawScene(256, 256); glBindFramebufferOES(GL_FRAMEBUFFER_OES, oldFBO); // 画面をクリア glViewport(0, 0, screenWidth, screenHeight); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // マテリアルを若干調整。アンビエントライトが効いていなかった(^^; const GLfloat lightAmbient[] = { 0.3f, 0.3f, 0.3f, 1.0f }; const GLfloat ambient[] = { 1.0f, 1.0f, 1.0f, 1.0f }; glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbient); glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient); // アスペクト比が違うので、射影行列を再設定 glMatrixMode(GL_PROJECTION); const GLfloat near = 0.1f, far = 1000.0f; const GLfloat aspect = screenWidth / screenHeight; const GLfloat width = near * tanf(M_PI * 60.0f / 180.0f / 2.0f); glLoadIdentity(); glFrustumf(-width, width, -width / aspect, width / aspect, near, far); // 頂点データを設定 glVertexPointer(3, GL_FLOAT, sizeof(Vertex), (GLvoid*)faceVertices); glNormalPointer(GL_FLOAT, sizeof(Vertex), (GLvoid*)&faceVertices[3]); glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), (GLvoid*)&faceVertices[6]); // 地球をレンダリングしたテクスチャを設定 glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, offscreenTexture); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // キューブを描画(平面を90°回転させながら4回描画して、キューブっぽく見せる) glMatrixMode(GL_MODELVIEW); for(int i = 0 ; i < 4 ; ++i) { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glRotatef(90, 0.0f, 1.0f, 0.0f); } // bindを解除 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); }
Render to Texture の肝は、最初にある「テクスチャにレンダリング」とコメントされているコードブロックです。 glGetIntegerv で現在の FBO (画面表示用の FBO)を待避した後、 glBindFramebufferOES で Render to Texture 用の FBO を bind します。そして従来の drawScene を呼び出して地球をレンダリングし、それが元の FBO を bind し直します。従来の drawScene がそのまま使えていることでわかるとおり、 Render to Texture でもレンダリングの方法は通常のシーンと変わりません。ただ、 bind されている FBO が違うだけです。
テクスチャに描画できたら、あとはそれを使ってキューブをレンダリングするだけです。 VBO/IBO を作成するのが面倒だったので、通常の配列に用意したデータを直接描画しています。しかも、データは一面分だけで、それを 90°回転させつつ 4 回描画するという手抜きぶり(^^; VBO/IBO を使わないと描画パフォーマンスが大幅に落ちますが、こんな方法もあるよということで。
上記の renderScene2 関数は、 -render メソッドで drawScene の代わりに呼び出します。
- (void) render { [EAGLContext setCurrentContext:context]; glBindFramebufferOES(GL_FRAMEBUFFER_OES, defaultFramebuffer); drawScene2(backingWidth, backingHeight); glBindRenderbufferOES(GL_RENDERBUFFER_OES, colorRenderbuffer); [context presentRenderbuffer:GL_RENDERBUFFER_OES]; }
これで完成です。ビルドして実行すれば、記事の最初にある画像のような画面が表示されるはずです。
以上、本日は iPhone で Render to Texture する方法をご紹介しました。最初に書いたとおり、とても応用範囲の広いテクニックですので、ぜひご活用ください!
詳しくはこちらの記事をどうぞ!
この記事にコメントする