JavaScript Advent Calendar/WebGLコース8日目・二次元図形を描く

JavaScript Advent Calendar 2011 WebGL駅伝参加者の方々、ありがとうございました。めでたく(?)最後のランナーまで辿り着きましたので、ここからは一人で走ってみたいと思います。(気力の続く限り…)

もし僕の代わりに何日か書いてもいいよという人がいましたらコメントで教えてください。

とりあえず、これまでの方々の記事へのリンクを貼っておきます。

なかなかタフな内容が続いてますね。今日からしばらくはWebGLとか3Dプログラミングをやったことがない人向けに連載します。内容は初日の内容をゆるく踏まえています。たぶん3日ぐらいで一区切りしてまた別の内容をやると思います。

今日はまず簡単な例として、256*256のcanvasいっぱいに赤い四角形を表示するだけのサンプルを作ってみました。

Learning WebGLLesson 1Lesson 2がこんな感じですね。僕もLearning WebGLで勉強したクチですが、あれはまったくの初心者がやるには冗長な処理が多すぎるような気がしてます。あとWebGLに関してはネット上にデモプログラムはいっぱいあるんですが、習うためのリソースがとても少ないので、自分なりの解説にも意味があるかなと思っています。

それでは見ていきます。

canvasの準備

サンプルプログラムでは、一番最初にこういうふうにしてWebGLコンテクストを作っています。

  var width = 256;
  var height = 256;

  var canv = document.getElementById('canv');
  canv.width = width;
  canv.height = height;
  var gl = canv.getContext('webgl') || canv.getContext('experimental-webgl');
  if (!gl) {
    alert('WebGL not supported by your browser');
    return;
  }

  gl.viewport(0, 0, width, height);

一番下のgl.viewport(0, 0, width, height);がこれから描く内容のサイズを指定しています。canv.widthcanv.heightcanvas要素のサイズです。

HTMLで画像を表示するときに、<img width=100 height=200>と書くと元の画像が適切に拡縮されて表示されますが、それと同じように、WebGLの内容のサイズとcanvasのサイズは別々でも構いません。

と言いたいところですが、Firefoxはまだ色々と問題があるので今回はどちらも256にしています。

(あとFirefoxcanvasが強制的にオフになってる場合はgetContext('experimental-webgl')でエラーを出すみたいなので、本当ならちゃんとエラー処理をしたほうがいいかもしれません)

シェーダープログラムの準備

シェーダープログラムを作ります。

  var vs = document.getElementById('vs').textContent;
  var fs = document.getElementById('fs').textContent;

  var program = initProgram(gl, vs, fs);

initProgramはいつも同じなので一回書いたらコピペで十分です。

function initProgram(gl, vs_source, fs_source) {
  var program = gl.createProgram();

  var vshader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vshader, vs_source);
  gl.compileShader(vshader);
  if (!gl.getShaderParameter(vshader, gl.COMPILE_STATUS)) {
    alert('Vertex shader compilation error');
    throw gl.getShaderInfoLog(vshader);
  }

  var fshader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(fshader, fs_source);
  gl.compileShader(fshader);
  if (!gl.getShaderParameter(fshader, gl.COMPILE_STATUS)) {
    alert('Fragment shader compilation error');
    throw gl.getShaderInfoLog(fshader);
  }

  var program = gl.createProgram();
  gl.attachShader(program, vshader);
  gl.attachShader(program, fshader);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    alert('Shader linking error');
    throw gl.getProgramInfoLog(program);
  }

  return program;
}

シェーダー

今回はこういうシェーダーを使います。

<script type="text/x-vertex-shader" id="vs">
  // 頂点シェーダー
  attribute vec2 aPosition;
  void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
  }
</script>
<script type="text/x-fragment-shader" id="fs">
  // フラグメントシェーダー
  precision mediump float;
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
</script>

頂点シェーダーのほうで、vec4(aPosition, 0.0, 0.0)というのがあります。これについて少し説明します。

まずGLSLにおいて、vec4は4つのfloatからなるベクトルのことです。vec4コンストラクター(?)は

vec4(float, float, float, float)
vec4(vec2, float, float)
vec4(vec3, float)
vec4(vec4)

などのようにかなり柔軟にオーバーロードされています。(ここで使っているのはvec4(vec2, float, float)ですね)

gl_PositionWebGLの組み込み変数で、頂点シェーダーではこれを指定しなければいけません。(指定しない場合の動作は未定義)

3次元空間内の頂点の座標なのにvec4とはどういうことでしょうか。

実は、最初の3つを(x, y, z)、最後をwとして、(x/w, y/w, z/w)が最終的な頂点の座標になります。この意味はまた今度考えてみたいと思います。

頂点の準備

このあたりは初日に解説しました。attributeを使えるようにしています。

  gl.useProgram(program);

  var aPositionLocation = gl.getAttribLocation(program, 'aPosition');
  gl.enableVertexAttribArray(aPositionLocation);

今回は単なる四角形なので座標は4つです。

  var vertices = [
    -1, -1,
     1, -1,
    -1,  1,
     1,  1
  ];
  var vertBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

この頂点座標の並べ方には意味があります。OpenGLでいう"triangle strip"という並べ方で、↓のaの図形をイメージしてくれればいいと思います。(ちなみにbは"triangle fan"でcが"triangles"です)

http://www.opengl.org/documentation/specs/version1.1/glspec1.1/node17.html

描画

ここも前に解説しました。さっき作った頂点バッファーをattributeに割り当ててから描画しています。

  gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
  gl.vertexAttribPointer(aPositionLocation, 2, gl.FLOAT, false, 0, 0);

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

vertexAttribPointerの第二引数はattribute変数の次数(1ならfloat、2ならvec2など)でした。第三引数以降は覚えないでもいいと思います。

drawArraysの第一引数は頂点の並べ方、第三引数は頂点をいくつ描くか、という意味です。第二引数は何番目の頂点から始めるかという意味です。

まとめ

座標変換という面倒なところを避けてWebGLプログラムを書いてみました。JSのコードとしては50行ぐらいです。

座標変換しなかったことで、GLSLのgl_Positionがどういうものなのかということがイメージしやすくなると思ってこういうサンプルにしました。

gl_Positionが(-1,-1,z,1)である頂点はcanvasの一番左下に相当します。(1,1,z,1)は一番右上です。zは奥行きになります。zを色々変えてみると、zが-1から1の範囲にあるときは赤い四角形が表示されますが、それ以外の場合は何も表示されません。

つまり、xyzそれぞれ-1から1までで表される立方体の空間の中にあるものは描画され、その枠の外にあるものは描画されない、ということです。この枠外をばっさり切り落とすことをクリッピングといいます。

床井研究室 - 第5回 座標変換

カメラはz軸の延長線上にあり、x軸の正の方向が右、y軸の正の方向が上です。


蛇足になりますが、gl_Position = vec4(aPosition, 0.0, 1.0);の最後を弄って、wを1より大きな数字にすると、正方形が小さくなっていきます。やっぱり最終的な座標は(x/w, y/w, z/w)なのですね。