21Bのseasonです。最近WebGLを触り始めました。WebGLで球を表示しようとしてバグって非常に嵌ったのでメモ。
バグの内容
やり方としては、JavaScriptの方で球の表面の頂点座標、色、インデックスバッファを生成して頂点シェーダーに渡す。
const [positions, colors, index] = genVertex(400, 200);
const positionAttributeLoation = gl.getAttribLocation(shaderProgram, 'aPosition');
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const colorAttributeLocation = gl.getAttribLocation(shaderProgram, 'aColor');
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(index), gl.STATIC_DRAW);
genVertex(a, b)
というのは自分が定義した関数で詳細は省く。a, b
は球の二方向の分割数。
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionAttributeLoation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionAttributeLoation);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(colorAttributeLocation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(colorAttributeLocation);
const rotateMatrix = mat4.create();
mat4.rotate(rotateMatrix, rotateMatrix, -Math.PI/2, [1, 0, 0]);
gl.uniformMatrix4fv(rotateMatrixLocation, false, rotateMatrix);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
すると以下のように表示された。(ちなみに球面上で地形生成をやろうとしてるので、パーリンノイズで作った地形ができてる。)
原因を探す
上手く表示されてない部分は無数にあったのだけど、表示するインデックバッファの長さを変えてみると、gl.drawElements(gl.TRIANGLES, 391416, gl.UNSIGNED_SHORT, 0);
にしたとき初めて表示が球からずれることが分かった(391416は3の倍数)。つまりindex[391413], index[391414], index[391415]
番目の頂点がなす三角形がおかしいということになる。ここでindex[391413] = 65335, index[391414] = 65336, index[391415] = 65536
だった(伏線1)。それぞれの座標(positions[3 * 65335], positions[3 * 65335 + 1], positions[3 * 65335 + 2]), (positions[3 * 65336], positions[3 * 65336 + 1], positions[3 * 65336 + 2]), (positions[3 * 65536], positions[3 * 65536 + 1], positions[3 * 65536 + 2])
をconsole.log
で表示してみると特に変な値にはなっていない。ちなみに謎現象があって、genVertex
に入れる分割数を小さくすると正しく表示される(伏線2)。
分からんのでシェーダに渡されている座標を見ることにする。色デバッグなるものを試してみた。
【WebGL / GLSL】GLSLはconsole.log()なんてできないから、「色デバッグ」をするらしい
gl.readPixels()
でcanvasの色を見てみたんだけど何故か指定した色と違ってた。エイリアシングとかの影響なのかもしれない。
結局if文で分岐させるのが楽だった。目視で外れてる頂点の座標が(0, 0, -1)
だったのでそこだけ色を変えるようにしたら当たってた。何らかの理由で三つの頂点の内の一つが(0, 0, -1)
に変わってるらしい。で、多分おかしいのは三つ目の頂点だと当たりが付いてた。genVertex()
では、球面座標でz軸の小さい方から生成して配列に加えてるから(0, 0, -1)
は丁度0番目の頂点ということになる。65536が0になってるということは...あああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああああ
結論
インデックスバッファをInt16Array
にしてたのでmod 65536されて0になってました。分割数が小さいと頂点数も少ないのでmod以下になってたんですね~
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(index), gl.STATIC_DRAW);
ついでにgl.drawElements()
でもgl.UNSIGNED_SHORT
を渡してました。
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
解決
まずInt32Array
に変更します。
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int32Array(index), gl.STATIC_DRAW);
それからgl.drawElements()
です。
WebGLRenderingContext.drawElements() - Web APIs | MDN
3番目の引数type
に渡せるのが
- gl.UNSIGNED_BYTE
- gl.UNSIGNED_SHORT
- gl.UNSIGNED_INT(When using the OES_element_index_uint extension)
とあります。BYTE
の方がSHORT
より小さいので拡張機能を導入してINT
を使います。
gl.getExtension('OES_element_index_uint');
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_INT, 0);
できました。
あーやっと解決した。