GLSL(ESSL)の練習では、キャンバス一面になんだかんだ描きたい。
つまり、フラグメントシェーダで塗りつぶす対象の図形をキャンバス全体に広げたい。
しかし、そのためだけに VBO をつくる等、WebGL コードが長くなるのはめんどくさいので、最初から頂点シェーダでキャンバスを覆う図形の頂点を計算させる。
WebGL 側ではほぼgl.drawArrays(gl.TRIANGLE_FAN, 0, 3)
を呼び出すだけ。
…というようなことを実現するスニペットが gist に転がっていた。
頂点シェーダだけ引用。(gl_VertexID
やビット演算を使っている時点で WebGL1 では動かない)
#version 300 es
void main() {
float x = float((gl_VertexID & 1) << 2);
float y = float((gl_VertexID & 2) << 1);
gl_Position = vec4(x - 1.0, y - 1.0, 0, 1);
}
このコードにより、最終的に(-1, -1)
, (1, -1)
, (-1, 1)
, (1, 1)
の 4 点を結ぶ矩形が描かれることになる。(その矩形上に何を描くかは、フラグメントシェーダ次第。)
ビット演算の思考練習も兼ねて、この謎めいた頂点シェーダがどういう原理でその 4 点を導くのか、ロジックを追ってみる。
gl_VertexID
は、現在の実行対象となっている頂点のインデックス。
gl.drawElements
でインデックスを指定している場合は、その指定された番号。
gl.drawArrays
でインデックスなしで描画している場合は、これまで処理された頂点の数、つまり、処理された順に勝手に振られた連番。
今回の場合は、取りうるgl_VertexID
の値は 0
, 1
, 2
, 3
。
同じ桁を見比べたときに、両方が1
なら1
、そうでなければ0
。
// gl_VertexID = 0 の場合
(gl_VertexID & 1) = 0000 & 0001 = 0000
(gl_VertexID & 2) = 0000 & 0010 = 0000
// gl_VertexID = 1 の場合
(gl_VertexID & 1) = 0001 & 0001 = 0001
(gl_VertexID & 2) = 0001 & 0010 = 0000
// gl_VertexID = 2 の場合
(gl_VertexID & 1) = 0010 & 0001 = 0000
(gl_VertexID & 2) = 0010 & 0010 = 0010
// gl_VertexID = 3 の場合
(gl_VertexID & 1) = 0011 & 0001 = 0001
(gl_VertexID & 2) = 0011 & 0010 = 0010
// gl_VertexID = 0 の場合
(gl_VertexID & 1) << 2 = 0000 << 2 = 0000
(gl_VertexID & 2) << 1 = 0000 << 1 = 0000
// gl_VertexID = 1 の場合
(gl_VertexID & 1) << 2 = 0001 << 2 = 0100
(gl_VertexID & 2) << 1 = 0000 << 1 = 0000
// gl_VertexID = 2 の場合
(gl_VertexID & 1) << 2 = 0000 << 2 = 0000
(gl_VertexID & 2) << 1 = 0010 << 1 = 0100
// gl_VertexID = 3 の場合
(gl_VertexID & 1) << 2 = 0001 << 2 = 0100
(gl_VertexID & 2) << 1 = 0010 << 1 = 0100
ここまで、簡略化のために上位 4 ビットだけを表示してきた。
float 型に変換した結果、どんな値になるかを探るために、32bit フル表示すると、0100 はこうなる。
01000000000000000000000000000000
浮動小数点数内部表現シミュレーターで可視化すると、こんな感じ。
浮動小数点数は、例えば を のように、指数表記で数値を表現する方法。
このとき、 を仮数部、 を基数、 を指数部と呼ぶ。
仮数部の最上位桁は 0 以外でなければならない。つまり、 とかはダメ。
コンピュータで扱う数字は 2 進数なので、以降、基数は とする前提で話を進める。
このような浮動小数点数をビット列でどう表すかというと、まあ形式はいろいろあるが、IEEE754 形式の float 型は、左から
という内訳の 32 ビットで表す。
指数部は 8 ビットあるので、00000000(10 進数の 0)から 11111111(10 進数の 255)までを表すことができる。 が、問題は、指数は負の数である場合もあるということだ。
そこで、0 ~ 255 のちょうど真ん中である 127 という数値を原点とし、
という対応づけをすることで、指数が負の数の場合もカバーできるようにする。
つまり、実際の指数 = 指数部ビット列を 10 進数表記した値 - 127だ。
2 進数では、0 と 1 の組み合わせであらゆる数値を表現するため、先ほど書いた「仮数部の最上位桁は 0 以外でなければならない」という規則に則ると、仮数部の最上位桁は必ず 1 になる。
このわかりきっている 1 のために 1 ビットを使うのは勿体無いということで、この部分は仮数部ビット列にはあえて書かない。
このような都合から、IEEE754 形式では、 という形式ではなく、という形式で表すことにしている。
そして、この の部分を 2 進数表記したものが、仮数部のビット列として格納される。
前提はこれで十分。では、float 型の 01000000000000000000000000000000 は何の数値を表すのか。
まず符号が 0 なので、正の数である。
指数部は 10000000 だが、これは 10 進数表記すると 128。127 を引いて、1 が実際の指数だとわかる。
仮数部は紛れもなく 0 だが、これは の小数部分 に過ぎないので実際の仮数は 1.0 だ。
つまり、 となる。
ビット列 0100 が表す数値は 2 なので、各場合の x, y の数値は、次のように[0, 2]範囲の整数の組み合わせとなる。
// gl_VertexID = 0 の場合
x = float((gl_VertexID & 1) << 2) = 0 // ビット列0000
y = float((gl_VertexID & 2) << 1) = 0 // ビット列0000
// gl_VertexID = 1 の場合
x = float((gl_VertexID & 1) << 2) = 2 // ビット列0100
y = float((gl_VertexID & 2) << 1) = 0 // ビット列0000
// gl_VertexID = 2 の場合
x = float((gl_VertexID & 1) << 2) = 0 // ビット列0000
y = float((gl_VertexID & 2) << 1) = 2 // ビット列0100
// gl_VertexID = 3 の場合
x = float((gl_VertexID & 1) << 2) = 2 // ビット列0100
y = float((gl_VertexID & 2) << 1) = 2 // ビット列0100
これら x, y から 1.0 を引くことで、gl_VertexID
の値に応じて、それぞれ(-1, -1)
, (1, -1)
, (-1, 1)
, (1, 1)
という座標が得られることになる。
gl_Position = vec4(x - 1.0, y - 1.0, 0, 1);