MRT(Multiple render targets)
今回のサンプルの実行結果
複数のカラーバッファへ同時出力
前回は GPGPU で大量のパーティクルを描画することに挑戦しました。GPGPU とは、本来はグラフィックス処理のために存在する GPU を、グラフィックス処理とは別の用途に利用する処理全般を指す言葉であり、前回のように頂点の座標計算に GPU を使うような処理は、GPGPU の代表的な処理だと言えるのでしたね。
一度に数万、数十万という数のパーティクルを描画することも、GPGPU を駆使すれば比較的簡単に行えました。この内容を応用すれば、様々なことに利用できると思いますので、ぜひ習得してください。
さて今回は、前回とは少し内容が変わり、MRT(Multiple Render Targets)をやりたいと思います。
MRT と略して表記されることがほとんどですが、その本来の表記を見るとわかるとおりで、MRT とは「レンダリング先となるバッファを同時に複数持つ」という意味を持つ技術です。もう少し具体的に言うなら、一度のドローコールで複数のバッファに書き出すということですね。
これまでの当サイトのあらゆるテキストやサンプルでは、一度のドローコールで同時に複数のバッファに書き出すという処理を行ったことはありません。法線と、深度、これらをそれぞれフレームバッファに書き込みたければ、個別にドローコールを発行して、複数回のステップに分けてレンダリングしていました。それが、MRT を利用すればたった一度のドローコールで行えることになります。
当然、ドローコールの回数が減らせるわけですから、より効率的に記述することができるだけでなく、負荷も下げることができますね。
そんな夢の様な技術である MRT は、いったいどのように実装してやればいいのでしょうか。
MRT を利用するための拡張機能有効化
MRT は、WebGL 1.0 においては、既定ではサポートされていない機能です。つまり、そのままでは、なにをどうがんばっても WebGL で MRT を利用することはできません。
それではどうしたらいいのかと言うと、WebGL の拡張機能を利用します。対象となる拡張機能は WEBGL_draw_buffers
です。拡張機能なので、環境によっては使えない可能性もあります。ほかの拡張機能がそうであったように、まずはこの拡張機能が有効化可能なのかどうか、調べないといけませんね。
拡張機能を有効化させる
var ext = gl.getExtension('WEBGL_draw_buffers');
if(!ext){
alert('WEBGL_draw_buffers not supported');
return;
}else{
// アタッチできるテクスチャの数などを調べる
console.log(gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_WEBGL));
console.log(gl.getParameter(ext.MAX_DRAW_BUFFERS_WEBGL));
}
これまでにも当サイトでは何度か拡張機能について扱っていますが、それと見比べても有効化の手順については特殊なことはありません。いつものように gl.getExtension
メソッドを使って拡張機能を有効化させましょう。
ちょっと特殊なのは、有効化が成功した場合の処理部分。ここでは gl.getParameter
を使って何かを調査しています。ここが、MRT をやる上ではちょっとだけ大事な部分になります。
ここで何を調べているのかというと、引数をよく見るとなんとなく意味がわかるかと思いますがMRT の最大性能がどれくらいなのかを調査しています。
まず先に登場する MAX_COLOR_ATTACHMENTS_WEBGL
は、フレームバッファにアタッチできるカラーバッファの最大数を調べるための定数になります。仮に MRT が有効化できたとしても、同時にいくつのカラーバッファをフレームバッファに対してアタッチできるのかは、ハードウェアに依存するということになります。
同様に、その次の行では MAX_DRAW_BUFFERS_WEBGL
を利用してメソッドを呼び出していますね。ここでは、同時に書き込むことができるバッファの最大数がわかります。
ちょっと紛らわしいので要注意なのですが「フレームバッファにアタッチできるバッファ」の数と、「同時に書き込めるバッファ」の数とは、必ずしも最大値が一致するとは限らないということです。アタッチはできるけど、書き込みは無理、というそんな状況が果たして起こりえるのかわかりませんが、少なくともパラメータとしては別々のものになりますので、混乱しないように気をつけましょう。
ハードウェアの性能により最大値は常に変動しますので、それを踏まえた実装を行うようにすればいいですね。
そもそもそれは何者なのか
さて、拡張機能を有効化し、MRT を利用できるかどうか、またそれを利用する上での最大上限はどの程度の性能なのか、調査する方法については理解できたでしょうか。
続いては実際に MRT を利用するために様々な手続きをしていくのですが、そもそも MRT とはどのような概念なのか、再度ここで少し掘り下げて理解しておきましょう。
MRT は先ほども書いたとおり、一度のドローコールで複数のカラーバッファに色を出力することができる機能です。
しかし、こう言葉で簡単に説明されただけでは、頭のなかでその概念をイメージするのが難しいのではないかなと個人的には思います。より具体的に、カラーバッファが複数ある状態をイメージするには、GLSL で普段どのようにして色を出力していたのか、思い出すところから始めてみましょう。
言うまでもなく、GLSL で最終的に出力する色を格納するのは gl_FragColor
ですよね。ここに vec4
で数値を与えてやれば、それがそのまま RGBA として出力される色になるのでした。
これはもう飽きるほどやってきたことなので、簡単ですね。
さて、それでは本題です。MRT を利用する場合のことを考えてみます。
MRT で同時に複数のカラーバッファに出力するということは、言うなれば、ひとつのフラグメントシェーダのコードのなかで、この gl_FragColor
がいくつも同時に存在している状態、ということと同じです。最終出力カラーを格納する gl_FragColor
が複数あるわけですから、ひとつ目には色を、ふたつ目には深度を……といった感じで、同時に複数の色を出力することができるわけです。
そして、ここまでのことを踏まえて考えると、MRT を利用する際には当然ながら、GLSL 側でもこれまでに登場してこなかった特殊な記述方法を用いる必要が出てきます。MRT を正しく理解し自在に操るためには、javascript での WebGL の手続きの他に GLSL 側でもしっかりそれに対応した記述を行ってやらなくてはいけないんですね。
一度に覚えなくてはならないことが多くなるので最初はちょっと大変ですが、多少手続きが複雑であっても、結局のところ gl_FragColor
がたくさんある状態なだけなのだな、というふうに落ち着いて考えることができればきっと理解できるはずです。
MRT 専用フレームバッファの生成
それではいよいよ MRT の具体的な実装について見ていきます。
まずは、javascript 側での記述からです。MRT は、よく考えてみれば当たり前のことですが、レンダリングのターゲットになるわけですからフレームバッファを用いてオフスクリーンで処理を行っていきます。
MRT を扱う場合、これ専用のフレームバッファを用意してやる必要があります。なぜかと言えば、一度のドローコールで複数のカラーバッファに異なるデータを書き出すわけで、そうなるとフレームバッファにアタッチするカラーバッファの数を通常のひとつだけのものよりも多く設定してやらなくてはなりません。
少しだけおさらいすると、従来のフレームバッファでは、テクスチャを生成してこれをカラーバッファとして利用していましたね。テクスチャの生成、生成したテクスチャをフレームバッファにアタッチ、という手順で、テクスチャをカラーバッファとしてフレームバッファに紐づけてやる必要がありました。
MRT では「同時に利用したいカラーバッファの数」と、「フレームバッファにアタッチするテクスチャの数」がイコールになります。当然ながら先述した最大アタッチ数以下の数が限度になりますが、今回は一度に4つのテクスチャをフレームバッファにアタッチしてみましょう。
MRT 用のフレームバッファ生成関数
// フレームバッファをオブジェクトとして生成する関数(MRT仕様)
function create_framebuffer_MRT(width, height) {
// フレームバッファの生成
var frameBuffer = gl.createFramebuffer();
// フレームバッファをWebGLにバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
// フレームバッファ用テクスチャを格納する配列
var fTexture = [];
// ループ処理でテクスチャを初期化
for(var i = 0; i < 4; ++i){
fTexture[i] = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, fTexture[i]);
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// フレームバッファにテクスチャを関連付ける
gl.framebufferTexture2D(gl.FRAMEBUFFER, ext.COLOR_ATTACHMENT0_WEBGL + i, gl.TEXTURE_2D, fTexture[i], 0);
}
// 深度バッファ用レンダーバッファ
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);
// 各種オブジェクトのバインドを解除
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// オブジェクトを返して終了
return {
f: frameBuffer,
d: depthRenderBuffer,
t: fTexture
};
}
少々長いですが、上記は従来から当サイトで利用してきた、フレームバッファを生成するユーザー定義の関数を、MRT が利用できるように修正したものです。かなり行数が多いのでポイントが絞りにくいですが、最大のポイントは、テクスチャをループ構造で一度に 4 枚生成している点でしょう。
掲載されているコードの中段辺りで for
ループを使って一気にテクスチャを生成しています。
生成したテクスチャは、最終的にフレームバッファにテクスチャをアタッチするメソッドである gl.framebufferTexture2D
によって処理されます。わかりやすくするために該当部分だけを再度抜粋してみます。
テクスチャをアタッチしている箇所だけを抜粋
gl.framebufferTexture2D(gl.FRAMEBUFFER, ext.COLOR_ATTACHMENT0_WEBGL + i, gl.TEXTURE_2D, fTexture[i], 0);
かなり横に長いのでわかりにくいですが、この部分で今までのフレームバッファ生成と異なる部分は一箇所だけ、第二引数の部分になります。
第二引数に当たる部分には、従来は gl.COLOR_ATTACHMENT0
を指定して、テクスチャをカラーバッファとしてフレームバッファにアタッチしていました。今回はここで拡張機能を有効化した際に取得したオブジェクト(ここではext
)のプロパティを使っているのがわかりますね。
ここで登場する ext.COLOR_ATTACHMENT0_WEBGL
は、少なくとも現時点では gl.COLOR_ATTACHMENT0
とまったく同じ値が入っているようですが、今回は一応拡張機能オブジェクトのプロパティのほうを使うようにしています。また、ここではループカウンタである変数 i
を加算するようにしているのですが、どうしてこのような処理を行うのか、わかるでしょうか。
この、テクスチャをカラーバッファとしてアタッチするための定数 gl.COLOR_ATTACHMENT0
や ext.COLOR_ATTACHMENT0_WEBGL
には、0 の次には 1、その次には 2 というふうに、インクリメントしていく連番の定数が定義されています。
カラーアタッチの定数
ext.COLOR_ATTACHMENT0_WEBGL
ext.COLOR_ATTACHMENT1_WEBGL
ext.COLOR_ATTACHMENT2_WEBGL
ext.COLOR_ATTACHMENT3_WEBGL // 以下連番で続いていく
このような連番の定数は、中身の値もインクリメントしてくように定義されているので、ループカウンタを加算してやることで、プロパティ定数を指定しているのと同様の指定を行うことができるのですね。
つまり、この部分でなにをやっているのかをまとめてみると、まずはループ構造を作って、そのなかでテクスチャが新規に生成されるようにします。このテクスチャは、ループ構造の最後でフレームバッファにアタッチするように処理するのですが、アタッチする際のフォーマットの指定には、連番で定義されている ext.COLOR_ATTACHMENT0_WEBGL
系の定数を用いることとし、ループカウンタを加算するような処理を追加することによって、カラーアタッチ定数をインクリメントしつつ指定できるようになっているのです。
カラーアタッチメントは通常だと 0 番しか利用できません。ですから今までのフレームバッファでは、描画先のバッファは常に一枚だけでした。しかし今回、MRT の拡張機能を有効化したことにより、一度に複数のカラーバッファをフレームバッファひとつに対して同時に設定できるようになり、ループ構造を利用して一気にまとめて初期化している、ということですね。
カラーバッファのアタッチ以外の、深度バッファを生成してアタッチする部分などは従来とまったく同じです。
要は、今までのフレームバッファの生成と異なる部分というのはテクスチャの生成を行っているループ構造部分だけなのですね。それがわかってしまえば、あまり特殊なことをやっているわけではない、ということが理解できるのではないでしょうか。
描画開始前のもうひと手間
さて、MRT を行うためには、専用のフレームバッファが必要となるわけですが、初期化処理そのものは、従来のものとほとんど同じ手順で生成できるということがわかりましたね。
しかし、こと描画ということになると、ここでは今までになかった新しい手順が追加されます。
とは言え、それほど難しいことではないので、まず先にコードを見てみることにしましょう。
レンダリングターゲットの設定
// フレームバッファをバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer.f);
// レンダリングターゲットの設定
var bufferList = [
ext.COLOR_ATTACHMENT0_WEBGL,
ext.COLOR_ATTACHMENT1_WEBGL,
ext.COLOR_ATTACHMENT2_WEBGL,
ext.COLOR_ATTACHMENT3_WEBGL
];
ext.drawBuffersWEBGL(bufferList);
ここで登場してくるのが、MRT を用いる場合の、描画を開始する前のお約束となるレンダリングターゲットの指定作業です。
まずは、実際にレンダリングを行う前にテクスチャの生成の際に利用した定数を使って、配列をひとつ定義しておきます。これは、レンダリングが行われる段階で「何番目のカラーバッファを利用するか」ということを明確にするために必要な手順で、配列の中身には ext.COLOR_ATTACHMENT0_WEBGL
などのテクスチャのアタッチに利用した定数をそのまま使います。
配列を定義したら、この配列を拡張機能オブジェクトが持っている専用のメソッド ext.drawBuffersWEBGL
メソッドに与えます。
この手順をあらかじめ行っておくと、GLSL 側、つまりシェーダ側で、何番目のカラーバッファに書き出すのかを指定することができるようになります。今回の場合は素直に 0 から 3 までの合計 4 つをそのまま配列に順番通り格納したので、シェーダ内で 0 番目に書き込めば 0 番目のカラーバッファに書き込みが行われるようになります。
この ext.drawBuffersWEBGL
メソッドの呼び出しが完了していれば、あとは描画命令(ドローコール)に関しては通常どおり呼び出すだけで大丈夫です。
シェーダ側の実装
さあ、それではいよいよシェーダ側の記述について見ていきます。
まずは、比較的単純な頂点シェーダのほうからです。
頂点シェーダのソース
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform mat4 invMatrix;
uniform vec3 lightDirection;
uniform vec4 ambient;
varying vec4 vDest;
varying vec4 vColor;
varying vec3 vNormal;
varying float vDepth;
void main(){
gl_Position = mvpMatrix * vec4(position, 1.0);
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
float diff = clamp(dot(normal, invLight), 0.1, 1.0);
vDest = vec4(color.rgb * ambient.rgb * diff, 1.0);
vColor = color * ambient;
vNormal = normal;
vDepth = gl_Position.z / gl_Position.w;
}
今回は頂点シェーダでライティングの計算を行うようにしました。
注目して欲しいのは 4 つ定義した varying 変数のそれぞれの意味です。
まず最初に vDest
ですが、これは vec4
のデータ型で、ライティングしたディフューズの色を加えた、いわゆる普通のライティング結果の色を格納し、フラグメントシェーダへと渡すのに使います。
それ以外の varying 変数に関しては、名前がそのまま意味のとおりになっており vColor
には頂点色とマテリアルの色を掛け合わせた色が、そして vNormal
に法線が、最後に vDepth
に深度の情報が、それぞれ格納されるようになっています。
頂点シェーダからフラグメントシェーダへ、varying 変数を利用して 4 種類の色データが渡されるということですね。
続いては、これらを受け取るフラグメントシェーダ。
こちらはコード自体はそれほど複雑ではありません。
フラグメントシェーダのソース
#extension GL_EXT_draw_buffers : require
precision mediump float;
varying vec4 vDest;
varying vec4 vColor;
varying vec3 vNormal;
varying float vDepth;
void main(){
gl_FragData[0] = vDest;
gl_FragData[1] = vColor;
gl_FragData[2] = vec4((vNormal + 1.0) / 2.0, 1.0);
gl_FragData[3] = vec4(vec3((vDepth + 1.0) / 2.0), 1.0);
}
ご覧のとおり、先ほど頂点シェーダで記述した 4 つの varying 変数を受け取るようになっているのがわかりますね。
ただし、ここで最も注目すべきはソースコードの最上段、一行目に書かれているステートメントです。
拡張機能の有効化を宣言する
#extension GL_EXT_draw_buffers : require
これは、今まで一度も当サイトのテキストで使ったことのない記述です。
シェーダの冒頭に #extension
ディレクティブを記述し、それに続けて有効化する拡張機能を明記します。今回の場合はこの部分に GL_EXT_draw_buffers
という拡張機能名が書かれています。さらに、それに続けてコロンを打ち require
と指定します。
この #extension
ディレクティブを用いた拡張機能の有効化が宣言されていないと、今回のキモである MRT を利用することができないので、十分に注意しましょう。
また、既にお気づきの方もいらっしゃるかと思いますが、MRT を用いた描画では、従来の gl_FragColor
というおなじみの組み込み変数が登場しません。その代わりに gl_FragData
という似たような名前の組み込み変数を使います。
もうこの辺りまでくると雰囲気でわかるかと思いますが、ここで配列のようにカギ括弧を使ってインデックスを指定すると、javascript 側で ext.drawBuffersWEBGL
に指定した配列の並び通りに、個別に色を出力することができます。javascript の配列の記述方法とほとんど同じ感覚で記述できるので、特に難しいことはないと思います。
注意すべき点は、まず冒頭のディレクティブによる拡張機能の有効化が必要だということ。そして gl_FragColor
ではなく、MRT 用の gl_FragData
を使うのだということ。これさえわかっていれば、MRT を利用すること自体は簡単なはずです。
今回のサンプルでは、ライティングした結果の色、マテリアルの素の色、法線をそのまま色として出した色、最後に深度、この 4 つが同時に一度のドローコールによって出力されます。
サンプルの実行結果詳細
サンプルでは、4 つのカラーバッファがアタッチされているフレームバッファに、トーラスを 9 個レンダリングしています。ただし、先ほどのシェーダのコードを見ればわかるとおり、それぞれのカラーバッファ(テクスチャ)に対して別々のものを書き込んでいるため、バインドするテクスチャを切り替えながら板ポリゴンを使ってバックバッファの様子を描いてみると、まったく異なる描画結果が 4 種類出てくるわけですね。
フレームバッファ自体は、たったのひとつだけです。
ドローコールに関しても、4 倍ではなく、一回分の描画を行うドローコールしか発行していません。
にもかかわらず、MRT の効果によってひとつのフレームバッファに同時に 4 つの異なるシーンを描画することができたというわけです。
gl_FragColor と gl_FragData
今回はじめて登場した gl_FragData
ですが、これを利用している場合には同じシェーダのなかで gl_FragColor
を使うことはできません。
MRT の場合には、先述した javascript 側でのレンダリングターゲットの指定なども行っているため、シェーダ内では基本的に gl_FragData
のみを使用していくことになります。
あえて共存させなくてはならない場面というのは普通に考えればありませんし、あまり困ってしまうということはないとは思いますが、いつもの癖や勢いで gl_FragColor
のほうを書いてしまわないように気をつけましょう。
まとめ
さて Multiple Render Targets、いかがでしたでしょうか。
かなり長文のテキストになってしまったので、途中で読むのが疲れてしまったかもしれませんが、注意深く見ていくと、それほど通常のフレームバッファと比較して複雑になっている部分はありませんね。
多少手順は多いですが、ひとつひとつ丁寧に見ていけば、必ず理解できると思いますので諦めずにチャレンジしてみてください。
MRT は、次期バージョンとなる WebGL 2.0 では標準機能に格上げされます。そして、現代の 3DCG においては、MRT というのはあまりにも普通の機能です。残念ながら、WebGL はかなり古い仕様を元にした実装なので、MRT ですら拡張機能を使わなければ利用できない、というのが見方としては正しいです。
MRT を利用することによって、遅延レンダリングや法線と深度による精度の高いエッジ検出など、かなりいろいろなことができるようになります。単純に、今まではいくつかのステップにわけて行っていた作業を、MRT を利用することによって、より効率的に記述するということも可能でしょう。
次期 WebGL では普通に使えるようになりますので、今のうちからしっかり研究しておくといいのではないかなと思います。
今回も、サンプルは以下のリンクから参照できます。