カスタムシェーダーの面白い活用方法の1つとして、ポストプロセス・エフェクトがあります。Three.jsに付属するポストプロセスの使い方は前回解説しましたが、カスタムシェーダーを書くことで、独自の新しいエフェクトを追加できます。以下は、この方法で画面が波立っているような効果を実現した例です。
以下がそのソースです。前回のポストプロセスのサンプルと同じく、Three.jsのリポジトリからsrc/examples/jsフォルダの内容をコピーしておいてください。
<!DOCTYPE html> <html lang="ja"> <head><meta charset="UTF-8"></head> <body> <script src="three.min.js"></script> <script src="js/shaders/CopyShader.js"></script> <script src="js/postprocessing/EffectComposer.js"></script> <script src="js/postprocessing/MaskPass.js"></script> <script src="js/postprocessing/RenderPass.js"></script> <script src="js/postprocessing/ShaderPass.js"></script> <script type="x-shader/x-vertex" id="vshader"> varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } </script> <script type="x-shader/x-fragment" id="fshader"> uniform sampler2D tDiffuse; varying vec2 vUv; void main() { vec2 uv = vUv - vec2(0.5, 0.5); float l = length(uv) * 2.0; uv *= 1.0 + sin(l*l * 40.0) * 0.2 * (1.0 - l); vec4 texel = texture2D( tDiffuse, uv + vec2(0.5, 0.5)); gl_FragColor = texel; } </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(15, 500 / 500); camera.position = new THREE.Vector3(0, 0, 8); camera.lookAt(new THREE.Vector3(0, 0, 0)); scene.add(camera); var light = new THREE.DirectionalLight(0xcccccc); light.position = new THREE.Vector3(0.577, 0.577, 0.577); var ambient = new THREE.AmbientLight(0x333333); scene.add(light); scene.add(ambient); var geometry = new THREE.SphereGeometry(1, 32, 16); var material = new THREE.MeshPhongMaterial({ color: 0xffffff, specular: 0xcccccc, shininess:50, ambient: 0xffffff, map: THREE.ImageUtils.loadTexture('images/earth.jpg') }); var mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // ポストプロセスの設定 var composer = new THREE.EffectComposer(renderer); composer.addPass(new THREE.RenderPass(scene, camera)); // オリジナルのポストプロセスを追加 composer.addPass(new THREE.ShaderPass({ vertexShader: document.getElementById('vshader').textContent, fragmentShader: document.getElementById('fshader').textContent, uniforms: { tDiffuse: { type:"t", value:null } }, })); var toScreen = new THREE.ShaderPass(THREE.CopyShader); toScreen.renderToScreen = true; composer.addPass(toScreen); // レンダリング var baseTime = +new Date; function render() { requestAnimationFrame(render); mesh.rotation.y = 0.3 * (+new Date - baseTime) / 1000; composer.render(); }; render(); </script> </body> </html>
レンダラやシーンの初期化、および基本的なポストプロセスの使い方については前回の記事を参照してください。今回のポイントは、「オリジナルのポストプロセスを追加」というコメントが付いている箇所です。
自分で書いたシェーダーをポストプロセスとして追加するには、ShaderPassを使用します。コンストラクタの引数には、シェーダーマテリアルと同様にvertexShader、fragmentShader、uniformsフィールドを持つオブジェクトを指定します(attributesフィールドや他のマテリアルパラメータは指定できません)。uniformsにはtDiffuseという名前のテクスチャ型の変数を必ず定義してください。このShaderPassをaddPass()で登録すれば、指定したシェーダーがポストプロセスとして自動的に実行されます。
次はシェーダーのコードに注目しましょう。まずは頂点シェーダーですが、ポストプロセスは一般的にフラグメントシェーダーで処理されるため、最低限の内容のみ(テクスチャ座標をvarying変数にコピーし、頂点座標に標準的な座標変換を施す)になっています。特殊な場合を除き、頂点シェーダーは常にこの内容で問題ありません。
フラグメントシェーダーでは、頂点シェーダーから渡されたテクスチャ座標(vUv)に基づいて元画像(tDiffuse)から色をフェッチし、必要な処理を施してgl_FragColorに設定します。サンプルではvUvの値によって色を取得する位置をずらすことにより、画像を波紋のように変形させています。
もちろん、ポストプロセスで実現できるのは画像の変形だけではありません。簡単なエフェクトを作成しつつ、よく使う機能の実装方法を見ていきましょう。
まずベースとして、何の加工も行わずに元画像をそのまま出力するフラグメントシェーダーを作ります。ここにコードを追加していくことで、さまざまな効果が作り出せます。
uniform sampler2D tDiffuse; varying vec2 vUv; void main() { vec4 texel = texture2D(tDiffuse, vUv); gl_FragColor = texel; }
色を変換するだけのエフェクトは比較的簡単に実現できます。以下は画面全体をセピア調に変換するフラグメントシェーダーの例です。先頭で定義している行列を変更することで、セピア調以外にもさまざまな効果を実現できます。
uniform sampler2D tDiffuse; varying vec2 vUv; void main() { mat3 m = mat3( 0.393, 0.769, 0.189, 0.349, 0.686, 0.168, 0.272, 0.534, 0.131); vec4 texel = texture2D(tDiffuse, vUv); gl_FragColor = vec4(texel.xyz * m, texel.w); }
元画像から複数箇所の色をフェッチして処理を行うことも可能です。以下は5点サンプリングによる単純なラプラシアンフィルタで輪郭抽出を行う例です(画面解像度は512x512を前提にしています)。
uniform sampler2D tDiffuse; varying vec2 vUv; void main() { vec3 color = vec3(0.5, 0.5, 0.5); color += texture2D(tDiffuse, vUv).xyz * -4.0; color += texture2D(tDiffuse, vUv + vec2(-1.0/512.0, 0.0)).xyz; color += texture2D(tDiffuse, vUv + vec2( 1.0/512.0, 0.0)).xyz; color += texture2D(tDiffuse, vUv + vec2(0.0, -1.0/512.0)).xyz; color += texture2D(tDiffuse, vUv + vec2(0.0, 1.0/512.0)).xyz; color = step(0.55, color); gl_FragColor = vec4(color, 1.0); }
シェーダー内で別のテクスチャにアクセスし、エフェクトに組み込むこともできます。以下のサンプルではノイズ画像を使って元画像のフェッチ座標を揺らし、曇りガラスのような効果を実現しています。
ポストプロセスのシェーダーにテクスチャ画像を追加するには、まずShaderPassのuniformsパラメータにテクスチャ型のパラメータ(以下のコード例ではtNoise)を追加します。valueへの画像の設定はShaderPassの初期化後に行わないと正しく読み込めないので注意してください。
// オリジナルのポストプロセスを追加 var myPass = new THREE.ShaderPass({ vertexShader: document.getElementById('vshader').textContent, fragmentShader: document.getElementById('fshader').textContent, uniforms: { tDiffuse: { type:"t", value:null }, tNoise: { type:"t", value:null } } }); myPass.uniforms.tNoise.value = THREE.ImageUtils.loadTexture('images/noise.png'); composer.addPass(myPass);
フラグメントシェーダーでも同じ名前のuniform変数を追加します。後は、元画像と同様にtexture2D()で任意の座標の色を取得できます。
uniform sampler2D tDiffuse; uniform sampler2D tNoise; varying vec2 vUv; void main() { vec2 noise = texture2D(tNoise, vUv).xy + vec2(-0.5, -0.5); gl_FragColor = texture2D(tDiffuse, vUv + noise * 0.03); }
以上はとても簡単な例ですが、フラグメントシェーダー次第でほとんどの効果が実現できます。ポストプロセスはカスタムシェーダーの活用手段としては最も手が出しやすく、見た目にも面白いものです。GLSLを覚えたら、ぜひ挑戦してみてください。
Copyright © ITmedia, Inc. All Rights Reserved.