この記事は「WebGLの基本」の続きである。 WebGLの話を進めるに当たって、WebGLやGPUが、実際には どのように動作しているのかを取り上げようと思う。
始めに押さえておきたいのは「GPUは2つのことをやる」という点である。 1点目は、「頂点データ(頂点座標に限らず、与えられたバッファ上のデータストリーム)」を 「クリップ空間の座標データに変換処理する」こと、 2点目は、「1つ目の処理の結果」を元に「ピクセルを描画する」ことである。
WebGLのコードで、
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 9;
gl.drawArrays(primitiveType, offset, count);
と書いた場合、それは「9つの頂点データを処理せよ」という意味の、 GPUに対する命令である。
図中、左の"Original Vertices"(元となる頂点情報)は、あなた(プログラマ)が用意したデータである。
図中、中央の"Vertex Shader"(頂点シェーダー)とあるのは、
あなたがGLSL言語で書いた関数である。
頂点シェーダーは、元となる頂点1つにつき1回呼び出される。
頂点シェーダーでは、呼び出しに使われた「元となる頂点情報」に対応する「クリップ空間上の値」を、
何らかの計算をして求め、その値を特別な変数である「gl_Position
」に書き込む。
GPUはその結果を取り出して、GPUが内部的に管理している専用の領域に保存する。
保存されたデータは、今回のコードではdrawArrays
呼び出しの際に「TRIANGLES
」を指定したので、
GPUは上記の処理を頂点3つ分繰り返すたびに、そこで得られた「クリップ空間上の値」
3つを使って三角形(triangle)を構成する。
これによって、「三角形の頂点」を画面上のどのピクセルに対応付ければ良いかが割り出され、
三角形が「ラスタライズ」、つまり、ファンシーな表現で言えば「ドット絵として、描かれる」
ことになる。
GPUは、そうして描かれることになったピクセル一つ一つに対して、
あなたが提供したフラグメントシェーダーを呼び出して、
「そのピクセルはどんな色であるか」質問する。
フラグメントシェーダーは、特別な変数gl_FragColor
に値をセットしてこの質問に回答
しなければならない。
ラスタライズとフラグメントシェーダーの仕組みは興味深いものであるが、ご存知のように 前回の講義で用意したサンプルプログラムでは 「フラグメントシェーダーが各ピクセルの色について答える」という部分は非常に単純なコードであり、 ほとんど何もしていなかった。 もっと多くの情報をフラグメントシェーダーに渡すことができるので、今回はそれをやってみよう。
頂点シェーダーからフラグメントシェーダーに情報を渡すために「varying」変数を使用する。 付け加えたい情報1件につき、1つの「varying」変数を定義する必要がある。
varyingの使い方の単純な例として、まずは「クリップ空間の座標データ」を 頂点シェーダーからフラグメントシェーダーに、そのまま、渡してみることにしよう。
今回は簡単な三角形を1つだけ描くことする。前回のプログラム で長方形を描いていたところを、三角形を描くように書き換える。
// 三角形を定義する頂点のデータをバッファにセットする
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, -100,
150, 125,
-175, 100]),
gl.STATIC_DRAW);
}
頂点数が3つになったので、シェーダーの呼び出し部分のcountも3に合わせる。
// シーン(scene)を描画する
function drawScene() {
...
// 形状(geometry)を描画する
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);
}
頂点シェーダーで、フラグメントシェーダーに渡すべきデータを varying変数として宣言する。
*varying vec4 v_color;
...
void main() {
// 座標データを行列で乗算する
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
// 「クリップ空間」から「色空間」へ変換する。
// クリップ空間は -1.0 ~ +1.0
// 色空間は 0.0 ~ 1.0
* v_color = gl_Position * 0.5 + 0.5;
}
同じvarying変数を、フラグメントシェーダーでも宣言する。
precision mediump float;
*varying vec4 v_color;
void main() {
* gl_FragColor = v_color;
}
これでWebGLは、「頂点シェーダで宣言されたvarying変数」を、 「同じ名前同じ型で宣言されたフラグメントシェーダー上のvarying変数」に接続する。
以上のコードを動かすと下のようになる。
上の画面でスライダを動かして、平行移動、回転、拡大縮小してみよう。 クリップ空間で計算された色が、三角形と共に動いていないことに気づいただろうか。 色は、背景に張り付いたような動きをしている。
ちょっと考えてみよう。今回頂点シェーダーで扱ったのは頂点3つだけである。 頂点シェーダーは3回だけ呼び出され、色についても3つの色を計算しただけである。 実際に表示されている三角形はたくさんの色で描かれているのに、である。 これは、varyingの名前の由来、「vary=変化する」に秘密がある。
WebGLは3頂点分の値を得て、2つのシェーダーを使ってそれを計算し、 頂点の間を「補間(つまり、あいだを補う)」することで三角形としてラスタライズしている。 フラグメントシェーダーは、この「補間によって三角形を構成することになったピクセル」 ひとつひとつについて1回ずつ呼び出されるのである。
上のサンプルでは、以下の3つの頂点情報を使っていた。
頂点 | |
---|---|
0 | -100 |
150 | 125 |
-175 | 100 |
我々の頂点シェーダーは、この頂点データに行列を適用することで、「平行移動、 回転、拡大縮小、クリップ空間への変換」という4つの操作を行っている。 「平行移動」、「回転」、「拡大縮小」については、変換行列のデフォルト値は 「translation = (200, 150)」、「rotation = 0」、「scale = (1, 1)」だったので、 実際には平行移動しているだけである。 バックバッファは400x300としているので、「クリップ空間への変換」 によって上で示した3つの頂点座標は、以下の「クリップ空間座標上の値」に変換される。
gl_Positionに書き込まれる値 | |
---|---|
0.000 | 0.660 |
0.750 | -0.830 |
-0.875 | -0.660 |
さらに、この「クリップ空間上の値」を、「色空間上の値」に変換して、 今回宣言したvarying変数のv_colorに書き込んでいる。
v_colorに書き込まれる値 | ||
---|---|---|
0.5000 | 0.830 | 0.5 |
0.8750 | 0.086 | 0.5 |
0.0625 | 0.170 | 0.5 |
v_colorに書き込まれたこれら3つの値は、補間され、 描かれるピクセルごとにフラグメントシェーダーに渡される。
以上で「頂点シェーダー経由でフラグメントシェーダーにデータを渡す」ことができた。
次は、2つの、色違いの三角形で構成された長方形を描いてみよう。 フラグメントシェーダーに別のデータを送る。
まず頂点シェーダーがデータを受け取れるように、新たなattributeを追加する。 そしてその内容を、そのままフラグメントシェーダーに渡すようする。
attribute vec2 a_position;
+attribute vec4 a_color;
...
varying vec4 v_color;
void main() {
...
// attributeで受け取った色のデータをvarying変数にコピーする。
* v_color = a_color;
}
使いたい色のデータをWebGLに与える。
// 頂点データの書き込み先となる、シェーダーのattributeのロケーションを得る。
var positionLocation = gl.getAttribLocation(program, "a_position");
+ var colorLocation = gl.getAttribLocation(program, "a_color");
...
+ // 色データを渡すためのバッファーを生成する。
+ var colorBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
+ // 色をセットするために用意した関数を呼び出す。
+ setColors(gl);
...
+// 長方形を構成する2つの三角形の色のデータを
+// バッファに書き込む。
+function setColors(gl) {
+ // 各三角形の色はランダム。
+ var r1 = Math.random();
+ var b1 = Math.random();
+ var g1 = Math.random();
+
+ var r2 = Math.random();
+ var b2 = Math.random();
+ var g2 = Math.random();
+
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array(
+ [ r1, b1, g1, 1,
+ r1, b1, g1, 1,
+ r1, b1, g1, 1,
+ r2, b2, g2, 1,
+ r2, b2, g2, 1,
+ r2, b2, g2, 1]),
+ gl.STATIC_DRAW);
+}
レンダリングのタイミングで、色データのattributeをセットする。
+gl.enableVertexAttribArray(colorLocation);
+
+// colorBufferをバインドする。
+gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
+
+// colorBuffer(ARRAY_BUFFER)に書き込んだデータを、
+// 頂点シェーダーがcolor attributeとしてどのように読み出すかを設定する。
+var size = 4; // 1回あたり4コンポーネント
+var type = gl.FLOAT; // データは32bit浮動小数点数(float)
+var normalize = false; // データの正規化は行わない
+var stride = 0; // 0 = 「次のデータはsize * sizeof(type)bytes先にある」という意味
+var offset = 0; // バッファの先頭から
+gl.vertexAttribPointer(
+ colorLocation, size, type, normalize, stride, offset)
シェーダーを呼び出す部分では、三角形2つ=6頂点なのでcountを6に変更する。
// Draw the geometry.
var primitiveType = gl.TRIANGLES;
var offset = 0;
*var count = 6;
gl.drawArrays(primitiveType, offset, count);
実行結果はこのようになる。
それぞれ別の色の、単色の三角形が2つできた。WebGLで各頂点に対して 色のデータを指定して、GPUは各頂点の色のデータを頂点間で補間している。 今回は各三角形の3つの頂点に同じ色を指定したため、各三角形は単色になっている。 3頂点に別の色を指定して、頂点間で補間される様子を見たいなら こんな風に変更すればよい。
// 長方形を構成する2つの三角形の色のデータを
// バッファに書き込む。
function setColors(gl) {
* // 各頂点の色はランダム。
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(
* [ Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1,
* Math.random(), Math.random(), Math.random(), 1]),
gl.STATIC_DRAW);
}
これで、varyingが補間されて「vary=変化」の様子がグラデーションとして見えるようなった。
以上、別におもしろい結果ではなかったかも知れないが、これで「複数のattributeのデータを 頂点シェーダー経由でフラグメントシェーダーに渡す」ことができるようになった。 「画像処理のサンプル」では、この方法を応用して シェーダーに対して「texture coordinate(テクスチャ内の座標)」を渡しているので、 興味があれば確認してみるとよいだろう。
「バッファ」とは、GPUが「頂点データや各頂点と1対1で結びついたデータ」を
取り込むための仕組みである。
gl.createBuffer
はバッファを生成する。
gl.bindBuffer
は、操作対象のバッファを指定する。
gl.bufferData
は、バッファにデータをコピーする。
これは通常、初期化のタイミングで行なわれる。
「バッファにデータを入れる」ことができたら、 次は、「バッファからデータを取り出す手順」、 つまり「頂点シェーダーのattributeへと取り込む手順」をWebGLに教える必要がある。
シェーダーのコード中でattribute変数が宣言されていると、WebGLは、 各attributeに対して自動的にロケーション(location)を割り当てる。 まず、それがどこなのかをWebGLに対して尋ねる。上の例では
// 頂点データの書き込み先となる、シェーダーのattributeのロケーションを得る。
var positionLocation = gl.getAttribLocation(program, "a_position");
var colorLocation = gl.getAttribLocation(program, "a_color");
の部分がこれに当たる。通常、この処理も初期化のタイミングで行なう。
attributeのロケーションが得られたら、描画する直前のタイミングで3つのコマンドを実行する。
gl.enableVertexAttribArray(location);
1つめのコマンドは、WebGLに「データはバッファから渡す」と伝えるためのものである。
gl.bindBuffer(gl.ARRAY_BUFFER, someBuffer);
2つめのコマンドは、「バッファ」を「ARRAY_BUFFER
バインドポイント」に
バインドする(バインド(bind)は「結びつける」といった意味である)。
ARRAY_BUFFER
は、WebGLが内部で定義しているグローバル変数である。
gl.vertexAttribPointer(
location,
numComponents,
typeOfData,
normalizeFlag,
strideToNextPieceOfData,
offsetIntoBuffer);
3つめのコマンドは、「現在ARRAY_BUFFERバインドポイントに結びつけられているバッファ
からデータを取得する」ように、WebGLに対して命令するもので、「numComponents:1つの
頂点に対していくつコンポーネントがあるか(1個~4個)」、「typeOfData:データの型
(BYTE
, FLOAT
, INT
, UNSIGNED_SHORT
など)はどれか」、
「strideToNextPieceOfData:次のデータまで何バイトあるか」、
「offsetIntoBuffer:オフセット」といった、データ取得に必要な情報が添えられる。
numComponentsは常に1個から4個である。
一種類のデータに対してバッファを一つ割り当てる場合、 「ストライド(strideToNextPieceOfData)」と「オフセット(offsetIntoBuffer)」は常に0となるだろう。 「ストライド」の値が0というのは、「ストライドの量が、データ型とサイズに相応」という意味である。 「オフセット」の値が0というのは、「バッファの先頭のデータから読み出す」という意味である。 これらを0以外の値とする場合、処理は複雑になってくる。 それによってWebGLのパフォーマンスを限界まで引き出すことができる場合もあるが、 複雑になることとのトレードオフに見合う状況は多くあるまい。
以上が「バッファ」と「属性(attribute)」についての説明となる。
次回は「シェーダーとGLSL」について説明する。