私たちは三次元 (3D) の世界に生きています。それにも関わらず、コンピューターやコンピューター化された機器の操作のほぼすべては、二次元 (2D) ユーザー・インターフェースで行われています。高速で滑らかに動くリアルな 3D アプリケーションは、かつてはコンピューター・アニメーター、科学関連のユーザー、そしてゲーム・マニアが独占していた領域でしたが、比較的最近になってようやく一般の PC ユーザーにも手が届くようになりました (囲み記事「3D ハードウェアの進化: 歴史概略」を参照してください)。
現在、3D グラフィックス・アクセラレーションは主流となっている PC 向け CPU のすべてに組み込まれています。さらにゲーム用 PC には 3D レンダリングを処理する専用のハイパフォーマンス GPU (グラフィックス・プロセッシング・ユニット) が追加されるようになっています。こうした傾向は、携帯電話やタブレットに搭載された RISC (縮小命令セット) ベースの CPU にも反映されており、現在のモバイル CPU には例外なく、強力な 3D 対応グラフィックス・アクセラレーション GPU が内蔵されています。また、サポート・ソフトウェア・ドライバーも成熟を遂げ、今では安定性と効率性を兼ね備えています。
ここ最近のブラウザー・テクノロジーの進化がブラウザーにもたらしたのは、ハードウェア・アクセラレーションを利用する WebGL です。WebGL は 3D JavaScript API であり、機能豊富な HTML5 と共に実行されます。従って、今や、JavaScript 開発者がインタラクティブな 3D ゲーム、アプリケーション、そして 3D で拡張された UI を作成することも可能です。主流のブラウザーに統合された WebGL により、ブラウザーとテキスト・エディターだけを武器とする大勢の開発者たちに、3D アプリケーション開発の門戸がついに開かれました。
この記事では、全 3 回からなる連載の第 1 回として、WebGL を紹介します。始めに 3D ソフトウェア・スタックの進化について簡単に概説した後、WebGL プログラミングの重要な側面をおさえた実践的なサンプル・アプリケーションで WebGL API を体験します (サンプル・コードについては「ダウンロード」を参照してください)。さらに、この完全ながらも理解しやすいサンプル・アプリケーションを開発しながら、3D グラフィックスに関する最も重要ないくつかの概念を説明します (HTML5 の canvas 要素について十分理解していることを前提とします)。今後の連載では、第 2 回で上位レベルの WebGL ライブラリーを紹介し、第 3 回では魅力的な 3D UI および 3D アプリの作成を始められるように、さまざまな要素を 1 つにまとめます。
3D アプリケーションのソフトウェア・スタック
初期の PC では、3D ハードウェア・ドライバーはアプリケーションと一緒にバンドルされるか、アプリケーションにコンパイルされることがほとんどでした。このような構成の場合、ハードウェアに備わるハードウェア・アクセラレーション機能へのアクセスが最適化されるため、最大限のパフォーマンスを得ることができます。基本的に、ハードウェアの機能に対して直接コーディングを行うことになるので、優れた設計のゲームや CAD (コンピューター支援設計) アプリケーションであれば、ハードウェアの能力を最大限に引き出すことができます。図 1 に、このソフトウェア構成を示します。
図 1. 組み込み 3D ハードウェア・ドライバーを使用したアプリケーション
一方で、バンドルするには相当コストがかかります。アプリケーションがリリースされてインストールされると、その時点でハードウェア・ドライバーには (バグを含むすべてに) 手を加えることができなくなります。グラフィックス・カードのベンダーがバグを修正したり、パフォーマンスを強化したドライバーを導入したりした場合でも、アプリケーションのユーザーがそれを利用するには、改めてアプリケーションのインストールや更新を行わなければなりません。
その上、グラフィックス・ハードウェアは急速に進化しているため、3D ドライバーがコンパイルされたアプリケーションやゲームは瞬く間に時代に遅れてしまいます。新しいハードウェアが (新規ドライバーや更新されたドライバーと共に) 登場すると同時に、ソフトウェア・ベンダーは新しいリリースを作成して配布しなければなりません。高速ブロードバンド・ネットワークが広く普及するようになる前は、これが、ソフトウェア配布に関する大きな問題でした。
ドライバー更新の問題に対する解決策として、オペレーティング・システムが 3D グラフィックス・ドライバーをホストする役割を担うようになりました。アプリケーションまたはゲームは、OS が提供する API を呼び出し、OS がその呼び出しをネイティブ 3D ハードウェア・ドライバーが受け入れるプリミティブに変換するという仕組みです。図 2 に、この仕組みを示します。
図 2. オペレーティング・システムの 3D API を使用したアプリケーション
この仕組みでは、(少なくとも理論上は) アプリケーションを OS の 3D API に対してプログラミングすることができます。そうすれば、アプリケーションは 3D ハードウェア・ドライバーに対する変更に影響されることも、3D ハードウェア自体の進化に影響されることもありません。主流となっているすべてのブラウザーを含め、多くのアプリケーションでは長い間、この構成が功を奏してきました。OS が、あらゆるタイプやスタイルのアプリケーションにも、競合するベンダーたちが提供するグラフィックス・ハードウェアにも果敢に応じる、仲介者の役割を果たしていたのです。けれどもこの OS ですべてを賄うというアプローチは、3D レンダリング・パフォーマンスには大きな打撃をもたらします。最高のハードウェア・アクセラレーション・パフォーマンスを必要とするアプリケーションでは、実際にインストールされているグラフィックス・ハードウェアをディスカバーし、ハードウェアの組み合わせごとにコードを最適化するための調整を実装する必要があることに変わりはありません。さらに、OS の API にベンダー固有の拡張機能をプログラミングしなければならない場合もよくあります。このことから、アプリケーションはやはり、下位にあるドライバーや物理ハードウェアに拘束されてしまいます。
WebGL の時代
時代を現代に移すと、最近ではデスクトップ端末やモバイル端末のすべてに、ハイパフォーマンス 3D ハードウェアが組み込まれています。ブラウザーの機能を利用するために、ますます多くのアプリケーションが JavaScript で開発されるようになり、Web デザイナーと Web アプリケーション開発者は、より高速でより優れた 2D/3D をブラウザーがサポートすることを要求するようになりました。その結果、主流のブラウザー・ベンダーで WebGL が広くサポートされるようになりました。
WebGL がベースとしている OpenGL ES (Embedded System) は、3D ハードウェアにアクセスするための下位レベルの手続き型 API です。1990年代初頭に SGI によって開発された OpenGL は、今では十分に理解された完成度の高い API と見なされています。WebGL は歴史上初めて、JavaScript を使用してネイティブ・スピードに近い速度で機器上の 3D ハードウェアにアクセスすることを可能にしました。WebGL と OpenGL ES はどちらも、非営利団体である Khronos Group の下で進化を続けています。
WebGL API は、下位にある OpenGL ハードウェア・ドライバーにほぼ直接アクセスします。ドライバーにアクセスする上で、ブラウザー・サポート・ライブラリーを使用してコードを変換し、さらにそのコードを OS の 3D API ライブラリーを使用して変換する、といった処理をする必要はありません。図 3 に、この新しいモデルを示します。
図 3. WebGL を使用して 3D ハードウェアにアクセスする JavaScript アプリケーション
ハードウェア・アクセラレーションを利用する WebGL は、ブラウザー上の 3D ゲーム、リアルタイムの 3D データ視覚化アプリケーション、斬新なインタラクティブ 3D UI をはじめ、さまざまな可能性を実現できます。OpenGL ES は、既存の WebGL ベースのアプリケーションに影響を与えることなく新しいベンダーのドライバーをインストールできるように標準化されているため、「あらゆるプラットフォームであらゆる 3D ハードウェアを使用できるようにする」という夢のような約束を果たします。
WebGL のハンズオン
ここからは、WebGL を実際に体験します。最新バージョンの Firefox、Chrome、または Safari を起動して、サンプル・コード (「ダウンロード」を参照) に含まれている triangles.html を開いてください。すると、図 4 に示すスクリーン・キャプチャーのようなページが表示されるはずです。このスクリーン・キャプチャーは、OS X 上で実行中の Safari を撮ったものです。
図 4. triangles.html ページ
このページには、一見したところまったく同じ青色の 2 つの三角形が表示されていますが、これらの三角形は等しく作成されているわけではありません。どちらも HTML5 の Canvas を使って描画されていますが、左側の三角形は 2D であり、10 行に満たない JavaScript コードで描画されています。一方、右側の三角形は、4 つの面を持つピラミッド状の 3D オブジェクトであり、これをレンダリングするには 100 行を超える JavaScript WebGL コードを必要としています。
このページのソース・コードを調べると、右側の三角形は大量の WebGL コードによって描画されていることがわかります。それにも関わらず、この三角形は 3D であるようには見えません (赤と青の 3D 眼鏡をかけても、無駄なことです)。
WebGL が描画するのは 2D ビューです
triangles.html の右側の図形が三角形に見える理由は、ピラミッドの向きにあります。このページでは、面ごとに色が塗り分けられたピラミッドの青色の面だけを見ています。これは、ビルの一面を真正面から見ると、2D の四角形しか見えないことと似ています (図 5 をチラッと見て、3D のピラミッドを確認してください)。このことを理解すると、ブラウザーで 3D グラフィックスを扱う場合の核心がより一層明らかになります。つまり、最終的な出力は常に 3D シーンの 2D ビューとなることから、WebGL による 3D シーンの静的レンダリングも必ず 2D のイメージになります。
今度は、ブラウザーに pyramid.html を読み込んでください。このページにピラミッドを描画するコードは、triangles.html のコードとほとんど同じですが、1 つ違う点は、pyramid.html では y 軸の周りにピラミッドを継続的に回転させるためのコードが追加されていることです。つまり、同じ 1 つの 3D シーンを構成する複数の 2D ビューが (WebGL を使用して) 時間をずらして連続して描画されるというわけです。ピラミッドが回転すると、triangles.html で右側に表示されていた青色の三角形は、実は面ごとに色が塗り分けられた 3D ピラミッドの一面であることが明らかにわかります。図 5 に、OS X 上の Safari で表示した pyramid.html のスナップショットを示します。
図 5. pyramid.html ページ上で回転して表示された 3D ピラミッド
WebGL コードを作成する
リスト 1 に、triangles.html に含まれる 2 つの canvas 要素の HTML コードを記載します。
リスト 1. 2 つの canvas 要素が含まれた HTML コード
<html> <head> ... </head> <body onload="draw2D();draw3D();"> <canvas id="shapecanvas" class="front" width="500" height="500"> </canvas> <canvas id="shapecanvas2" style="border: none;" width="500" height="500"> </canvas> <br/> </body> </html>
onload
ハンドラーは、2 つの関数 draw2D()
と draw3D()
を呼び出します。draw2D()
関数は、左側の Canvas (shapecanvas) で 2D を描画します。draw3D()
関数は、右側の Canvas (shapecanvas2
) で 3D を描画します。
リスト 2 に、左側の Canvas で 2D 三角形を描画するコードを記載します。
リスト 2. HTML5 の Canvas で 2D 三角形を描画する
function draw2D() { var canvas = document.getElementById("shapecanvas"); var c2dCtx = null; var exmsg = "Cannot get 2D context from canvas"; try { c2dCtx = canvas.getContext('2d'); } catch (e) { exmsg = "Exception thrown: " + e.toString(); } if (!c2dCtx) { alert(exmsg); throw new Error(exmsg); } c2dCtx.fillStyle = "#0000ff"; c2dCtx.beginPath(); c2dCtx.moveTo(250, 40); // Top Corner c2dCtx.lineTo(450, 250); // Bottom Right c2dCtx.lineTo(50, 250); // Bottom Left c2dCtx.closePath(); c2dCtx.fill(); }
リスト 2 の単純な 2D の描画コードでは、描画コンテキスト c2dCtx
が Canvas から取得されます。すると、このコンテキストの描画メソッドが呼び出されて、三角形をたどる一連のパスが作成されます。最後に、閉じられたパスで囲まれた領域が RGB 色 #0000ff
(青) で塗りつぶされます。
WebGL の 3D 描画コンテキストを取得する
リスト 3 に示す、canvas 要素から 3D 描画コンテキストを取得するコードは、2D の場合とほとんど変わりません。違っているのは、要求するコンテキスト名が 2d
ではなく experimental-webgl
となっていることです。
リスト 3. canvas 要素から WebGL の 3D コンテキストを取得する
function draw3D() { var canvas = document.getElementById("shapecanvas2"); var glCtx = null; var exmsg = "WebGL not supported"; try { glCtx = canvas.getContext("experimental-webgl"); } catch (e) { exmsg = "Exception thrown: " + e.toString(); } if (!glCtx) { alert(exmsg); throw new Error(exmsg); } ...
リスト 3 の draw3D()
関数は、ブラウザーで WebGL がサポートされていない場合には、アラートを表示してエラーを発生させます。本番アプリケーションでこの状況に対処するには、そのアプリケーションに応じた固有のコードを使用することをお勧めします。
ビューポートを設定する
WebGL に出力のレンダリング先を指示するには、Canvas 内で WebGL が描画できる領域をピクセル単位で指定して、ビューポートを設定する必要があります。triangles.html では、Canvas 領域全体を使用して出力をレンダリングします。
// set viewport glCtx.viewport(0, 0, canvas.width, canvas.height);
次のステップでは、WebGL レンダリング・パイプラインにフィードするデータの作成に取り掛かります。このデータによって、シーンを構成する 3D オブジェクトを記述する必要があります。この例の場合、異なる色で塗り分けられた 4 つの面を持つ 1 つのピラミッドのみで、シーンが構成されます。
3D オブジェクトを記述する
WebGL でレンダリングする 3D オブジェクトを記述するには、三角形を使用してそのオブジェクトを表現する必要があります。WebGL では、オブジェクトを個別の三角形のセットという形か、頂点を共有するひとつながりの三角形という形で、記述することができます。ピラミッドの例では、4 つの面からなるピラミッドが、4 つの個別の三角形のセットで記述されます。各三角形は、その三角形の 3 つの頂点で指定されます。図 6 に、ピラミッドの面のうちの 1 つに対して指定された頂点を示します。
図 6. ピラミッドの一面を記述する頂点
図 6 では、面の 3 つの頂点が、y軸上の (0,1,0)
、z 軸上の (0,0,1)
、x 軸上の (1,0,0)
に指定されています。ピラミッド上では、この面が黄色で塗りつぶされ、青色の面の右側に配置されます。これと同じパターンをピラミッドの他の 3 つの面にも適用することで、すべての面を記述することができます。リスト 4 のコードでは、verts
という名前の配列内に、ピラミッドの 4 つの面を定義しています。
リスト 4. ピラミッドを構成する三角形のセットを記述する頂点の配列
// Vertex Data vertBuffer = glCtx.createBuffer(); glCtx.bindBuffer(glCtx.ARRAY_BUFFER, vertBuffer); var verts = [ 0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, -1.0, 0.0, 0.0 ]; glCtx.bufferData(glCtx.ARRAY_BUFFER, new Float32Array(verts), glCtx.STATIC_DRAW);
verts
配列には、ピラミッドの底面 (実際は、x-z 面上にある正方形) は含まれていないことに注目してください。ピラミッドは y 軸を中心軸として回転されるため、ページの閲覧者にピラミッドの底面が見えることはないためです。閲覧者が決して目にすることのないオブジェクトの面はレンダリングしないのが、3D 作業の慣例となっています。表示されない面をレンダリングしないことで、複雑なオブジェクトのレンダリングに要する時間を大幅に短縮することができます。
リスト 4 では、3D ハードウェアが効率良くアクセスできるバイナリー・フォーマットのバッファーに、verts
配列内のデータが入れられます。この処理は一貫して、JavaScript の WebGL 呼び出しによって行われます。まず、WebGL の glCtx.createBuffer()
を呼び出してサイズがゼロの新しいバッファーを作成し、glCtx.bindBuffer()
の呼び出しによって、新しく作成されたバッファーを OpenGL レベルの ARRAY_BUFFER
ターゲットにバインドします。次に、JavaScript でロードするデータ値の配列が定義されると、glCtx.bufferData()
を呼び出して、現在バインドされているバッファーのサイズを設定します。最後に、JavaScript データを (最初に JavaScript 配列を Float32Array
バイナリー・フォーマットに変換してから) デバイス・ドライバー・レベルのバッファーに入れます。
従って、vertBuffer
変数が参照するハードウェア・レベルのバッファーに、必要な頂点情報が格納されることになります。WebGL レンダリング・パイプラインの他のプロセッサーは、このバッファー内のデータに効率良く、直接アクセスすることができます。
ピラミッドの面の色を指定する
次にセットアップする必要があるのは、colorBuffer
で参照される下位レベルのバッファーです。このバッファーには、ピラミッドの各面の色情報が入れられます。この例で使用する色は、青、黄、緑、赤です。リスト 5 に、colorBuffer
のセットアップ内容を記載します。
リスト 5. ピラミッドの各面の色を指定する colorBuffer
のセットアップ
colorBuffer = glCtx.createBuffer(); glCtx.bindBuffer(glCtx.ARRAY_BUFFER, colorBuffer); var faceColors = [ [0.0, 0.0, 1.0, 1.0], // front (blue) [1.0, 1.0, 0.0, 1.0], // right (yellow) [0.0, 1.0, 0.0, 1.0], // back (green) [1.0, 0.0, 0.0, 1.0], // left (red) ]; var vertexColors = []; faceColors.forEach(function(color) { [0,1,2].forEach(function () { vertColors = vertColors.concat(color); }); }); glCtx.bufferData(glCtx.ARRAY_BUFFER, new Float32Array(vertexColors), glCtx.STATIC_DRAW);
リスト 5 に示されている下位レベルの colorBuffer
バッファーは、createBuffer()
、bindBuffer()
、および bufferData()
を呼び出すことによってセットアップされています。これらの呼び出しは、vertBuffer
に使用されているものとまったく同じです。
ただし、WebGL にはピラミッドの「面」の概念がありません。WebGL は面の代わりに三角形と頂点のみを使用して動作するため、色のデータを頂点に関連付ける必要があります。リスト 5 では、仲介となる faceColors
という名前の JavaScript 配列が vertColors
配列を初期化します。vertColors
は、下位レベルの colorBuffer
をロードする際に使用される JavaScript 配列です。faceColors
配列には、4 つの面のそれぞれに対応する 4 つの色 (青、黄、緑、赤) が格納されます。これらの色は、RGBA (Red, Green, Blue, Alpha) 形式で指定されます。
vertColors
配列には、各三角形の頂点ごとの色が、vertBuffer
内での色の出現順で格納されます。4 つの三角形にはそれぞれに 3 つの頂点があるため、最終的な vertColors
配列には色のエントリーが合計 12 個含まれることになります (各エントリーは、4 つの float
型の数値で構成される配列です)。ピラミッドの面を表現する各三角形の 3 つの頂点のそれぞれに同じ色を割り当てるために、ネストされた forEach
ループが使用されています。
OpenGL シェーダーの概要
ここで当然頭に浮かんでくる疑問は、三角形の 3 つの頂点に色を指定すると、どのような仕組みで三角形全体がその色でレンダリングされるのか、というものです。この疑問に対する答えを明らかにするには、WebGL レンダリング・パイプラインに含まれる、「頂点シェーダー」と「フラグメント (ピクセル) シェーダー」という 2 つのプログラマブル・コンポーネントの動作を理解する必要があります。これらのシェーダーは、3D アクセラレーション・ハードウェアの GPU で実行可能なコードにコンパイルすることができます。最近の 3D ハードウェアの中には、ハイパフォーマンス・レンダリングを実現するために、何百ものシェーダー動作を並行して実行可能なものもあります。
頂点シェーダーは、指定された頂点ごとに実行されます。頂点シェーダーは特定の頂点に関連付けられた色、位置、テクスチャー、およびその他の情報を入力として取ります。この入力データを計算して変換することで、その特定の頂点をレンダリングするビューポート上の 2D 位置、ならびに頂点の色やその他の属性を決定します。一方、フラグメント・シェーダーは、各頂点によって形成される三角形を構成する各ピクセルの色とその他の属性を決定します。WebGL では、頂点シェーダーもフラグメント・シェーダーも GLSL (OpenGL Shading Language) を使用してプログラミングします。
GLSL
GLSL は、ANSI C と同様の (そして、C++ のいくつかの概念を追加した) 構文を使用する、ドメインに特化されたプログラミング言語であり、使用可能なオブジェクト情報 (形状、位置、パースペクティブ (遠近法)、色、照明、テクスチャー (質感)、およびその他の関連する情報) を、3D オブジェクトがレンダリングされる 2D Canvas の各ピクセルとして表示される実際の色にマッピングするために使用されます。
GLSL を使用して独自のシェーダー・プログラムを作成する方法についての詳細は、この記事では説明しませんが、このサンプル・プログラムの残りの部分を理解するには、GLSL コードに関する最小限の知識が必要です。そこで、シェーダーを中心としたコード全体を理解できるように、サンプル・プログラムで使用されている 2 つの単純な GLSL シェーダーの動作を説明することにします。
この連載の次回の記事では、より上位レベルの 3D ライブラリーとフレームワークを使用して WebGL を扱う方法を説明しますが、これらのライブラリーとフレームワークは GLSL コードを透過的に統合するため、自分でシェーダーを作成する必要はないはずです。
WebGL でシェーダー・プログラムを処理する
シェーダー・プログラムとは、ハードウェアの GPU でそのまま実行できる、関連するシェーダー (通常は、WebGL の頂点シェーダーとフラグメント・シェーダー) がリンクされたバイナリーのことです。シェーダーは、単純なほぼ 1 行のコードであることも、極めて複雑な複数の機能を実行する、数百行からなる並列コードになることもあります。
WebGL でシェーダーを実行するには、その前に、シェーダー・プログラムの GLSL ソース・コードをバイナリー・コードにコンパイルしてからリンクしておく必要があります。ベンダー提供の 3D ドライバーには、コンパイラーとリンカーが組み込まれています。開発者に必要な作業は、GLSL コードを JavaScript でサブミットし、コンパイル・エラーが発生していないことを確認した後、パラメーターとして作成した行列をリンクすることですが、WebGL にはこれらの全操作を行うための API があります。図 7 に、WebGL を介した GLSL コードのサブミットについて説明した図を示します。
図 7. WebGL を介した GLSL シェーダー・コードのコンパイルとリンク
リスト 6 に、サンプル・プログラムの GLSL シェーダーを取得、コンパイル、リンクするコードを記載します。
リスト 6. WebGL で GLSL シェーダー・コードをコンパイルおよびリンクする
var vertShaderCode = document.getElementById("vertshader").textContent; var fragShaderCode = document.getElementById("fragshader").textContent; var fragShader = glCtx.createShader(glCtx.FRAGMENT_SHADER); glCtx.shaderSource(fragShader, fragShaderCode); glCtx.compileShader(fragShader); if (!glCtx.getShaderParameter(fragShader, glCtx.COMPILE_STATUS)) { var errmsg = "fragment shader compile failed: " + glCtx.getShaderInfoLog(fragShader); alert(errmsg); throw new Error() } var vertShader = glCtx.createShader(glCtx.VERTEX_SHADER); glCtx.shaderSource(vertShader, vertShaderCode); glCtx.compileShader(vertShader); if (!glCtx.getShaderParameter(vertShader, glCtx.COMPILE_STATUS)) { var errmsg = "vertex shader compile failed : " + glCtx.getShaderInfoLog(vertShader); alert(errmsg); throw new Error(errmsg) } // link the compiled vertex and fragment shaders shaderProg = glCtx.createProgram(); glCtx.attachShader(shaderProg, vertShader); glCtx.attachShader(shaderProg, fragShader); glCtx.linkProgram(shaderProg);
リスト 6 では、頂点シェーダーのソース・コードがストリングとして vertShaderCode
に格納され、フラグメント・シェーダーのソース・コードが fragShaderCode
に格納されます。2 つのソース・コードはどちらも document.getElementById().textContent
プロパティーを介して DOM の <script>
要素から抽出されます。
glCtx.createShader(glCtx.VERTEX_SHADER)
によって頂点シェーダーが作成され、glCtx.createShader(glCtx.FRAGMENT_SHADER)
によってフラグメント・シェーダーが作成されます。
glCtx.shaderSource()
によってソース・コードがシェーダーにロードされた後、glCtx.compileShader()
によってソース・コードがコンパイルされます。
コンパイルが完了すると、コードが正常にコンパイルされたことを確認するために、glCtx.getShaderParameter()
が呼び出されます。コンパイル・エラーをコンパイラー・ログから取り出すには、glCtx.getShaderInfoLog()
を使用します。
頂点シェーダーとフラグメント・シェーダーが両方とも正常にコンパイルされた後、この 2 つをリンクしてシェーダーの実行プログラムの形にします。それにはまず、glCtx.createProgram()
を呼び出して、下位レベルのプログラム・オブジェクトを作成します。続いて glCtx.attachShader()
を呼び出し、コンパイル済みバイナリーをプログラムに関連付けます。そして最後に、glCtx.linkProgram()
を呼び出すことで、これらのバイナリーをリンクします。
頂点シェーダーおよびフラグメント・シェーダーの GLSL コード
頂点シェーダーは、先ほど JavaScript の vertBuffer
変数と colorBuffer
変数として準備した入力データ・バッファーに対して動作します。リスト 7 に、頂点シェーダーの GLSL ソース・コードを記載します。
リスト 7. 頂点シェーダーの GLSL ソース・コード
attribute vec3 vertPos; attribute vec4 vertColor; uniform mat4 mvMatrix; uniform mat4 pjMatrix; varying lowp vec4 vColor; void main(void) { gl_Position = pjMatrix * mvMatrix * vec4(vertPos, 1.0); vColor = vertColor; }
リスト 7 について説明すると、attribute
というキーワードは、頂点データごとのデータに対して WebGL と頂点シェーダーとの連携を指定するストレージ修飾子です。この例の場合、シェーダーが実行されるたびに、vertBuffer
に入れられた頂点の位置情報が vertPos
に格納されます。vertColor
には、先ほどセットアップした colorBuffer
で指定された色が、その頂点の色として格納されます。
uniform
というストレージ修飾子は、JavaScript で設定された値を、シェーダー・コード内で読み取り専用パラメーターとして使用するように指定します。これらのバッファー内の値は、(特に、アニメーション時に) JavaScript コードによって変更される可能性がありますが、シェーダー・コードによって変更されることは決してありません。別の言い方をすれば、これらの値を変更できるのは CPU だけであり、レンダリング GPU が変更することはできません。設定後の uniform
値は、頂点シェーダー・コードで処理されるすべての頂点に共通で使用されます。この例の mvMatrix
には、JavaScript でセットアップされたモデル・ビュー行列が格納され、pjMatrix
にはプロジェクション (投影) 行列が格納されます (モデル・ビュー行列とプロジェクション行列については次のセクションで取り上げます)。
lowp
というキーワードは精度の修飾子で、vColor
変数が低い精度の float
型の数値であることを指定していますが、WebGL の色空間で色を記述するにはこれで十分です。gl_position
は、シェーダーの変換後の出力値で、これは 3D レンダリング・パイプラインがさらに処理を加えるために内部で使用されます。
vColor
変数には varying
ストレージ修飾子が設定されています。varying
は、この変数を頂点シェーダーとフラグメント・シェーダーの間のインターフェースとして使用するように指定します。vColor
は、頂点ごとに固有の値を 1 つ格納しますが、フラグメント・シェーダーでは、その値が頂点の間で補間されます (フラグメント・シェーダーは、頂点の間にあるピクセルに対して実行されることを思い出してください)。この記事の GLSL の例では、vColor
変数が、頂点シェーダーで各頂点の colorBuffer
に指定された色に設定されます。今度は、フラグメント・シェーダーのコードをリスト 8 に記載します。
リスト 8. フラグメント・シェーダーの GLSL ソース・コード
varying lowp vec4 vColor; void main(void) { gl_FragColor = vColor; }
フラグメント・シェーダーは平凡なもので、補間される vColor
の値を頂点シェーダーから取得し、その値を出力として使用します。vColor
の値は、ピラミッドの各面の 3 つの頂点のそれぞれに対して同じに設定されることから、フラグメント・シェーダーで補間される色はそのまま同じ色に維持されます。
各三角形の少なくとも 1 つの頂点が異なる色になるようにすると、補間の効果を確認することができます。pyramid.html を変更して、リスト 9 の太字で示されたコードを反映させてください。
リスト 9. フラグメント・シェーダーの補間が明らかになるように pyramid.html を変更する
var vertexColors = []; faceColors.forEach(function(color) { [0,1].forEach(function () { vertColors = vertColors.concat(color); }); vertColors = vertColors.concat(faceColors[0]); }); glCtx.bufferData(glCtx.ARRAY_BUFFER, new Float32Array(vertexColors), glCtx.STATIC_DRAW);
この変更では、各三角形の 1 つの頂点の色は青であることを確実にしています。変更後の pyramid.html をブラウザーにロードすると、青色の面 (この面の頂点はすべて青色のままです) を除くピラミッドのすべての面に、色のグラデーション (補間された色) が適用されているはずです。図 8 に、補間された色のグラデーションが適用されたピラミッドの面を示します (OS X 上の Chrome での表示)。
図 8. 補間された色のグラデーションが適用されるように、ピラミッドの面が変更された pyramid.html
モデル・ビュー行列とプロジェクション行列
Canvas にレンダリングされる 3D シーンの変換を制御するには、2 つの行列を指定します。それは、モデル・ビュー行列とプロジェクション (投影) 行列です。前に説明したように、頂点シェーダーはこれらの行列を使用して、各 3D 頂点の変換方法を決定します。
モデル・ビュー行列には、モデル (この例ではピラミッド) とビュー (シーンを見るために覗く「カメラ」) の変換が結合されます。モデル・ビュー行列は基本的に、シーンにおけるオブジェクトの位置とそれを見るカメラの位置を制御します。以下のコードは、カメラから 3 ユニット離れた位置にピラミッドを配置して、この例のモデル・ビュー行列をセットアップします。
modelViewMatrix = mat4.create(); mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -3]);
vertBuffer
のセットアップから、ピラミッドは 2 ユニットの幅であることがわかっています。従って、上記のコードでは、ピラミッドがビューポートの「フレーム全体」を占められるようにします。
プロジェクション行列は、カメラのビューを通じて 3D シーンを 2D ビューポートへと変換するのを制御します。この例でのプロジェクション行列をセットアップするコードは、以下のとおりです。
projectionMatrix = mat4.create(); mat4.perspective(projectionMatrix, Math.PI / 4, canvas.width / canvas.height, 1, 100);
カメラは、Math.PI / 4
(πラジアンの 4分の1、つまり 180 度 / 4 = 45 度) の視野を持つように設定されています。表示対象との距離が 1 ユニットから 100 ユニットまでの範囲である限り、カメラは遠近法による (歪んでいない) 表示を維持することができます。
ビューポートに 3D シーンをレンダリングする
すべてのセットアップが完了した状態でリスト 10 のコードを実行すると、3D シーンが 2D ビューとしてビューポートにレンダリングされます。このコードに含まれる draw()
関数は、update()
ラッパーによって呼び出されます。update()
ラッパーを使用することで、ピラミッドを回転するための pyramid.html 内のコードをあとで変換するのが容易になります。
リスト 10. シーンをレンダリングする draw()
関数
function draw(ctx) { ctx.clearColor(1.0, 1.0, 1.0, 1.0); ctx.enable(ctx.DEPTH_TEST); ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT); ctx.useProgram(shaderProg); ctx.bindBuffer(ctx.ARRAY_BUFFER, vertBuffer); ctx.vertexAttribPointer(shaderVertexPositionAttribute, 3 , ctx.FLOAT, false, 0, 0); ctx.bindBuffer(ctx.ARRAY_BUFFER, colorBuffer); ctx.vertexAttribPointer(shaderVertexColorAttribute, 4 , ctx.FLOAT, false, 0, 0); ctx.uniformMatrix4fv(shaderProjectionMatrixUniform, false, projectionMatrix); mat4.rotate(modelViewMatrix, modelViewMatrix, Math.PI/4, rotationAxis); ctx.uniformMatrix4fv(shaderModelViewMatrixUniform, false, modelViewMatrix); ctx.drawArrays(ctx.TRIANGLES, 0, 12 /* num of vertex */); }
リスト 10 ではまず、ctx.clearColor()
を呼び出してビューポートを白にクリアした後、ctx.enable(ctx.DEPTH_TEST)
を呼び出してデプス・バッファー (z-buffer
) を使用可能にします。続いて ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT)
を呼び出すことで、カラー・バッファーとデプス・バッファーをクリアします。
その後の ctx.useProgram()
の呼び出しでは、前にコンパイルしてリンクした vertShader
と fragShader
からなる shaderProg
を GPU で実行できるようにロードします。次に、ctx.bindBuffer()
と ctx.vertexAttribPointer()
を続けて呼び出すことで、前に JavaScript でセットアップした下位レベルのデータ・バッファー (vertBuffer
および colorBuffer
) を GLSL シェーダー・プログラムの属性にバインドします (このワークフローは、SQL プログラミングでのストアード・プロシージャーと概念が似ています。SQL プログラミングでは、ストアード・プロシージャーによって実行時にパラメーターをバインドすることにより、プリペアード・ステートメントを再実行できるようにします)。その後にある 2 つの、ctx.uniformMatrix4fv()
呼び出しは、頂点シェーダーが読み取り専用でアクセスするモデル・ビュー行列、プロジェクション行列をそれぞれセットアップします。
最後の同じく重要な ctx.drawArrays()
の呼び出しでは、4 つの三角形のセット (合計 12 の頂点) がビューポートにレンダリングされます。
お気付きかもしれませんが、頂点シェーダーがアクセスするモデル・ビュー行列をセットアップする直前に、mat4.rotate(modelViewMatrix, modelViewMatrix, Math.PI/4, rotationAxis)
が呼び出されます。この呼び出しは、レンダリングする直前に y 軸を中心軸としてピラミッドを Math.PI/4
(つまり 45 度) 回転させます。図 6 をもう一度見れば、その理由は明らかです。ピラミッドの面のセットアップでは、ピラミッドの端を z 軸上に配置することに注意してください。z 軸上のカメラがスクリーンに向けられている場合、ピラミッドを回転させなければ、カメラに映るのは青色の面の半分と黄色の面の半分です。ピラミッドを 45 度回転させることで、青色の面だけが表示されます。この効果は、mat4.rotate()
呼び出しをコメントアウトして、ページを再びロードすることで、簡単に確認することができます。OS X 上の Firefox でこの結果を表示したのが図 9 です (このバージョンでは、頂点の色補間コードの変更が元に戻されています)。
図 9. ピラミッドを回転させる前の 2 つの面を表示する pyramid.html
ピラミッドの回転をアニメーションにする
下位レベルの API である WebGL には、元々アニメーションのサポートが備わっていません。
回転のアニメーションを表示するために、この例では requestAnimationFrame()
関数 (rAF) を使用して、ブラウザーによるアニメーションのサポートを利用しています。requestAnimationFrame()
が引数として取るのは、コールバック関数です。ブラウザーは次の画面更新 (通常、1 秒あたり最大 60 回の更新) の前に、この関数をコールバックします。コールバック内では requestAnimationFrame()
を再び呼び出して、次の画面更新の前にこの関数が呼び出されるようにする必要があります。
requestAnimationFrame()
を呼び出す pyramid.html 内のコードは、リスト 11 のとおりです。
リスト 11. 画面を更新するために requestAnimationFrame
を呼び出す
function update(gl) { requestAnimationFrame(function() { update(gl); }); draw(gl); }
リスト 11 では、update()
関数がコールバックとして指定されています。update()
も同じく、次のフレームを要求するために requestAnimationFrame()
を呼び出す必要があります。update()
が呼び出されるたびに、draw()
も呼び出されます。
リスト 12 に、y 軸を中心軸としてピラミッドをインクリメンタルに回転させるように変更した triangles.html の draw()
関数を記載します。追加または変更されたコードは太字で示されています。
リスト 12. ピラミッドの回転をアニメーション化する
var onerev = 10000; // ms var exTime = Date.now(); function draw(ctx) { ctx.clearColor(1.0, 1.0, 1.0, 1.0); ctx.enable(ctx.DEPTH_TEST); ... ctx.uniformMatrix4fv(shaderProjectionMatrixUniform, false, projectionMatrix); var now = Date.now(); var elapsed = now - exTime; exTime = now; var angle = Math.PI * 2 * elapsed/onerev; mat4.rotate(modelViewMatrix, modelViewMatrix, angle, rotationAxis); ctx.uniformMatrix4fv(shaderModelViewMatrixUniform, false, modelViewMatrix); ctx.drawArrays(ctx.TRIANGLES, 0, 12 /* num of vertex */); }
リスト 12 では、フレームごとの回転角度を計算するために、経過時間 elapsed
を完全に一回転するために必要な時間で除算しています (この例では、onerev
= 10 秒です)。
この連載の今後の記事で、他にも rAF を使用する例を取り上げます。
WebGL の限界を押し広げる
ピラミッドの例では、WebGL プログラミングの重要な基本概念を探り、WebGL で実現できることをほんの少しだけ学びました。
WebGL で可能な驚くべきアプリケーションの例 (現時点での WebGL の限界を押し広げるアプリケーション) を見るには、比較的最近のマシンで実行されている最新の Firefox ブラウザーで、Epic Games による Unreal Engine 3 でのWebGL のデモンストレーション ― Epic Citadel にアクセスしてください。図 10 に、実行中の Epic Citadel を示します (Windows 上の Firefox で実行)。
図 10. Epic Citadel — Epic Games による Unreal Engine 3 でのWebGL のデモンストレーション
Epic Citadel は、(元々 C/C++ で書かれた) 有名なゲーム・エンジン製品を JavaScript および WebGL にコンパイルしたことによる作品です (使用されているコンパイラー・テクノロジーは emscripten であり、出力された JavaScript サブセットは asm.js として知られています)。このゲームでは、WebGL でレンダリングされた中世都市の石畳の道、城、アニメーション化された滝を (インタラクティブな操作によって) 歩き回ることができます。
別の興味深い例としては、Greggman および Human Engines による Google の WebGL Aquarium (水族館) もあります。図 11 に、この水族館が Windows 上の Chrome で実行されている様子を示します。
図 11. Google による斬新なガラスのドーム型水族館の WebGL サンプル
この水族館アプリケーションでは、全面がガラスでできた球状の水槽を泳ぎ回る魚の数をユーザーが選択することができます。また、光の反射具合 (Reflection)、水の透明度 (Fog)、光線の射し具合 (Light Rays) などのレンダリング効果を試してみることもできます。
まとめ
WebGL は、JavaScript API で 3D ハードウェアに直接アクセスすることを可能にしますが、この API が下位レベルの API であることに変わりはありません。
- WebGL は、3D シーン内で何が表示されているかについてはまったく関与せず、そのための概念も備えていません。この記事の例で扱った単純な 3D ピラミッド・オブジェクトは、WebGL API 層では認識されないため、オブジェクトを構成する頂点データを辛抱強く追跡する必要があります。
- WebGL シーンを描画するための呼び出しごとに、1 つの 2D 画像だけがレンダリングされます。アニメーションそのものは WebGL では扱われないため、アニメーションを実装するにはコードを追加する必要があります。
- オブジェクトと環境との間での動きや相互作用 (光の反射やオブジェクトの物理特性など) も、コードを追加して実装する必要があります。
- ユーザー入力、オブジェクトの選択、あるいはオブジェクトの衝突などといったイベントの処理は、上位レベルのコードで実装する必要があります。
ピラミッドを回転させるためだけに 100 行を超えるコードを作成するのは、気が遠くなるような作業に感じられることでしょう。また、かなり複雑な WebGL アプリケーションを作成するには、上位レベルのライブラリーやフレームワークを使用しなければならないのは明らかです。ありがたいことに、WebGL ライブラリー/フレームワークはいくらでもあります。その多くは、オープンソース・コードのコミュニティーから無料で入手できます。第 2 回では、WebGL ライブラリーを利用する方法を探ります。
ダウンロード
内容 | ファイル名 | サイズ |
---|---|---|
Sample code | webgl1dl.zip | 20KB |
参考文献
学ぶために
- WebGL: Khronos Group のサイトにある WebGL ホーム・ページにアクセスし、WebGL 仕様の最新のワーキング・ドラフトを読んでください。
- WebGL クイック・リファレンス: WebGL API の構文や概念が一目でわかる便利なチート・シートを活用してください。
- OpenGL のレンダリング・パイプライン: ハードウェア・アクセラレーションを利用した 3D レンダリングで使用される概念上のハードウェア「パイプライン」の概要を理解してください。
- 「HTML5 の Canvas を使って素晴らしいグラフィックスを作成する」(Ken Bluttman 著、developerWorks、2011年2月): HTML5 の Canvas を使用した 2D 描画手法について復習する必要があるならば、この入門記事を調べてください。
- Chromium のレンダリング用スタック: オープンソースの Chromium ブラウザーが内部でデュアル・レンダリング・スタックにより GPU アクセラレーションをサポートしている方法を調べてください。
- 「OpenGL Shading Language」: シェーダー・コードの作成、または実験的な作成についての詳細な GLSL ドキュメントを読んでください。
- JavaScript 型付き配列: Khronos Group から最新の仕様を入手してください。
- 「Can I use WebGL?」: この価値あるサイトでは、最新のブラウザーによる WebGL のバージョンごとのサポートを追跡しています。
- Epic Citadel: この Epic Games による WebGL ゲーム・エンジンのデモンストレーションで、完全に 3D でレンダリングされた中世都市を歩き回ってください。また、このプロジェクトで使用している emscripten コンパイラーと asm.js (下位レベルの JavaScript サブセット) について調べてください。
- WebGL Aquarium: この Greggman と Human Engines による WebGL サンプルの Google コードを基に、読者独自の斬新な球状の 3D 水族館の作成を始めてください。
製品や技術を入手するために
- JavaScript の行列ベクトル・ライブラリーである glMatrix: WebGL アプリケーションでハイパフォーマンスの行列処理を行うためのデファクト・スタンダードとなっている JavaScript ライブラリーをダウンロードしてください。
- requestAnimationFrame ポリフィル: (Paul Irish 氏によるおおもとのブログ投稿に基づいて作成された) Erik Moller 氏によるこのポリフィルを使用すると、ブラウザーによらず同一の構文を使用して
requestAnimationFrame
を呼び出すことができます。
議論するために
- developerWorks コミュニティーに参加してください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。
コメント
IBM PureSystems
IBM がどのように IT に革命をもたらしているのかをご自身でお確かめください
Knowledge path
developerWorks の Knowledge path シリーズでは、テーマ別の学習資料をご提供しています
ソフトウェア評価版: ダウンロード
developerWorksでIBM製品をお試しください!