33
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WebGLデバッグテクニック:「このタイミングの描画の様子を画面に出したいんや!」

Last updated at Posted at 2016-06-25

はじめに

 皆さんごきげんよう。エマ・デュランダルさんです。

 今回は小ネタです。

 WebGLのコードがどんどん大規模になってくると、途中でエラーが入り込んだ時のデバッグが非常にやりづらく感じませんか?

 大量の描画命令。大量のステート変更。いったいどのタイミングでエラーが発生しているのかわからない。

 よし、こういう時はデバッガを使って怪しい部分で実行を止めるんだ!

 止めた。

 あれ、なんか想定と違う絵なんだけど……。

 なんでやねーん!

 となるわけです。今回の記事では、そうなってしまう理由の説明と、対処方法についてご紹介します。

なぜ、デバッガで怪しい部分を止めても、画面がそのタイミングでの描画内容にならないのか

 デバッガで怪しい部分にブレークポイントを仕掛けて、止めますよね。

 そうすると大抵、(エラーが致命的でなければ)画面には、ブレークポイント時点での描画内容ではなく、それ以後の処理も反映した完全な3Dシーンが描画されてしまっています。

 エラーが致命的な場合は、そのエラーの内容やそれがどこの時点で発生したかによって、画面に表示される内容は異なります。エラー原因が判明していないこの時点では、どんな表示内容になるかは予想がつきません。少なくとも言えるのは、「ブレークポイントで止めた時点での描画内容ではない」ということです。残念ながら……。

 どうしてこんな、こちらが思うような描画内容になってくれないんでしょう?

1つ目の理由:GPUはCPUと非同期で動く

 これはですね。GPUの処理がCPU側と非同期で行われることに関係しています。

 CPUがGPUに対して描画命令等を発した時、例えばglDrawArraysを呼んだ時。その瞬間にGPUが描画を始めるかというと、違うんですね。
 描画命令がコマンドととしてWebGLドライバ内部のキューに溜まっていき、WebGLドライバが判断した頃合で初めてGPUにその描画コマンドが送られるので、glDrawArrays呼び出しが終わった時点で、その描画が終わっている保証はどこにもなく、それどころか描画処理が始まっている保証すらないわけです。

 これがまず、ブレークポイントで止めたタイミングでの描画が画面に反映されない「理由の一つ」です。

2つ目の理由:ダブルバッファリング

 そして、2つ目の理由。WebGLでは「ダブルバッファリングが標準で有効になっている」というものです。

 ダブルバッファリングとは何か。これは、Canvasに映し出す画面のピクセルデータであるフレームバッファが、前面(Canvasに映し出す)用と背面(WebGLがピクセルデータを書き出す対象)用の2つあるということです。

 この仕組みがない、フレームバッファが1つしかないシングルバッファの場合。画面にWebGLが描画している途中の過程の絵がそのまま見えてしまったり、画面をgl.clearでクリアすることによる画面のちらつきが目につくようになってしまいます。

 これがダブルバッファリングだと、一度全ての描画命令が実行され、最終的な絵が描き終わってから、背面のバッファと前面のバッファを入れ替えます。この仕組みにより、画面のちらつきや不正確な途中の絵が見えてしまうような現象が回避できるわけです。

 ただ、このダブルバッファリング。バッファの切り替えが描画ループの最後のタイミングで行われます。requestAnimationFrameでループを回しているなら、そのループが終わった時点です。

 つまり、全てのWebGLの処理が終わって、requestAnimationFrameによるループ関数が終わった段階で、ようやくCanvasにその内容(全てのWebGLの描画命令が実行されてできた最終的な絵)が反映されるわけです。

つまり...

 先ほど、

(エラーが致命的でなければ)画面には、ブレークポイント時点での描画内容ではなく、
それ以後の処理も反映した完全な3Dシーンが描画されてしまっています。

 というのは、こういうことです。現在の描画ループの内容はまだ、GPUが非同期実行のため描画処理の実行段階が中途半端、かつダブルバッファリングのために現在の描画ループでの途中の描画処理の結果がまだ背面バッファにあり、(バッファの交換がまだなので)前面バッファに反映されていない。

 なので、1フレーム前の(完全な)描画結果がCanvasに表示されてしまっている、ということなのです。

 もちろん、エラーが致命的なものなら、1フレーム前の描画結果ももしかしたら中途半端なものになっているかもしれません。いずれにせよ、Canvasに映っているのは「1フレーム前の」結果であって、現在のフレームのものではないのです。

そんなの嫌だ! ブレークポイントで止めた段階の現在のフレームの絵を確認する方法はないのか!

 というのが今回のテーマ。
 
 ブレークポイントと連携するのはWebGLでは無理ですが、任意の位置での絵を確認する方法はあります(後述)

ネイティブOpenGLなら話は簡単なのだが...

 ネイティブのOpenGLなら、簡単な方法があります。以下のような感じで、glFinish()で完全にGPUの描画処理が終わるのを待機し、その上でダブルバッファの交換命令を明示的に呼ぶのです。

glDrawArrays(...);
glFinish();
glutSwapBuffers();

hogehoge(); // ← ここでブレークポイントを仕掛ける

上記コード、実際に試してないけど、多分うまくいくんじゃないかな。うん。誰か試してレポートしてくれると嬉しいです(`・ω・´)

 しかし、WebGLだとこのやり方ができません。gl.finishは有るんだけど、バッファを切り替える命令がないんです(ブラウザの内部的にはあるんでしょうけどね)。

 そこで、エマさん。次善の策を思いつきました。

 それは、JavaScriptの例外を使う方法です。

WebGLでは例外を使え!

 JavaScriptの例外は他の言語と同じく、例外をthrowした瞬間から、catchを張っているところまで一足飛びで処理を飛び越えてくれます。

 つまり、「このコード呼び出しのタイミングでの描画結果を確認したい!」という行の直後で例外をthrowし、requestAnimationFrameループの終わりの部分で例外をcatchすればいいんです。

 そうすれば、例外をthrowした以後のWebGL処理は一切行われず、すぐにcatch文のあるrequestAnimationFrameループの終わりにジャンプできます。あとはもうWebGLドライバが自動的にバッファを交換してくれますので、「現在のフレームの、例外をthrowする直前までに行ったWebGL処理の結果が見れる」というわけなのです!
 (もちろん、例外をthrowする前のWebGL処理の途中で致命的なエラーが起きていた場合は、画面にどんな絵が出てくるかはわかりませんよ。当然ですが。)

 実際にデモをしてみましょう。

 以下のサンプルは、WebGLのエラーがあるサンプルです。

Runstant:エラーがあるサンプル

 戦闘機のモデルと立方体のモデルの2つを描画しようとしているのですが、あれあれ? 後者の立方体が表示されず、コンソールにはエラー文が...

 ここで、試してみましょう。JavaScriptの例外を!
 戦闘機の描画はうまくできているようだから、戦闘機の描画の直後で例外をthrowしてみましょう。こんな風にします。

  function render(){
    gl.enable(gl.DEPTH_TEST);
    gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    try {
      (function() {
        gl.bindBuffer(gl.ARRAY_BUFFER, fighterVbo);
        gl.vertexAttribPointer(attribLocationPosition, 3, gl.FLOAT, gl.FALSE, 32, 0)
        gl.vertexAttribPointer(attribLocationNormal, 3, gl.FLOAT, gl.FALSE, 32, 12)
        gl.vertexAttribPointer(attribLocationTexcoord, 2, gl.FLOAT, gl.FALSE, 32, 24)
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, fighterIbo);
        initMatrix([-3, 0, 0]);
        gl.drawElements(gl.TRIANGLES, fighterIndexArray.length, gl.UNSIGNED_SHORT, 0);

        throw new Error('GotoSwapBuffer');

        gl.bindBuffer(gl.ARRAY_BUFFER, cubeVbo);
        gl.vertexAttribPointer(attribLocationPosition, 3, gl.FLOAT, gl.FALSE, 32, 0)
        gl.vertexAttribPointer(attribLocationNormal, 3, gl.FLOAT, gl.FALSE, 32, 12)
        gl.vertexAttribPointer(attribLocationTexcoord, 2, gl.FLOAT, gl.FALSE, 32, 24)
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeIbo);
        initMatrix([3, 0, 0]);
        gl.drawElements(gl.TRIANGLES, fighterIndexArray.length, gl.UNSIGNED_SHORT, 0);
      })();
    } catch (e) {
      if (e.message === 'GotoSwapBuffer') {
        console.log("途中で中断されました!" + " 例外が発生したファイル:" + e.fileName + ", 行:" + e.lineNumber);
      }
    }

    requestAnimationFrame( render );

  }

(例外が発生したファイル名(e.fileName)と行番号(e.lineNumber)はFirefoxでのみ正常に表示されます)

 これでコンソールへのWebGLエラー出力がなくなったら、それ以後の処理(おそらく立方体の描画処理)になんらかの原因があるということが特定できるわけですね。

 以下のサンプルは、戦闘機の描画の直後で例外をthrowしているサンプルです。

Runstant:例外による描画処理中断サンプル

 コンソールをみてください。エラー出力がなくなりました。ということは、例外throw以後の処理に原因があります。
 どれどれ、コードをよくつぶさに見てみよう。あ!立方体の描画処理に間違いが。glDrawElementsの指定インデックス数に、立方体のインデックス数でなくて、間違えて戦闘機のインデックス数を指定してしまっていました。単純な凡ミスですね。ではここを直して……と。

Runstant:エラーを直したサンプル

 直りましたね!

注意点

 このWebGLでのJavaScriptの例外を使うデバッグ方法、一点だけ気をつけなければならないことがあります。
 それはWebGLの描画以外の設定関連の話です。

 先ほど紹介したネイティブOpenGLでの確認方法、

glDrawArrays(...);
glFinish();
glutSwapBuffers();

hogehoge(); // ← ここでブレークポイントを仕掛ける

glBindBuffer(...); // ブレークポイントから処理を再開すると、ここ以降も実行される。

 こちらは、ブレークポイントでの停止を解除して絵を確認した後、処理をまた進めて、2回目以降のループで再びこのブレークポイントに来た時、どうなっているかというと、前フレームにおいてブレークポイントの後続のWebGL処理もちゃんと実行されています。
 
 しかし、WebGLでの例外の方法では、

gl.drawArrays(...);

throw new Error('GotoSwapBuffer'); // ← ここで例外を投げる

gl.bindBuffer(...); // ここ以降の処理は実行されない

 例外を投げた行以降のWebGL処理は当然ながら実行されません。

 つまり、ネイティブOpenGLでのデバッグ方法とWebGLでの例外によるデバッグ方法とでは、
 毎フレームの途中結果が(後半のWebGLの設定処理いかんによっては)異なってくる可能性があるということです。
 
 この点にはちょっと留意して、このテクニックを使う必要があります。
 気をつけ方としては、

  • いつ処理を中断されて描画ループをやり直しても、破綻がないようにWebGL設定処理を記述する。
  • 例外のcatch場所を工夫する

 のいずれか(もしくは合わせ技)になるかと思います。そういう調整が面倒くさい場合は、一番最初の描画フレームしか確認できませんが、以下の方法があります。

  • 怪しい場所(または描画ループの開始地点)にブレークポイントを仕掛けて、1回目の停止ではそのまま動作を再開させて、2回目のブレークポイント停止のタイミングでCanvasの画面の様子を確認する。(その時のタイミングでは、一番最初のフレームでの例外throw直前までのWebGL処理の結果が表示されている。1フレーム目の実行結果なので、当然ながら後続WebGL処理が無いことによる影響は無い。)

このテクニック、もっと複雑なWebGLコードでこそ威力を発揮します。

 今回は本当に単純なサンプルだったので、別にこのテクニックを使わなくても原因発見は楽ですが、実際はもっと複雑に入り組んだWebGLコードでこそ、威力を発揮します。
 どんなに処理が複雑に隠蔽されていても、JavaScriptの例外のおかげで、後続の処理をすっ飛ばしてバッファ交換まで行けるわけです。

 ちなみに、この方法以外にも、途中で処理を止める方法もあるようです。Chrome拡張の「WebGL Inspector」といったWebGLデバッガを使うと、途中のWebGL命令で処理を止め、その時の様子を確認できるようです。

 ただ、これらも万能というわけではありません。本当に処理を止めてしまうので、アニメーション処理させていたりとかすると、そのアニメーション処理も止まってしまいます。
 一方の私の例外を使う方法なら、処理が止まっているわけではないので、アニメーション処理もしっかりさせながら途中の結果を確認できる、というわけです。

 これ、自分のWebGLコードで試しても良いですし、他の有名なWebGLライブラリで、「あれ、もしかしてこれバグじゃないの?」と思った時に、自分で怪しい位置に例外を仕掛けて、バグを発見して開発者に報告する、ということもやりやすくなるでしょう。

 ちょっとした小ネタではありますが、結構使える技ではないかな、と思います。

 みなさんもぜひ活用してください ^o^ /

33
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?