この記事はWebGL2の基本からの続きです。 続きを始める前にWebGLとGPUがどのように動作してるか、説明する必要があります。 GPUには2つの基本的な動作があります。 1つ目は頂点データ(頂点座標に限らず与えられたバッファ上のデータストリーム)をクリップ空間の座標データに変換します。 2つ目は1つ目の処理結果を元にピクセルを描画します。
以下のコードでは
gl.drawArrays(gl.TRIANGLES, 0, 9);
第3引数は9つの頂点を処理するという意味でGPUに対する命令です。
図の左列は「元となる頂点情報」であなたが用意したデータです。
図の中央は「頂点シェーダー」でGLSLで書いた関数です。
「頂点シェーダー」は元となる頂点1つにつき1回呼び出されます。
「元となる頂点情報」に対応する「クリップ空間上の値」(図の右列)を何らかの計算を行い、その値を特別な変数 gl_Position
に設定します。
GPUはその結果を取得し、内部で管理している専用の領域に保存します。
gl.drawArraysの第1引数に TRIANGLES
を指定したので、GPUは3つの頂点を生成するたびに「クリップ空間上の値」3つを使い三角形を作ります。
三角形の3点がどのピクセルに対応しているかを計算し、三角形を描画します。
各ピクセルに対してフラグメントシェーダーを呼び出すと、そのピクセルをどんな色にするかを尋ねてきます。
フラグメントシェーダーは、そのピクセルに必要な色のvec4を出力します。
描画とフラグメントシェーダーの仕組みは非常に興味深いですが、ここまでの例を見て頂ければわかるように、 フラグメントシェーダーは1ピクセルあたりの情報量が非常に少ないのです。 幸いな事にもっと多くの情報を渡す事ができます。 頂点シェーダーからフラグメントシェーダーに渡すそれぞれの値のヴァリイング(varying)を定義します。
簡単な例として、頂点シェーダーから直接計算したクリップ空間の座標データをフラグメントシェーダーに渡してみましょう。
簡単な三角形を描画していきます。 前回の例に引き続き、長方形を三角形に変更してみましょう。
// Fill the buffer with the values that define a triangle.
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, -100,
150, 125,
-175, 100]),
gl.STATIC_DRAW);
}
頂点数が3つになったので、シェーダーの呼び出し部分のcountも3にします。
// Draw the scene.
function drawScene() {
...
// Draw the geometry.
* gl.drawArrays(gl.TRIANGLES, 0, 3);
}
頂点シェーダーでは、フラグメントシェーダーにデータを渡すために varying のv_colorを out
で定義します。
out vec4 v_color;
...
void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
// Convert from clip space to color space.
// Clip space goes -1.0 to +1.0
// Color space goes from 0.0 to 1.0
* v_color = gl_Position * 0.5 + 0.5;
}
フラグメントシェーダーでも同じ varying のv_colorを in
で定義します。
#version 300 es
precision highp float;
in vec4 v_color;
out vec4 outColor;
void main() {
* outColor = v_color;
}
頂点シェーダーのvaryingをフラグメントシェーダーの同名・同型のvaryingに接続します。
以下が動くサンプルコードです。
上記のサンプルコードでスライダーを動かし、平行移動・回転・拡大縮小してみましょう。 色はクリップ空間から計算されているので、三角形と一緒に移動してない事に気づくと思います。 色は背景に張り付いたような動きをしています。
ちょっと考えてみましょう。 頂点シェーダーで扱ったのは頂点3つだけです。 頂点シェーダーは3回だけ呼び出され色も3つの色を計算しただけですが、三角形はたくさんの色で描画されています。 これは varying に秘密があります。
各頂点に対して計算した3つの値を取り三角形を描画する時、頂点に対して計算した値の間を補間します。 各ピクセルでは、補間された値を持つフラグメントシェーダーを呼び出します。
上記の例では3つの頂点を使っていました。
頂点 | |
---|---|
0 | -100 |
150 | 125 |
-175 | 100 |
頂点シェーダーに行列を適用し、移動・回転・拡大縮小してクリップ空間への変換を行います。 移動・回転・拡大縮小のデフォルト値は、移動 = 200, 150、回転 = 0、拡大縮小=1, 1 なので移動しただけです。 バックバッファが 400 x 300 である事を考えると、頂点シェーダーは行列を適用し次の3つのクリップ空間座標に変換して、gl_Positionに書き込みます。
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つの三角形で構成される長方形を2色で描いてみましょう。 まず頂点シェーダーに別の属性を追加し、もっと多くのデータを渡せるようにします。 そして、そのデータを直接フラグメントシェーダーに渡します。
in vec2 a_position;
+in vec4 a_color;
...
out vec4 v_color;
void main() {
...
// Copy the color from the attribute to the varying.
* v_color = a_color;
}
使いたい色のデータをWebGLに伝えます。
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
+ var colorLocation = gl.getAttribLocation(program, "a_color");
...
+ // Create a buffer for the colors.
+ var buffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+
+ // Set the colors.
+ setColors(gl);
// setup attributes
...
+ // tell the color attribute how to pull data out of the current ARRAY_BUFFER
+ gl.enableVertexAttribArray(colorLocation);
+ var size = 4;
+ var type = gl.FLOAT;
+ var normalize = false;
+ var stride = 0;
+ var offset = 0;
+ gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset);
...
+// Fill the buffer with colors for the 2 triangles
+// that make the rectangle.
+function setColors(gl) {
+ // Pick 2 random colors.
+ 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);
+}
そして、その結果がこちらです。
2つの単色カラーの三角形ができました。 varying で値を渡しているので、各頂点の色のデータを頂点間で補間しています。 今回は各三角形の3つの頂点に同じ色を指定したため、各三角形は単色になっています。 それぞれの頂点色を違う色にすると、頂点の間で補間された色になります。
// Fill the buffer with colors for the 2 triangles
// that make the rectangle.
function setColors(gl) {
// Make every vertex a different color.
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 で色が補間されてグラデーションとして見えるようなりました。
あまり面白い結果ではなかったかもしれないが、複数の属性を使用して頂点シェーダーからフラグメントシェーダーにデータを渡す事ができるようになりました。 2D画像を処理する方法では、属性にテクスチャ座標を渡しているので興味があればチェックして見て下さい。
バッファは、GPUが頂点データや各頂点と1対1で結びついたデータを取り組むための仕組みです。
gl.createBuffer
でバッファを作成します。
gl.bindBuffer
は操作対象のバッファを設定します。
gl.bufferData
は現在のバッファにデータをコピーします。
データをバッファに入ったらどのようにデータを取り出し、頂点シェーダーの属性に渡すか指定する必要があります。
これはまず、属性に割り当てたロケーションを確認します。 例えば上記のコードでは
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
var colorLocation = gl.getAttribLocation(program, "a_color");
の部分でロケーション確認をしています。 属性のロケーションが分かったら、2つのコマンドを発行します。
gl.enableVertexAttribArray(location);
このコマンドは、データはバッファから渡す事をWebGLに伝えます。
gl.vertexAttribPointer(
location,
numComponents,
typeOfData,
normalizeFlag,
strideToNextPieceOfData,
offsetIntoBuffer);
このコマンドは、最後にgl.bindBufferでバインドされたバッファからデータを取得するようにWebGLに指定します。
numComponents = 頂点あたりのコンポーネント数 (1 〜 4)、
typeOfData = データの型 (BYTE
, FLOAT
, INT
, UNSIGNED_SHORT
など)、
strideToNextPieceOfData = 次のデータまで何バイトあるかストライド、
offsetIntoBuffer = データがバッファのどこまであるかを表すオフセット
などを指定します。
numComponentsは常に1〜4個です。
データの型ごとに1つのバッファを割り当てる場合は、ストライドとオフセットの両方は常に0にできます。 ストライド = 0は「データの型とサイズに合ったストライドを使う」という意味です。 オフセット = 0はバッファの先頭からの開始という意味です。 これらを0以外の値に設定すると処理が複雑になりますが、パフォーマンスの面ではメリットがあるかもしれません。 しかし、パフォーマンスを限界まで引き出す必要がなければ複雑にする価値はありません。
以上の説明でバッファと属性が明確になると良いと思います。
WebGLの仕組みを理解する別の方法として、 このインタラクティブな状態図を見てみるのもいいかもしれません。
次はシェーダーとGLSLを見てみましょう。