MRT (Multiple Render Targets)

実行結果

今回のサンプルの実行結果

WebGL 2.0 で標準機能に格上げされた MRT

前回は Sampler Object について扱いました。

WebGL 1.0 のころは、テクスチャオブジェクトに対して直接サンプリングに関する設定を行うしか方法がありませんでしたが、Sampler Object に対応したことで、WebGL 2.0 ではテクスチャとサンプリングの設定を個別に切り離して行うことができるようになったのでした。小さなことのようですが、場面によっては便利なこともある機能なのかなと思います。

さて、今回ですが MRT (Multiple Render Targets) をやってみようと思います。

MRT は WebGL 1.0 の頃から、拡張機能を有効化することで実は利用することが可能でした。これは以前に、当サイトでも既に実装方法について紹介したとおりです。

WebGL 2.0 ではこれが既定の状態で普通に使える標準機能となりました。これにより、WebGL 2.0 に対応した環境では、遅延シェーディングを始めとする MRT を用いて実現するテクニックがより手軽に使えるようになったと言えます。基本的な概念そのものは特に 1.0 と 2.0 で違うということはないのですが、シェーダの書き方などは結構違いますので、そのあたりを中心に見ていきましょう。

GLSL ES 3.0 を前提にした記述スタイル

まず最初に書いておきますが、今回のサンプルは GLSL ES 3.0 を用いてシェーダを記述しています。これは、MRT を実装するにあたり、GLSL ES 3.0 で登場した layout 修飾子を利用するからです。

MRT は、Multiple Render Targets の頭文字を取った略語ですが、その英語表記からもわかるとおり複数のレンダリングターゲットに対して同時に異なる出力を行うことができます。要するに、単体のドローコールで、複数のバッファに対して描画が行えるわけですね。機能の概要だけでも、いかにも便利な機能そうな感じがしますよね。

この MRT を実現するにあたっては、JavaScript だけでなくシェーダに対しても特殊な記述をしてやる必要があります。GLSL を記述する際に、かつての WebGL 1.0 の頃は gl_FragData を配列のように扱い、0 番目のインデックスのバッファにはこの結果を、1 番目のインデックスのバッファには別の結果を……というように、配列のインデックス指定のような感じで個別に値を出力していました。

WebGL 2.0 と GLSL ES 3.0 を用いる場合は、そもそも gl_FragData は存在せず、先述した layout 修飾子を用いて出力先を個別に指定します。このあたりが拡張機能として利用可能だった以前と比較すると、感覚的にかなり違っている感じですね。

JavaScript 側だけで見ると、拡張機能から標準機能に格上げされたことで、若干初期化はシンプルになったような気もします。でもまあ、手順は原則としては同じです。丁寧に見ていけばそれほど難しくありませんので、ぜひトライしてみてください。

JavaScript 側でフレームバッファに仕込み

さて MRT の具体的な実装方法を、おさらいも兼ねて JavaScript の方から見ていきましょう。

MRT は、レンダリングターゲットとして特殊なフレームバッファを用います。特殊、といっても、なにも型が違うとかそういうことではなく、単に、複数のテクスチャをカラーバッファ用にアタッチした状態にしておく感じです。通常は、フレームバッファひとつに対してひとつのテクスチャをアタッチするかと思いますが、これが複数のテクスチャになるということですね。

今回のサンプルでは、フレームバッファを生成するためのユーティリティ関数を MRT 対応の形にしたものを使っています。

引数にいくつのテクスチャを生成してアタッチするのかを指定できるようにしてあり、生成関数のなかでループを回しながら処理していきます。

MRT 対応フレームバッファ生成関数

function create_framebuffer_MRT(width, height, count) {
    var frameBuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
    var fTextures = [];
    for(var i = 0; i < count; ++i){
        fTextures[i] = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, fTextures[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, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, fTextures[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 {
        framebuffer: frameBuffer,
        renderbuffer: depthRenderBuffer,
        textures: fTextures
    };
}

ちょっとコードの量が多いですが、通常のフレームバッファ生成と異なる部分は、本当にテクスチャをループで順に複数処理しているということくらいです。

フレームバッファオブジェクトの生成の仕方が変わるとか、そういうことは特にないので、まあ簡単ですね。

この関数に、フレームバッファの幅と高さ、そして何枚のカラーバッファを持つのかの数値を引数として与えて呼び出すと、戻り値としてオブジェクトにパックされたバッファ類を返してきます。あとは、これを使って次の手順に進みます。

JavaScript 側でのバッファの関連付け

MRT では、まず先に JavaScript 側で、フレームバッファにアタッチしたテクスチャに 番号を振って しまいます。

上記の生成関数の、ループのなかでテクスチャをフレームバッファにアタッチしている瞬間のコードをまず抜粋してみましょう。

フレームバッファにテクスチャをアタッチしている箇所

gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, fTextures[i], 0);

これを見ると framebufferTexture2D メソッドの第二引数で、テクスチャを何番目のカラーアタッチメントとして扱うかが指定されているのがわかると思います。

MRT を用いない、単体のカラーバッファのアタッチでは常に gl.COLOR_ATTACHMENT0 を使っていましたが、ループのなかでこの定数に対してインクリメントされた値が使われているわけです。ですから、もしカラーバッファを 3 つアタッチしたフレームバッファを生成するよう設定して当該関数を呼び出した場合は COLOR_ATTACHMENT0 ~ COLOR_ATTACHMENT2 までの連番が使われることがわかりますね。あとは、このときの連番の指定に従って、バッファ(テクスチャ)がどのような構成になっているのかを事前に登録しておきます。

その登録処理は、以下のような感じで行います。

カラーアタッチメントのバッファ登録

var fBuffer = create_framebuffer_MRT(bufferSize, bufferSize, 3);
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer.framebuffer);
var bufferList = [
    gl.COLOR_ATTACHMENT0,
    gl.COLOR_ATTACHMENT1,
    gl.COLOR_ATTACHMENT2
];
gl.drawBuffers(bufferList);

まず対象となるフレームバッファをバインドした状態にします。続けて、配列に定数を詰め込んだものを用意して、それを gl.drawBuffers メソッドの引数に与えれば OK です。これも、簡単ですね。

このように、フレームバッファの生成時とそのあとの登録処理が、通常のフレームバッファの処理とは異なっている部分です。基本的な流れはそれほど複雑でもありませんし、順番に見ていけば非常に簡単でしょう。さあ続いてはシェーダのほうです。こっちはちょっとややこしいです。

出力先を layout で指定する

シェーダのほうは、GLSL ES 3.0 を用いて記述します。

当サイトの WebGL 2.0 のテキストでは、一貫して GLSL ES 3.0 を使ってきているので、順番に見てきていれば、そろそろ 3.0 の記述スタイルにも慣れてきているころかもしれません。今回のケースでは、出力用のレイアウト指定が複数必要になることがポイントです。

最終的に色を出力することになる、フラグメントシェーダのコードを見てみましょう。

MRT 用フラグメントシェーダのソース

#version 300 es
precision highp float;

uniform vec3 lightPosition;
uniform vec3 eyePosition;
uniform sampler2D texture2dSampler;

in vec3 vPosition;
in vec3 vNormal;
in vec2 vTexCoord;

layout (location = 0) out vec4 outColor0; // 出力先の指定
layout (location = 1) out vec4 outColor1; // 出力先の指定

void main(){
    vec3 light = normalize(lightPosition - vPosition);
    vec3 eye = normalize(vPosition - eyePosition);
    vec3 ref = normalize(reflect(eye, vNormal));
    float diffuse = max(dot(light, vNormal), 0.2);
    float specular = max(dot(light, ref), 0.0);
    specular = pow(specular, 20.0);
    vec4 samplerColor = texture(texture2dSampler, vTexCoord);
    outColor0 = vec4(samplerColor.rgb * diffuse + specular, samplerColor.a);
    outColor1 = vec4(vNormal * vec3(diffuse + specular), samplerColor.a);
}

結構な長さがありますね……

でもポイントになる部分は、おおよそ中段あたりにコメントと共に記載されている layout out を併用している定義の部分。ここが、出力先のカラーバッファを指定しているところです。

今回のサンプルでは、カラーバッファを同時にふたつアタッチしたフレームバッファを用意して、それぞれに異なる結果を出力しています。ひとつめの outColor0 のほうにはテクスチャからサンプリングした色と、ライティングの効果を加えた結果を出力しています。もう一方の outColor1 のほうには、テクスチャの色ではなく法線を使った結果を出力している感じですね。

出力先の指定を行う際には layout (location = 0) out といったような感じで、何番目のカラーアタッチメントに対して出力するのかが指定できるわけですね。WebGL 1.0 の頃の、GLSL ES 1.0 には出力先を out で任意に指定する機能などはなかったので、慣れていないとちょっと混乱するかもしれません。でも落ち着いて見てみれば、それほど難しくないのかなという感じもします。

まとめ

さて、MRT について見てきましたがいかがでしたでしょうか。

正直なところ、WebGL 1.0 のころと比較しても、手順そのものはあまり変わりません。むしろ、標準機能になったことで使い勝手は向上したように感じます。ただシェーダの記述がかなりガラッと変わっているので、そこがハマりやすいところかなと思うので焦らず取り組んでみてください。

MRT を利用すると、たった一度のドローコールから、異なるデータを個別にテクスチャに書き出すことができます。活用事例としてよく遅延シェーディング(Deferred Shading)が引き合いに出されますが、それ以外にも工夫次第で活用できる場面は非常に多くあるのかなと思います。今までは複数回のドローコールを発行して無理やり行っていた処理が、MRT を用いることで 1 パスであっさりできちゃった、みたいなこともあると思いますのでぜひトライしてみてください。

今回も実際に動作するサンプルは以下のリンクから確認できます。

entry

PR

press Z key