OpenGLなにそれうまいの?? という人のためのWebGLの始め方
追記:例題の頂点シェーダーで何をしているか説明を追加しました
追記:動作環境の所修正しました、IE9では動作しません。
皆さんはじめまして、荒川智則です。この記事はJavaScript Advent Calendarの21日目です。
この記事では、Google I/OやFirefox Developers Conferenceで華麗なデモがバリバリ出てくるにもかかわらず、実際に使っている人が異常なまでに少ないWebGLについて書きます。対象読者はWebGLに興味があるor手を出してみたけどクソ難しそうだし既に諦めそう、という人です。
WebGLの概要
WebGLはOpenGL ES 2.0のグラフィックAPIをCanvas要素上で使える様にした物です。OpenGL ESはOpenGLの組み込み機器向けのサブセットで、iPhoneやandroid端末にも搭載されています。OpenGL ESなにそれ??という人は、少ないリソースで動作させるのに最適化されている3DグラフィックスAPIの仕様だと覚えておけば良いでしょう。
OpenGL ES 2.0の特徴として、シェーダーがプログラムで書ける(プログラマブルシェーダー)、というのもあります。シェーダーの説明はまた後で。
動作環境
自分の環境で動作するかは次のデモページを開いてみるとわかります。
Hello Triangle
環境は整ったらWebGLの最初の一歩、三角形の描画をしてみます。Hello Worldでは無く、Hello Triangleな理由は、WebGLで描画できるものは三角形のみだからです。*2
Leaning WebGLというブログのLesson 1が参考になるので見てみましょう。
ざっと目を通すだけで、三角形と四角形を一つづつ表示するだけなのに半端無い量の解説がなされているのがわかります。実際のhtmlソースを見てみてもJavaScriptが100行を越えています。さらに行列計算ライブラリをscriptタグで読み込んでいるので、全て時前で書いたら100行どころではありません。さらに <script type="x-shader/x-fragment"> といった見慣れない物まで出てくる始末。数行のJavaScriptで四角や円が描画できたCanvasに比べると、WebGLはこの時点で習得を諦めたくなります。しかし、ここではWebGLが難しいのではなく*3、Canvasが簡単すぎるのだと考えましょう。
次は、上記チュートリアルを初めて見た時にひっかかりそうな点をいくつか説明します。
WebGLの描画処理の流れ
まずWebGLの描画処理の流れを把握しましょう。単純化して書くと左の図になります。頂点データを頂点シェーダーとフラグメントシェーダーを介してフレームバッファに叩き込む作業が最低限必要になります。頂点データ、頂点シェーダ、フラグメントシェーダは自分で作る必要があります。
頂点データ
3角形でいえば各頂点の3次元の座標(x,y,z)になります。
頂点シェーダー
各頂点毎の処理、例えば頂点座標の変換を行ないます。Leaning WebGL Lesson1のコードでは次の箇所が該当します。
<script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; // 頂点座標 uniform mat4 uMVMatrix; // パラメータ uniform mat4 uPMatrix; // パラメータ void main(void) { // 次の処理に渡される出力値 gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); } </script>
WebGLではシェーダーを記述するのにOpenGL ES シェーダー記述言語というのを使います。ここではuPMatrixとuMVMatrixの分だけ頂点を移動させています。コードを読むとmvTranslateに渡した値がuMVMatrixまで渡っています。つまりuMVMatrixは描画開始位置に相当します。頂点シェーダに値を渡しているのは次のコードです。
// 描画開始位置へ移動(ちょっと左 and 奥の方) mvTranslate([-1.5, 0.0, -7.0]); gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer); // 頂点シェーダに頂点座標データをセット gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); // 頂点シェーダにパラメータをセット(setMatrixUniformsで) setMatrixUniforms(); // 描画 gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
この例ではscriptタグからシェーダーのコードを読みこんでいますが、最初からJavaScriptで文字列として持っておいても使えます。
プリミティブの構築とラスタライズ
頂点シェーダーから受けとった頂点データを元にプリミティブ(OpenGLで扱える図形)を構築します。ラスタライズ処理ではプリミティブから平面(ピクセルの集まり)を生成します。
フラグメントシェーダー
OpenGL以外ではピクセルシェーダーと呼ばれる物です。ピクセル毎の処理をします。次のサイトを見ていただくとフラグメントシェーダーの効果がわかりやすいでしょう。
うねうね動いているのはフラグメントシェーダーに時間をパラメータとして渡して、操作に使っているためです。Leaning WebGL Lesson1のコードでは次の箇所が該当します。このシェーダーにパラメータはありません。単純に各ピクセルを白で塗りつぶしています。
<script id="shader-fs" type="x-shader/x-fragment"> #ifdef GL_ES precision highp float; #endif void main(void) { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } </script>
シェーダーのコードをWebGLで使う為には、コンパイルしてプログラムオブジェクトにアタッチします。この処理はLesson1のコードでは getShader, initShaders という関数で行なっています。必ず書く事になる処理なのですぐに覚えられるでしょう。余談ですが、シェーダーが書ける様になってもメシは食えないとの意見もありますので、あまりシェーダーを書く事に拘らない方がいいかもしれません。
バッファへの書きこみ
フラグメントシェーダーから出力されたピクセルデータをバックバッファに書き込みます。書き込みが完了したら、フロントバッファ(表示しているコマ)と差し替えます。これで1フレームの処理になります。
この辺りが理解できれば、あとは複雑な頂点データを作ったり、細かいテクニックを使いこなしていく段階でしょう。三角形を描画するだけで100行を越えるコードが必要になるといっても、ここに出てきた9割のコードはお決まりの物です。一度覚えてしまえばなんという事はありません。OpenGL ES 2.0について、私はOpenGL ES 2.0 プログラミングガイドを読んでだいたい理解できました。
最初はLeaning WebGLのLessonを何個か進めてみてから、自分で何か作ってみると良いでしょう。
行列演算ライブラリって?
すばらしい資料があるのでこちらをどうぞ!!
canvasとの組み合わせ
2Dプログラミングや文字の描画は、圧倒的にcanvasの方が簡単です。私はcanvas要素を二つ重ねあわせて、一つをWebGL用にして使っています。さらにCSSアニメーションを使って楽をしたい時もありますが、DOM操作はcanvas及びWebGLの描画をブロックしてしまうので注意が必要です。
物理演算
JavaScriptで使える3Dの物理演算ライブラリ。まだ使った事は無いのですが……。
テクスチャの罠
テクスチャの使い方を覚えたら、自分の好きな画像を回したくなるのが世の常。しかし、WebGLのテクスチャに使える画像サイズはそれぞれの辺が2の乗数(64px, 128px, 256px...)に限定されます。これはOpenGL ESの制限みたいです。Twitterのアイコンを無差別に取ってきてテクスチャとして貼るプログラムを書くとほぼ失敗します*4。
デバッグ
OpenGL ESのコマンドを実行した時のOpenGL ESのエラーは、ブラウザのコンソールには出力されません。別途デバッガが必須になるので2つ紹介します。
エラーを表示するだけの奴
Leaning WebGLのサンプルや人の書いたコードををいじる時に仕込んでおくと、突然動かなくなった時に助かります。
リンク
- Learning WebGL
- http://learningwebgl.com/blog/
- Main Page - WebGL Public Wiki
- http://khronos.org/webgl/wiki/Main_Page
地震がきたのでとりあえずこの時点でこの記事はまとめます。次はKinectと絡ませて使うといった内容で書く予定です。突っ込みがあったらコメ欄でもブコメにでも書いていただけると助かります。