前ページまで、パーティクルやポストプロセスに対してカスタムシェーダーを適用しましたが、通常の物体はあえて避けてきました。ライティング計算を伴う物体を描画するには、比較的複雑な処理が発生するためです。
もちろん、Three.jsが標準で提供している描画機能もGLSLで実装されているので、それをベースにして独自の処理を追加できれば理想でしょう。しかし、残念ながら現在のThree.jsでは標準シェーダーを拡張するための簡単な手段が提供されておらず、その実現にはマテリアルの内部処理に分け入ることが必要です。
Three.jsが標準の描画処理で使用するプログラマブルシェーダーは、リポジトリ内のsrc/renderers/WebGLShaders.jsにまとめられています。中をのぞくとコード量の多さに圧倒されますが、よく見ると以下の4つのオブジェクトを構築しているだけなのが分かります。
変数名 | 説明 |
---|---|
THREE.ShaderChunk | 共通で使われるGLSLのコード片 |
THREE.UniformsUtils | uniformsの定義を操作するユーティリティ関数群 |
THREE.UniformsLib | 共通で使われるuniforms定義 |
THREE.ShaderLib | 各マテリアルで使用するシェーダー定義 |
これらのうち、描画処理で直接使われるのがShaderLibです。basic、phong、particle_basicなどマテリアルの種類に対応するフィールドがあり、ShaderMaterialのパラメータと同じ形式でuniforms、vertexShader、fragmentShaderが定義されています(標準シェーダーはカスタムアトリビュートを使わないので、attributesはありません)。
例として、MeshPhongMaterialに対応するShaderLibの 'phong' フィールドを見てみましょう。内容を以下に抜粋しました。
'phong': { uniforms: THREE.UniformsUtils.merge( [ THREE.UniformsLib[ "common" ], THREE.UniformsLib[ "bump" ], // ...中略... { "ambient" : { type: "c", value: new THREE.Color( 0xffffff ) }, "emissive" : { type: "c", value: new THREE.Color( 0x000000 ) }, // ...中略... } ] ), vertexShader: [ "#define PHONG", "varying vec3 vViewPosition;", "varying vec3 vNormal;", THREE.ShaderChunk[ "map_pars_vertex" ], THREE.ShaderChunk[ "lightmap_pars_vertex" ], // ...中略... "void main() {", THREE.ShaderChunk[ "map_vertex" ], THREE.ShaderChunk[ "lightmap_vertex" ], // ...中略... "}" ].join("\n"), fragmentShader: [ "uniform vec3 diffuse;", "uniform float opacity;", // ...中略... "void main() {", "gl_FragColor = vec4( vec3 ( 1.0 ), opacity );", THREE.ShaderChunk[ "map_fragment" ], THREE.ShaderChunk[ "alphatest_fragment" ], // ...中略... "}" ].join("\n") },
UniformsLibやShaderChunkから必要な内容を取り込みつつ、uniforms、vertexShader、fragmentShaderフィールドが定義されているのが分かります。残念ながらこれらのフィールドをShaderMaterialに設定するだけでは正しく機能しないのですが、標準シェーダーと同じ機能を再現したい場合の参考にはなるでしょう。
ShaderLibの定義をShaderMaterialに設定しても機能しないのは、uniform変数の値が正しく設定されないためです。uniform変数にはWebGLRendererがマテリアルのクラスごとに適切な値を設定するのですが、ShaderMaterialでは必要な多くのパラメータ(ライトの状態など)がスキップされてしまうのです。
前述の通り、Three.jsは標準シェーダーの拡張手段を提供していないのですが、筆者が独自に調査した結果、該当するマテリアルの派生クラスを作成し、そのシェーダーを差し替えれば実現できることが分かりました。ここでは、MeshPhongMaterialに若干の処理を追加する例を使って、その方法をチュートリアル形式で解説します。
まず、MeshPhongMaterialを使ってモデルを表示するHTMLファイルを用意してください。前回の記事(多彩な表現力のWebGLを扱いやすくする「Three.js」)の最初のサンプル(単に地球を表示し、回転アニメーションさせるだけのもの)などがいいでしょう。
そして、MeshPhongMaterialを生成する前のどこかに以下のコードを挿入し、派生クラスを定義します。
var MyMeshPhongMaterial = function(parameters) { this.fragmentShader = "void main() {}"; this.vertexShader = "void main() {}"; this.uniforms = {}; this.defines = {}; this.attributes = null; THREE.MeshPhongMaterial.call(this, parameters); }; MyMeshPhongMaterial.prototype = Object.create( THREE.MeshPhongMaterial.prototype);
MeshPhongMaterialを生成する箇所をMyMeshPhongMaterialに差し替え、パラメータに独自シェーダーの定義(uniforms、vertexShader、fragmentShaderフィールド)を追加します。ここではShaderLib['phong']の内容をベースにして、uniformsフィールドにtNoise変数の定義を、フラグメントシェーダーにtNoise変数の定義とそれを利用した処理(曇りガラスのポストプロセスとほぼ同じもの)を加えています。
var material = new MyMeshPhongMaterial({ // MeshPhongMaterialのパラメータ color: 0xffffff, specular: 0xcccccc, shininess:50, ambient: 0xffffff, map: THREE.ImageUtils.loadTexture('images/earth.jpg'), // 独自シェーダー uniforms: THREE.UniformsUtils.merge([ THREE.ShaderLib['phong'].uniforms, { tNoise: { type:'t', value:null } } ]), vertexShader:THREE.ShaderLib['phong'].vertexShader, fragmentShader:[ // ...中略... // 独自のuniform変数 "uniform sampler2D tNoise;", "void main() {", "gl_FragColor = vec4( vec3 ( 1.0 ), opacity );", // カラーテクスチャのフェッチを独自のコードに差し替える // THREE.ShaderChunk[ "map_fragment" ], "vec2 noise = texture2D(tNoise, vUv).xy + vec2(-0.5, -0.5);", "gl_FragColor = texture2D(map, vUv + noise * 0.01);", // ...中略... "}" ].join('\n'), }); material.uniforms.tNoise.value = THREE.ImageUtils.loadTexture('images/noise.png');
これだけで動いてくれるのが理想ですが、残念ながらそううまくはいきません。Three.jsはShaderMaterial以外の標準のマテリアルクラス(およびその派生クラス)に対して強制的に標準のシェーダーを適用してしまうため、そこをごまかす必要があります。そのためのモンキーパッチが以下になります。レンダラのrender()メソッドを呼ぶ前に実行してください。
var oldInitMaterial = renderer.initMaterial; renderer.initMaterial = function(material, lights, fog, object) { if(material instanceof MyMeshPhongMaterial) { var dummy = {}; for(var i in material) dummy[i] = material[i]; oldInitMaterial.call(this, dummy, lights, fog, object); for(var i in dummy) material[i] = dummy[i]; } else { oldInitMaterial.call(this, material, lights, fog, object); } };
マテリアルの初期化処理が呼ばれる前に、MyMeshPhongMaterialの内容を全コピーした単なるオブジェクトに差し替え、初期化終了後に値を書き戻しています。その場しのぎ感が強くて残念なコードですが、現状でThree.js本体を書き換えずに実現する方法としては最善ではないかと考えています。
多少イレギュラーな方法ではありますが、以上の変更でカラーテクスチャのフェッチ位置をノイズテクスチャで変動させるMeshPhongMaterialが実現できます。標準シェーダーを少しだけ拡張したいという場面は頻繁にあるので、覚えておくと便利でしょう。
参考までに、上記の変更を前回の最初のサンプルに適用したものを掲載しておきます。
今回はThree.jsでプログラマブルシェーダーを扱う方法を取り上げました。プログラマブルシェーダーを利用することにより、Canvas APIなどでは難しかった非常に凝った画像処理をリアルタイムに実行できます。ゲームなどの3Dアプリケーションはもちろん、ペイントツールや動画編集・加工などに幅広く応用できるでしょう。
WebGLとは独立した規格になりますが、プログラマブルシェーダーでWebページの描画結果を加工する「CSSシェーダー」も策定が進行しています。今回解説した頂点シェーダーやフラグメントシェーダーの機能を、任意の要素に対して適用できます。Webの表現力を大幅に向上する、画期的な技術です。筆者のブログで概要を解説しているので、興味のある方はご参照ください。
Webにおけるグラフィックス表現は急速に進化しています。そうした変化をキャッチアップするために、この連載が少しでも皆さんの役に立てばうれしく思います。短い期間でしたが、お付き合いありがとうございました。
伊藤 千光
白髪になってもプログラムを組んでいたい、ゲームプログラマあがりのWebエンジニア。日本ファルコム、Microsoft GameStudios JapanにてPCやXbox、Xbox 360のゲームの開発に従事し、グラフィックエンジンやコリジョン、スクリプトなどを幅広く担当。その後、全世界のコンピュータを1つにつなげるWeb技術に魅力を感じ、Webエンジニアに転身。Irvine SystemsにてRuby on Railsによるシステム開発に携わった後、フリーランスに。現在はGoogle AppsやGoogle App Engineなどを利用した案件をこなしつつ、ブログ「WebOS Goodies」にて情報発信を行っている。Google API Expert(ソーシャル担当)。WebOS Goodies : http://webos-goodies.jp/
Copyright © ITmedia, Inc. All Rights Reserved.