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において描画に必要となる最低限の処理を実装してみましょう。基本的な流れとしては、以下のようになります。

  1. canvas要素からWebGLコンテキストを取得
  2. Vertex ShaderとFragment Shaderを作成
  3. Vertex ShaderとFragment ShaderにGLSL ESのソース文字列を設定
  4. ソースをコンパイルし、シェーダーで実行するコードを生成
  5. Programオブジェクトを作成
  6. Programオブジェクトにシェーダーを設定
  7. Vertex Shaderとデータをやり取りするattribute変数の情報(番号)を取得
  8. attribute変数を有効化
  9. Vertex Shaderに送信する頂点データを格納するFloat32配列を作成
  10. バッファを作成し、WebGLコンテキストにバインド
  11. バッファにデータを書き込む
  12. バッファのデータをattribute変数に設定
  13. 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の仕様書で確認してみてください。


創作プログラミングの街