WebGL 2.0 で追加されたrenderbufferStorageMultisample()メソッドは、レンダーバッファーにMulti-Sampled Anti-Aliasing(MSAA)をかけられるAPIです。

WebGLでオフスクリーンレンダリングを行ったとき、「描画結果にアンチエイリアスがかかっていないな」と思ったことはありませんか? Canvas.getContext()メソッドで指定しているアンチエイリアスのパラメーターを確認し、「やっぱり設定しているはずなのに」と思ったことはありませんか?

実はWebGL 1.0 では、デフォルト以外のフレームバッファーに描画する場合、ハードウェアがサポートするアンチエイリアスをかけられませんgetContext()メソッドで指定するアンチエイリアスの設定はあくまでデフォルトのフレームバッファーの設定であり、オフスクリーンレンダリングで使用するフレームバッファーには反映されないのです。そのため、アンチエイリアスのかかったオフスクリーンレンダリングを行いたい場合、ポストプロセスとしてシェーダーでアンチエイリアスを行う必要がありました。renderbufferStorageMultisample()メソッドを使うと、アンチエイリアス用のシェーダーを使用することなく、オフスクリーンレンダリングに手軽にアンチエイリアスをかけられます

サンプルの紹介

renderbufferStorageMultisample()メソッドを使ってオフスクリーンレンダリングの結果にアンチエイリアスをかけるサンプルを紹介します。

画面下部のセレクトボックスで描画する方法を変更できます。「通常のレンダリング」はオフスクリーンレンダリングを行わず、三角形を画面(デフォルトのフレームバッファー)に直接描画しています。getContext()メソッドのアンチエイリアスの設定をオンにしているため、ハードウェアがサポートしていればアンチエイリアスのかかった見た目になります。

「通常のオフスクリーンレンダリング」は、まず、テクスチャにオフスクリーンレンダリングで三角形を描画し、次にそのテクスチャを使用して階調化のポストエフェクトをかけて画面表示しています。この手順はWebGLにおけるポストエフェクトの常套手段です。前述の通り、通常のオフスクリーンレンダリングではアンチエイリアスがかけられないため、三角形の斜めの線を見るとカクカクしていることがわかります。

「マルチサンプリングを使用したオフスクリーンレンダリング」では、あらかじめrenderbufferStorageMultisample()メソッドを使ってレンダーバッファーにマルチサンプリングの設定を行っています。まず、マルチサンプリングが設定されたレンダーバッファーにオフスクリーンレンダリングで三角形を描画し、次にレンダーバッファーをテクスチャにコピーします。その後は「通常のオフスクリーンレンダリング」と同じくテクスチャを使用してポストエフェクトをかけています。「通常のオフスクリーンレンダリング」と同様のポストエフェクトが適用されつつ、アンチエイリアスがかかっていることがわかると思います。

「通常のオフスクリーンレンダリング」と「マルチサンプリングを使用したオフスクリーンレンダリング」の手順を簡単に比較すると、以下のようになります。「マルチサンプリングを使用したオフスクリーンレンダリング」の手順には登場するオブジェクトが多いため、番号をふってあります。ポイントはフレームバッファーを2つ使用していることです。

通常のオフスクリーンレンダリング

  1. フレームバッファーにテクスチャをアタッチする
  2. フレームバッファーに三角形を描画する
  3. テクスチャを使用してポストエフェクトを画面に描画する

マルチサンプリングを使用したオフスクリーンレンダリング

  1. レンダーバッファー(①)にマルチサンプリングの設定をする
  2. フレームバッファー(②)にレンダーバッファー(①)をアタッチする
  3. フレームバッファー(③)にテクスチャ(④)をアタッチする
  4. フレームバッファー(②)に三角形を描画する
  5. フレームバッファー(②)をフレームバッファー(③)にコピーする
  6. テクスチャ(④)を使用してポストエフェクトを画面に描画する

APIの使い方

上記の「マルチサンプリングを使用したオフスクリーンレンダリング」の手順に沿って使い方を紹介します。最初にWebGL 2.0 のコンテキストを取得します。以下gl2は、ここで取得したWebGL2RenderingContextオブジェクトを指します。

// WebGL2コンテキスト(WebGL2RenderingContext)を取得
const gl2 = canvas.getContext('webgl2');

1. レンダーバッファーにマルチサンプリングの設定をする

まずはレンダーバッファーを作成します。

// マルチサンプリング用のレンダーバッファーを作成
const multiSampleRenderBuffer = gl2.createRenderbuffer();

次に、レンダーバッファーに格納するデータの設定を行います。WebGL 1.0 ではレンダーバッファーを使用する場合、renderbufferStorage()メソッドを使用していました。マルチサンプリングを有効にするには、代わりに WebGL 2.0 で追加されたrenderbufferStorageMultisample()メソッドを使います。

gl2.bindRenderbuffer(gl2.RENDERBUFFER, multiSampleRenderBuffer);
// レンダーバッファーにマルチサンプリングを設定
gl2.renderbufferStorageMultisample(
  gl2.RENDERBUFFER,
  samples,
  gl2.RGBA8,
  TEXTURE_WIDTH,
  TEXTURE_HEIGHT
);

renderbufferStorageMultisample()メソッドの引数は以下の通りです。renderbufferStorage()メソッドと比べると、第二引数の「サンプル数」が増えただけです。

引数名 内容
第一引数 target ターゲット(gl2.RENDERBUFFER固定)
第二引数 samples サンプル数
第三引数 internalFormat レンダーバッファーのフォーマット
第四引数 width レンダーバッファーの幅
第五引数 height レンダーバッファーの高さ

サンプル数には48などの数値を指定します。ここで指定した値がアンチエイリアスのなめらかさに影響します。サンプル数が大きいほどなめらかになりますが、負荷も増大します。

「フォーマット」には格納したいデータ形式を指定します。WebGL 1.0 ではrenderbufferStorage()メソッドにRGBA4など、16ビットまでしか指定できませんでした。WebGL 2.0 ではレンダーバッファーにRGBA8などの32ビットのデータを指定できるようになりましたので、通常の範囲のカラー値が扱えます。

2. フレームバッファーにレンダーバッファーをアタッチする

マルチサンプリング用のフレームバッファーを作り、手順1で設定したレンダーバッファーをCOLOR_ATTACHMENT0にアタッチします。この手順は通常のオフスクリーンレンダリングでフレームバッファーを扱う際の手順と同じです。

// マルチサンプリング用のフレームバッファーを作成
const multiSampleFrameBuffer = gl2.createFramebuffer();
gl2.bindFramebuffer(gl2.FRAMEBUFFER, multiSampleFrameBuffer);
// フレームバッファーにレンダーバッファーをアタッチ
gl2.framebufferRenderbuffer(gl2.FRAMEBUFFER, gl2.COLOR_ATTACHMENT0, gl2.RENDERBUFFER, multiSampleRenderBuffer);

3. フレームバッファーにテクスチャをアタッチする

テクスチャを作成し、フレームバッファーにアタッチします。この手順も通常のオフスクリーンレンダリングでテクスチャを扱う際の手順と同じです。texStorage2D()メソッドの第三引数は手順1でレンダーバッファーに指定したフォーマットと合わせます。

// 通常のフレームバッファーに紐付けるテクスチャを作成
const frameBufferTargetTexture = gl2.createTexture();
gl2.bindTexture(gl2.TEXTURE_2D, frameBufferTargetTexture);
gl2.texStorage2D(gl2.TEXTURE_2D, 1, gl2.RGBA8, TEXTURE_WIDTH, TEXTURE_HEIGHT);

// 通常のフレームバッファーを作成
const normalFrameBuffer = gl2.createFramebuffer();
gl2.bindFramebuffer(gl2.FRAMEBUFFER, normalFrameBuffer);
gl2.framebufferTexture2D(gl2.FRAMEBUFFER, gl2.COLOR_ATTACHMENT0, gl2.TEXTURE_2D, frameBufferTargetTexture, 0);

4. フレームバッファーに三角形を描画する

レンダーバッファーをアタッチしたフレームバッファーに三角形を描画(オフスクリーンレンダリング)します。下記のプログラム中のtriangleProgramSetには三角形を描画するためのGLSLシェーダープログラムや頂点の情報を格納してあります。手順1でレンダーバッファーにマルチサンプリングの設定をしたため、アンチエイリアスが適用された三角形が描画されます。しかし、この段階ではレンダーバッファーの内容を使用できません。

// オフスクリーンレンダリングの場合、描画先のフレームバッファーを指定する
// 描画先のフレームバッファーとしてマルチサンプリングのフレームバッファーを指定
gl2.bindFramebuffer(gl2.FRAMEBUFFER, multiSampleFrameBuffer);
gl2.viewport(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
gl2.clear(gl2.COLOR_BUFFER_BIT);

// 三角形をレンダリング
gl2.useProgram(triangleProgramSet.program);
gl2.bindVertexArray(triangleProgramSet.vertexArrayObject);
gl2.drawArrays(gl2.TRIANGLES, 0, triangleProgramSet.vertexCount);

5. フレームバッファーをフレームバッファーにコピーする

そこで、レンダーバッファーをアタッチしたフレームバッファーをテクスチャをアタッチしたフレームバファーにコピーします。フレームバッファー同士のコピーにはblitFramebuffer()メソッドを使います。blitFramebuffer()メソッドについては『WebGL 2.0 - blitFramebufferでフレームバッファーをコピーする』で詳しく説明しているので、こちらを参照ください。こうすることでレンダーバッファーに描画した内容がテクスチャに転送されます。

// bindFramebuffer()メソッドでマルチサンプリングしたフレームバッファーをテクスチャと紐付いたフレームバッファーへコピー
gl2.bindFramebuffer(gl2.READ_FRAMEBUFFER, multiSampleFrameBuffer);
gl2.bindFramebuffer(gl2.DRAW_FRAMEBUFFER, normalFrameBuffer);
gl2.blitFramebuffer(
  0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT,
  0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT,
  gl2.COLOR_BUFFER_BIT, gl2.NEAREST
);

6. テクスチャを使用してポストエフェクトを画面に描画する

マルチサンプリングされた三角形がテクスチャに転送されたので、これを使ってポストエフェクトを描画します。下記のプログラム中のpostEffectProgramSetにはテクスチャを使用したポストエフェクトを描画するためのGLSLシェーダープログラムや頂点の情報を格納してあります。この手順は通常のオフスクリーンレンダリングでテクスチャを扱う際の手順と同じです。

// 描画先のフレームバッファーとしてデフォルトのフレームバッファーを指定
gl2.bindFramebuffer(gl2.FRAMEBUFFER, null);
gl2.viewport(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
gl2.clear(gl2.COLOR_BUFFER_BIT);

// ポストエフェクトをレンダリング
gl2.useProgram(postEffectProgramSet.program);
gl2.activeTexture(gl2.TEXTURE0);
gl2.bindTexture(gl2.TEXTURE_2D, frameBufferTargetTexture);
gl2.uniform1i(postEffectProgramSet.textrueLocation, 0);
gl2.bindVertexArray(postEffectProgramSet.vertexArrayObject);
gl2.drawElements(gl2.TRIANGLES, postEffectProgramSet.numIndices, gl2.UNSIGNED_INT, 0);

これで完成です! 通常のオフスクリーンキャンバスと比べると、少し手順は増えていますが、以下の2点をおさえておけば理解がしやすいと思います。

  • マルチサンプリングの設定をしたレンダーバッファーを使うこと
  • フレームバッファーを介してレンダーバッファーの内容をテクスチャにコピーすること

WebGL 2.0 を使ってみなさんもアンチエイリアスの効いたポストエフェクトを試してみてください。

TIPS

レンダーバッファーに設定できるサンプル数を取得する

renderbufferStorageMultisample()メソッドの第二引数に指定するサンプル数には、環境ごとに上限があります。この最大値はgetParameter(MAX_SAMPLES)で取得できます。指定するサンプル数が最大値より大きくならないよう注意しましょう。

// サンプル数の最大値を取得
const maxSamples = gl2.getParameter(gl2.MAX_SAMPLES);
console.log('MAX_SAMPLES: ', maxSamples);
const samples = Math.min(8, maxSamples);

テクスチャのアンチエイリアスはかけられない

MSAAは描画する図形のエッジに対してアンチエイリアスをかける手法です。ですので、マッピングするテクスチャ画像内にもともとあるジャギーに対してはアンチエイリアスが効きません。こういった場合には従来通りシェーダーでアンチエイリアスをかける必要があります。シェーダーでアンチエイリアスをかける方法はThree.jsの実装が参考になります。

テクスチャに直接マルチサンプリングを設定できる拡張機能

レンダーバッファーにマルチサンプリングを設定してテクスチャにコピー、という手順を聞いて「なぜテクスチャにマルチサンプリングを直接設定できないの?」と思う方も多いでしょう。そうなんです。できないんです。面倒ですね。

そんなあたなに朗報です。WebGL 2.0 の拡張機能として「WEBGL_texture_multisample」という拡張機能が提案されています。

WebGL WEBGL_texture_multisample Extension Proposed Specification

texStorage2D()メソッドの代わりに、この拡張機能のtexStorage2DMultisample()メソッドを使うことで、テクスチャに直接マルチサンプリングした描画ができます。残念ながらこの拡張機能はまだ提案段階で、どのブラウザも対応していないようですが、いつか使えるようになるかもしれません。頭の片隅に覚えておきましょう。

リファレンス

renderbufferStorageMultisample()メソッドについて、詳しくは下記のドキュメントを参照ください。