WebGLの勉強を始めました。

  • 43
    Like
  • 2
    Comment
More than 1 year has passed since last update.

WebGLの勉強を始めました。
勉強した内容を備忘録としてまとめていきます。
今回書くのは、以下の単純なポリゴンを描くまでです。
それでもすごいコード量・・。


[2013.08.01追記]
GLSLについてのメモを書きました。
GLSL関連についてはこちらに書いていきます。


[2013.07.29追記]

自作WebGL Helperを書いてみる

動作の理解とちょっとしたデバッグをさくっとやるために、
WebGLメソッドの自作のHelperを作ってみました。(まだ開発中)

WebGLHelperと長いnamespeceなので、jQueryくらい手軽に、ってことを意味して
$glにエイリアスを作っています。

基本的な使い方は以下のように$gl.getGLContext(canvas);を呼び出してからごにょごにょしていきます。

webgl-helper.js
var canvas = document.getElementById('canvas');
var gl = $gl.getGLContext(canvas);
var v_shader = $gl.getShaderSourceFromDOM('vs');
var vs = $gl.createShader('vertex', v_shader);

最初に$gl.getGLContextを呼び出すのが必須です。
これ以降、内部にcontextを保持してそれを通して各種メソッドを実行していく、というのが基本コンセプトです。


ちなみに本記事を書くに当たって、WebGLの詳細な情報を掲載しているwgld.orgさんを参考にさせて頂きました。
ありがとうございます。

WebGLで板ポリ一枚描いた結果
サンプルはjsdo.itに上がっています。

基本的なポリゴンを描くまでのフロー

  • まずはなにはともあれ、canvas要素の取得と、webglコンテキストの取得を行います。
var cv = document.getElementById('canvas');
var gl = cv.getContext('webgl') || cv.getContext('experimental-webgl');
  • experimental-webglは現状では実験的機能のためこう記述しています。
    将来的には2d同様、webglのみで取得できるようになります。
  • 続いて、画面をクリアする色と深度を設定(基本的に、今後の処理は取得したコンテキスト(上記例ではglに生えたメソッドを通して、データのバインドや設定などを行なっていきます)
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clearDepth(1.0);

//設定した色と深度でクリア
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  • 次に、vertex shaderとfragment shaderをテキストで読み込み、それらをコンパイルします。(言語はGLSLで記述)
    基本的にはscriptタグなど、評価されない場所にテキストデータとして書いておき、それを取得・コンパイルしてWebGLのプログラムに渡してやります。
//Vertex shaderの例
attribute vec3 position; //3次元ベクトル
attribute vec4 color;    //色は4次元(RGBA)ベクトル

varying vColor; //fragment shaderに渡す値

uniform mat4 mvpMatrix; //座標変換マトリクス

void main() {
    //fragment shaderへ値を渡す(varying変数)
    vColor = color;

    //渡された頂点座標を、変換行列を用いてワールド空間座標に変換
    gl_Position = mvpMatrix * vec4(position, 1.0);
}
//Fragment shaderの例
precision mediump float; //floatの精度を指定

varying vec4 vColor;

void main() {
    //ここではvertex shaderから渡された値をそのまま設定
    gl_FragColor = vColor;
}

※varying変数はvertex shaderとfragment shader間で値を渡せる仕組みです。
vertex shader内で色に関する計算(例えば頂点位置に応じた色の算出など)をし、それをfragment shaderに伝え、fragment shader側ではそれを元に色を決定します。

//vertex shaderの生成例
var shader = gl.createShader(gl.VERTEX_SHADER); //fragment shaderの場合は定数を`gl.FRAGMENT_SHADER`にする。
var source = '…'; //上記のGLSLのサンプルテキストを某かで読み込む。

//生成したshaderオブジェクトにソースを設定し、それをコンパイルする。
gl.shaderSource(shader, source);
gl.compileShader(shader);

//コンパイルが正常に終了していればshaderオブジェクトを返して終了。
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    return shader;
}
else {
    alert(gl.getShaderInfoLog(shader);
}

流れは、まず空のshaderオブジェクトを生成します。生成する際、vertex shaderなのかfragment shaderなのかを定数で指定します。

生成した空のshaderオブジェクトに、取得したシェーダ用ソースをgl.shaderSourceメソッドで設定します。

設定したらそれをコンパイルします。コンパイルすると、GLSLにエラーがある場合はcompile statusがfalse相当になるのでそれを元に分岐を行い、エラーがあった場合はそれを出力する処理を書いています。

基本的にgl.get****Parameter(****, 定数)という書式でパラメータを取得し、gl.get****InfoLog(****)で、ログを取得します。(****の部分はshaderだったりprogramだったりです)

  • ふたつのシェーダを生成(コンパイル)したら、それをリンクします。リンクするにはgl.createProgram()を利用します。
var prg = gl.createProgram();

//vertex shaderとfragment shaderをそれぞれアタッチ
gl.attachShader(prg, vs);
gl.attachShader(prg, fs);

//アタッチしたらそれをリンク
gl.linkProgram(prg);

if (gl.getProgramParameter(prg, gl.LINK_STATUS)) {
    gl.useProgram(prg);
    return prg;
}
else {
    alert(gl.getProgramInfoLog(prg);
}

流れとしては、まずprogramオブジェクトを生成します。

次に、生成したふたつのシェーダ(Vertex shaderとFragment shader)をリンクします。

リンク作業でエラーが出ているかを確認し、問題がなければgl.useProgram(prg);で、プログラムオブジェクトを使用可能な状態にし、それをreturnします。

大まかな流れ

さて、これでシェーダとJavaScriptを結びつけるところまでを解説しました。
ここまで書いても、まだポリゴンは一枚も描かれていません。そしてまだまだ先があります・・( ;´Д`)

この後の大まかな流れを先に説明しておくと、3D空間に必要な情報(頂点情報)と、その情報を扱うバッファ、そしてそれらをWebGLに通知する、という処理をずらずらと書いていくことになります。

JavaScriptとWebGLは別物で、それぞれ独立した空間にあり、その通知に関してはインデックス番号や名前でしかやり取りできない、と考えておくと混乱が少ないかもしれません。

ということで、まずは頂点の情報を準備します。(ちなみに頂点の情報といわれると位置情報(x, y, z)をイメージするかと思いますが、頂点情報として扱われるものは非常にたくさんあります。もちろん位置情報だけは必須ですが、それ以外にもカラー情報やテクスチャマップをする場合のテクスチャの座標など、実に様々な情報を扱うことができます)

var position = [
    1.0, 1.0, 0.0,
    0.0, 0.0, 0.0,
    2.0, 0.0, 0.0
];

var color = [
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0
];

var index = [
    0, 1, 2
];

上記が「頂点情報」としての「位置」と「色」、そして最後がそれらの頂点情報を使う順番(インデックス)を定義している箇所になります。

この情報を、今度はWebGLに通知する箇所を記述していきます。
流れとしては、

  1. GLSL側での変数の位置(インデックス)の取得
  2. それの有効化
  3. そしてその変数へのデータの送信、というのが手順となります。

順番に見て行きましょう。

var attLocation = [];

//各シェーダをリンクさせたprogramオブジェクトから該当の変数のインデックス(Location)を取得
attLocation[0] = gl.getAttribLocation(prg, 'position');
attLocation[1] = gl.getAttribLocation(prg, 'color');

//それぞれの頂点情報がいくつの要素から構成されているかの整数値。
//(例えばカラーであればひとつの頂点に対して4つの情報(RGBA)があるため「4」と指定する。
var attDiv = [3, 4];

gl.getAttribLocationメソッドは、第一引数に指定されたプログラムオブジェクト(prg)の中から、第二引数で指定した変数名の位置(インデックス(Location))を取得するメソッドです。

つまり、attLocation[0]にはインデックスである整数が返ります。
このインデックス番号を使って、該当するGLSL側の変数に適切な値を送信する、というわけです。

バッファの生成

var vPosition = create_vbo(position);
var vColor = create_vbo(color);
var ibo = create_ibo(index);

さて、ここで関数が登場しました。が、これは独自で生成したヘルパー関数です。
関数の中身は以下のようになっています。

function create_vbo(data) {
    //バッファオブジェクトの生成
    var vbo = gl.createBuffer();

    //生成したバッファをバインドする
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo);

    //バインドしたバッファにデータを登録
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);

    //バインドしたバッファを解除
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    //生成・データ登録の済んだVBO(Vertex Buffer Object)を返す
    return vbo;
}

渡されたデータ(data)を元に、バッファオブジェクトを生成します。(VBOはVertex Buffer Objectの略)

WebGLでは基本的に、バインド・設定・登録・解除といった一連の流れを踏みます。

上記はVBOの生成でした。続いてIBO(Index Buffer Object)の生成ヘルパー関数を見ます。

function create_ibo(data) {
    //バッファオブジェクトの生成
    var ibo = gl.createBuffer();

    //バッファをバインド
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);

    //バインドしたバッファにデータを登録
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);

    //バインドしたバッファを解除
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

    //生成したIBO(Index Buffer Object)を返す
    return ibo;
}

こちらはIBO(Index Buffer Object)の生成ヘルパー関数です。
見てもらうと分かりますが、基本的にやっていることはほぼVBOのものと同じです。
違うのは、生成したバッファをgl.ELEMENT_ARRAY_BUFFERでバインドしている点のみです。

生成したバッファオブジェクトをWebGLに通知

さて、ここまでではまだバッファオブジェクトを生成したのみで、それを利用できるようにはなっていません。
引き続き、これをWebGLに通知して使える状態にしてやる必要があります。

//WebGLへのVBOの登録、通知
gl.bindBuffer(gl.ARRAY_BUFFER, vPosition);

//通知したい変数(のインデックス)を有効化
gl.enableVertexAttribArray(attLocation[0]);
//第一引数は頂点属性の番号、第二引数はひとつの頂点の要素数、第三引数は要素のデータ型
gl.vertexAttribPointer(attLocation[0], attDiv[0], gl.FLOAT, false, 0, 0);

//上記と同様、各頂点ごとの色情報を登録、通知
gl.bindBuffer(gl.ARRAY_BUFFER, vColor);
gl.enableVertexAttribArray(attLocation[1]);
gl.vertexAttribPointer(attLocation[1], attDiv[1], gl.FLOAT, false, 0, 0);

ここでは生成した各種頂点情報(VBO)をWebGLに登録、通知しています。
大まかな流れは、

  1. バッファのバインド
  2. バインドしたバッファの通知先(変数)を有効化
  3. 有効化した変数に、頂点属性番号、頂点情報の要素数、データ型などを渡して通知

という流れになります。
若干流れが把握しづらいですが(俺だけかな;)、バッファをバインドし、バッファの通知先を有効化(つまり該当変数に格納する入り口を開けるイメージ)し、有効化した変数に対して現在バインドしているデータ(バッファ)を通知(送信)する、という感じです。

ちなみにvertexAttribPointerの第4〜6引数はそれぞれ
第4引数 - 渡された値を正規化するかどうかのフラグ
第5引数 - 値のストライド
第6引数 - バッファのどの位置から値が始まるか
を指定しています。

var uniLocation = [];
uniLocation[0] = gl.getUniformLocation(prg, 'mvpMatrix');

var m = new matIV();
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());

m.lookAt([0, 0, 10], [0, 0, 0], [0, 1, 0], vMatrix);
m.perspective(45, cv.width / cv.height, 0.1, 100, pMatrix);
m.multiply(pMatrix, vMatrix, mvpMatrix);
m.multiply(mvpMatrix, mMatrix, mvpMatrix);

gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);

続いてuniform変数へのデータの引渡しです。
attribute変数のときと同様、まずはuniform変数の位置(Location)を取得します(gl.getUniformLocation)。

その後のマトリクスの計算はそれぞれ、モデル、ビュー、プロジェクション3種(MVP)座標変換行列の計算です。(座標変換については以前書いたこちらの記事に情報を書いています。あんまりまとまってませんが・・)

計算した結果を、gl.uniformMatrix4fvメソッドでWebGL側にデータを通知します。


ちなみにuniformMatrix4fvはuniform変数への通知に使うメソッドです。
これにはいくつか種類があり、[1]uniform系、[2]uniform*v系、[3]uniformMatrix系があります。

[1]は1〜4まで数字を後ろに付けて呼び出します。数字の意味は、uniform変数に渡すデータの数です。(e.g. gl.uniform2f(uniformLocation, data1, data2))
さらに上記例では2の後ろにfがついていますが、これは渡されるデータの型です。(例ではfloat型。int型の場合はiとなる。(e.g. gl.uniform2i)

[2]はuniform系と同様、1〜4までの数字が付きますが、渡されるデータを配列として渡す場合に使います。(e.g. gl.uniform3fv(uniformLocation, array))

[3]はその名の通り行列(matrix)を渡す場合に使用します。
行列の性質上、2〜4の数字を使用します。(1だと行列にならない)
さらにuniformMatrixは基本的に浮動小数点数のデータなので整数型であるiは存在せず、データも配列として使うことが前提となっているためすべてにvが付与されます。

//ebGLへのIBOの登録、通知
//ここで設定されたインデックス情報を元に、レンダリングが行われる。
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);

//レンダリングするタイプ, indexの要素数, indexデータのデータサイズ, indexのオフセット
//を指定する。
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
gl.flush();

さて、やっとポリゴンをレンダリングする瞬間がやってきました。
これだけお膳立てをしてやっと、三角形1枚のポリゴンを描くことができます。
3Dプログラミングおそるべし、です。

最後のところでやっているのは、生成・登録してきた頂点情報群をどういった順番(インデックス)でレンダリングするのか、そのためのバッファ(IBO)をバインドし、それを元にレンダリングを行なっています。

レンダリングを行なっているのはgl.drawElementsの部分です。
引数は「レンダリングする種類、インデックスの配列数、渡されるデータの型」です。

そして最後のgl.flush()によって設定したデータを元にポリゴンが画面に表示されるようになります。

いやー長かった。
ただ、どういうことをやっているのかをひとつひとつ紐解いていくことで、だいぶクリアになるかと思います。

最初のほうで書いた、JavaScriptとWebGLは別空間にあり、それぞれは変数の位置(Location)でしかやり取りできず、データの通知にはバインドと位置を利用して設定していく、というのが基本の流れぽいので、そのあたりをうまくイメージできればそれほどむずかしくないと思います。(とりあえず上記までであれば、ですが・・( ;´Д`))

Link集

memo / Reference

http://www.nihilogic.dk/labs/webgl_cheat_sheet/WebGL_Cheat_Sheet.htm