フレームバッファ
今回のサンプルの実行結果
オフスクリーンレンダリング
前回はステンシルバッファを用いて、擬似的にですがモデルのアウトラインをレンダリングするテクニックを解説しました。ステンシルバッファは、その概念を一度理解してしまえばそれほど使いこなすのが難しいものではないと思います。反面、どのように活用したらいいのかについてはわかりにくい部分もあると思います。
前回解説したアウトラインのレンダリングは、ステンシルバッファの活用事例の一つです。ステンシルバッファには他にも様々な活用方法がありますので、興味のある方は調べてみるのもいいかもしれません。
さて、今回はフレームバッファ(frame buffer)をやります。このフレームバッファを使いこなせるようになると、今までとは比べ物にならないほど実現できる処理の幅が広がります。
フレームバッファを利用することによって実現できる代表的な処理にオフスクリーンレンダリングがあります。これは読んで字の如く、スクリーンには表示されないレンダリングのことで、バックグラウンドでメモリ空間上に粛々とレンダリングを行なうことが可能になります。
この説明だけではオフスクリーンレンダリングの何が嬉しいのかはわかりにくいかもしれませんが、たとえば一度バックグラウンドでレンダリングした風景を、そのまま背景用のテクスチャとして利用するなど、リアルタイム 3D プログラミングだからこそできる高度な処理を実現できます。
フレームバッファにレンダリングした結果を元に光のエフェクトを追加したり、レーシングゲームなどでよく見かけるバックミラーのような処理を実現したり、あるいは環境マッピングのような技術にもフレームバッファは活躍してくれます。利用するための手順は少々冗長になりますが、それに見合ったメリットを持つフレームバッファ。今回のテキストでしっかり基本を習得していただければと思います。
フレームバッファとは
そもそも、フレームバッファとはいったいなんなのでしょうか。
WebGL でバッファと名の付くものには、これまでも様々なものが登場しました。頂点データを扱う頂点バッファや、深度を扱う深度バッファ、そのほかステンシルバッファもバッファと名の付くものの一つですね。
フレームバッファは言うなればバッファを統括するバッファであり、プログラムとスクリーンとの間を取り持つ仲介役のような役割を持っています。
というのも、WebGL コンテキストを取得し、canvas へのレンダリングが行なえるようになった時点で、実は既にデフォルトのフレームバッファが暗黙に用意されています。今まで、様々な描画処理を解説してきましたが、それらは全てこの既定のフレームバッファに対して処理が適用されていたに過ぎません。
フレームバッファは、色を扱うためのカラーバッファや、深度を扱う深度バッファ(デプスバッファ)、前回解説したステンシルバッファなどを内包しています。フレームバッファを新しく生成してオフスクリーンレンダリングに利用するためには、これらレンダリングに使われるバッファ群に関しても新しく生成しなければなりません。必要となるバッファ群を準備することができたら、これらをフレームバッファに紐付けます。そうすることで、初めてオフスクリーンレンダリングを行うことが可能になります。
正確には、フレームバッファに対して求める性能によって、紐付けするためのバッファをどこまで用意するのかは変わってきます。たとえば、厳密に言えば、深度テストや深度に関する情報が必要ないのなら深度バッファは必要ありませんよね。それならフレームバッファにわざわざ深度バッファを紐付けする必要はありません。
要するに、フレームバッファをどのように扱うのかはプログラマの裁量によって自由に調整できるわけですね。
今回のサンプルでは、フレームバッファを用いたレンダリングを行ないます。具体的には、以下のような手順でフレームバッファを使える状態にまで持っていきます。
- フレームバッファを生成する
- 色を扱うためのバッファを生成する
- 深度を扱うためのバッファを生成する
- 二つのバッファをフレームバッファに紐付ける
- 完成したフレームバッファにレンダリング
さて、それでは次項からさらに詳しく見ていきましょう。
データを格納するための入れ物
色や、深度値に関するデータを格納するバッファを用意するためにはレンダーバッファ(render buffer)を利用します。レンダーバッファはその名前からも連想できる通り、レンダリングを行う対象として利用できるバッファ領域で、言うなればデータを格納するための入れ物のような役割を果たします。
レンダーバッファは非常に柔軟な仕様となっており、どのように設定したのかによってカラーバッファとしても、深度バッファとしても、ステンシルバッファとしても使うことができます。本家 OpenGL には、このレンダーバッファに設定できるフォーマットの種類として非常に多くの選択肢が用意されていますが、WebGL においては現時点でいくつかを除いて使用できない状態です。これは今後、WebGL の普及とバージョンアップによって変更されていく可能性はあります。
レンダーバッファはこれまでに登場したバッファのいくつかがそうであったように、新しく生成したあと、WebGL にバインドすることによって操作可能になります。
レンダーバッファを生成するには createRenderbuffer
メソッドを使います。
レンダーバッファを生成する
var renderBuffer = gl.createRenderbuffer();
これで空のレンダーバッファが生成できます。そして、先ほども書いたように、これを WebGL にバインドすることによって操作を加えることが可能になります。レンダーバッファのバインドには bindRenderbuffer
メソッドを使います。
レンダーバッファのバインド
gl.bindRenderbuffer(gl.RENDERBUFFER, renderBuffer);
第一引数には、WebGL の組み込み定数 gl.RENDERBUFFER
を指定します。第二引数がバインドするレンダーバッファですね。
バインドが完了したら、このレンダーバッファをどのようなフォーマットとして扱うのかを設定します。ここで設定した内容に応じて、このレンダーバッファがどのように利用できるのか決まります。
レンダーバッファへのフォーマットの設定には renderbufferStorage
メソッドを使います。
レンダーバッファへのフォーマットの設定
gl.renderbufferStorage(gl.RENDERBUFFER, format, width, height);
第一引数は先ほどのバインド処理と同様 gl.RENDERBUFFER
を指定します。第二引数がレンダーバッファに設定したいフォーマットの指定です。第三引数と第四引数を使って、このレンダーバッファのサイズを指定します。
第二引数に指定できるフォーマットの種類は以下のようになります。
renderbufferStorage メソッドに指定できるフォーマット
フォーマット | 意味 |
---|---|
gl.RGBA4 | RGBA それぞれに 4 ビットを割り当てカラーバッファとして利用する |
gl.RGB5_A1 | RGB それぞれに 5 ビット、アルファ値として 1 ビットを割り当てるカラーフォーマットを利用する |
gl.RGB565 | R 要素と B 要素に 5 ビットを割り当て、G 要素に 6 ビットを割り当てるカラーフォーマット |
gl.DEPTH_COMPONENT16 | 16 ビット深度を持つ深度バッファとして利用する |
gl.STENCIL_INDEX8 | 8 ビットのビット長を持つステンシルバッファとして利用する |
カラーバッファとしてレンダーバッファを利用する際にはいくつかの選択肢があることがわかります。ただしビット長が合計 16 ビットなのは微妙に使いにくいですね。
深度バッファとして利用する場合とステンシルバッファとして利用する場合には、現状では事実上一択ですね。これは今後拡張されていくでしょう。※WebGL 1.0 でも既にこれ以外のフォーマットに関しても定数値自体は盛り込まれています
細かいことはさておき、レンダーバッファの生成からバインド、そしてフォーマットの設定までは理解できたでしょうか。
さて、それでは一度ここで考えてみてください。
フレームバッファを用いたオフスクリーンレンダリングによって、バックグラウンドでメモリ領域上に何かをレンダリングしたとします。そのレンダリングした結果を使って、今度は画面上に実際にそのレンダリング結果を表示してやるにはどうしたらいいでしょうか。
たとえばレーシングゲームでバックミラーの処理を行いたいと考えたとします。ドライバーの視点で車の前方の風景をレンダリングすることに加え、バックミラーで見える領域、つまり後方の風景をレンダリングした結果が必要になるのはわかりますね。
車の後方の風景をオフスクリーンレンダリングしたとして、そのレンダリング結果を、前景にバックミラーとして合成するにはどうすればいいのでしょうか。
一番簡単なのは、バックミラー用の矩形領域をポリゴンで用意して、そこに後方の風景をレンダリングした結果をテクスチャとして貼り付けてしまうのがいいですね。
そう、つまりはオフスクリーンレンダリングを行なった結果はテクスチャとして再利用できるようにしておくのが定石です。そして、このような処理を実現するための手段として、フレームバッファに紐付けるカラーバッファの代替としてテクスチャを使う方法が WebGL には実装されています。
カラーバッファとしてテクスチャを使うと、ビット長による縛りもありませんし、今まで使ってきたテクスチャに関する技術がそのまま流用できます。
テクスチャをカラーバッファとして利用する
フレームバッファに紐付けるカラーバッファにテクスチャを利用するには、まず空のテクスチャオブジェクトを用意することから始めます。そして、これまで同様、対象のテクスチャに何かしらの処理を加える場合には、まずテクスチャをバインドしなければなりません。
テクスチャの生成とバインド
// フレームバッファ用テクスチャの生成
var fTexture = gl.createTexture();
// フレームバッファ用のテクスチャをバインド
gl.bindTexture(gl.TEXTURE_2D, fTexture);
ここまでは今までもやってきたことの焼き直しなので簡単ですね。ちょっとややこしいのはこの次の手順。生成したテクスチャに、カラーバッファとして利用できるように設定を行なわなければなりません。
テクスチャをカラーバッファ用に設定する
// フレームバッファ用のテクスチャにカラー用のメモリ領域を確保
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
かなり横に長い上、引数も膨大なので戸惑うかもしれませんがポイントを絞って考えましょう。まずは、第四・第五引数ですが、ここにはテクスチャの横幅と縦幅を指定します。そして最後の第九引数に null
が指定されていることが重要です。本来、この最後の引数にはテクスチャに設定するイメージデータを渡しますが、ここに null
が指定されていることによって、テクスチャにはそのサイズやフォーマットだけが設定され、中身は空っぽの状態になります。
要は、この空の領域にオフスクリーンレンダリングを行なうわけです。ですから、重要なのはテクスチャのサイズを正しく指定すること、そして最後の引数を必ず null
にすることです。その他の引数の意味はここでは説明しませんが、上記の例のように指定すればとりあえずは問題ないでしょう。
これでカラーバッファ用のテクスチャが準備できました。
フレームバッファへのアタッチ
レンダーバッファや、カラーバッファ用のテクスチャは、最終的にフレームバッファと紐付けすることで初めて利用可能になります。続いてはフレームバッファについて見ていきましょう。
フレームバッファは、レンダーバッファと同じようにまずは生成するところからスタートします。そして、やはりここでも、バインドすることによって操作を加えることが可能になります。
フレームバッファの生成とバインド
// フレームバッファの生成
var frameBuffer = gl.createFramebuffer();
// フレームバッファをWebGLにバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
フレームバッファの生成には createFramebuffer
メソッドを使います。生成されたフレームバッファをバインドするためのメソッドが bindFramebuffer
ですね。ここでは第一引数に gl.FRAMEBUFFER
という組み込み定数を使い、第二引数に実際にバインドするフレームバッファを指定します。
ここまでは非常に簡単ですね。続いて、フレームバッファにレンダーバッファやテクスチャを紐付けます。今回は、深度バッファとしてレンダーバッファを、カラーバッファとしてテクスチャを使う想定で見てみます。
フレームバッファへの深度バッファの紐付け
// フレームバッファにレンダーバッファを深度バッファとして関連付ける
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderBuffer);
フレームバッファにレンダーバッファを紐付けるには framebufferRenderbuffer
メソッドを使います。メソッド名がまんまや……
このメソッドは四つの引数を取ります。第一引数には gl.FRAMEBUFFER
を指定し、第二引数には紐付けするレンダーバッファをどのようなフォーマットとするかを定数値で指定します。第三引数には gl.RENDERBUFFER
を、第四引数には紐付けするレンダーバッファを渡します。
第二引数に指定できるフォーマットには、以下のものが使えます。
framebufferRenderbuffer メソッドの第二引数
フォーマット | 意味 |
---|---|
gl.COLOR_ATTACHMENT0 | カラーバッファとしてアタッチ |
gl.DEPTH_ATTACHMENT | 深度バッファとしてアタッチ |
gl.STENCIL_ATTACHMENT | ステンシルバッファとしてアタッチ |
これは簡単ですね。レンダーバッファをどう扱いたいのかによって、第二引数に渡す定数値を変更すればいいだけです。
さて、ここまではレンダーバッファを紐付けする場合の話でした。テクスチャをフレームバッファに紐付けする場合には、専用のメソッドを使います。
フレームバッファへのテクスチャの紐付け
// フレームバッファにテクスチャを関連付ける
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
フレームバッファにテクスチャを紐付けするには framebufferTexture2D
を使います。第一引数、第二引数までは、レンダーバッファのときと同様に指定すれば大丈夫です。第三引数には、紐付けするテクスチャのフォーマットを指定しますので、通常は gl.TEXTURE_2D
を指定します。第四引数はフレームバッファに紐付けるテクスチャオブジェクト、第五引数はテクスチャレベルなので通常 0 を指定すれば問題ないでしょう。
さてこれでフレームバッファを使うための事前準備が整いました。実際にフレームバッファを活用して、オフスクリーンでテクスチャにレンダリングしてみましょう。
テクスチャへのレンダリング
冒頭で書いたように、WebGL には既定で暗黙のフレームバッファが設定されています。特別な指定を行なわない場合、自動的にこの既定のフレームバッファが使われます。利用するフレームバッファを切り替えるには、単純にフレームバッファをバインドするだけで大丈夫です。
バインドを解除すると、再び既定のフレームバッファが使われるようになります。
フレームバッファのバインドと解除
// フレームバッファをバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
// フレームバッファのバインドを解除
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
フレームバッファがバインドされると、事前にアタッチ(紐付け)してあったレンダーバッファやテクスチャが使われるようになります。つまり、フレームバッファを切り替えるだけで、あとは勝手にオフスクリーンレンダリングが行なわれるようになるわけです。
自前のフレームバッファに対してレンダリングを行なう際には、既定のフレームバッファを利用する場合と同様の手順をそのまま行ないます。つまり clear
メソッドを使って色や深度をクリアし、適切にシェーダへデータをプッシュしレンダリングすればそれで OK です。
既定のフレームバッファを使う場合には、最後にコンテキストをリフレッシュするために flush
メソッドを実行する必要がありましたが、自前のフレームバッファでは必ずしも行なわなくても大丈夫です。フレームバッファのバインドが解除された時点で、自動的にフレームバッファ上のレンダリングは更新されます。
自前のフレームバッファのカラーバッファにテクスチャを利用している場合には、レンダリング結果はそのままテクスチャとして利用できますので、今までやってきたのと同様の手順で、普通にポリゴンなどにテクスチャとして適用することが可能です。
サンプルの解説
今回のサンプルでは、テクスチャへのオフスクリーンレンダリングを行い、そのレンダリング結果をキューブ型のモデルにテクスチャとして貼り付けます。オフスクリーンレンダリングにおいては、大きな球体モデルを背景としてレンダリング、その手前に小さめの球体モデルをレンダリングします。
サンプル内では、フレームバッファを生成するためのヘルパー関数を定義しています。そのコードだけを抜粋してみます。
フレームバッファ生成関数
// フレームバッファをオブジェクトとして生成する関数
function create_framebuffer(width, height){
// フレームバッファの生成
var frameBuffer = gl.createFramebuffer();
// フレームバッファをWebGLにバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
// 深度バッファ用レンダーバッファの生成とバインド
var depthRenderBuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depthRenderBuffer);
// レンダーバッファを深度バッファとして設定
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
// フレームバッファにレンダーバッファを関連付ける
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthRenderBuffer);
// フレームバッファ用テクスチャの生成
var fTexture = gl.createTexture();
// フレームバッファ用のテクスチャをバインド
gl.bindTexture(gl.TEXTURE_2D, fTexture);
// フレームバッファ用のテクスチャにカラー用のメモリ領域を確保
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
// テクスチャパラメータ
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// フレームバッファにテクスチャを関連付ける
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fTexture, 0);
// 各種オブジェクトのバインドを解除
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// オブジェクトを返して終了
return {f : frameBuffer, d : depthRenderBuffer, t : fTexture};
}
この関数は引数を二つ取ります。フレームバッファの横幅と、縦の幅、この二つですね。
レンダーバッファやテクスチャを設定する部分で、この引数として受け取ったサイズが使われます。ここで注意しなければならないのは、あくまでもカラーバッファとして利用するのはテクスチャであるという点です。
思い出してみてください。テクスチャには、そのサイズに関する縛りがありましたよね。これは以前のテキストでも書きましたが、現状 WebGL で利用できるテクスチャのサイズは 2 の累乗になっている必要がありました。これはオフスクリーンレンダリング用のテクスチャであっても同様のルールが適用されます。
この自前の関数の内部ではサイズのチェック機構は設けていませんので注意してください。
関数内ではフレームバッファの生成、深度バッファ用のレンダーバッファの生成から設定まで、カラーバッファ用のテクスチャの生成から設定までを行なっています。関数の終了間際には、全てのオブジェクトのバインドを解除し、最終的に生成された一連のオブジェクト群を、オブジェクト型で呼び出し元に返します。
この関数の利用例としては以下のようになりますね。
自前関数の記述例
// フレームバッファオブジェクトの取得
var fBufferWidth = 256;
var fBufferHeight = 256;
var fBuffer = create_framebuffer(fBufferWidth, fBufferHeight);
上記のようなコードが走ると、変数 fBuffer
を介してフレームバッファや、それに内包されるテクスチャへの参照を行うことが可能になります。
あえてフレームバッファのサイズを個別に変数として保持しているのは、後々、座標変換行列を生成する際にアスペクト比などを指定する処理に利用するためです。フレームバッファのサイズとして 2 の累乗である 256 が指定されていることも小さなポイントの一つでしょうか。
全てのコードを掲載すると長くなってしまうので載せませんが、やっていることは今までのテキストで解説してきた内容プラスアルファのことだけです。コードをしっかり解析すれば、自ずと理解できるはずです。
先ほども書いたように、今回はオフスクリーンに背景用の球体と、もう一つの球体をレンダリングします。背景用には宇宙っぽい感じのイメージデータを、小さい球体には地球のイメージデータを使っています。
シェーダはグーローシェーディングによるライティングを行なうもので、ライティングのオン・オフを uniform 変数で選択できるような仕様になっています。それ以外には特別なことはやっていません。
フレームバッファにおけるステンシルバッファ
今回のサンプルでは、フレームバッファにアタッチしているのはカラーバッファ用のテクスチャと、深度バッファ用のレンダーバッファだけです。ステンシルバッファはアタッチしていません。
実は、ステンシルバッファのアタッチにも対応しようと思っていたのですが、現状、WebGL ではステンシルバッファをフレームバッファに適用することができない模様です。
もしくは、実行環境によっては可能なのかもしれません。
少なくとも、ステンシルバッファとしてレンダーバッファを用意しておくことまでは問題なくできました。ただ、このレンダーバッファをフレームバッファにアタッチした瞬間に、フレームバッファが使用できなくなる現象を確認しています。
今後の WebGL のアップデートで対応されるのか、それともこちらの実行環境の問題なのかは定かではありませんが、今回のサンプルではとりあえずステンシルバッファについては考えず作成してあります。
まとめ
さて、フレームバッファ、非常に長いテキストになりましたが理解できたでしょうか。
フレームバッファを使いこなせば、一度レンダリングした結果を再利用することが可能になるだけでなく、特殊なエフェクト処理(たとえば影や光の処理など)を行うことが可能になります。
手順は冗長ですが、仕組み自体は簡単だと思いますので、じっくり考えてみてください。
サンプルはいつものように下記にあるリンクから。