WebGL
GLSL
three.js
WebGLDay 17

GLSLでマウスによるインタラクションを付ける

はじめに

この記事は WebGL Advent Calendar 2017 の17日目の記事です。

本記事の対象者

・とりあえずGLSLを使えばどんなこと出来るか知りたい人
・GLSLを使った表現をWebサイト上で取り入れてみたい人
・Webサイト上でのユーザーアクションとGLSL表現を連動させてみたい人 〜etc

やってみたこと

2017年はGLSLを使用した表現を駆使したWebサイトをたくさん見かけた気がします。(筆者がGLSLに着目していたのでそう感じただけかもしれません)
Webサイト上でのユーザーアクションというと大まかにクリック、マウスオーバー、スクロールなどがありますが、今回は比較的簡単なマウスオーバーに連動させてみようと思います。
具体的には画像をマウスオーバーした際にブラーをかけてみます。ブラーも全体にかけるのではなく、画像の中心に向かってぼやけていくようなズームブラーをかけてみます。

全体の流れ

全体の流れとしてはシンプルに以下になります。
①THREE.TextureLoaderでテクスチャを読み込む
②読み込んだテクスチャに対してズームブラーのシェーダーをアタッチ
③マウスオーバー、マウスアウトでブラーの強度を変化させる
※three.jsの基本的な部分については理解している前提で進めます※
※①〜③に記載のソースコードはそのまま抜粋したものになりますのでコンテキスト・スコープ等の関連性については後述の全体ソースのリンクからご確認ください※

①THREE.TextureLoaderでテクスチャを読み込む

THREE.TextureLoaderを使用して画像を読み込む関数を用意します。

_loadTexture(image, callback) {
    let that = this;
    const loader = new THREE.TextureLoader();
    loader.load(image, (texture) => {
        texture.magFilter = THREE.NearestFilter;
        texture.minFilter = THREE.NearestFilter;
        that.textureUnit = texture;
        this.mesh = this.createMesh();
        callback();
    });
}

②読み込んだテクスチャに対してズームブラーのシェーダーをアタッチ

three.jsでのシェーダーの使用についてはこちらを参照ください。

_createMesh() {
    // シェーダーに送り込むuniforms変数を定義
    this.uniforms = {
        u_time: { type: "f", value: this.u_time },
        strength: { type: "1f", value: 0 },
        u_resolution: { type: "v2", value: new THREE.Vector2(512, 512) },
        textureUnit: { type: 't', value: this.textureUnit }
    };
    // メッシュを作成
    return new THREE.Mesh(
        new THREE.PlaneBufferGeometry(512, 512),
        new THREE.RawShaderMaterial({
            uniforms: this.uniforms,
            vertexShader: require('../../../../glsl/zoomblur.vert'),
            fragmentShader: require('../../../../glsl/zoomblur.frag'),
        })
    );
}

③マウスオーバー、マウスアウトでブラーの強度を変化させる

TweenMaxを使用してマウスオーバーした時にブラーの強度を最大に、マウスアウトした時に最小にします。

_updateStrength(){
    let that = this;
    this.canvasEl
        // マウスオーバー
        .mouseover(function() {
            TweenMax.to(that.mesh.material.uniforms.strength, 0.8, {
                value: 15,
                ease: Linear.easeNone,
                overwrite: true,
                onUpdate: () => {
                       // 再描画
                    return that.draw();
                }
            });
        })
        // マウスアウト
        .mouseout(function() {
            TweenMax.to(that.mesh.material.uniforms.strength, 0.8, {
                value: 0,
                ease: Linear.easeNone,
                overwrite: true,
                onUpdate: () => {
                       // 再描画
                    return that.draw();
                }
            });
        });
}

// 再描画関数
_draw(){
    this.renderer.render(this.scene, this.camera);
}

ズームブラーのシェーダー

ズームブラーのシェーダーの記述については一部今回の実装用にカスタマイズしていますが、wgld.orgを流用させて頂いているので本記事での詳しい解説は省略させて頂きます。
今回のズームブラーシェーダー以外にも言えることですが、ネット上にあるようなサンプルを自分の環境用に移植する際にうまくいかない場合は正規化やUV座標、uniforms変数の定義が正しく設定されていなかったりする事があるのでその部分を疑ってみるのもいいかもしれません。
一応シェーダーのソースコードを記載します。

zoomblur.frag
precision mediump float;

uniform sampler2D textureUnit;
uniform float strength;
uniform vec2 u_resolution;
uniform float animationParam;
varying vec2 vUv;

const float tFrag = 1.0 / 512.0;
const float nFrag = 1.0 / 30.0;
const vec2  centerOffset = vec2(256.0, 256.0);

float rnd(vec3 scale, float seed){
    return fract(sin(dot(gl_FragCoord.stp + seed, scale)) * 43758.5453 + seed);
}

float getAnimationParam(float animationParam) {
  float pST = animationParam;
  return pST;
}

void main(){

    vec3  destColor = vec3(0.0);
    float random = rnd(vec3(12.9898, 78.233, 151.7182), 0.0);
    vec2  fc = vec2(gl_FragCoord.s, gl_FragCoord.t);
    vec2  fcc = fc - centerOffset;
    float totalWeight = 0.0;

    for(float i = 0.0; i <= 30.0; i++){
        float ppp = getAnimationParam(strength);

        float percent = (i + random) * nFrag;
        float weight = percent - percent * percent;
        vec2  t = fc - fcc * percent * ppp * nFrag;
        destColor += texture2D(textureUnit, t * tFrag).rgb * weight;
        totalWeight += weight;
    }
    gl_FragColor = vec4(destColor / totalWeight, 1.0);
}
zoomblur.vert
precision mediump float;

attribute vec3  position;
attribute vec2 uv;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec2 vUv;

void main(){

  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

}

完成形

マウスオーバー前
sample01.png

マウスオーバー後
sample02.png

DEMO
※動作確認はPCのみです※
ソースコード

まとめ

フラグメントシェーダーだけですと、40行程で某RPGゲームでモンスターに遭遇した時に見るようなズームブラーが実装できました。今回は解像度が512×512と固定値でしたのでハードコーディングで設定していたパラメータ値がありますが、画面全体に対してズームブラーをかける場合などの解像度が可変する場合はズームの中心座標が変化すると思うのでリサイズイベント処理を組み込んであげる必要があります。
GLSLを使用するとズームブラー以外にも紙が溶けていくような効果など、CSSではできない一歩踏み込んだ効果が実現できるので、オリジナルのシェーダーを作ってWebサイトに取り入れてみてはいかがしょうか。

参考

wgld.org
The Book of Shaders