WebGLを簡単に使うためのzero.jsを作成しました

WebGLの登場で我らがウェブ業界にも徐々に3Dの波が押し寄せており、スプライトを利用した2Dゲームがいつのまにか3Dに駆逐されたように、3Dのできないウェブプログラマが駆逐されるのも時間の問題のようにさえ感じられます。

しかしいざWebGLを始めようと思っても、たとえば画面にドットを1つ表示しようと思っただけで

<html>
<head>
  <script src="CanvasMatrix.js"></script>
  <script>
  function main() {
    var domElement = document.createElement('canvas');
    domElement.width = 500;
    domElement.height = 500;
    document.body.appendChild(domElement);

    var gl = domElement.getContext('webgl') || 
      domElement.getContext('experimental-webgl');
    if (!gl) throw 'WebGL is not supported.';

    var positions = [
      -0.5,  0.5, 0.0,
       0.5,  0.5, 0.0,
      -0.5, -0.5, 0.0,
       0.5, -0.5, 0.0
    ];
    var indices = [
      0, 1, 2,
      2, 1, 3
    ];
    var numIndices = indices.length;

    var vbuffers = [positions, positions];
    for (var i = 0; i < vbuffers.length; i++) {
      var data = vbuffers[i];
      var vbuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
      vbuffers[i] = vbuffer;
    }
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    var ibuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(indices), gl.STATIC_DRAW);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

    var vshader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vshader, 
      "#ifdef GL_ES\n" +
      "precision highp float;\n" +
      "#endif\n" +
      "uniform mat4 mvpMatrix;\n" +
      "uniform mat4 normalMatrix;\n" +
      "uniform vec4 lightVec;\n" +
      "uniform vec4 lightColor;\n" +
      "uniform vec4 materialColor;\n" +
      "attribute vec3 position;\n" +
      "attribute vec3 normal;\n" +
      "varying vec4 color;\n" +
      "void main() {\n" +
      "  float light = clamp(dot(vec3(0.0, 0.0, 1.0), lightVec.xyz), 0.0, 1.0) * 0.8 + 0.2;\n" +
      "  color       = min(min(materialColor, lightColor), vec4(light, light, light, 1.0));\n" +
      "  gl_Position = mvpMatrix * vec4(position, 1.0);\n" +
      "}"
    );
    gl.compileShader(vshader);
    if (!gl.getShaderParameter(vshader, gl.COMPILE_STATUS)) {
      throw gl.getShaderInfoLog(vshader);
    }

    var fshader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fshader, 
      "#ifdef GL_ES\n" +
      "precision highp float;\n" +
      "#endif\n" +
      "varying vec4 color;\n" +
      "void main() {\n" +
      "  gl_FragColor = color;\n" +
      "}"
    );
    gl.compileShader(fshader);
    if (!gl.getShaderParameter(fshader, gl.COMPILE_STATUS)) {
      throw gl.getShaderInfoLog(fshader);
    }

    var program = gl.createProgram();
    gl.attachShader(program, vshader);
    gl.attachShader(program, fshader);

    gl.bindAttribLocation(program, 0, 'position');
    gl.bindAttribLocation(program, 1, 'normal');

    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      throw gl.getProgramInfoLog(program);
    }

    var uniformVars = [
      gl.getUniformLocation(program, 'mvpMatrix'),
      gl.getUniformLocation(program, 'normalMatrix'),
      gl.getUniformLocation(program, 'lightVec'),
      gl.getUniformLocation(program, 'lightColor'),
      gl.getUniformLocation(program, 'materialColor')
    ];

    gl.clearColor(0, 0, 0, 1);
    gl.clearDepth(1000);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.enable(gl.DEPTH_TEST);
    gl.useProgram(program);

    var lightVec = [0.0, 0.0, 1.0, 0.0];
    var lightColor = [1.0, 1.0, 1.0, 1.0];

    var modelMatrix = new CanvasMatrix4();

    var mvpMatrix = new CanvasMatrix4(modelMatrix);
    mvpMatrix.translate(0, 0, -500);
    mvpMatrix.perspective(30, 1.0, 0.1, 1000);

    var normalMatrix = new CanvasMatrix4(modelMatrix);
    normalMatrix.invert();
    normalMatrix.transpose();

    var materialColor = [1.0, 1.0, 1.0, 1.0];

    var values = [mvpMatrix, normalMatrix, lightVec, lightColor, materialColor];
    for (var i = 0; i < values.length; i++) {
      var value = values[i];
      if (value instanceof CanvasMatrix4) {
        gl.uniformMatrix4fv(uniformVars[i], false, value.getAsWebGLFloatArray());
      }
      else {
        gl.uniform4fv(uniformVars[i], new Float32Array(value));
      }
    }

    var strides = [3, 3];
    for (var i = 0; i < strides.length; i++) {
      var stride = strides[i];
      gl.enableVertexAttribArray(i);
      gl.bindBuffer(gl.ARRAY_BUFFER, vbuffers[i]);
      gl.vertexAttribPointer(i, stride, gl.FLOAT, false, 0, 0);
    }

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer);

    gl.drawElements(gl.TRIANGLES, numIndices, gl.UNSIGNED_SHORT, 0);
    gl.flush();
  }
  document.addEventListener('DOMContentLoaded', main, false);
  </script>
</head>
<body>
</body>
</html>

信じがたいことにこのようなコードが要求されます*1

特にC言語様のGLSLコードを文字列として渡さないと画面に何も表示されず、そのGLSLのコードにJS側から値を与えるには「GLSLコード内で何番目に宣言された変数か」という情報が必要だったりするところなど、まるで何か悪い夢を見ているようですらあります。

WebGLの難しさはこのようなWebGL自体の複雑さに加えて、さらに3Dの知識までもが要求されるところにあるといえるでしょう。

さて、このように複雑過ぎる問題に直面した時に我々プログラマが取るべき手段とはなんでしょうか?そうです。分割統治です。そこで私はこの問題から3Dという問題を除き、まずは「WebGL自体の複雑さ」だけを解決するためのライブラリを開発しました。


http://technohippy.github.io/zero.js/

それがこのzero.jsです。zero.jsは名前の通りWebGLを使用して原点、すなわちゼロ次元を描画するためのライブラリです。このライブラリを使用すれば煩わしい3Dについて考えることなく、WebGLを利用することに集中できます。例えば最初に上げたコードと同様の表示を次のように非常にシンプルに記述することができます。

function main() {
  var scene = new ZERO.Scene();

  var geometry = new ZERO.PointGeometry();
  var material = new ZERO.MeshBasicMaterial({color: 0xffffff});
  var mesh = new ZERO.Mesh(geometry, material);
  scene.add(mesh);
  
  var width = 500;
  var height = 500;
  var fov = 30;
  var aspect = width / height;
  var near = 0.1;
  var far = 1000;
  var camera = new ZERO.PerspectiveCamera(fov, aspect, near, far);
  camera.position.set(500);

  var directionalLight = new ZERO.DirectionalLight(0xffffff);
  directionalLight.position.set(1.0);
  scene.add(directionalLight);

  var renderer = new ZERO.WebGLRenderer();
  renderer.setSize(width, height);
  document.getElementById('demo').appendChild(renderer.domElement);

  renderer.render(scene, camera);
}
document.addEventListener('DOMContentLoaded', main, false);

これ以上なくシンプルなコードで原点が描画できていることが分かるでしょう。

もちろんここでライブラリに存在する唯一のジオメトリであるZERO.PointGeometryは原点であるため一切の座標が指定できません。これは光速が観測者によらず一定であることを考えると分かりやすいのではないでしょうか。

またカメラ(視点)についても指定できるのは基本的に原点からの距離だけです。ただし原点はゼロ次元であるため当然"大きさ"という概念が存在せず、カメラの位置に関わらず視角つまり表示上のサイズは一定になります。

このようにzero.jsを使用すれば座標をほぼ意識せずにWebGLが利用できることが分かっていただけるでしょう。zero.jsが皆さんがWebGLを使い始める零歩目になることを願っています。

・・・

なお、姉妹品として1次元を描画するためのone.jsも合わせて作成しました。zero.jsに習熟した方の次の一歩としてよろしければこちらもご利用ください。

http://technohippy.github.io/one.js/