three.jsにおけるシェーディング：光源と反射材質の概要、法線ベクトルの設定方法

[three.jsによるHTML5 3Dグラフィックス(上)](http://www.cutt.co.jp/book/978-4-87783-324-4.html)の第5章を読んで学習した内容をまとめる。

3Dオブジェクトを2Dのディスプレイに表示する場合、立体的に見せるには陰影が重要になってくる。
陰影付けによって立体感を出す作業を「シェーディング」と呼ぶ。

シェーディングに必要な要素は次の３つ。

* 光源オブジェクト
* 反射材質オブジェクト
* 法線ベクトル

# 光源オブジェクト

three.jsでは平行光源・点光源・スポット光源・環境光源が用意されている。
複数の光源を同時に設定することもできる。
シーンに追加するタイミングは、カメラや表示するオブジェクトの前でも後でも良い。

## 平行光源

`DirectionalLight`クラスで表現する。
無限遠にある点光源からの光で、光の照射角が物体の位置や座標に左右されないので
シェーディングのための演算が簡単で処理が軽いという特徴がある。

光源の位置座標からターゲットの座標へ向かうベクトルが光源ベクトルとなるが、
ターゲット（`target`プロパティ）が設定されていない場合には、原点がターゲットとなる。
なお、この光源ベクトルの長さと明るさは関係ない。

```js
/* 使用例 */
var directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.0); // 光源色と光強度を指定して生成
directionalLight.position.set(20, 20, 100); // 光源位置を設定
scene.add(directionalLight); // シーンに追加
```

## 点光源

`PointLight`クラスで表現する、全方位に光を発する光源。
光が当たるジオメトリのポリゴン数が少ないと、ポリゴンの境目が不自然に光ったりするので、ある程度
`Geometry`クラスのプロパティである`widthSegments`と`heightSegments`を多くしておく必要がある。

```js
/* 使用例 */
var pointLight = new THREE.PointLight(0xFFFFFF, 1, 0); // 光源色、光強度、光源からの減衰係数を指定して生成
pointLight.position.set(0, 0, 100); // 光源位置を設定
scene.add(pointLight); // シーンに追加
```

## スポットライト光源

`SpotLight`クラスで表現する。
3D空間中の一点に対して光を当てる光源で、光源軸からの減衰係数というパラメータ`exponent`をもつ。
これを設定することで、スポットライトの当たる範囲を広くしたり狭くしたりできる。

任意の3Dオブジェクトを`target`プロパティに設定することで、簡単にオブジェクトを狙った光を当てることが可能。
`target`プロパティには3Dオブジェクトしか指定できないため、任意の座標を狙いたいときには
透過したダミーオブジェクトを狙いたい座標位置に置き、`target`に設定すると良い。

なお、点光源と同様に、ジオメトリのポリゴン数が適切でないと
光の当たっている部分の境界付近に、ジャギーが発生することがあるので注意。

```js
/* 使用例 */
var spotLight = new THREE.SpotLight(0xFFFFFF, 1, 0, Math.PI / 6, 20); // 光源色、光強度、光源からの減衰係数、光源の傾き、光源軸からの減衰係数を指定して生成
spotLight.position.set(0, 0, 100); // 光源位置を設定
scene.add(spotLight); // シーンに追加
```

## 環境光源

`AmbientLight`クラスを使用する。基本的には他の光源と一緒に使用するもの。
3Dオブジェクトに対し均等に光を当てることができるため、
他の光源からの光が当たらない部分であっても真っ暗にならなくなる。

実世界でも散乱する光によって、影になっている部分も真っ暗とはならないが、
環境光源を使うことでCG上でも簡単にその状態を表現することができる。

環境光源を使用する際は、反射材質オブジェクトの`ambient`プロパティを
`color`プロパティと同じ色にしておく必要があったが、
r71より`ambient`プロパティが削除されたため、最新バージョンを使う場合には特に意識する必要がなくなった。

```js
/* 使用例 */
var ambientLight = new THREE.AmbientLight(0x555555); // 光源色を指定して生成
scene.add(ambientLight); // シーンに追加
```

# 反射材質オブジェクト

three.jsにおいて、シェーディングの対象となる「反射材質」は下の2種類が用意されている。

反射材質とは、普段自分たちの目で見えているように、光源からの光の反射や吸収によって
物体の色が変わるという法則をCGで表現するためのモデルである。
（可視光領域の光をすべて反射する物体は白く見えるとか。）

#### ランバート反射材質

`MeshLambertMaterial`クラスを使用する。
「材質色」と「拡散色」のみ考慮していて、木材やコンクリートなどのざらついた表面を表現するのに向いている。

#### フォン反射材質

`MeshPhongMaterial`クラスを使用する。
「材質色」「拡散色」に加えて、ランバート反射材質で考慮しない「鏡面色」も考慮して、
入射光に対する表面の色の見え方を決定する。

鏡面反射の色や光沢の強さ（鏡面指数）をパラメータとして設定できるため、
プラスチックや金属のような光を反射させる表面を表現するのに向いている。


# 「材質色」の表現

光源に照らされて見える物体の色を、書籍では「材質色」と呼んでいる。

太陽光のもとで白色に見える物に、赤や青など色のついた光を当てると、物体はその光源の色に見える。
CGで描画される場合、光源の色と材質の色（太陽光のもとでの色）の積によって材質色が変化する。

例えば、緑（0x00FF00）の物体にマゼンタ（0xFF00FF）の光を当てたときの色は
0x00FF00と0xFF00FFの積によって0x000000（黒色）となる。

色の明るさは、光の向きと法線ベクトル（面の向き）によって変化する。
光の向きと法線ベクトルが平行であれば明るく、垂直であれば暗くなる。

例えば、立方体の真上に光源を置くと、
下方向の光と立方体上面の法線ベクトルが平行になって上面は明るく見える。
一方で、光と法線ベクトルが垂直となる側面は暗く見える。


# 「拡散色」の表現（`color`プロパティ）

拡散色とは、入射光が物体表面で拡散反射する光による色のこと。
カメラの位置座標に関わらず、同じ色に見える。

# 「鏡面色」の表現（`emissive`プロパティ）

鏡面色は光の向きである光源ベクトルと、面の向きである法線ベクトル、そしてカメラの位置座標の3つに依存する。

法線ベクトルに対する光源ベクトルの入射角と、法線ベクトルに対するカメラ方向の角度が一致するときに
鏡面色は最大に（もっとも明るく）なる。


# 法線ベクトルの設定

平面オブジェクトや基本的な形状オブジェクトでは暗黙のうちに法線ベクトルが設定されるので、
明示的に法線ベクトルを設定する必要はない。
しかし、任意の3Dオブジェクトを表現する場合には自分で法線ベクトルを設定する必要がある。
（少ないポリゴンで細かい陰影を付けられたりする。参考：[法線マッピング - Wikipedia](https://ja.wikipedia.org/wiki/%E6%B3%95%E7%B7%9A%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0)）

## メソッドによる自動設定

`computeFaceNomals()`で、ポリゴンを形成する三角形の3つの頂点座標から、
三角形面の法線ベクトルを計算することができる。
また、facesプロパティで設定した面すべてに対して一度で計算できるので便利。

使用例はこんな感じ。

```js
// 形状オブジェクトの宣言
var geometry = new THREE.Geometry();

// 頂点座標を設定
geometry.vertices[0] = new THREE.Vector3(30, 0, 0);
geometry.vertices[1] = new THREE.Vector3(0, 30, 0);
geometry.vertices[2] = new THREE.Vector3(-30, -30, 0);

// 面を設定
geometry.faces[0] = new THREE.Face3(0, 1, 2);

// 法線ベクトルの計算
geometry.computeFaceNormals();
```

## 手動設定

法線ベクトルは`Face3`クラスの`normal`プロパティでも指定できる。
任意に法線ベクトルを指定したい場合には、この方法を使う。

### 1つの面に1つの法線ベクトルを設定する場合

```js
// 形状オブジェクトの宣言
var geometry = new THREE.Geometry();

// 頂点座標を設定
geometry.vertices[0] = new THREE.Vector3(30, 0, 0);
geometry.vertices[1] = new THREE.Vector3(0, 30, 0);
geometry.vertices[2] = new THREE.Vector3(-30, -30, 0);

// 法線ベクトルを生成
var normal = new THREE.Vector3(0, 0, 1);

// 面に法線ベクトルを設定
geometry.faces[0] = new THREE.Face3(0, 1, 2, normal);
```

### 1つの面の各頂点に法線ベクトルを設定する場合

配列で法線ベクトルを指定すると、頂点ごとに設定できる。
このとき、頂点間の法線ベクトルは線形補完される。
（線形補完には、`MeshLambertMaterial`クラスと`MeshPhongMaterial`クラス共通の
`shading`プロパティがデフォルト値の`SmoothShading`に設定されている必要がある。）

```js
// 形状オブジェクトの宣言
var geometry = new THREE.Geometry();

// 頂点座標を設定
geometry.vertices[0] = new THREE.Vector3(30, 0, 0);
geometry.vertices[1] = new THREE.Vector3(0, 30, 0);
geometry.vertices[2] = new THREE.Vector3(-30, -30, 0);

// 法線ベクトルを生成
var normals = [];
normals[0] = new THREE.Vector3(1, 0, 0);
normals[1] = new THREE.Vector3(0, 1, 0);
normals[2] = new THREE.Vector3(0, 0, 1);

// 面に法線ベクトルを設定
geometry.faces[0] = new THREE.Face3(0, 1, 2, normals);
```