メインスレッド以外での WebGL の利用方法

この記事は 2016 年 1 月 22 日にポストされた “WebGL Off the Main Thread” の抄訳です。

Firefox 44 以降で、WebGL を Web Workers で利用出来るようになりました!OffscreenCanvas API を利用することで、メインスレッド以外で利用可能な WebGL コンテキストを作成できるようになります。

以下の例を実行するためには、Firefox 44 以降が必要です。現在のところ Firefox Developer Edition もしくは Firefox Nightly が該当します(訳注:記事執筆当時。現在はリリース版の Firefox でも利用可能です)。また API は利用するためには about:config から gfx.offscreencanvas.enabled の値を true に設定する必要があります。なお、この機能は ANGLE がサポートされるまで Windows では利用できません。AlteredQualia が指摘するように、Windows の Firefox Nightly 46 ではこの機能が利用出来るようになりました。ああ、恥ずかしい。

ユースケース

これは、初めてユーザに表示されているものの変更をメインスレッド以外のものに許可する API です。描画はメインスレッドの状態とは関係なく行われます。詳細はワーキンググループで策定中の仕様をごらんください。

コードの変更点

私の WebGL に関する講演で利用した、基本的な WebGL アニメーションを例に解説します。このメインスレッドで描画を行っているコードを、Worker を利用するように変更してゆきましょう。

変更対象の WebGL コードによるアニメーション

最初に WEBGL コンテキストの作成から描画関数の呼び出しまでを別ファイルに分離します。

<script src="gl-matrix.js"></script>
<script>
  // main thread
  var canvas = document.getElementById('myCanvas');
  ...
  gl.useProgram(program);
  ...

上記のコードが以下のように変更されます:

// main thread
var canvas = document.getElementById('myCanvas');
if (!('transferControlToOffscreen' in canvas)) {
  throw new Error('webgl in worker unsupported');
}
var offscreen = canvas.transferControlToOffscreen();
var worker = new Worker('worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
...

HTMLCanvasElement.prototype.transferControlToOffscreen を呼び、コンテキストを新しく作成したワーカスレッドに移譲している点に注目してください。transferControlToOffscreenHTMLCanvasElement ではなく、OffscreenCanvas のインスタンスを返します。HTMLCanvasElement と同様、offscreen.clientWidthoffscreen.clientHeight のような属性にはアクセスできませんが、offscreen.widthoffscreen.height のような属性にはアクセス可能です。postMessage の第 2 引数を通じて、変数の所有権を別スレッドに移します。

ここからはワーカスレッドでの作業になります。canvas 要素を引き渡すメインスレッドからのメッセージを受信するまで、WebGL コンテキストの作成できません。WebGL コンテキストの取得、バッファの作成と初期化、属性や uniform の設定と取得、そして描画といったコードの変更は必要ありません。

// worker thread
importScripts('gl-matrix.js');

onmessage = function (e) {
  if (e.data.canvas) {
    createContext(e.data.canvas);
  }
};

function createContext (canvas) {
  var gl = canvas.getContext('webgl');
  ...

OffScreenCanvas によって、WebGLRenderingContext.prototypecommit メソッドを追加されます。このメソッドを呼ぶことで描画内容を、 WebGL コンテキストによって利用されている OffscreenCanvas の生成元である canvas 要素に反映させられます。

アニメーションの同期

次にアニメーションについて取り上げます。requestAnimationFrame による同期を postMessage 経由でワーカスレッドに伝達します。

// main thread
(function tick (t) {
  worker.postMessage({ rAF: t });
  requestAnimationFrame(tick);
})(performance.now());

ワーカ側の onmessage 関数は以下のようになります:

// worker thread
onmessage = function (e) {
  if (e.data.rAF && render) {
    render(e.data.rAF);
  } else if (e.data.canvas) {
    createContext(e.data.canvas);
  }
};

requestAnimationFrame による繰り返しを設定する代わりに、gl.commit() メソッドを呼ぶよう、render 関数を変更します。

// main thread
function render (dt) {
  // update
  ...
  // render
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
  requestAnimationFrame(render);
};

このコードを、以下のように変更します:

// worker thread
function render (dt) {
  // update
  ...
  // render
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
  gl.commit(); // new for webgl in workers
};

この手法に対する制約

上記のコードでは、requestAnimationFrame によって渡される値を利用した速度を適切に考慮したアニメーションを行っていません。速度を考慮するならフレームレートとは独立にアニメーションが行われますが、この例はそうでないため、フレームレートに依存したアニメーションとなっています。その点ではまだ問題があるコードとなっています。

JavaScript のガベージコレクション (GC) に起因する処理の一時停止を避けるため、メインスレッドから描画ロジックの実行を別スレッドに動かすこともあると思います。GC はメインスレッドによる requestAnimation フレームの呼び出しを遅らせてしまいます。requestAnimationFrame ループ中で呼び出される postMessage が契機となって、gl.drawArraysgl.commit は非同期的に呼び出されます。このループはメインスレッドで実行されるため、結果として描画はメインスレッド中の GC によってブロックされることとなります。ただし、ワーカスレッドの実行自体はメインスレッド中の GC によってブロックされません(少なくとも Firefox の SpiderMonkey バーチャルマシンでは)。

この問題を解決するためには、ワーカスレッド中での処理をより賢いものに変更する必要があります。そしてその解決策は、requestAnimationFrameWorker コンテキストで呼び出せるようにすることでしょう。実装状況はこちらのトラッキングバグで確認できます。

まとめ

requestAnimationFrame を Worker で利用できるようになるためには未だ開発が必要ですが、OffscreenCanvas API を利用することで、メインスレッドのブロックとは関係なく描画処理を行えるようになりました。既存の WebGL コードをワーカで実行するための変更は数分で済むことも示しました。animation.htmlanimation-worker.htmlworker.js を利用)をご覧いただければ、変更前と変更後を比較できます。

Nick Desaulniers について

オープン Web のために Mozilla で戦うソフトウェアエンジニア。Firefox OS に対するサードパーティ開発者を助けるのではなく、WebGL を利用したグラフィックスプログラミング、オープンソースへの貢献、そして Emscripten を利用した C / C++ コードの JavaScript へのトランスコンパイルについての講演を行う。多くのプログラミング言語、JIT コンパイル、JavaScript の高速化、グラフィックスプログラミングに多くの知見を持つ。オープンソースソフトウェアと、全ての人がインターネットを利用できるようにすることに情熱を傾けている。


Nick Desaulniers によるその他の記事はこちら