WebGL+HTML5で3DCGを勉強してみた! Part1

    どうも。営業部の伊藤です。

    前回のブログで書いたように、今更感は否めないですが…最近WebGLの勉強をしています。
    まだ超々初心者レベルまでしか勉強できていないですが、これはなかなか頭の体操になります。
    今まであまり経験した事のない思考が必要という事もあり、抜け毛の本数が増えそうではありますが…初心者レベルになれるまでは頑張ろうと思います。

    という事で、復習も兼ねてWebGLを利用した3Dグラフィックの描画方法について書こうと思います。
    ※3DCG、WebGLに着目しているため、実装で利用するHTML、JavaScript、数学的知識の基礎については省かせていただきます。

    3DCGのサンプルは以下をご覧ください。

    ※ピカピカしているのは描画遅延ではないです。チカチカしたらすいません。。。

    【補足情報】
     利用しているオープンソースは以下2種類となります。
     ○Google webgl-utils.js
      setIntervalやsetTimeoutだと色々と問題が多い(複数オブジェクトの描画に向いていない、処理の効率化ができない(GPUに優しくない))ので、上記再帰処理はwebgl-utilsを利用しています。

     ○glMatrix.js
      3DCGを表現するために必要となる行列計算用ライブラリとして利用しています。
      後述しますが、STEP6の座標変換行列を生成&通知のところで利用します。

     勉強に利用しているサイトは以下になります。
     ○wgld.org
      初心者向けに基礎から細かくレクチャーしてくれており、非常に分かりやすいです。オススメ!!
      
    それではサンプルをもとに実装内容を書いていきます。
    3DCG描画までのSTEPは以下のようになります。

    STEP1 canvasエレメントを取得
    STEP2 canvasからWebGLコンテキストを取得
    STEP3 シェーダを生成&コンパイル
    STEP4 モデルデータを用意
    STEP5 モデルデータからVBO(VertexBufferObject)を生成&バインド
    STEP6 座標変換行列を生成&通知
    STEP7 描画命令
    STEP8 STEP6~7の再帰処理(canvasをレンダリング)

    それでは早速STEP1から処理の内容から確認していきます。

    ■STEP1(canvasエレメントを取得)
    WebGLを利用した3DレンダリングはHTML5で策定されたcanvasタグ上で動きますので、まずはHTML内のcanvasエレメントを取得します。
    今回の場合、以下部分でエレメントを取得しています。

    var canvas = document.getElementById(“cube”);

    ■STEP2(canvasからWebGLコンテキストを取得)
    次に、描画処理用オブジェクトとして、STEP1で取得したcanvasエレメントからWebGLコンテキストを取得します。
    このコンテキストで各種描画処理や属性の設定をするため、ここでコンテキストが取得できなれけばWebGLを利用できないブラウザという事になります。
    今回の場合、以下部分でコンテキストを取得しています。

    gl = c.getContext(“webgl”) || c.getContext(“experimental-webgl”);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    canvasに”webgl”という名前のコンテキストを要求しますが、仕様が策定中の時は失敗してしまうため”experimental-webgl”という名前のコンテキストも要求します。(失敗時用)
    その後、コンテキスト内部を初期化(clearColor)し、黒色に塗りつぶしています。
    3行目のenable(gl.DEPTH_TEST)はコンテキストに深度(DEPTH)を計算(TEST)しておけよ!という命令です。
    近くにあるものが遠くにあるものを隠すためにも描画の前に深度を把握しておく必要があります。

    サクサク進みますね。
    以外と簡単なのでは?と僕もここまでは今までの知識で問題なく吸収できたのですが、STEP3から急に難しくなり髪の毛が数本抜けたのを覚えています。。。

    ■STEP3(シェーダを生成&コンパイル)
    まず3D関係の知識が無い人はここで壁にぶるつかるでしょう。
    「シェーダ」ってなに?
    ウィキペディア先生には以下のように記載されています。

    主にライティング(光源計算)・シェーディング(陰影処理)とレンダリング(ピクセル化)を実行するためにグラフィックリソースに対して使用するソフトウェア命令の組み合わせである。
    「shade」とは「次第に変化させる」「陰影・グラデーションを付ける」という意味で、「shader」は頂点色やピクセル色などを次々に変化させるもの(より具体的に、狭義の意味で言えば関数)を意味する。
    ※wikipedia内の文章を引用

    難しい………

    超々初心者なりに要約します(間違えていたらすいません)。
    ソース内のコメントにも記載しておりますが、シェーダには大きく以下2種類のシェーダ存在します。

    1. バーテックスシェーダ(頂点シェーダ)
    2. フラグメントシェーダ(ピクセルシェーダ)

    【バーテックスシェーダ(頂点シェーダ)】
     3次元描画はポリゴンで表示する事が多く、ポリゴンは一般的に三角形の面をつなげたものが使われます(四角形を利用する事もあります)。
     三角形が利用されるポイントは、三角形は頂点の位置さえ決めれば面の位置やサイズが確定するという点です。
     四角形、五角形、、、それ以上の多角形も頂点だけでは面の位置やサイズを決める事はできません。
     この頂点の位置を計算するのがバーテックスシェーダ(頂点シェーダ)です。
     ※バーテックスシェーダは頂点の位置情報だけ管理する訳ではなく、頂点に関する”全ての情報(頂点の法線、テクスチャ座標、頂点色など)”を管理します。

    【フラグメントシェーダ(ピクセルシェーダ)】
     バーテックスシェーダ(頂点シェーダ)で頂点の位置や形状を決め、その後ラスタライズ(着色前準備)まで終えると、次にオブジェクトへの「着色」が必要になります。
     この着色内容を計算、決定する役割を担うのがフラグメントシェーダ(ピクセルシェーダー)です。
     色の計算だけであれば簡単に思えますが、これがそうでもないんです。
     3次元を表現するため、陰影、深度、光源、重複(オブジェクトやテクスチャの重なり)、乗算(透過時の後ろのテクスチャ色)などを考慮した着色計算が必要です。
     よって、上記のような色を計算するため、バーテックスシェーダ(頂点シェーダ)の情報が不可欠になります。
     このシェーダ連携についてはSTEP6で書かせていただきます。

    シェーダの生成&コンパイルは以下部分で実施しています。

    var shader;
    var shaderElement = document.getElementById(id);
    if (!shaderElement) {
    return null;
    }

    switch(shaderElement.type){
    case “x-shader/x-vertex”:
    shader = gl.createShader(gl.VERTEX_SHADER);
    break;
    case “x-shader/x-fragment”:
    shader = gl.createShader(gl.FRAGMENT_SHADER);
    break;
    default:
    return null;
    }

    //子ノード取得
    var source = “”;
    var childNodes = shaderElement.firstChild;
    while (childNodes) {
    if (childNodes.nodeType == 3) {
    source += childNodes.textContent;
    }
    childNodes = childNodes.nextSibling;
    }
    gl.shaderSource(shader, source); // ソース割り当て
    gl.compileShader(shader); // コンパイル

    見慣れないエレメントタイプがありますね。

    「x-shader/x-vertex」
    「x-shader/x-fragment」

    JavaScriptファイルではなく、HTMLファイルのheader部分も見てみてください。
    この2つのキーワードが上記で説明したバーテックスシェーダ(頂点シェーダ)とフラグメントシェーダ(ピクセルシェーダ)です。

    <script id=”vertex” type=”x-shader/x-vertex”>
    attribute vec3 vertexPosition;
    attribute vec2 vertexTexture;
    uniform mat4 vertexMatrix1;
    uniform mat4 vertexMatrix2;
    varying vec2 textureCoord;

    void main(void) {
    gl_Position = vertexMatrix1 * vertexMatrix2 * vec4(vertexPosition, 1.0);
    textureCoord = vertexTexture;
    }
    </script>
    <script id=”fragment” type=”x-shader/x-fragment”>
    precision mediump float;
    uniform sampler2D fragmentTexture;
    varying vec2 textureCoord;

    void main(void) {
    gl_FragColor = texture2D(fragmentTexture, vec2(textureCoord));
    }
    </script>

    中途半端にはなりますが…
    ここから説明が一気に難しくなる&細かくなり始めるので今回のブログはここまでにします。

    ではでは。