three.js超入門 第6回 ShaderMaterialでメッシュを変形、着色する

# 概要
この記事では「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でメッシュを変形、着色する
[three.js超入門 第7回 シェーダーに変数を渡す](https://qiita.com/watabo_shi/items/1b48ca54a9034f3614d8)
[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)

# three.jsでシェーダーを使うには
マテリアルに`ShaderMaterial`か`RawShaderMaterial`のどちらかを使用します。
`ShaderMaterial`は、three.js側で便利な変数（モデルの頂点座標やテクスチャ座標など）をシェーダーに自動で挿入してくれます。
逆に`RawShaderMaterial`は、three.js側での補助が一切ないプレーンな状態のシェーダーを記述したいときに使うクラスです。
シェーダーに慣れるまでは`ShaderMaterial`を使用することをおすすめします。

## サンプルコード（シェーダーソースを文字列として記述する場合）
GLSLのコードは改行を含むので、JSファイルに直接記述する場合はテンプレート文字列を使うと書きやすいです。

```javascript:ShaderMaterialSample.js
import { ShaderMaterial } from 'three/src/materials/ShaderMaterial';
     
// 頂点シェーダーのソース
const vertexSource = `
void main() {
  gl_Position = vec4(position, 1.0);
}
`;

// ピクセルシェーダーのソース
const fragmentSource = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

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

## サンプルコード（シェーダーソースを別ファイルにする場合）
テンプレート文字列ではエディタのシンタックスハイライトが効かないのと、コードごとにファイルを分けたいので、`webpack-glsl-loader`を使ってシェーダーの拡張子（`.vert` `.frag` `.glsl`）を読めるようにします。

```javascript:webpack.config.js
// 省略
module.exports = {
  // 省略
  module: {
    rules: [
      {
        test: /\.(vert|frag|glsl)$/,
        use: {
          loader: 'webpack-glsl-loader'
        }
      }
    ]
  }
};
```
```javascript:shader.vert
void main() {
  gl_Position = vec4(position, 1.0);
}
```
```javascript:shader.frag
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
```
```javascript:ShaderMaterialSample.js
import { ShaderMaterial } from 'three/src/materials/ShaderMaterial';
     
// シェーダーソース
import vertexSource from './shader.vert';
import fragmentSource from './shader.frag';

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

# 実際のコード
[リポジトリ](https://github.com/watab0shi/threejs-workshop)のプロジェクトルートで`npm start`してローカルサーバーを起動し、`http://localhost:3000/00_shader_empty/`にアクセスします。
10 x 10の平面メッシュがワイヤーフレームで表示されます。
![スクリーンショット 2019-03-19 17.20.28.png](https://qiita-image-store.s3.amazonaws.com/0/86966/1afdefd7-dad8-63cd-8089-0b795c47455e.png)

プロジェクトフォルダの`src/00_shader_empty/`にシェーダー用のテンプレートを用意しているので、これを書き換えていきましょう。

```
00_shader_empty
├── Canvas
│   ├── index.js
│   └── shaders
│       ├── shader.frag
│       └── shader.vert
└── index.js
```

まず、`Canvas`クラスの中身はこうなっています。

```javascript:Canvas/index.js
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer';
import { OrthographicCamera } from 'three/src/cameras/OrthographicCamera';
import { Scene } from 'three/src/scenes/Scene';
import { PlaneGeometry } from 'three/src/geometries/PlaneGeometry';
import { ShaderMaterial } from 'three/src/materials/ShaderMaterial';
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial';
import { Mesh } from 'three/src/objects/Mesh';
import { Vector2 } from 'three/src/math/Vector2';

// シェーダーソース
import vertexSource from './shaders/shader.vert';
import fragmentSource from './shaders/shader.frag';

export default class Canvas {
  constructor() {
    // ウィンドウサイズ
    this.w = window.innerWidth;
    this.h = window.innerHeight;

    // レンダラーを作成
    this.renderer = new WebGLRenderer();
    this.renderer.setSize(this.w, this.h);// 描画サイズ
    this.renderer.setPixelRatio(window.devicePixelRatio);// ピクセル比

    // #canvas-containerにレンダラーのcanvasを追加
    const container = document.getElementById("canvas-container");
    container.appendChild(this.renderer.domElement);

    // カメラを作成（背景シェーダーだけならパースいらないので、OrthographicCameraをつかう）
    this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, -1);

    // シーンを作成
    this.scene = new Scene();

    // 平面をつくる（幅, 高さ, 横分割数, 縦分割数）
    const geo = new PlaneGeometry(2, 2, 10, 10);

    // シェーダーソースを渡してマテリアルを作成
    const mat = new ShaderMaterial({
      vertexShader: vertexSource,
      fragmentShader: fragmentSource,
      wireframe: true
    });

    this.mesh = new Mesh(geo, mat);

    // メッシュをシーンに追加
    this.scene.add(this.mesh);

     // 描画ループ開始
    this.render();
  }

  render() {
    // 次のフレームを要求
    requestAnimationFrame(() => { this.render(); });

    // ミリ秒から秒に変換
    const sec = performance.now() / 1000;

    // 画面に表示
    this.renderer.render(this.scene, this.camera);
  }
};
```

今回はページの背景をシェーダー表現にしたいだけなので、ジオメトリには平面の`PlaneGeometry`を使用し、カメラは`PerspectiveCamera`ではなく`OrthographicCamera`（パースのないカメラ）を使用しています。
ファイル上部で`import`したシェーダーソース（`shader.vert` `shader.frag`）を`ShaderMaterial`に渡してシェーダーマテリアルを作成しています。

## 頂点シェーダー
次に頂点シェーダー（`shader.vert`）を見てみましょう。

```glsl:shader.vert
// vertex shader ( 頂点シェーダー )
// このファイルに各頂点ごとの処理を記述します

void main() {
  vec3 pos = position;// position: ShaderMaterialで補完される vec3 型(xyz)の変数。ジオメトリの頂点のこと。

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

`position`は、`ShaderMaterial`側で自動的に宣言してくれている`vec3`型の変数で、ジオメトリの頂点座標を表しています。
`gl_Position`は`vec4`型の座標を代入することで頂点の位置を決定するGLSLの組み込み変数です。

### 頂点シェーダーでメッシュを変形させてみる
```glsl:shader.vert
void main() {
  vec3 pos = position;

  pos.y = ( pos.y * 0.5 ) + sin( pos.x * 3.0 ) * 0.5;// 縦を半分のサイズにして、sinでy座標を歪ませる

  gl_Position = vec4( pos, 1.0 );
}
```
![スクリーンショット 2019-03-19 17.34.28.png](https://qiita-image-store.s3.amazonaws.com/0/86966/fac0f16c-bac2-ebc4-df5a-04b400bad16a.png)
たった1行追加するだけでメッシュを変形させることができました。
このように、CPUだといちいちforループを回さないとできないことが、シェーダーを使うとほんの数行で実現できます。すばらしいですね。
※ あとに影響がでるので頂点シェーダーの変更は元に戻しておきましょう。

```glsl:shader.vert
void main() {
  vec3 pos = position;

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

## ピクセルシェーダー
次にピクセルシェーダーをいじるので、`PlaneGeometry`の分割を`1 x 1`にして、ワイヤーフレーム表示をオフにしておきます。

```javascript:Canvas/index.js
// 平面をつくる（幅, 高さ, 横分割数, 縦分割数）
const geo = new PlaneGeometry(2, 2, 1, 1);

// シェーダーマテリアルに GLSL のソースを渡す
const mat = new ShaderMaterial({
  vertexShader: vertexSource,
  fragmentShader: fragmentSource,
  wireframe: false// ワイヤーフレームをオフ
});
```

ピクセルシェーダー（`shader.frag`）を見てみましょう

```glsl:shader.frag
// fragment shader ( フラグメントシェーダー、ピクセルシェーダー )
// このファイルに各ピクセルごとの処理を記述します

void main() {
  vec4 color = vec4(1.0, 1.0, 1.0, 1.0);// rgba

  gl_FragColor = color;// gl_FragColor に vec4 型（rgba）の色を入れることでピクセル色を決定する。
}
```
`gl_FragColor`は`vec4`型の色を代入することでピクセルの色を決定するGLSLの組み込み変数です。
`vec4(1.0, 1.0, 1.0, 1.0)`で、`rgba`が全て`1.0`となっているので、白色になっています。

### ピクセルシェーダーでメッシュの色を変えてみる

```glsl:shader.frag
void main() {
  vec4 color = vec4(1.0, 0.0, 0.0, 1.0);// 赤

  gl_FragColor = color;
}
```
![スクリーンショット 2019-03-19 17.54.43.png](https://qiita-image-store.s3.amazonaws.com/0/86966/5ccf13c9-5214-4858-619d-878867aaa5e4.png)
メッシュにピクセルシェーダーで色をつけられました。



シェーダーの最低限の機能は以上です。
次回はさらに踏み込んで、頂点シェーダーからピクセルシェーダーに変数を渡したり、CPUからシェーダーに変数を渡す方法をやります。