three.js超入門 第9回 シェーダーでテクスチャにエフェクトをかける

# 概要
この記事では「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回 シェーダーをインタラクティブに動かす](https://qiita.com/watabo_shi/items/39d21d53ca16541fd498)
three.js超入門 第9回 シェーダーでテクスチャにエフェクトをかける

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

前回はピクセルシェーダーのみで描いた発光する円をマウスでインタラクティブに動かしました。
今回は`uniform`変数でシェーダーにテクスチャ（画像）を渡して、ピクセルシェーダーでテクスチャにエフェクトをかける方法を紹介します。

# 下準備
## テクスチャを読み込む
※ 以下はテクスチャの読み込み方の例なので、実際のコードに書く必要はありません。
`TextureLoader`クラスの`load()`関数に画像のURLを渡して読み込みます。

```javascript:loadTextureSample.js
import { TextureLoader } from 'three/src/loaders/TextureLoader';
import { ShaderMaterial } from 'three/src/materials/ShaderMaterial';

import vertexSource from './shaders/shader.vert';
import fragmentSource from './shaders/shader.frag';

const loader = new TextureLoader();// テクスチャローダーを作成
const texture = loader.load('/resource/img/img.jpg');// テクスチャ読み込み

const mat = new ShaderMaterial({
  uniforms: {
    uTex: { value: texture }// テスクチャを uTex として渡す
  },
  vertexShader: vertexSource,
  fragmentShader: fragmentSource
});
```

```glsl:shader.frag
varying vec2 vUv;
   
uniform sampler2D uTex;// テクスチャは sampler2D 型

void main() {
  vec3 color = texture2D( uTex, vUv ).rgb;// texture2D() でテクスチャのuv座標地点の色 rgba を取得

  gl_FragColor = vec4( color, 1.0 );
}
```

### テクスチャ画像のサイズについて
WebGLではメモリ効率化のため、テクスチャに使用する画像は縦横が2の累乗サイズの画像の利用が推奨されています。
例：`256 x 256`、`512 x 1024`、`2048 x 128` など
非推奨サイズの画像を使った場合、以下のようなアラートがコンソールに出ます。
`⚠️ THREE.WebGLRenderer: image is not power of two (2048x826). Resized to 2048x512`

## アスペクト補正
テクスチャのアスペクト補正は、CSSの`background-size: cover;`みたいに画面中心からスケールして全体を覆い尽くすようにしたいので、普通の座標を補正するときとは計算方法が異なります。

```javascript:Canvas/index.js
import { TextureLoader } from 'three/src/loaders/TextureLoader';

// 省略

export default class Canvas {
  constructor() {
    // 省略
    
    const loader = new TextureLoader();// テクスチャローダーを作成
    const texture = loader.load('/resource/img/img.jpg');// テクスチャ読み込み

    // uniform変数を定義
    this.uniforms = {
      uTime: {
        value: 0.0
      },
      uMouse: {
        value: new Vector2(0.5, 0.5)
      },
      uRadius: {
        value: this.targetRadius
      },
      uFixAspect: {
        value: this.h / this.w// 逆アスペクト
      },
      uTex: {
        value: texture// テクスチャ
      }
    };
    
    // 省略
  }
  
  // 省略
};
```

前回まではピクセルシェーダーでUV座標のアスペクト補正をしていましたが、頂点シェーダーでも同じ処理が書けるので今回は頂点シェーダーで`vUv`を送る前に補正をかけます。

```glsl:shader.vert
varying vec2 vUv;

uniform float uFixAspect;

void main() {
  // 余白ができないようにアスペクト補正
  vUv = uv - .5;
  vUv.y *= uFixAspect;
  vUv += .5;

  gl_Position = vec4( position, 1.0 );
}
```

```glsl:shader.frag
varying vec2 vUv;

uniform sampler2D uTex;

void main() {
  vec3 color = texture2D( uTex, vUv ).rgb;

  gl_FragColor = vec4( color, 1.0 );
}
```

補正前
![スクリーンショット 2019-03-26 14.55.40.png](https://qiita-image-store.s3.amazonaws.com/0/86966/cba38c41-b088-579a-68de-77d866f89e55.png)

補正後
![スクリーンショット 2019-03-26 14.55.52.png](https://qiita-image-store.s3.amazonaws.com/0/86966/fca65bc7-91d0-0769-63bf-2f6a9074ce53.png)

## エフェクトを作る準備
`uRadius`と`targetRadius`を`uPercent`と`targetPercent`に変更して、デフォルト値を`0.0`、最大値を`1.0`に変更します。

```javascript:Canvas/index.js
constructor() {
  // 省略

  this.targetPercent = 0.0;// Radius -> Percent

  const loader = new TextureLoader();// テクスチャローダーを作成
  const texture = loader.load('/resource/img/img.jpg');// テクスチャ読み込み

  // uniform変数を定義
  this.uniforms = {
    // 省略
    uPercent: {// Radius -> Percent
      value: this.targetPercent// Radius -> Percent
    },
    // 省略
  };
}

// 省略
mousePressed(x, y) {
  this.mouseMoved(x, y);
  this.targetPercent = 1.;// マウスを押したら進捗度の目標値を 1.0 に
}
mouseReleased(x, y) {
  this.mouseMoved(x, y);
  this.targetPercent = 0.0;// マウスを離したら進捗度の目標値を 0.0 に
}
```

# エフェクト：階調反転
GLSLは色を`0.0` ~ `1.0`で表現するので、`1.0 - color`とすることで色が反転します。
`mix()`関数は第一、第二引数に入れたふたつの色（色でなくでも可）を第三引数の`float`値をパーセントとして混ぜる関数です。
参考：[The Book of Shaders: mix](https://thebookofshaders.com/glossary/?search=mix)

```glsl:shader.frag
varying vec2 vUv;

uniform sampler2D uTex;
uniform float uPercent;

void main() {
  vec3 color = texture2D( uTex, vUv ).rgb;
  vec3 invert = 1. - color;

  color = mix( color, invert, uPercent );

  gl_FragColor = vec4( color, 1.0 );
}
```
![Kapture 2019-03-26 at 18.16.20.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/2721e3d2-c79a-9a8b-6cb0-28fae2c58be6.gif)


# エフェクト：チャンネルシフト
RとBのUV座標を左右に少しずらすと色収差みたいなエフェクトができます。

```glsl:shader.frag
varying vec2 vUv;

uniform float uPercent;
uniform sampler2D uTex;

void main() {
  float shift = uPercent * .01;

  float r = texture2D( uTex, vUv + vec2( shift, 0.0 ) ).r;
  float g = texture2D( uTex, vUv ).g;
  float b = texture2D( uTex, vUv - vec2( shift, 0.0 ) ).b;

  vec3 color = vec3( r, g, b );

  gl_FragColor = vec4( color, 1.0 );
}
```

![Kapture 2019-03-26 at 18.10.12.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/50979d8c-6c7c-49aa-4b49-3d854a761c85.gif)


# エフェクト：モザイク
指定した数値でUV座標を縦横に分割して、分割された中心の色を取得します。

```glsl:shader.frag
varying vec2 vUv;

uniform float uPercent;
uniform sampler2D uTex;

void main() {
  vec2 uv = vUv;

  float moz = uPercent * 0.02;

  if( moz > 0. ) {// 0では割れないので、if文で保護
    uv = floor( uv / moz ) * moz + ( moz * .5 );
  }

  vec3 color = texture2D( uTex, uv ).rgb;

  gl_FragColor = vec4( color, 1.0 );
}
```
![Kapture 2019-03-26 at 18.13.34.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/09de7efa-83b3-2231-b755-25232d0e0d7a.gif)


# エフェクト：ゆらゆら
`texture2D()`に渡すUV座標を`sin()`, `cos()`でオフセットして、色の取得先を波っぽくずらします。

```glsl:shader.frag
varying vec2 vUv;

uniform float uTime;
uniform float uPercent;
uniform sampler2D uTex;

void main() {
  vec2 uv = vUv;

  float t = uTime * 6.;
  float amount = uPercent * 0.02;

  vec2 uvOffset = vec2( cos( uv.y * 20. + t ), sin( uv.x * 10. - t ) ) * amount;

  vec3 color = texture2D( uTex, uv + uvOffset ).rgb;

  gl_FragColor = vec4( color, 1.0 );
}
```
![Kapture 2019-03-26 at 19.24.35.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/d1a91295-bc3a-5017-0069-8f489d4b3837.gif)

シェーダーエフェクトはどれも小さな計算式の組み合わせで成り立っているので、こまめに途中式を変数に入れて名前をつけておくと、あとで見返したときにもわかりやすくなるとおもいます。