WebGLで複数のシェーダー使用時にハマる罠「俺のVBOがアタッチされてないわけがない！」


# はじめに

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

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

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

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

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

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

（以降のやり取りは、私が開発しているWebGLライブラリ「[GLBoost](https://github.com/emadurandal/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サービスでサンプルコードを見る](http://runstant.com/emadurandal/projects/ac925fe7)


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

Bさん「こんばんは。」

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

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

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

```javascript
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サービスでサンプルコードを見る](http://runstant.com/emadurandal/projects/102344ba)

Aさん「おお！？　本当だ！　なぜに！？」

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

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

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

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

Aさん「マジですか！」

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

```javascript
  // 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サービスでサンプルコードを見る](http://runstant.com/emadurandal/projects/44ff1a7a)

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

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

Aさん「はい」

エマ「例えばコード中の

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

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

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

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

Aさん「ふむふむ」

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

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

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

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

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

## 補足

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

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

## 閑話休題。。。


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

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

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

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

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

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

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

エマ「そう」

Aさん「使うシェーダーに応じて、`gl.enableVertexAttribArray`や`gl.disableVertexAttribArray`で、VBOをアタッチする必要のあるレジスタ番号を有効化したり、逆にアタッチする必要のないレジスタ番号を無効化したり、というのを律儀に制御する方法（エマさんアプローチ）と
　　　最初に使う予定のある最大数の頂点アトリビュート（レジスタ）番号を全て有効化しておいて、とりあえず最初に全レジスタに何らかのVBOをアタッチしておく方法（Bさんアプローチ）、
　　　どっちが良いんでしょう？」
　　　
エマ「まぁ、一番お行儀のよいのは私のアプローチですが、`gl.enableVertexAttribArray`や`gl.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初心者〜中級者の人がハマるエラーの中で、１、２を争うものが、本記事のケースだと感じています。
これ、今回説明したWebGLの仕様を知らないと、デバッグの方針が定まらなくていつまでたっても不具合が取れない、ということにもなりかねません。

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

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

あと、最後私から二言。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">WebGL（OpenGL）系のAPIが（その時のステート状態がAPIの呼び出し効果に影響することが多々あるので）分かりにくい、って声が多々あるわけですが、私はどうして多少詳しくなったのか、というと、やはり困った時はOpenGL ESの仕様書を読んだりしてるわけですよ。</p>&mdash; エマ・デュランダル (@emadurandal) <a href="https://twitter.com/emadurandal/status/736271278470926336">2016年5月27日</a></blockquote> <script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">GL仕様書は英語で書かれてて分量が多くて大変かもしれませんが、最初から舐めるように全部読む必要はないので（それはそれで有意義だと思うけど）、ぜひ読むことに挑戦して欲しいですね。さすがに仕様書にはステート状態とAPIの関係とかもしっかり書いてあるので、読めば迷いがなくなります。</p>&mdash; エマ・デュランダル (@emadurandal) <a href="https://twitter.com/emadurandal/status/736272036733947904">2016年5月27日</a></blockquote> <script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

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