64
55

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で複数のシェーダー使用時にハマる罠「俺のVBOがアタッチされてないわけがない!」

Last updated at Posted at 2016-05-25

はじめに

皆さんこんにちは!
日本のWebGL界において、意識高い系WebGLおじさんとして知られるエマ・デュランダルさんですよ!(挨拶)

さて、皆さんWebGL楽しんでますか?

WebGLはネイティブのOpenGLと違い、glGetErrorとかglShaderInfoLogとかで明示的にエラー確認を行わなくても、エラーが発生した際はブラウザが親切にちゃんとコンソールにエラーメッセージを出してくれます。
非常に3Dプログラミングしやすい環境だと思います。

それでも、「どうしてもこのエラーが取れない!! 正しくWebGLのAPIを呼び出してるはずなのに、どうしてエラーが出るの!? 亡霊かよこのエラー!」っと叫びたくなる時もあるものです。

どうしてか。大抵の原因は、WebGL(及びそのベースとなったOpenGL ES)のAPIについての理解不足にあるんです。

おやおや、今日もWebGLで悩める患者さんが来たようですよ。どうやら、複数種類のメッシュ(とそれぞれに付随する各シェーダープログラム)の描画時に起きるエラーに悩まされているようですが…。

(以降のやり取りは、私が開発しているWebGLライブラリ「GLBoost」の公式Gitterチャットルームにて実際にあったメンバーとのやりとりをベースに、その方達の許可を得て(登場人物を匿名化して)再構成したものです。)

原因不明な「GL ERROR :GL_INVALID_OPERATION : glDrawElements: attempt to render with no buffer attached to enabled attribute 1」亡霊かおまいは……。

Aさん「エマ先生!」

エマ「今日はどうされました?」

Aさん「ワーニングがとれんのです! 描画の際に謎のGL ERROR :GL_INVALID_OPERATION : glDrawElements: attempt to render with no buffer attached to enabled attribute 1というワーニングが出て(Chromeの場合のワーニング文)、それ以降正常に描画できません!」

エマ「ほうほう、これはよくみんながハマるワーニングなんですよね。私もたまにありますよ」

Aさん「最初はANGLE_instanced_array(ジオメトリ・インスタンシング拡張)の使い方が間違っているのかな、と思っていたんですが。使わないようにもっとコードをシンプルにしてみたんです。するとどうやらANGLE_instanced_arrayは関係ないようで……」

エマ「まぁ、まずはコードを見てみましょうか」

Runstantサービスでサンプルコードを見る

エマ「ふむ。ここをこうして……と(コードをいじって検証中)」

Bさん「こんばんは。」

エマ「おお、Bさんこんばんは。」

Aさん「こんばんはー。」

Bさん「ログみました。2番用意、のところで、こんな感じに2行追加したところワーニングが出なくなったんですけど。どうでしょう?」

vbo2 = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo2);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      //
      -0.5, +0.5, 0, 0, 0,
      //
      +0.5, +0.5, 0, 1, 0,
      //
      -0.5, -0.5, 0, 0, 1,
    ]), gl.STATIC_DRAW);
    gl.vertexAttribPointer(attribute2["position"], 3, gl.FLOAT, false, 20, 0); // 追加
  gl.vertexAttribPointer(attribute2["uv"], 2, gl.FLOAT, false, 20, 12); // 追加
    

Runstantサービスでサンプルコードを見る

Aさん「おお!? 本当だ! なぜに!?」

Bさん「正確な理由は僕にもわからないんですけど。VBOを作成した直後にはgl.vertexAttribPointerの指定が必要になるんですかね?」

エマ「いや、違いますね。」

Bさん「そうなんですか?」

エマ「Aさん、原因がわかりましたよ」

Aさん「マジですか!」

エマ「まずは以下のコードで試してみてください」

  // 1番描画
  var draw1 = function() {
    gl.useProgram(program1);

    gl.uniformMatrix4fv(uniform1["mMatrix"], false, mMatrix1);
    gl.uniformMatrix4fv(uniform1["vMatrix"], false, vMatrix);
    gl.uniformMatrix4fv(uniform1["pMatrix"], false, pMatrix);

    gl.disableVertexAttribArray(attribute2["uv"]); // 追加
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo1);
    gl.vertexAttribPointer(attribute1["noitisop"], 2, gl.FLOAT, false, 8, 0);

    gl.activeTexture(gl.TEXTURE0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo1);

    gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  };
  // 1番描画ここまで

  // 2番描画
  var draw2 = function() {
    gl.useProgram(program2);

    gl.uniformMatrix4fv(uniform2["mMatrix"], false, mMatrix2);
    gl.uniformMatrix4fv(uniform2["vMatrix"], false, vMatrix);
    gl.uniformMatrix4fv(uniform2["pMatrix"], false, pMatrix);
    gl.uniform1i(uniform2["texture"], 0);

    gl.enableVertexAttribArray(attribute2["uv"]); // 追加
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo2);
    gl.vertexAttribPointer(attribute2["position"], 3, gl.FLOAT, false, 20, 0);
    gl.vertexAttribPointer(attribute2["uv"], 2, gl.FLOAT, false, 20, 12);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture2);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo2);

    gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  };
  // 2番描画ここまで

Runstantサービスでサンプルコードを見る

Aさん「おお、警告が出なくなりました。ん? gl.disableVertexAttribArray? これは一体どういうことなんでしょう」

エマ「まず、シェーダーのattribute変数は、GPU上のレジスタと対応付けられるんです」

Aさん「はい」

エマ「例えばコード中の

attribute2["position"] = gl.getAttribLocation(program2, "position”);

gl.getAttribLocationは、その"position"attribute変数に割り当てられているレジスタのインデックス番号を問い合わせますが、結局、レジスタ番号はレジスタ番号なのです。」

Aさん「・・・? どういうことでしょう?」

エマ「つまり、gl.getAttribLocationで取得できて、gl.vertexAttribPointerで指定することになる整数値(レジスタのインデックス番号)は、完全に独立の存在で、このシェーダープログラム"program2"に属しているわけではない、ということです。」

Aさん「ふむふむ」

エマ「で、Aさんのオリジナルコードでは、1番用意と2番用意のコード部分で、gl.enableVertexAttribArrayを使って、指定したレジスタ番号に対応する頂点アトリビュートを有効化しています。まぁ、大抵はgl.getAttribLocationで番号としては0と1が取れていると思うので、gl.enableVertexAttribArrayによって、0番と1番の頂点アトリビュート(レジスタ)が有効化されたわけです」

Aさん「そこまではわかります」

エマ「にもかかわらず、1番描画では、

gl.vertexAttribPointer(attribute1["noitisop"], 2, gl.FLOAT, false, 8, 0);

によって、レジスタ番号0番(attribute1["noitisop"])はVBOをアタッチしたけれども、レジスタ番号1については、まだアタッチできていませんよね? つまり、有効な頂点アトリビュート(レジスタ番号)なんだけど、VBOが未アタッチのものがある状態です。その状態で描画を行うと例のワーニングが出るわけです。」

補足

有効にした頂点アトリビュート(レジスタ)には、頂点データを供給する必要があります。つまり、VBOをアタッチしなければなりません。ただ、そのアタッチのためのWebGL関数gl.vertexAttribPointerには、レジスタ番号を指定する引数はありますが、VBOを指定する引数はありません。どないすりゃこの二つを関連付けられるんじゃ! とお思いでしょう。
実は、gl.vertexAttribPointerを呼ぶ前に、gl.bindBufferの呼び出しによってバインド状態になっていたVBOこそが、gl.vertexAttribPointerによって指定されたレジスタへアタッチされる、という仕組みになっているのです。
これは、非常にわかりづらいAPI仕様だと私は思っています。っていうか、知ってました? そもそも知らない人もいたかもしれません。こういうのがWebGL(OpenGL)の面倒臭いところです。さらに最近はこのgl.vertexAttribPointerによるVBOのアタッチ設定を記録する「頂点配列オブジェクト(VAO)」という拡張も登場し、さらにややこしくなっています。
(VAOについては、以前私が書いたこの記事をご覧ください)

(まぁ、こういうOpenGL系のAPIのややこしい部分も、glNextと言われているVulkan APIでは解決されているようですよ。まぁ、Vulkanはローレベルすぎて別の意味でややこしいんですが)

閑話休題。。。

Aさん「それが原因だったのか! あ、でもそれだとdrawを呼び出すたびに警告が出るんじゃないでしょうか。現状だと最初の一回だけです」

エマ「いやいや、2番目の描画で"uv"(レジスタ1)にも(ようやく)VBOが割り当てられたでしょう?」

Aさん「あ、なるほど! ようやく疑問が氷解しました! あとエマさんの言っていた、 「レジスタ番号はシェーダープログラムに属しているわけではなく、独立の存在」 という意味もわかりました。私は gl.useProgramでシェーダープログラムを切り替えれば、gl.enableVertexAttribArrayとかの設定もクリアされると思ってたんです。 だって、gl.enableVertexAttribArrayで指定する番号は、gl.getAttribLocationで取得しますが、そのgl.getAttribLocationの引数にはシェーダープログラムを指定しますもの!」

エマ「ええ、ところが……gl.enableVertexAttribArraygl.useProgramとは何の関係もないんです。 gl.useProgramでのシェーダープログラムの切り替えにかかわらず、gl.enableVertexAttribArrayによるレジスタの有効化、gl.disableVertexAttribArrayによるレジスタの無効化は、効果が持続するんですよ

Aさん「だから、エマさんの修正コードでは、1番描画ではレジスタ1を無効化して、VBOがレジスタ1にアタッチされていなくても問題ないようにしたんですね。で、2番描画ではレジスタ1も必要になるから、また有効化した、と」

エマ「そういうことです。ここまで見てきた知識を動員すれば、Bさんの回避策でワーニングが収まった理由もわかりますね」

Bさん「描画前の段階で、有効化したレジスタ0と1に対して、漏れなくVBOを最初にアタッチしたからですね。未アタッチのレジスタがなくなって、GL ERROR :GL_INVALID_OPERATION : glDrawElements: attempt to render with no buffer attached to enabled attribute 1が出なくなったんだ。」

エマ「そう」

Aさん「使うシェーダーに応じて、gl.enableVertexAttribArraygl.disableVertexAttribArrayで、VBOをアタッチする必要のあるレジスタ番号を有効化したり、逆にアタッチする必要のないレジスタ番号を無効化したり、というのを律儀に制御する方法(エマさんアプローチ)と
   最初に使う予定のある最大数の頂点アトリビュート(レジスタ)番号を全て有効化しておいて、とりあえず最初に全レジスタに何らかのVBOをアタッチしておく方法(Bさんアプローチ)、
   どっちが良いんでしょう?」
   
エマ「まぁ、一番お行儀のよいのは私のアプローチですが、gl.enableVertexAttribArraygl.disableVertexAttribArrayはそれなりにコストのかかるgl命令なので、スピードを求めるならBさんアプローチもありな気がします。ただ、これもね。WebGLの実装系によっては、シェーダーで実際に使っていないのに有効化されているレジスタ番号があるとワーニングが出るケースもあるかもしれませんし、あともしかしたら、不必要なレジスタを有効化しているだけで、GPUの実行パフォーマンスに悪影響が出る処理系も、中にはあるかもしれません。ここらへんは私も深いノウハウがあるわけではないので、断言はできないですね。もし、不都合が出たら、私のアプローチに戻ると良いかと思います。」

Aさん「了解です。先生!」

エマ「それにしても、こういうところがWebGL(OpenGL)系のわかりづらいところなんですよね。どのAPIがどのステートと関連づいているのか、あるいは、今回のレジスタ番号のように、どういったものが独立している設定なのか。ぱっと見わからないんです。」

Aさん「そうですね。gl.getAttribLocationにしても、これシェーダープログラムを指定しますけど、結局のところこのシェーダープログラムのこのattribuet変数に割り当てられているレジスタ番号はいくつ? っていう、レジスタ番号を問い合わせるためのただの便利機能みたいなものだったわけですしね。これ、今回のことを知らない人がこのgl.getAttribLocationの関数仕様を見ると、シェーダープログラムの切り替えで頂点アトリビュート(レジスタ)の状態も切り替わるって思っちゃっても不思議はないですよ。実際は違うわけですが」

まとめ

という次第でした。まとめると、

  • 描画したいメッシュが複数種類あり、それらの頂点レイアウトや頂点シェーダーのattribute変数の数が異なっている場合に起きやすいWebGLエラーが、今回のGL ERROR :GL_INVALID_OPERATION : glDrawElements: attempt to render with no buffer attached to enabled attribute {レジスタ番号}である。
  • レジスタ番号の有効・無効状態は、どんなGLのステートからも独立した設定。gl.useProgramによるシェーダープログラムの切り替えの影響も受けない。
  • 描画するメッシュの種類によって頂点シェーダーのattribute変数の数が異なる場合、メッシュ種類の処理順番などによっては、有効になっている頂点アトリビュート(レジスタ)番号に対して、gl.vertexAttribPointerでVBOをアタッチできていないケースがあり、その状態で描画をするとGL ERROR :GL_INVALID_OPERATION : glDrawElements: attempt to render with no buffer attached to enabled attribute {レジスタ番号}が発生する。
  • このワーニングが発生したら、現在有効中のアトリビュート(レジスタ番号)の数・種類を確認して、それらに対してVBOをアタッチできていないケースがないか、またその状態で描画をしているタイミングがないか、確認するのが一番のデバッグ方針。

という感じになります。

私の感触として、WebGL初心者〜中級者の人がハマるエラーの中で、1、2を争うものが、本記事のケースだと感じています。
これ、今回説明したWebGLの仕様を知らないと、デバッグの方針が定まらなくていつまでたっても不具合が取れない、ということにもなりかねません。

Aさんがハマったこの問題。きっと同じことで悩んで、げんなりしている人は少なからずいるだろうと思いまして、急遽、本記事をGitterのメンバーの皆さんから許可を取って作成・公開した次第です。

WebGL、元のOpenGLの仕様をひきづって、ちょっとややこしいところもあるんですけど、まぁ現状Webで手軽に3Dができる(FLASH除けば)唯一の環境です。これからもみんなで可愛がっていきましょう。

あと、最後私から二言。

あ、本記事に関して、突っ込みどころあったらどうぞマサカリ飛ばしてくださいw

64
55
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
64
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?