three.js超入門 第1回 レンダリングまでの流れ

# 概要
この記事では「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回 レンダリングまでの流れ
[three.js超入門 第2回 アニメーションと時間ベースでの制御](https://qiita.com/watabo_shi/items/1c3c50429279eedec04e)
[three.js超入門 第3回 マウスやスクロールでのインタラクション](https://qiita.com/watabo_shi/items/0811d03390c18e46be86)
[three.js超入門 第4回 getBoundingClientRect()を使ったDOM要素との連携](https://qiita.com/watabo_shi/items/9ab12f62434a1e858226#_reference-a437d85928b89c9bb5e5)
[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://qiita.com/watabo_shi/items/2fc671f2147e799787f9)

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

[前回](https://qiita.com/watabo_shi/items/f7c559c3cdbcdd0f2629)はコードを書く前段階として、3Dコンピュータグラフィックスについて説明しました。
今回はthree.jsでの3Dオブジェクトのレンダリングまでの流れをやっていきます。

# 下準備
[こちらのリポジトリ](https://github.com/watab0shi/threejs-workshop)からプロジェクトをクローンします。(各種コマンドはREADMEを参照)

`npm install`でインストール。
`npm start`でサーバーを起動しながらウォッチを開始します。

空のサンプルァイルを用意してあるので、このファイルに書き加えていきます。
`src/00_empty/Canvas/index.js`にthree.jsを使ったWebGL表現の処理を書きます。
`src/00_empty/index.js`には、canvasより外の処理（マウスイベントやリサイズイベントの登録など）を書きます。

```javascript:src/00_empty/Canvas/index.js
// このクラス内に three.js のコードを書いていきます
export default class Canvas {
  constructor() {

  }
};
```

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

// このクラス内にページごとのcanvas外の処理を書いていきます
export default class Page00 {
  constructor() {
    new Canvas();
  }
};
```

`public/00_empty/index.html`には、canvasが入るコンテナだけが用意されています。

```html:public/00_empty/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>00 - empty | three.js tutorial</title>

  <!-- canvasを全画面表示させるスタイル -->
  <link rel="stylesheet" href="/resource/css/common.css">
</head>
<body>
  <!-- この中にcanvasが入ります -->
  <div id="canvas-container"></div>
  
  <script src="/resource/js/vendor.bundle.js"></script>
  <script src="/resource/js/common.bundle.js"></script>
</body>
</html>
```

# レンダリングに必要なもの
大きく分けて`Mesh` `Light` `Scene` `Camera` `Renderer`の5つのクラスを使用します。

## `THREE.Mesh`
`THREE.Geometry`と`THREE.Material`から成る3Dモデルです。
[前回](https://qiita.com/watabo_shi/items/f7c559c3cdbcdd0f2629)書いた「頂点バッファ」の`position`と`normal`が`THREE.Geometry`で制御され、`color`が`THREE.Material`によって制御されます。

#### `THREE.Geometry`
頂点や法線など、モデルの形状を設定するデータです。
球体（`THREE.SphereGeometry`）や直方体（`THREE.BoxGeometry`）、平面（`THREE.PlaneGeomentry`）などいくつかのプリミティブ形状が組み込まれています。

#### `THREE.Material`
色やテクスチャなど、モデルの質感を設定するデータです。
`color`を指定することで、モデルの色を一括で設定してくれます。



## `THREE.Light`
モデルに陰影を表現するためのライトです。
環境光の`THREE.AmbientLight`、点光源の`THREE.PointLight`、方向光源の`THREE.DirectionalLight`など、いろんな種類のライトを目的によって使い分けます。

## `THREE.Scene`
カメラで撮影するためのステージのようなものです。
`THREE.Mesh`や`THREE.Light`をシーンに追加することでカメラに映るようになります。

## `THREE.Camera`
シーン内の3Dモデルを撮影するためのカメラです。
いくつか種類がありますが、ほとんどの場合`THREE.PerspectiveCamera`を使います。

## `THREE.Renderer`
カメラで撮影した3Dモデルを画面に表示するためのレンダラーです。
レンダラーにもいくつか種類がありますが、ほとんどの場合`THREE.WebGLRenderer`を使います。
レンダラーの`render`関数に`THREE.Scene`と`THREE.Camera`を渡すことで`canvas`要素にレンダリングします。


# レンダリングの流れ
上記の逆順でインスタンスを作成していきます。
`Canvas`クラスのコンストラクタ内に以下を追記します。
（レンダリングまでの最短距離のコードなので、細かい設定などは省いています。）

```javascript:src/00_empty/Canvas/index.js
import * as THREE from 'three';

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

    // レンダラーを作成
    this.renderer = new THREE.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);

    // カメラを作成 (視野角, 画面のアスペクト比, カメラに映る最短距離, カメラに映る最遠距離)
    this.camera = new THREE.PerspectiveCamera(60, this.w / this.h, 1, 10);
    this.camera.position.z = 3;// カメラを遠ざける

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

    // ライトを作成
    this.light = new THREE.PointLight(0x00ffff);
    this.light.position.set(2, 2, 2);// ライトの位置を設定

    // ライトをシーンに追加
    this.scene.add(this.light);

    // 立方体のジオメトリを作成(幅, 高さ, 奥行き)
    const geo = new THREE.BoxGeometry(1, 1, 1);

    // マテリアルを作成
    const mat = new THREE.MeshLambertMaterial({ color: 0xffffff });

    // ジオメトリとマテリアルからメッシュを作成
    this.mesh = new THREE.Mesh(geo, mat);

    // メッシュをシーンに追加
    this.scene.add(this.mesh);
    
    // 画面に表示
    this.renderer.render(this.scene, this.camera);
  }
};
```

`Mesh` `Camera` `Light`には`CSS Transform`のように移動、回転、スケールを制御できる`position` `rotation` `scale`のプロパティがあります。
それぞれが`x, y, z`の値を持った`THREE.Vector3`型になっています。
一部の軸だけ設定するときは`position.x = newX`と書き、全てを設定するときは`position.set(newX, newY, newZ)`と書きます。

![box.png](https://qiita-image-store.s3.amazonaws.com/0/86966/a4ddeb90-fa9c-bec1-a320-6529a860344f.png)


立方体が真正面を向いていてライティングがよくわからないので、メッシュを回転させてみましょう。
`rotation`の角度の単位はラジアンなので、`Math.PI`を使って計算します。
`PI = 180°`なので、`180° / 4 = 45°`になります。

```javascript
// ジオメトリとマテリアルからメッシュを作成
this.mesh = new THREE.Mesh(geo, mat);
this.mesh.rotation.x = Math.PI / 4;
this.mesh.rotation.y = Math.PI / 4;
```
また、`THREE.Math.DEG2RAD`を使えば度数法で指定することもできます。

```javascript
// ジオメトリとマテリアルからメッシュを作成
this.mesh = new THREE.Mesh(geo, mat);
this.mesh.rotation.x = THREE.Math.DEG2RAD * 45;
this.mesh.rotation.y = THREE.Math.DEG2RAD * 45;
```

![box-2.png](https://qiita-image-store.s3.amazonaws.com/0/86966/e61c82f2-d141-2872-b99b-9fe992ec197a.png)

立方体が回転して、ちゃんとライティングされていることがわかりますね。

これで3Dオブジェクトの静止画のレンダリングができました。

色や座標を変えたり、メッシュやライトを追加してみましょう！

# おまけ１：canvasの背景を透明にする
`WebGLRenderer`にオプションで`alpha: true`を入れると背景が透明になります。

```javascript
this.renderer = new WebGLRenderer({ alpha: true });
```

確認用に`body`に背景色をつけてみます。

```html
<body style="background-color: #cfc;">
```
![スクリーンショット 2019-03-21 15.54.40.png](https://qiita-image-store.s3.amazonaws.com/0/86966/37bb8741-9067-6e3a-ab01-99982b98cb71.png)



# おまけ２：必要なモジュールだけimportしてバンドル後の容量を削減する
上のコードでは、`import * as THREE from 'three';`でthree.jsの全ての機能をimportして、`THREE.`で使うクラスを呼び出していますが、以下のような書き方をすることでバンドル後のファイルサイズをグッと小さくすることができます。

```javascript:src/00_empty/Canvas/index.js
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer';
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera';
import { Scene } from 'three/src/scenes/Scene';
import { PointLight } from 'three/src/lights/PointLight';
import { BoxGeometry } from 'three/src/geometries/BoxGeometry';
import { MeshLambertMaterial } from 'three/src/materials/MeshLambertMaterial';
import { Mesh } from 'three/src/objects/Mesh';

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);

    // カメラを作成 (視野角, 画面のアスペクト比, カメラに映る最短距離, カメラに映る最遠距離)
    this.camera = new PerspectiveCamera(60, this.w / this.h, 1, 10);
    this.camera.position.z = 3;// カメラを遠ざける

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

    // ライトを作成
    this.light = new PointLight(0x00ffff);
    this.light.position.set(2, 2, 2);// ライトの位置を設定

    // ライトをシーンに追加
    this.scene.add(this.light);

    // 立方体のジオメトリを作成(幅, 高さ, 奥行き)
    const geo = new BoxGeometry(1, 1, 1);

    // マテリアルを作成
    const mat = new MeshLambertMaterial({ color: 0xffffff });

    // ジオメトリとマテリアルからメッシュを作成
    this.mesh = new Mesh(geo, mat);
    this.mesh.rotation.x = Math.PI / 4;
    this.mesh.rotation.y = Math.PI / 4;

    // メッシュをシーンに追加
    this.scene.add(this.mesh);
    
    // 画面に表示
    this.renderer.render(this.scene, this.camera);
  }
};
```