three.js超入門 第8回 シェーダーをインタラクティブに動かす

# 概要
この記事では「three.js超入門」と題して、three.jsの基礎からシェーダーの利用までをやっていきます。
ターゲットは主に「canvas表現を触ったことがないフロントエンドエンジニア」を想定しているので、jsの構文などの説明は省略しています。
three.jsのバージョンは執筆時点で最新の[r98](https://github.com/mrdoob/three.js/releases/tag/r98)を使用します。

[three.js超入門 第0回 3Dコンピュータグラフィックスの基礎](https://qiita.com/watabo_shi/items/f7c559c3cdbcdd0f2629)
[three.js超入門 第1回 レンダリングまでの流れ](https://qiita.com/watabo_shi/items/bf9bcd4569b6d480c608)
[three.js超入門 第2回 アニメーションと時間ベースでの制御](https://qiita.com/watabo_shi/items/1c3c50429279eedec04e)
[three.js超入門 第3回 マウスやスクロールでのインタラクション](https://qiita.com/watabo_shi/items/0811d03390c18e46be86)
[three.js超入門 第4回 DOM要素との連携](https://qiita.com/watabo_shi/items/9ab12f62434a1e858226)
[three.js超入門 第5回 シェーダー(GLSL)の基礎](https://qiita.com/watabo_shi/items/7325948db5c07f6fe9c7)
[three.js超入門 第6回 ShaderMaterialでメッシュを変形、着色する](https://qiita.com/watabo_shi/items/c0d4ef11cba6f21189ed)
[three.js超入門 第7回 シェーダーに変数を渡す](https://qiita.com/watabo_shi/items/1b48ca54a9034f3614d8)
three.js超入門 第8回 シェーダーをインタラクティブに動かす
[three.js超入門 第9回 シェーダーでテクスチャにエフェクトをかける](https://qiita.com/watabo_shi/items/2fc671f2147e799787f9)

[リポジトリ](https://github.com/watab0shi/threejs-workshop)

今回は、前回書いた発光する円をマウス座標やクリックイベントでインタラクティブにうごかしてみます。

# `uniform`変数でマウス座標を渡す
[前回](https://qiita.com/watabo_shi/items/c0d4ef11cba6f21189ed)と[第3回](https://qiita.com/watabo_shi/items/0811d03390c18e46be86)でやった内容のおさらいです。
今回はマウス座標をシェーダーに渡すため、ウィンドウ座標系（左上原点）からテクスチャ座標系（左下原点）に変換します。

```javascript:Canvas/index.js

// 省略
export default class Canvas {
  constructor() {
    // 省略

    // マウス座標
    this.mouse = new Vector2(0.5, 0.5);

    // uniform変数を定義
    this.uniforms = {
      uAspect: {
        value: this.w / this.h
      },
      uTime: {
        value: 0.0
      },
      uMouse: {
        value: this.mouse
      }
    };

    // 省略
  }

  // 省略

  mouseMoved(x, y) {
    // 左上原点から左下原点に変換
    this.mouse.x = x / this.w;
    this.mouse.y = 1.0 - (y / this.h);
  }
};
```

```glsl:shader.frag
varying vec2 vUv;
   
uniform float uAspect;
uniform float uTime;
uniform vec2  uMouse;// マウス座標

void main() {
  vec2 uv = vec2( vUv.x * uAspect, vUv.y );
  vec2 center = vec2( uMouse.x * uAspect, uMouse.y );// アスペクト補正したマウス座標
  float radius = 0.05 + sin( uTime ) * 0.025;
  float lightness = radius / length( uv - center );
  // lightness = clamp( lightness, 0.0, 1.0 );
  vec4 color = vec4( vec3( lightness ), 1.0 );
  color *= vec4( 0.2, 1.0, 0.5, 1.0 );
  
  gl_FragColor = color;
}
```

`Page`クラスで`window`の`mousemove`イベントが発生したときに`canvas`の`mouseMoved`関数を呼びます。

```javascript:00_shader_empty/index.js
import Canvas from './Canvas';

export default class Page00 {
  constructor() {
    const canvas = new Canvas();

    window.addEventListener('mousemove', e => {
      canvas.mouseMoved(e.clientX, e.clientY);
    });
  }
};
```
![Kapture 2019-03-21 at 16.00.33.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/651a9ec2-b0ac-99ce-f6a0-e8064dbe9816.gif)
円がマウスについてくるようになりました。


# 簡易イージングでなめらかに追いかける
普通にマウス座標を割り当てるだけだとちょっと味気ないので、簡易的なイージングを挟んでマウス座標をゆっくり追いかけるようにしましょう。

```javascript:Canvas/index.js
// 省略
export default class Canvas {
  constructor() {
    // 省略
    
    // uniform変数を定義
    this.uniforms = {
      uAspect: {
        value: this.w / this.h
      },
      uTime: {
        value: 0.0
      },
      uMouse: {
        value: new Vector2(0.5, 0.5)// this.mouse とは別のベクトル
      }
    };
    
    // 省略
  }
  
  render() {
    requestAnimationFrame(() => { this.render(); });

    const sec = performance.now() / 1000;

    this.uniforms.uTime.value = sec;

    // シェーダーに渡すマウスを更新
    this.uniforms.uMouse.value.lerp(this.mouse, 0.2);
    
    this.renderer.render(this.scene, this.camera);
  }
  
  // 省略
};
```
![Kapture 2019-03-21 at 15.40.19.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/e2a5a86d-9a67-71d4-8cef-38b34c41118a.gif)
GIFだとちょっとわかりづらいですが、マウス座標をぬるっと追いかけるようになりました。
`vecA.lerp(vecB, 0.2);`と書くと、`vecA`を`vecB`の位置に`20%`近づけるという意味になります。
この処理が毎フレーム呼ばれているので、現在位置から目標位置までの差分の`20%`ずつ近づいていきます。
目標位置までの距離が近くなるほど、当然差分の`20%`の量も小さくなるので、だんだんゆっくりになります。
上記は`Vector`クラスでの書き方ですが、`float`値で同じ処理を書く場合は`val += (targetVal - val) * 0.2;`になります。

# クリック中に半径を大きくする
`targetRadius`という変数を作り、`mousePressed`で最大値に、`mouseReleased`でデフォルトに設定します。
`uRadius`という`uniform`変数を作って`targetRadius`の値を追いかけさせます。

```javascript:Canvas/index.js
// 省略
export default class Canvas {
  constructor() {
    // 省略
    
    this.mouse = new Vector2(0.5, 0.5);
    this.targetRadius = 0.005;// 半径の目標値
    
    // uniform変数を定義
    this.uniforms = {
      uAspect: {
        value: this.w / this.h
      },
      uTime: {
        value: 0.0
      },
      uMouse: {
        value: new Vector2(0.5, 0.5)
      },
      uRadius: {
        value: this.targetRadius
      }
    };
    
    // 省略
  }
  
  render() {
    requestAnimationFrame(() => { this.render(); });

    const sec = performance.now() / 1000;

    this.uniforms.uTime.value = sec;

    this.uniforms.uMouse.value.lerp(this.mouse, 0.2);

    // シェーダーに渡す半径を更新
    this.uniforms.uRadius.value += (this.targetRadius - this.uniforms.uRadius.value) * 0.2;

    this.renderer.render(this.scene, this.camera);
  }

  // 省略
  mousePressed(x, y) {
    this.mouseMoved(x, y);
    this.targetRadius = 0.25;// マウスを押したら半径の目標値を大きく
  }
  mouseReleased(x, y) {
    this.mouseMoved(x, y);
    this.targetRadius = 0.005;// マウスを押したら半径の目標値をデフォルト値に
  }
};
```

シェーダー側では、`radius`だったところを`uRadius`に置き換えます。

```glslshader.frag
varying vec2 vUv;
   
uniform float uAspect;
uniform float uTime;
uniform vec2  uMouse;
uniform float uRadius;// 半径
   
void main() {
  vec2 uv = vec2( vUv.x * uAspect, vUv.y );
  vec2 center = vec2( uMouse.x * uAspect, uMouse.y );
  float lightness = uRadius / length( uv - center );// 半径を uRadius に変更
  // lightness = clamp( lightness, 0.0, 1.0 );
  vec4 color = vec4( vec3( lightness ), 1.0 );
  color *= vec4( 0.2, 1.0, 0.5, 1.0 );

  gl_FragColor = color;
}
```

`Page`クラスにイベントリスナを追加して、`mousedown`で`canvas.mousePressed`、`mouseup`で`canvas.mouseReleased`を呼びます。

```javascript:00_shader_empty/index.js
import Canvas from './Canvas';

export default class Page00 {
  constructor() {
    const canvas = new Canvas();

    window.addEventListener('mousemove', e => {
      canvas.mouseMoved(e.clientX, e.clientY);
    });
    window.addEventListener('mousedown', e => {
      canvas.mousePressed(e.clientX, e.clientY);
    });
    window.addEventListener('mouseup', e => {
      canvas.mouseReleased(e.clientX, e.clientY);
    });
  }
};
```
![Kapture 2019-03-21 at 17.01.10.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/649f0311-491b-8acf-1b65-ae9c048958f7.gif)

簡易イージングを使うと段階的な目標値の変化に対して値がスムーズに移動してくれるので便利ですね。
次回はシェーダーでテクスチャにエフェクトをかける方法を紹介します。