three.js超入門 第7回 シェーダーに変数を渡す

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

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

前回は、シェーダーを使ってメッシュを変形させたり、メッシュ全体の色を変えたりしました。
今回はさらに細かい制御ができるように、頂点シェーダーからピクセルシェーダーに変数を送ったり、CPU（JavaScript）からGPU（シェーダー）に変数を送る方法を紹介します。

# `varying`変数
`main()`関数の前（グローバル）に変数を宣言する際に、行頭に`varying`と記述するとその変数が頂点シェーダーからピクセルシェーダーに送られます。
頂点シェーダーとピクセルシェーダー両方で同じ`varying`変数の記述が必要です。
変数名に特に制限はありませんが、頭に接頭辞の`v`をつけるとわかりやすいです。
`varying`変数は初期化時に代入はできません。（初期化代入で定数にする場合はピクセルシェーダーに書けばいいので。）
値の代入は`main()`関数の中に記述します。

## サンプルコード
```glsl:shader.vert
varying float vSample;// varying: 頂点シェーダーからピクセルシェーダーに変数を送るための装飾子
       
void main() {
  vSample = 1.0;// main() の中で値を代入する
  
  gl_Position = vec4( position, 1.0 );
}
```

```glsl:shader.frag
varying float vSample;// 頂点シェーダーから送られてきた varying 変数を受け取る
        
void main() {
  gl_FragColor = vec4( vSample, 0.0, 0.0, 1.0 );
}
```

## メッシュのテクスチャ座標をピクセルシェーダーに送る
### テクスチャ座標とは
文字通りメッシュにテクスチャを貼る際に参照される座標です。
頂点ごとにテクスチャのどの部分を対応させるのかを設定するものです。

### 頂点座標とテクスチャ座標
今回のコードでの対応関係は以下のようになっています。
テクスチャの座標系では左下が原点になることに注意しましょう。
![coordinate_uv.png](https://qiita-image-store.s3.amazonaws.com/0/86966/b1bb0a1f-4022-de5e-1d23-ed373daf70ec.png)


```glsl:shader.vert
varying vec2 vUv;
       
void main() {
  vUv = uv;// uv: ShaderMaterialで補完される vec2 型(xy)の変数。テクスチャ座標のこと。
  
  gl_Position = vec4( position, 1.0 );
}
```

```glsl:shader.frag
varying vec2 vUv;
       
void main() {
  vec4 color = vec4( vUv.x, vUv.y, 0.0, 1.0 );// テクスチャ座標を r g に入れる
 
  gl_FragColor = color;
}
```
![スクリーンショット 2019-03-20 14.38.42.png](https://qiita-image-store.s3.amazonaws.com/0/86966/bd45b7f8-3ed7-1853-0ec6-d97ec722e717.png)
赤緑黄色のグラデーションが表示されました。
左下が黒で、右上が黄色になっているので、テクスチャ座標が左下原点で`0.0 ~ 1.0`になっていることがわかります。


## テクスチャ座標を使って発光する円を書いてみる
GLSLはグラデーション表現が得意なので、発光しているような表現もほんの数行で書くことができます。

### 画面中心から現在のピクセルまでの距離を求める
```glsl:shader.frag
varying vec2 vUv;// 頂点シェーダーから varying 変数を受け取る
     
void main() {
  vec2 center = vec2( .5, .5 );// 画面の中心
  float dist = length( vUv - center );// 中心から現在のピクセルへの距離を取得
  vec4 color = vec4( vec3( dist ), 1.0 );// 距離を rgb に変換
 
  gl_FragColor = color;
}
```
![スクリーンショット 2019-03-20 15.07.29.png](https://qiita-image-store.s3.amazonaws.com/0/86966/76a68b89-e9af-7775-1373-d8b87ddc472a.png)
画面中心から外側に広がる円形グラデーションができました。
画面中心から各ピクセルまでの距離をビジュアライズしているので中心が暗く、外側が明るくなっています。
（画面比率で伸びてしまっていますが、後ほど修正するのでこのまま進めます。）

### 内接円の半径を距離で割る
テクスチャ座標の`x` `y`がそれぞれ`0.0 ~ 1.0`の範囲なので、ウィンドウに内接する円の半径は`0.5`になります。
`dist`を`lightness`に変更して、半径の`0.5`を距離で割ります。

```glsl
float lightness = 0.5 / length( vUv - center );// 半径を、中心から現在のピクセルへの距離で割る 
vec4 color = vec4( vec3( lightness ), 1.0 );
```
![スクリーンショット 2019-03-20 15.29.34.png](https://qiita-image-store.s3.amazonaws.com/0/86966/e49633e2-1986-3ad4-5651-d1c62b1d4f9b.png)
画面にぴったり収まる発光した円ができました。
なぜ半径を距離で割ると発光するのでしょうか？
仮にこの部分を数学の式にすると`y = 0.5 / x`となります。
![スクリーンショット 2019-03-20 15.42.27.png](https://qiita-image-store.s3.amazonaws.com/0/86966/007d71ad-96a0-b1e1-d538-1e1eeb553268.png)
`x`（中心からの距離）が`0.5`より小さくなるとき、`y`（明るさ）は`1.0`よりおおきくなります。
円の内側では`rgb`の値が`1.0`を超えているので、真っ白になるわけです。

### 半径を小さくする
さきほど`0.5`を入れていた値を変更することで、任意の半径に変更できます。

```glsl
float lightness = 0.05 / length( vUv - center );// 割られる値が円の半径になる
```
![スクリーンショット 2019-03-20 16.04.06.png](https://qiita-image-store.s3.amazonaws.com/0/86966/11ada03b-1573-8bfd-c17f-861b6f9856a1.png)

### 円に色をつける
シェーダーでは色を`0.0 ~ 1.0`で表現するので、乗せたい色を乗算することで白い部分に色を乗せることができます。

```glsl
color *= vec4( 0.2, 1.0, 0.5, 1.0 );// 着色する
```
![スクリーンショット 2019-03-21 12.01.50.png](https://qiita-image-store.s3.amazonaws.com/0/86966/3aa5943a-8942-c991-c6b4-ff1b6f65246d.png)

円の中心に近いほど、`lightness`が`1.0`以上の大きな値になるため、乗算した色が発光しているような見た目になります。
着色する色をそのまま乗せたい場合は、`lightness`の値の範囲を`clamp()`で`0.0 ~ 1.0`に制限します。

```glsl
lightness = clamp( lightness, 0.0, 1.0 );// 値の範囲を 0.0 ~ 1.0 に制限
vec4 color = vec4( vec3( lightness ), 1.0 );
```
![スクリーンショット 2019-03-21 12.02.07.png](https://qiita-image-store.s3.amazonaws.com/0/86966/5e1c0f49-7ec2-d5ba-80c1-be5a72714f47.png)


# `uniform`変数
`main()`関数の前（グローバル）に変数を宣言する際に、行頭に`uniform`と記述するとその変数はCPUから値を受け取ります。
`uniform`変数は、頂点シェーダーとピクセルシェーダー両方に送られますが、変数を使用しないシェーダーには記述する必要はありません。
変数名に特に制限はありませんが、頭に接頭辞の`u`をつけるとわかりやすいです。
`uniform`変数はCPUから値を受け取るため、代入はできません。

## アスペクト比による画像の伸びを補正する
`Canvas`クラスで`uniforms`という`uniform`変数を格納しておくためのオブジェクトを作成して、`ShaderMaterial`を作成する際に渡します。
任意の変数名のオブジェクトの`value`要素にシェーダーに渡す数値が入ります。
`float`を渡す場合はそのまま数値を入れ、`vec`系で渡す場合はthree.jsの`Vector`クラスを入れます。

```javascript:Canvas/index.js
// uniform変数を定義
this.uniforms = {
  uAspect: {
    value: this.w / this.h
  }
};

// uniform変数とシェーダーソースを渡してマテリアルを作成
const mat = new ShaderMaterial({
  uniforms: this.uniforms,
  vertexShader: vertexSource,
  fragmentShader: fragmentSource
});
```

```glsl:shader.frag
varying vec2 vUv;
uniform float uAspect;// 画面のアスペクト比

void main() {
  vec2 uv = vec2( vUv.x * uAspect, vUv.y );// xをアスペクト補正したテクスチャ座標
  vec2 center = vec2( .5 * uAspect, .5 );// xをアスペクト補正した画面の中心
  float lightness = 0.05 / 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;
}
```
![スクリーンショット 2019-03-21 12.09.22.png](https://qiita-image-store.s3.amazonaws.com/0/86966/59c4c64b-7a0b-00a9-f3cc-427c394d8459.png)

伸びが補正されて正円になりました。

## アニメーションさせる
アニメーションをさせるには時間の数値が必要なので、CPUから`uniform`変数で渡します。

```javascript:Canvas/index.js
// 省略
export default class Canvas {
  constructor() {
    // 省略
       
    // uniform変数を定義
    this.uniforms = {
      uAspect: {
        value: this.w / this.h
      },
      uTime: {
        value: 0.0
      }
    };
    
    // 省略
  }
  
  render() {
    requestAnimationFrame(() => { this.render(); });

    const sec = performance.now() / 1000;

    this.uniforms.uTime.value = sec;// シェーダーに渡す時間を更新

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

```glsl:shader.frag
varying vec2 vUv;
  
uniform float uAspect;
uniform float uTime;// 時間

void main() {
  vec2 uv = vec2( vUv.x * uAspect, vUv.y );
  vec2 center = vec2( .5 * uAspect, .5 );
  float radius = 0.05 + sin( uTime * 2.0 ) * 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;
}
```
![Kapture 2019-03-21 at 12.12.02.gif](https://qiita-image-store.s3.amazonaws.com/0/86966/53cf66e2-2e73-26be-2aed-0e2c5c40475c.gif)

ピクセルシェーダーでアニメーションができました。
同様に頂点シェーダーでも`uTime`を受け取ることで頂点をアニメーションさせることができます。

次回はマウスをつかってシェーダーをインタラクティブに動かします。