検索
連載

WebGLの能力を引き出すプログラマブルシェーダーWebグラフィックをハックする(最終回)(1/5 ページ)

プログラマブルシェーダーの基本的な書き方と、Three.jsの各機能に組み込む方法を解説します

Share
Tweet
LINE
Hatena

WebGLの能力を引き出すプログラマブルシェーダー

 Webページ上で利用できるグラフィック技術を紹介する本連載も、ついに最終回となりました。フィナーレを飾る題材は、WebGLの最も強力な機能である「プログラマブルシェーダー」です。前回(多彩な表現力のWebGLを扱いやすくする「Three.js」)と同様にThree.jsの使用を前提として、プログラマブルシェーダーの基本的な書き方と、Three.jsを各機能に組み込む方法を解説します。

 前回はThree.jsの代表的な機能を解説し、いずれもWebGLでなければ実現の難しいものばかりでした。しかし、実はそれでもWebGLの能力のごく一部を使っているにすぎません。独自のプログラマブルシェーダー(カスタムシェーダー)を書くことができれば、描画処理の大部分を柔軟にカスタマイズでき、望み通りの表現を得られます。Three.jsの使い方に慣れたら、ぜひ挑戦してほしいトピックです。

プログラマブルシェーダーとは

 具体的なプログラミングに入る前に、プログラマブルシェーダーの概要について少し触れておきます。一般的にプログラマブルシェーダーとは、C言語ライクなシェーダー言語(WebGLの場合、GLSLと呼ばれます)でGPUが行う処理をプログラミングする機能です。WebGLでは頂点計算を行う「頂点シェーダー」と、ピクセル単位の描画色を計算する「フラグメントシェーダー」が利用できます。

 頂点シェーダーは物体の各頂点の座標をスクリーン上の2次元座標に投影する、いわゆる座標変換と呼ばれる処理を担うものです。前回の記事で地球(球体)を回転させるサンプルがありましたが、そうした回転・移動などを頂点座標に反映する処理も頂点シェーダーで行われています。その他、モーフィングなどによる物体の変形や、キャラクターアニメーションなども実現できます。

 フラグメントシェーダーは個々のピクセルの描画色を計算するものです。その過程でテクスチャ画像へのアクセスも可能で、前回の記事で取り上げたバンプマッピングや環境マッピングといった処理もフラグメントシェーダーで実現されています。

 実はWebGLはこれらのデフォルト処理を持っていないため、たとえ単色の三角形を1つ描くだけでもGLSLによるプログラミングが必要です。一方のThree.jsではマテリアルなどの設定を基にして適切なシェーダーを自動生成することで、利用者がそれを意識せずに多彩な描画機能を活用できるようにしているわけです。しかし、今回はあえてその領域に踏み込み、カスタムシェーダーを使う方法を探っていきます。

Three.jsでカスタムシェーダーを使う

 それでは、実際にThree.js上でカスタムシェーダーを使ってみましょう。前回の記事で説明したパーティクルシステムとカスタムシェーダーを組み合わせると、膨大な数のパーティクルに個別の動きを与えられます。それを利用して、噴水のようにパーティクルが吹き出し、地面を跳ねて流れていくアニメーションを作ってみました。

ALt
サンプルの実行結果

実際のサンプルを見る

 以下がそのソースコードです。前回と同じく、three.min.jsが同じディレクトリにあることを前提にしています。

<!DOCTYPE html>
<html lang="ja">
  <head><meta charset="UTF-8"></head>
  <body>
    <script src="three.min.js"></script>
 
    <!-- (1) 頂点シェーダー -->
    <script type="x-shader/x-vertex" id="vshader">
      uniform float time;
      uniform float size;
 
      attribute float lifetime;
      attribute float shift;
 
      varying float alpha;
 
      void main() {
        float t = fract(time / lifetime + shift);
        float c = pow(t, 1.7) * 10.0;
        float s = ceil(c);
        float y = (1.0 - pow((s-c) * 2.0 - 1.0, 2.0)) / (s * s);
 
        alpha = 1.0 - smoothstep(0.8, 1.0, t);
 
        vec3 p = position * vec3(t, y, t);
        vec4 mvPosition = modelViewMatrix * vec4(p, 1.0);
        gl_PointSize = (size * (256.0 / length( mvPosition.xyz)) *
          (1.0 + smoothstep(0.8, 1.0, t) * 6.0));
        gl_Position = projectionMatrix * mvPosition;
      }
    </script>
 
    <!-- (1) フラグメントシェーダー -->
    <script type="x-shader/x-fragment" id="fshader">
      uniform vec3      color;
      uniform sampler2D texture;
 
      varying float alpha;
 
      void main() {
        gl_FragColor = texture2D(texture, gl_PointCoord) * vec4(color, alpha) * 0.9;
      }
    </script>
 
    <script>
      // レンダラの初期化
      var renderer = new THREE.WebGLRenderer({ antialias:true });
      renderer.setSize(500, 500);
      renderer.setClearColorHex(0x000000, 1);
      document.body.appendChild(renderer.domElement);
 
      var scene = new THREE.Scene();
 
      // カメラの作成
      var camera = new THREE.PerspectiveCamera(70, 500 / 500);
      camera.position = new THREE.Vector3(0, 8, 12);
      camera.lookAt(new THREE.Vector3(-1, 0, 4));
      scene.add(camera);
 
      // (2) マテリアルの作成
      var texture  = THREE.ImageUtils.loadTexture('images/particle.png');
      var material = new THREE.ShaderMaterial({
        vertexShader: document.getElementById('vshader').textContent,
        fragmentShader: document.getElementById('fshader').textContent,
        uniforms: {
          time: { type: 'f', value: 0 },
          size: { type: 'f', value: 0.13 },
          color: { type: 'c', value: new THREE.Color(0xffcc88) },
          texture: { type: 't', value: texture }
        },
        attributes: {
          lifetime: { type:'f',  value: [] },
          shift: { type:'f',  value: [] }
        },
 
        // 通常マテリアルのパラメータ
        blending: THREE.AdditiveBlending, transparent: true, depthTest: false
      });
 
      // (3) 形状データを作成(同時に追加の頂点情報を初期化)
      var geometry   = new THREE.Geometry();
      var attributes = material.attributes;
      var numParticles = 100000;
      for(var i = 0 ; i < numParticles ; i++) {
        var a = Math.PI * 2 * Math.random();
        var d = 8 + Math.random() * 8;
        geometry.vertices.push(new THREE.Vector3(
          Math.sin(a)*d, 3 + Math.random() * 2, Math.cos(a)*d));
 
        // 追加の頂点情報を初期化
        attributes.lifetime.value.push(3 + Math.random());
        attributes.shift.value.push(Math.random());
      }
 
      // 物体を作成
      var mesh = new THREE.ParticleSystem(geometry, material);
      mesh.position = new THREE.Vector3(0, 0, 0);
      mesh.sortParticles = false;
      scene.add(mesh);
 
      // (4) レンダリング
      var baseTime = +new Date;
      function render() {
        requestAnimationFrame(render);
        material.uniforms.time.value = (+new Date - baseTime) / 1000;
        renderer.render(scene, camera);
      };
      render();
    </script>
  </body>
</html>

 前回の記事のパーティクルシステムのサンプルをベースに、プログラマブルシェーダーを追加した形になっています。大きく変更した箇所のコメントに数字を振っていますので、その部分ごとにポイントを解説します。

(1) 頂点シェーダー・フラグメントシェーダ

 これらの2つのscriptタグの内容が、GLSLで書かれたカスタムシェーダーのソースコードです。scriptタグで記述してはいるものの、Webブラウザが直接解釈するわけではありません(type属性が "text/javascript" ではないことに注意してください)。これらのソースコードは後に文字列として取り出し、Three.jsのマテリアルに設定します。JavaScriptの文字列リテラルなどで記述することもできますが、特殊記号をエスケープする必要のないscriptタグの方がよく使用されます。

 GLSLについては次ページ以降で解説しますので、処理内容の説明は省略します。

(2) マテリアルの作成

 Three.jsでは、ShaderMaterialという特殊なマテリアルクラスを使用してカスタムシェーダーを管理します。ShaderMaterialのコンストラクタ引数に指定するオブジェクトには、通常のものに加えて以下のフィールドを指定できます。

フィールド名 説明
vertexShader 頂点シェーダーのソースコード
fragmentShader フラグメントシェーダーのソースコード
uniforms 全頂点・フラグメントの共通パラメータの定義
attributes 追加の頂点情報の定義

 vertexShaderとfragmentShaderのフィールドには、scriptタグに記述したそれぞれのソースコードを文字列で格納します。

 uniformsとattributesは、いずれもカスタムシェーダーに引き渡すパラメータの定義です。値はシェーダー上での変数名をキーにしたオブジェクトであり、その各アイテムの値もtypeフィールドとvalueフィールドを持つオブジェクトになっています。typeフィールドはパラメータのデータ型の指定で、下表に示す文字列が指定できます(ただし、attributesにはi、f、 c、 v2、 v3、 v4のみ指定可能)。

type値 値のクラス
i 数値(整数に丸められる)
f 数値
c THREE.Color
v2 THREE.Vector2
v3 THREE.Vector3
v4 THREE.Vector4
m4 THREE.Matrix4
t THREE.Texture
iv1 整数の配列
iv 整数の配列(長さは3の倍数)
fv1 浮動小数点値の配列
fv 浮動小数点値の配列(長さは3の倍数)
v2v THREE.Vector2の配列
v3v THREE.Vector3の配列
v4v THREE.Vector4の配列
m4v THREE.Matrix4の配列
tv THREE.Textureの配列

 uniformsに指定した値は、頂点シェーダーとフラグメントシェーダーのいずれからもアクセスできる変数(uniform変数)となります。valueフィールドにはtypeフィールドに指定したものと合致する型の値を指定してください。

 attributesは物体の各頂点への付加情報(カスタムアトリビュート)で、頂点シェーダーからのみアクセスできる変数(attribute変数)となります。valueフィールドの値はtypeフィールドに指定した型の値を保持する配列で、物体の頂点数と同じ長さでなければなりません。サンプルでは最初に空の配列を指定し、頂点データを追加するのと同時にattributesの各フィールドの配列にも要素を追加しています。

 uniform変数とattribute変数の詳細は、次ページ以降で解説します。

(3) 形状データを作成(同時に追加の頂点情報を初期化)

 for文でループを回して頂点データを生成し、同時にマテリアルのattributesにも値を追加しています。サンプルではlifetimeとshiftという2つのattributesフィールドを定義しおり、いずれも頂点ごとに異なるランダムな値を設定しています。

(4) レンダリング

 基本的にシーンを繰り返しレンダリングしているだけですが、フレームごとにuniform変数の「time」に、現在時刻(ページがロードされてからの秒数)を代入しています。頂点シェーダーでは、この値を基にパーティクルの位置などを算出しています。

 このように、ShaderMaterialに適切なフィールドを指定することで、カスタムシェーダーを物体の描画に適用できます。サンプルではパーティクルシステムを使用しましたが、他の種類の表示オブジェクトにも同様の手順で適用できます。

 次ページからは、WebGLのシェーダー言語であるGLSLの記述方法を解説していきます。

Copyright © ITmedia, Inc. All Rights Reserved.

       | 次のページへ
ページトップに戻る