WebGLによる描画の流れ
Webブラウザ上でJavaScriptを通して3Dグラフィックを描けるWebGL。ただ、WebGL(OpenGL ES 2.0)は、「シェーダー」を直接プログラムする形で利用することになるので、通常の3Dグラフィックシステムに比べると、かなり敷居が高い面があります。
従来のOpenGLやOpenGL ES 1.xでは、カメラや視体積を設定すると3D(ワールド)座標からデバイス/スクリーン座標への変換を行う行列を自動的に作成・設定してくれましたが、WebGLではこれらの座標変換を行う配列も自分でJavaScriptとシェーダーのコード(GLSL ES)を通して設定しないといけないのです。「3D座標系に配置した頂点」に対する「見せ方(スクリーン上の座標へ配置する演算)」さらに「ピクセルに設定する具体的なRGB値」などを、「頂点などのデータを受け取って処理するシェーダーのコード」として記述する必要があります。
もっとも座標変換に関する行列の計算は定型的なものですし、JavaScriptのライブラリを使う方法もあるので、「一度できるようになれば」それほど難しくないとも言えるかもしれません。ただ、まず最初にそこにたどり着くまでの「最初の一歩」がかなり辛いものになっているんですよね。
そこで、今回はまず「頂点の計算や渡されるデータを演算して色付けを行う具体的な処理」は省き、シェーダーに簡単なコードを設定して画面上に赤い三角形を描くだけのプログラム(JavaScriptを含むHTMLファイル)を作ってみることにしました。WebGLの「最初の半歩」として、「指定された頂点をそのまま出力し、適当に色をつける」(ほとんど何もしない)シェーダーを作成し、WebGLにおける描画の流れを確認してみるわけです。
今回の実験では、WebGLにおいて描画に必要となる最低限の処理を実装してみましょう。基本的な流れとしては、以下のようになります。
- canvas要素からWebGLコンテキストを取得
- Vertex ShaderとFragment Shaderを作成
- Vertex ShaderとFragment ShaderにGLSL ESのソース文字列を設定
- ソースをコンパイルし、シェーダーで実行するコードを生成
- Programオブジェクトを作成
- Programオブジェクトにシェーダーを設定
- Vertex Shaderとデータをやり取りするattribute変数の情報(番号)を取得
- attribute変数を有効化
- Vertex Shaderに送信する頂点データを格納するFloat32配列を作成
- バッファを作成し、WebGLコンテキストにバインド
- バッファにデータを書き込む
- バッファのデータをattribute変数に設定
- attribute変数に渡した頂点情報を使って描画コマンドを実行
シェーダーにProgram、配列、バッファ……図形一つ描くのにやけに「準備」が多いですね。一つ一つの準備はコードにしてせいぜい数行なので、全体のコード量はそれほど多くはないのですが、最初のうちは全体が見通しにくいかもしれません。
まずは、バーテックス/フラグメントの両シェーダーから作ってみましょう。シェーダーは、WebGLコンテキストのcreateShader()で作成します。
// バーテックス/フラグメントシェーダーを作成
var vshader = gl_context.createShader(gl_context.VERTEX_SHADER);
var fshader = gl_context.createShader(gl_context.FRAGMENT_SHADER);
続いて、シェーダーのソースコードを文字列として設定し、それぞれコンパイルします。ソースコードはC言語風の書き方で、シェーダーのコードが呼び出されるとmain()関数が実行されます。
// バーテックスシェーダーにソースコードを設定
gl_context.shaderSource(vshader, 'attribute vec2 vPos; void main() { gl_Position = vec4(vPos, 0.0, 1.0); }');
// バーテックスシェーダーのソースをコンパイル
gl_context.compileShader(vshader);
// フラグメントシェーダーにソースコードを設定
gl_context.shaderSource(fshader, 'void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }');
// フラグメントシェーダーのソースをコンパイル
gl_context.compileShader(fshader);
バーテックスシェーダーに設定しているソースコードは、以下のようになっています。
attribute vec2 vPos;
void main() {
gl_Position = vec4(vPos, 0.0, 1.0);
}
vec2やvec4というのは、GLSLの型です。vec2は2次元ベクトル(2つの要素を持つ配列)、vec4は4次元ベクトルで、今回のコードではそれぞれ「JavaScriptからシェーダーに渡す2次元座標」「シェーダーが処理結果として出力する3次元上の同次座標」を格納するのに使っています。
vec4()は、コンストラクタで今回は(vec2型変数, a1, a2)という形で呼び出しています。この場合、vec2型の二つの要素にa1、a2二つの要素を加えたvec4型オブジェクトを作ることになります。たとえば、vPosが(0.5, 0.5)という値を持つのなら
gl_Position = vec4(vPos, 0.0, 1.0);
は、(0.5, 0,5, 0.0, 1.0)という4つの要素を持つvec4型オブジェクトをgl_Positonに格納することになるわけです。
attributeは、JavaScriptからデータを受け取るために付ける属性です。今回はattribute変数vPosにJavaScriptから頂点のx/y座標を入れ、それをシェーダーのコードで「そのまま(zとして0を追加して))出力変数gl_Positionに入れるようにしてみました。
フラグメントシェーダーのコードは、単に「赤(RGBA:1.0, 0.0, 0.0, 1.0)を出力」するだけです。
シェーダーにコードを設定したら、Programオブジェクトを作成し、そこにシェーダーを割り当てて利用できるようにします。
// programオブジェクトを作成
var gl_program = gl_context.createProgram();
// programオブジェクトのシェーダーを設定
gl_context.attachShader(gl_program, vshader);
gl_context.attachShader(gl_program, fshader);
// シェーダーを設定したprogramをリンクし、レンダラーに割り当て
gl_context.linkProgram(gl_program);
gl_context.useProgram(gl_program);
これで、WebGLでシェーダーのコードを使える状態になったので、シェーダーに頂点データを渡すattribute変数vPosの管理情報(WebGLシステム内での番号)を取得し、データを送ってみましょう。
シェーダー内部のattribute変数に値を設定するには、まずenableVertexAttribArray()で変数を有効化します。続いてWebGLのバッファを作成し、バッファをバインドしてからvertexAttribPointer()でバッファを変数のデータとして設定すると、そのデータが反映されます。
頂点データとして設定するデータには、原点(0. 0)を中心とする三角形を入れてみました。WebGLでは、-1~1の範囲に描画が行われるので、今回のように±0.5の座標を指定すると、画面の半分程度の長さを占めるようになります。
// attribute変数vPosの番号を取得
var vattLocation = gl_context.getAttribLocation(gl_program, 'vPos');
// vPosを有効化
gl_context.enableVertexAttribArray(vattLocation);
// 頂点データをFloat32配列として作成
var vlist = new Float32Array(new Array(0.0, 0.5, 0.5, -0.5, -0.5, -0.5));
// バッファ作成
var vbuf = gl_context.createBuffer();
// バッファをバインドしてデータ領域を初期化・データを転送する
gl_context.bindBuffer(gl_context.ARRAY_BUFFER, vbuf);
gl_context.bufferData(gl_context.ARRAY_BUFFER, vlist, gl_context.STATIC_DRAW);
// データを書き込んだバッファをvPosのデータとして設定
gl_context.vertexAttribPointer(vattLocation, 2, gl_context.FLOAT, false, 0, 0);
最後に、WebGLの描画コマンドで設定した頂点に三角形を描いてみましょう。
gl_context.drawArrays(gl_context.TRIANGLES, 0, 3);
以上のJavaScriptを埋め込んだテストHTML全体は、以下のようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<canvas id="screen" width="300" height="300" style="background: #000000;"></canvas>
<script>
var screen_elm = document.getElementById("screen");
// canvas要素のWebGLコンテキストを取得
var gl_context = screen_elm.getContext("experimental-webgl");
// バーテックス/フラグメントシェーダーを作成
var vshader = gl_context.createShader(gl_context.VERTEX_SHADER);
var fshader = gl_context.createShader(gl_context.FRAGMENT_SHADER);
// バーテックスシェーダーにソースコードを設定
gl_context.shaderSource(vshader, 'attribute vec2 vPos; void main() { gl_Position = vec4(vPos, 0.0, 1.0); }');
// バーテックスシェーダーのソースをコンパイル
gl_context.compileShader(vshader);
// フラグメントシェーダーにソースコードを設定
gl_context.shaderSource(fshader, 'void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }');
// フラグメントシェーダーのソースをコンパイル
gl_context.compileShader(fshader);
// programオブジェクトを作成
var gl_program = gl_context.createProgram();
// programオブジェクトのシェーダーを設定
gl_context.attachShader(gl_program, vshader);
gl_context.attachShader(gl_program, fshader);
// シェーダーを設定したprogramをリンクし、割り当て
gl_context.linkProgram(gl_program);
gl_context.useProgram(gl_program);
// attribute変数vPosの番号を取得
var vattLocation = gl_context.getAttribLocation(gl_program, 'vPos');
// vPosを有効化
gl_context.enableVertexAttribArray(vattLocation);
// 頂点データをFloat32配列として作成
var vlist = new Float32Array(new Array(0.0, 0.5, 0.5, -0.5, -0.5, -0.5));
// バッファ作成
var vbuf = gl_context.createBuffer();
// バッファをバインドしてデータ領域を初期化・データを転送する
gl_context.bindBuffer(gl_context.ARRAY_BUFFER, vbuf);
gl_context.bufferData(gl_context.ARRAY_BUFFER, vlist, gl_context.STATIC_DRAW);
// データを書き込んだバッファをvPosのデータとして設定
gl_context.vertexAttribPointer(vattLocation, 2, gl_context.FLOAT, false, 0, 0);
// シェーダーにvPosを通して渡した頂点データで三角形を描画
gl_context.drawArrays(gl_context.TRIANGLES, 0, 3);
</script>
</body>
</html>
テストHTMLを開く(動作を見るには、WebGL対応のWebブラウザ/GPUが必要です)
全体を見るとJavaScriptコードの行数としてはそれほど多くはないものの、シェーダーがJavaScriptとは「別世界」にある分シェーダーとのやり取りでかなり手数が増えてしまいますね。今回使用したAPIの詳細については、WebGLの仕様書で確認してみてください。