バッファを使わずにキャンバスを覆う三角形を描く

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_VertexIDは、現在の実行対象となっている頂点のインデックス。

gl.drawElementsでインデックスを指定している場合は、その指定された番号。

gl.drawArraysでインデックスなしで描画している場合は、これまで処理された頂点の数、つまり、処理された順に勝手に振られた連番。

今回の場合は、取りうるgl_VertexIDの値は 0, 1, 2, 3

AND 演算

同じ桁を見比べたときに、両方が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

float

ビット列フル表示

ここまで、簡略化のために上位 4 ビットだけを表示してきた。

float 型に変換した結果、どんな値になるかを探るために、32bit フル表示すると、0100 はこうなる。

01000000000000000000000000000000

浮動小数点数内部表現シミュレーターで可視化すると、こんな感じ。

浮動小数点数そもそも論

浮動小数点数は、例えば 0.00025-0.000250.25×103-0.25 \times 10^{-3} のように、指数表記で数値を表現する方法。

このとき、2525 を仮数部、1010 を基数、3-3 を指数部と呼ぶ。

仮数部の最上位桁は 0 以外でなければならない。つまり、0.025×102-0.025 \times 10^{-2} とかはダメ。

コンピュータで扱う数字は 2 進数なので、以降、基数は22 とする前提で話を進める。

このような浮動小数点数をビット列でどう表すかというと、まあ形式はいろいろあるが、IEEE754 形式の float 型は、左から

という内訳の 32 ビットで表す。

指数部

指数部は 8 ビットあるので、00000000(10 進数の 0)から 11111111(10 進数の 255)までを表すことができる。 が、問題は、指数は負の数である場合もあるということだ。

そこで、0 ~ 255 のちょうど真ん中である 127 という数値を原点とし、

という対応づけをすることで、指数が負の数の場合もカバーできるようにする。

つまり、実際の指数 = 指数部ビット列を 10 進数表記した値 - 127だ。

仮数部

2 進数では、0 と 1 の組み合わせであらゆる数値を表現するため、先ほど書いた「仮数部の最上位桁は 0 以外でなければならない」という規則に則ると、仮数部の最上位桁は必ず 1 になる。

このわかりきっている 1 のために 1 ビットを使うのは勿体無いということで、この部分は仮数部ビット列にはあえて書かない。

このような都合から、IEEE754 形式では、0.M×2N0.M \times 2^N という形式ではなく、1.M×2N1.M \times 2^Nという形式で表すことにしている。

そして、この MM の部分を 2 進数表記したものが、仮数部のビット列として格納される。

0100 の正体

前提はこれで十分。では、float 型の 01000000000000000000000000000000 は何の数値を表すのか。

まず符号が 0 なので、正の数である。

指数部は 10000000 だが、これは 10 進数表記すると 128。127 を引いて、1 が実際の指数だとわかる。

仮数部は紛れもなく 0 だが、これは 1.M1.M の小数部分 MM に過ぎないので実際の仮数は 1.0 だ。

つまり、1.0×21=2.01.0 \times 2^1 = 2.0 となる。

[1,1][-1, 1]範囲に収める

ビット列 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);

参考