JavaScript Advent Calendar 2011一人打ち上げ

メリークリスマス。

というわけで今年は4つもやってみたんですが、Node駅伝が完走することができました。3つ完走できなかったことについてはひとえに僕の準備不足だと思いますが、お忙しい中参加してくださった皆様と、Node駅伝のたすきを繋いでくださった方々には感謝を申し上げたいです。

僕としてはWebGL駅伝を完走したかったのですが、突然3日間ほど家を開けないといけなくなるというハプニングもありまして、18日でリタイアとなりました。ネタはあと2つぐらいは用意してたんですが、手元で試す時間がなかった…

それではみなさま良いお年を。また新年1月1日からのJavaScript日めくりカレンダー(仮)をお楽しみください。

WebGLでライフゲームしてみた

JavaScript Advent Calendar 2011 WebGL駅伝17日目になります。ほぼ独走状態でやってきましたが、明日から数日は書けそうにないと思うので、もしよかったら誰か代わりに書きませんか? id:ultraist さんとか id:ndruger さんとかいかがでしょうか(チラッ)。とりあえず明日は id:nakamura001 さんに書いていただけることになりました。

さて、表題のライフゲーム(Conway's Game of Life)というのは、ゲーミフィケーションソーシャルゲーム的な意味のゲームではなく、ゲーム理論的な意味のゲームです。ある条件に添って個々の構成単位がほぼ独立に振る舞うことによって、全体として何らかの現象が観測できる、という遊びです。

白マスを死んだセル(細胞)、黒マスを生きたセルとして、それぞれのセルはある時刻の次の時刻に以下のような状態に変化したりしなかったりします。

誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

こういった並列性の高い問題はGPUに持って来いです。

というわけでWebGLでやってみました。

実装

このAdvent Calendarの8〜10日目で、WebGLに2Dの描画をする方法を説明しました。それを見ながらやれば2時間ぐらいで簡単に作れます。

ソースは全部合わせても200行ぐらいです。

今回新しいところは、canvas要素を新しく作って、2D contextのImageDataオブジェクトをテクスチャーにしていることです。

テクスチャーを用意→それを使ってフレーム描画→描画したものをテクスチャーに保存→次のフレームを描画という流れです。

WebGLのテクスチャーとして使えるのは、img、canvas、それからImageDataオブジェクトだけです。

  var canvas = document.createElement('canvas');
  var ctx = canvas.getContext('2d');
  canvas.width = width;
  canvas.height = height;
  //document.body.appendChild(canvas); // デバッグ用。documentに追加しなくてもいい。

  var imdata = ctx.createImageData(width, height); // ImageData作る
  var i = 0;
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      var color = (Math.random() > 0.2) * 255;
      imdata.data[i++] = color;
      imdata.data[i++] = color;
      imdata.data[i++] = color;
      imdata.data[i++] = 255;
    }
  }

...

  var inputTexture = gl.createTexture();

  gl.bindTexture(gl.TEXTURE_2D, inputTexture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imdata); // テクスチャーにする
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);

それから、ターンの最後にWebGLで描画したものを再度ImageDataオブジェクトの中に書き戻さないといけません。それをやるのがreadPixelsメソッドです。

    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelarray);

さて、ここからが今日の本題です。

このpixelarrayとして使えるのは、Typed ArrayのArrayBufferViewのうちのどれかです。

すなわち、Float64Array、Float32Array、Uint32Array、Int32Array、Uint16Array、Int16Array、Uint8ClampedArray、Uint8Array、Int8Arrayになります。

Uint8ClampedArray

って何?と思う人も多いかもしれません。これは、Uint8Arrayと基本的に同じものですが、代入するときに0より小さければ0に、255より大きければ255に切られる(挟まれる)という違いがあります。

clampは挟むという意味です。

昔はこのクラスはありませんでした。なぜ導入されたのかというと、canvas 2D contextに昔からあったCanvasPixelArrayをなくしてTyped Arrayを使おうという流れから来たものです。ただ、CanvasPixelArrayはUint8Arrayとは上述の制限の違いがあったため、Uint8ClampedArrayが作られました。(↓このあたりから議論が始まった)

そういうわけで、上のコードはpixelarrayって書かなくても、↓だけでいいのです。

    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, imdata.data);

ただし、現在のところUint8ClampedArrayを実装してるのはFirefoxだけです。

なのでFirefox以外は別にUint8Arrayを作って1要素ずつコピーしていかないといけません。それがこの処理です。

  var uint8ClampedArrayAvailable = 
    typeof Uint8ClampedArray !== "undefined" && imdata.data instanceof Uint8ClampedArray;

  var pixelarray;
  if (!uint8ClampedArrayAvailable) {
    pixelarray = new Uint8Array(width * height * 4);
  } else {
    pixelarray = imdata.data;
  }

...

    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelarray);
    if (!uint8ClampedArrayAvailable) {
      for (var i = 0, l = pixelarray.length; i < l; i++) {
        imdata.data[i] = pixelarray[i];
      }
    }

まとめ

そういうわけで今日のデモをFirefoxChromeで試すと、うちではExecution time per frameがChromeで17〜20msなのに対し、Firefoxでは8msぐらいです。(ただし、Chromeは書き戻しもさることながら、なぜかgl.texImage2Dでかなり時間を使ってました)

25万回の計算で8msって、どうなんでしょうかねえ。JavaScriptだけでも実装してみたらおもしろかったんでしょうが、面倒だったのでやってません。誰かやってみたら教えて下さい。


あと、ImageDataじゃなくて描画するcanvasを直接次のテクスチャーに指定したりしてみたんですが、なんかおかしなことになりました。Chromeでは動かず、Firefoxだと動くのは動くんですが、数十ターン一気に動いてるような…まあそういうことはやめておきましょうということで。

createFramebufferしたらいいじゃん、っていうツッコミは受け付けません。今日のはImageDataを使うための記事です。framebufferの話はまた今度書くかも。

JavaScript Advent Calendar/WebGLコース10日目・画像フィルターを作る

JavaScript Advent Calendar 2011 WebGL駅伝10日目、独走3日目になります。今からでも参加したい方がいましたらぜひ教えてください。

今日はこのオルセー美術館をTiltShiftさせてみます。実はこれがやりたくて一昨日から書いてきたのでした。

サンプルです。

シェーダー部分以外は9割ぐらい昨日と同じです。

シェーダー

昨日は頂点シェーダーでテクスチャーの座標をvaryingに入れてましたが、今日は別の方法でやるので、頂点シェーダーはたったこれだけです。

  attribute vec2 aPosition;

  void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
  }

フラグメントシェーダーでは、ガウスフィルター(Gaussian Blur)のための関数を定義しています。フィルターのパラメーターは適当にググッて拾ってきました。そこはまあ重要ではありません。

  precision mediump float;

  uniform sampler2D uSampler;
  uniform vec2 uTextureSize;

  vec4 GaussianBlur(in sampler2D tex, in vec2 c, in vec2 size) {
    // http://homepages.inf.ed.ac.uk/rbf/HIPR2/gsmooth.htm
    return (
      41.0 * texture2D(tex, vec2(c / size)) +
      26.0 * (
        texture2D(tex, (c + vec2( 1.0, 0.0)) / size) +
        texture2D(tex, (c + vec2(-1.0, 0.0)) / size) +
        texture2D(tex, (c + vec2( 0.0, 1.0)) / size) +
        texture2D(tex, (c + vec2( 0.0,-1.0)) / size)
      ) + 
      16.0 * (
        texture2D(tex, (c + vec2( 1.0, 1.0)) / size) +
        texture2D(tex, (c + vec2(-1.0, 1.0)) / size) +
        texture2D(tex, (c + vec2( 1.0,-1.0)) / size) +
        texture2D(tex, (c + vec2(-1.0,-1.0)) / size)
      ) +
      7.0 * (
        texture2D(tex, (c + vec2( 2.0, 0.0)) / size) +
        texture2D(tex, (c + vec2(-2.0, 0.0)) / size) +
        texture2D(tex, (c + vec2( 0.0, 2.0)) / size) +
        texture2D(tex, (c + vec2( 0.0,-2.0)) / size)
      ) +
      4.0 * (
        texture2D(tex, (c + vec2( 1.0, 2.0)) / size) +
        texture2D(tex, (c + vec2(-1.0, 2.0)) / size) +
        texture2D(tex, (c + vec2( 1.0,-2.0)) / size) +
        texture2D(tex, (c + vec2(-1.0,-2.0)) / size) +
        texture2D(tex, (c + vec2( 2.0, 1.0)) / size) +
        texture2D(tex, (c + vec2(-2.0, 1.0)) / size) +
        texture2D(tex, (c + vec2( 2.0,-1.0)) / size) +
        texture2D(tex, (c + vec2(-2.0,-1.0)) / size)
      ) +
      1.0 * (
        texture2D(tex, (c + vec2( 2.0, 2.0)) / size) +
        texture2D(tex, (c + vec2(-2.0, 2.0)) / size) +
        texture2D(tex, (c + vec2( 2.0,-2.0)) / size) +
        texture2D(tex, (c + vec2(-2.0,-2.0)) / size)
      )) / 273.0;
  }

  void main() {
    float y = gl_FragCoord.y / uTextureSize.y;
    if (y < 0.3 || y > 0.4) {
      gl_FragColor = GaussianBlur(uSampler, gl_FragCoord.xy, uTextureSize);
    } else {
      gl_FragColor = texture2D(uSampler, gl_FragCoord.xy / uTextureSize);
    }
  }

一番のポイントは、gl_FragCoordという組み込み変数を使っていることです。この変数は、ビューポートの左下からの座標がピクセル単位で入っています。つまり、ビューポートをテクスチャー画像のサイズと同じにした場合は、xは0から画像の幅まで、yは0から画像の高さまでの値をとります。

uTextureSizeにその画像のサイズを入れてあります。(この場合300x240)

  var uTextureSizeLocation = gl.getUniformLocation(program, "uTextureSize");

  …

  gl.uniform2f(uTextureSizeLocation, img.naturalWidth, img.naturalHeight);

このように一枚画像を全面に表示する場合は、gl_FragCoord.xy / uTextureSizeが昨日やった(0,0)から(1,1)のテクスチャー座標と同じものになります。(gl_FragCoord.xyってこれ以外の用途はあるんだろうか?)

まとめ

実は、WebGLによる画像フィルターばかりを集めたデモが公開されていて、TiltShiftもちゃっかりあります。


WebGLで画像処理はCanvas2DContextより簡単です。魚眼レンズエフェクトとかも簡単にできるはずです。

↓のように端をclamp(GLSLのtexture2D関数に0より小さい座標を与えた場合は0、1より大きな座標を与えた場合は1と考える)にしておけばエッジのことも考え無くていいですし。

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

しかし、FirefoxWebGLがバグってるため、今のところはやはりテクスチャーのサイズは2の累乗にしたほうがいいです。今日のサンプルもFirefoxChromeでは縮小のされ方が違います。

JavaScript Advent Calendar/WebGLコース9日目・テクスチャーの使い方

JavaScript Advent Calendar 2011 WebGL駅伝9日目、独走2日目になります。今からでも参加したい方がいましたらぜひ教えてください。

さて、今日は昨日の内容を発展させて、画像を貼ってみたいと思います。

WebGLのテクスチャーとして使える画像は、通常は辺の長さが2の累乗である必要があります。ただし、やり方によってはどんなサイズの画像でも使えます

今回は256x256で用意したこの画像を表示してみます。ただ表示するだけではおもしろくないので、色を反転させてみます。

Learning WebGLLesson 5に似た内容です。

シェーダー

今回用意したシェーダーはこんな感じです。昨日と比べてみてください。

<script type="text/x-vertex-shader" id="vs">
  // 頂点シェーダー
  attribute vec2 aPosition;
  attribute vec2 aTexCoord;
  varying vec2 vTexCoord;

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

  varying vec2 vTexCoord;
  uniform sampler2D uSampler;

  void main() {
    gl_FragColor = vec4(1.0 - texture2D(uSampler, vTexCoord).rgb, 1.0);
  }
</script>

今日の新しいところは、uniform sampler2D uSampler;texture2D(uSampler, vTexCoord)です。

sampler2Dというのはテクスチャーの型だと思ってください。実態は整数で、テクスチャーの入ってるレジスタ番号ですが。

そのテクスチャーのある座標の色を取得するのがtexture2Dという組み込み関数です。座標はvec2で、各要素が0から1の範囲です。0から255(元画像のサイズ)ではありません。

texture2Dはvec4を返します。rgbでその最初の3つの要素を取ってきて、1.0から引くことで反転しています。GLSLはfloatとvec3の加減算がvec3になるなど、直感的にベクトル演算が書けます。要素へのアクセスは.xとか.rとか.xyとか.rgbとか書くと思った通りのものが得られます。

頂点オブジェクトを準備

シェーダープログラムをコンパイルするところまでは昨日と同じなので省略して、頂点オブジェクトを考えてみます。

verticesの各座標がcoordsの各座標に対応します。つまり、(-1,-1)の点はテクスチャーの(0,0)の点の色を使うという具合です。

  // init vertex buffer
  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);

  var coords = [
     0, 0,
     1, 0,
     0, 1,
     1, 1,
  ];
  var texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(coords), gl.STATIC_DRAW);

テクスチャーの準備

いよいよテクスチャーを作ります。

  // init texture
  var img = document.getElementById('jimmy');
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

img要素をDOMから取得して、texImage2Dでセットしています。

img要素は下のようにドキュメントから切り離されていても構いません。ただし、texImage2Dを呼ぶときは画像がロード済みでないといけません。

var img = new Image();
var texture = gl.createTexture();
img.onload = function(){
  ...
}
img.src = 'foo.jpg';

texParameteriとかのことは7日目の記事を参照してください。

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);WebGLだけのAPIで、テクスチャーの座標を上下反転させて左下を(0,0)、右上を(1,1)とするためのものです。これをしないときは左上が(0,0)、右下が(1,1)になります。

描画

いつもどおりです。

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

  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.vertexAttribPointer(aTexCoordLocation, 2, gl.FLOAT, false, 0, 0);

  gl.activeTexture(gl.TEXTURE0); // ←ここの数字と
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.uniform1i(uSamplerLocation, 0); // ←ここの数字をあわせる

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

activeTextureは、これからn番目のレジスターについて行うテクスチャー関連の操作を行いますという意味です。ここでは0番目のレジスターに先ほど作ったテクスチャーを割り当て、uSamplerが0番目を指すようにしています。この3行はいつもセットで使います。

結果

元の画像と並べてみました。


テクスチャーの貼り付け方が分かったと思います。

ところで、画像ロードを非同期に行う場合、drawArrayをする時点でtexImage2Dがまだ呼ばれてなくてもエラーになることはありません。その時はGLSLのtexture2D関数が(0,0,0,1)を返すだけです。なので、とりあえずテクスチャー以外のもの(基本色とか)を描画しまって、アニメーションしていくうちに徐々にロードされたテクスチャーが表示されるようにしたほうが、ユーザーを待たせないで済みます。

明日は応用編ということで、画像処理みたいなことをやってみたいと思います。

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)なのですね。

JavaScript Advent Calendar 2011/WebGLコース初日、WebGLの基本的な考え方

↑まだまだ参加者募集中です。

WebGL駅伝は、もし25人に満たなかったら、最後の人の次の日から僕が一人で走りたいと思います。ネタと時間的余裕が続く限り…

なんてことを言っておきながら、実は僕自身はWebGLも3Dプログラミングも初めて2ヶ月の初心者なので、自分がWebGLを始めるときにどういう解説が欲しかったかということを考えながら入門編を書いてみたいと思います。(WebGL駅伝の他の参加者の方々にはつまらない内容だと思いますが)

WebGLプログラムの流れはおおまかに言ってこれだけです。

  • シェーダーを用意する
  • 頂点座標をシェーダーに投げる
  • フレームごとにパラメータを変えてシェーダープログラムを走らせる

シェーダーは頂点(vertex)シェーダーとフラグメント(fragment)シェーダーの2つで1組になっていて、頂点シェーダーは「座標変換」を担当し、後者は各ピクセルの色付けを担当します。

頂点シェーダーと座標変換

ある物体が空間内のどこかの座標に置かれているとします。そして、それを撮影するカメラがあります。(↓のサイトの絵が分かりやすかったので引用)


上の図のような配置で実際にカメラの視点から見たのが下の図になります

http://sorceryforce.com/xna/tips_abouttransform3d.html

最終的に描画するのはカメラが撮影した画像だけです。

ここで座標系というものを考えてみます。

描画したい物体の頂点は、例えばサイコロだったら8つの頂点 (1,1,1),(1,1,-1),(1,-1,1),(-1,1,1),(1,-1,-1),(-1,1,-1),(-1,-1,1),(-1,-1,-1) で表すことが多いと思います。この座標は「物体自身の原点」を基準にした座標です。(この場合はサイコロの中心)

次に、カメラの位置も座標で与えます。例えば (0,0,10) など。これは何を基準にした座標かというと、自分が勝手に決めた「世界の中心」を原点としたものです。

また、物体の位置もこの世界の座標で表すことができます。物体が一つだけで、止まっているなら、世界の中心に物体の原点を置くのが普通です。

そうでない場合は物体基準の座標と世界基準の座標が存在することになります。この時、「物体の頂点の位置を世界の中心基準の座標で表すと何になるか?」というのが座標変換です。

さらにカメラ基準の座標を考えます。カメラの位置を原点として、カメラの上方向をy軸、見つめる方向をz軸としたものです。(あとパースペクティブとかありますが省略)

頂点シェーダーの目的は「物体の頂点をカメラ基準の座標系で表す」ことです。言い換えれば、物体座標を世界座標に変換し、さらにカメラ座標に変換することです。

フラグメントシェーダー

fragmentというのは断片という意味です。

3Dのポリゴンは、頂点の集まりと言えるとともに、三角形の断片の集まりとも言えます。

カメラ座標で物体を見ると、どの面がこちらを向いているかとか、ある面は別の面の後ろにあるから見えないということがわかります。すなわち、カメラが撮影した画像のあるピクセルにどの面のどの部分が相当するのかが分かります。ここはWebGLがやってくれるので計算する必要はありません。

また、頂点オブジェクトが持ってる法線やら基本色やらテクスチャーの座標やらの情報を、ピクセルごとに補間(interpolate)して与えてくれます。これもWebGLがやってくれます。

フラグメントシェーダーの役割は、それぞれのピクセルにどの色を置くかを決めることです。

つまり、受け取った法線や基本色やテクスチャー座標を組み合わせて、最終的な色を決定します。(例えば法線が光源の方向と逆を向いていたら光があたってないと判断して暗くするなど)

ちなみにshaderというのは「影をつけるもの」という意味ですね。

一番簡単なシェーダーの例

頂点シェーダーの一番簡単な例はこんな感じです。

attribute vec3 aPosition;
attribute vec4 aColor;
varying vec4 vColor;
uniform mat4 uMVPMatrix;

void main() {
  vColor = aColor;
  gl_Position = uMVPMatrix * vec4(aPosition, 0.0);
}

main関数が各頂点について実行されます。ここでは各頂点オブジェクトはメンバーとして位置と色を持っています。attributeとして宣言された値は「頂点オブジェクトの属性(attribute)」という意味です。

varyingとして宣言された変数は、頂点シェーダーからフラグメントシェーダーへの受け渡しのために使われます。ここでは、各頂点の色をフラグメントシェーダーで使えるようにしています。

uniformとして宣言された値は定数です。attributeは各頂点が持つ値であるのに対し、uniformはすべての頂点に画一(uniform)な定数です。上のコードでは座標変換に相当する行列を指定しています。MVPという名前の意味はあとで説明します。

attributeとuniformについてはプログラム(シェーダーをコンパイルしたものをシェーダープログラムとかプログラムと呼びます)を走らせる前に登録してやります。

gl_Positionは組み込み変数で、カメラ基準の座標における頂点の位置を与えてあげなければいけません。頂点シェーダーでは唯一必須の操作です。

次に一番簡単なフラグメントシェーダーです。

//↓おまじない。気にする必要はない
precision mediump float;

varying vec4 vColor;

void main() {
  gl_FragColor = vColor;
}

これだけです。最初の数行は実数の精度を上げるためのものだそうです。よくわかってませんがとりあえず書いとくものみたいです。

gl_FragColorという組み込み変数は、ピクセルの色を指定します。これは実は必須ではないけど面倒なので割愛。

varyingで頂点シェーダーから送った値は、ピクセルごとに補間されてます。なので、両方のシェーダーで同じように宣言してるけど微妙に違うものです。

プログラムの準備

上のシェーダーを使うとしてJavaScriptのコードを見ていきます。シェーダーは<script type="text/vs-shader">とかの中に書いておくことが多いようです。(CoffeeScriptの複数行文字列リテラルのほうが便利)

まずシェーダーをコンパイルします。本当はエラー処理をきちんとしないといけませんが、今回は省略します。

var canvas = document.getElementById('canvas');
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

var vshader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vshader, document.getElementById('vs').textContent);
gl.compileShader(vshader);

var fshader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fshader, document.getElementById('fs').textContent);
gl.compileShader(fshader);

var program = gl.createProgram();
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program); // 頂点シェーダーとフラグメントシェーダーをリンク

vshaderfshaderはもう変数としては用無しなので忘れてください。glprogramが重要です。

次に、コンパイルしたプログラムからattribute/uniform変数の場所を得ます。attributeはaPositionaColor、uniformはuMVPMatrixでした。

gl.useProgram(program); // このプログラムを使う。複数プログラムがあるときは使い分けるため

var aPositionLocation = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(aPositionLocation); // ←おまじない

var aColorLocation = gl.getAttribLocation(program, 'aColor');
gl.enableVertexAttribArray(aColorLocation); // ←おまじない

var uMVPMatrixLocation = gl.getUniformLocation(program, 'uMVPMatrix');

aPositionLocationとかはあとで使います。Learning WebGLのサンプルとかだと、グローバル変数を作らずにprogramのメンバーにしています。それでもかまいません。programは元々何もメンバーを持ってないオブジェクトなので混乱することは無いと思います。

(どうでもいいですが、なぜかgetAttibLocationのほうは返り値が数字で、getUniformLocationのほうはWebGLUniformLocationとかいうオブジェクトなんですよね…なんか非対称で美しくない…)

頂点を登録

3D物体(モデル)の頂点座標をattributeとして登録します。これは普通は最初の1回だけです。繰り返しになりますがモデルの頂点はモデル基準の座標のままで登録します。

頂点は何万とか何十万とかいう単位になってくるので、一つ一つの位置をJavaScriptで動かすことはありません。(まあ数万ぐらいのループなら今は一瞬だけど)

ここでは数万の頂点ではなく、Learning WebGL Lesson 4から借りてピラミッドを作ってみます。

var pyramidVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
var vertices = [
    // Front face
     0.0,  1.0,  0.0,
    -1.0, -1.0,  1.0,
     1.0, -1.0,  1.0,
    // Right face
     0.0,  1.0,  0.0,
     1.0, -1.0,  1.0,
     1.0, -1.0, -1.0,
    // Back face
     0.0,  1.0,  0.0,
     1.0, -1.0, -1.0,
    -1.0, -1.0, -1.0,
    // Left face
     0.0,  1.0,  0.0,
    -1.0, -1.0, -1.0,
    -1.0, -1.0,  1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
pyramidVertexPositionBuffer.itemSize = 3;
pyramidVertexPositionBuffer.numItems = 12;

createBufferで頂点バッファーオブジェクトを作り、bindBufferで「今からこのバッファーについての操作をします」とWebGLコンテクストに宣言し、bufferDataでバッファーの中身を登録しています。STATIC_DRAWはおまじないです(一回登録したら変えないよとかなんとか)。

目につくのは、頂点の座標 (x,y,z) を全部まとめて一つの大きな配列にしてることです。Front face とある上から3行が3つの頂点、つまり一つの断片になります。この配列は4つの断片からなる物体を表しています。

itemSizenumItemsは、attribute vec3に相当する変数を12個用意したという意味です。ここではグローバル変数を用意するのが嫌だったので単にメンバーとして登録してあるだけで、実際に使われるのは描画の時です。

同じように頂点の色(rgba)を登録します。

pyramidVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
var colors = [
    // Front face
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    // Right face
    1.0, 0.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    // Back face
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    // Left face
    1.0, 0.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    0.0, 1.0, 0.0, 1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
pyramidVertexColorBuffer.itemSize = 4;
pyramidVertexColorBuffer.numItems = 12;

ここではattribute vec4に相当する変数を12個用意しました。

頂点の色を指定しただけで、面の真ん中の色は指定していませんが、間の色に補間されます。

座標変換

座標はベクトル、座標変換は行列です。(数学的な説明は↓あたりに任せますが、数学的にもちゃんと理解出来ないと3Dプログラミングは難しいでしょう)

OpenGLでは物体(モデル)を基準とした座標から世界の中心を基準とした座標への変換をモデル変換と呼ぶようです。他にワールド変換とも言うみたいです。

世界の中心を基準とした座標からカメラを基準とした座標への変換をビュー変換と呼ぶようです。

最後に、遠くのものは小さく見えて近くのものは大きく見えるという効果を出すパースペクティブ変換というのを施します。

これらの変換行列をかけ合わせたものがMVPMatrixです。(MVPMatrix = PMatrix * VMatrix * MMatrix

この行列を計算するのには行列演算用のライブラリを使わないと厳しいでしょう。glMatrixとかclosure vecとかmjsとかEWGLとかいっぱいあります。

各フレームごとに変換行列を動かすことで、頂点の座標を動かさずにモデルを動かすことができます。直立姿勢の人型モデルでも、体の各パーツ(に付随する頂点のグループ)を別々の変換行列にかけるとモーションができます。

MVPMatrixを作ったということにして先へ進みたいと思います。

描画

頂点バッファーをattributeの変数と結びつけます。

gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
gl.vertexAttribPointer(aPositionLocation, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
gl.vertexAttribPointer(aColorLocation, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

itemSizeはvec3かvec4かという意味でした。bindBufferがさっきも書いたように「今からこのバッファーについての操作をします」という宣言で、vertexAttribPointerは、指定した位置にあるattribute変数をそのバッファーへのポインターとする、という意味です。後ろの方の引数は…覚えてません。

次に、uniformを登録します。uniform変数の型によってuniform1iuniform3funiform4fvMatrixなどの関数を使い分けないといけません。

gl.uniformMatrix4fv(uMVPMatrixLocation, false, MVPMatrix);

MVPMatrixはJavaScriptでは16要素の配列かFloat32Arrayです。シェーダーからは4x4の行列になります。第二引数のfalseは転置するかどうか。

最後にdrawArraysを呼びます。

gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);

numItemsはいくつ頂点があるかということでした。12個の頂点を「最初から3つずつ取って一つの断片とする」というのがTRIANGLESの意味です。他にもTRIANGLE_STRIPとか色々ありますが省略。

また、drawArraysじゃなくてdrawElementsというのを使う場合もあります。それは頂点の並びが最初から3つずつという順番になってない場合で、並び順を表す配列を別に登録してやる必要があります。

最後に

長かったですが12月1日に間に合わせないといけないので十分推敲してません…

これを読んだおかげでLearning WebGLのLesson 4がすんなり理解できるようになった!という人がいれば嬉しいです。

JavaScript Advent Calendar 2011 参加者募集!

なんと今年は4本立て!

それぞれコピペで紹介してみます。

JavaScript Advent Calendar 2011 (フレームワークコース) : ATND

JavaScript Advent Calendar 2011 (フレームワークコース)」では jQuery, dojo, ExtJS, Sencha などなど好きな JavaScript フレームワーク(クライアントサイド)について書いてください。

JavaScript Advent Calendar 2011 (WebGLコース) : ATND

JavaScript Advent Calendar 2011 (WebGLコース)」では、WebGL とその周辺技術について書いてください。Three.js や 3D Matrxi ライブラリや Kinect 連携等も含みます。

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース) : ATND

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)」では、Node.js または WebSockets に関連したネタを書いてください。クライアントサイド、サーバーサイド、JS じゃないけど WebSockets 関係等です。

JavaScript Advent Calendar 2011 (オレ標準コース) : ATND

JavaScript Advent Calendar 2011 (オレ標準コース)」では、他のコースに当てはまらないネタで好きなネタを書いてください。次期ウェブ標準、ブラウザ限定標準、忘れ去られた標準、オレの中では標準的に使ってるテクニックなどです。NPAPI, NaCl, Web Audio API, Extension/Add-on, etc. を想定しています。


jQ 駅伝、WebGL 駅伝、Node 駅伝、オレ標準駅伝、と勝手に呼んでます。

どなたでも気軽にご参加ください。