Three.jsを使ってみる(入門編)

# はじめに
おはようございます。こんにちは。こんばんは。
Watatakuです。
本日は題名にもあるとおり「Three.js」をやっていきます。

# Three.jsとは
[公式サイト](https://threejs.org/)
Three.jsはHTMLの3D技術「WebGL」を扱いやすくしたフレームワークです。Three.jsを使えばGPUによる本格的な3D表現をプラグイン無しで作成できます。

# 始める前に
「Three.js」を始める間にcdnを読み込んでおきます。

```html
<script src="https://unpkg.com/three@0.137.4/build/three.min.js"></script>
```
WebGLの処理はHTMLページの読み込みが終わってから実行させます。addEventListener()関数を使ってDOMContentLoadedイベントが発生するのを監視させ、ページが読み込み終わったときに実行させたい関数を指定します。この関数init()の中にThree.jsのコードを書いていきます。

```html
<script>
window.addEventListener('DOMContentLoaded', init);
const init = () => {
  // 処理
}
</script>
```
## canvas要素を用意する
Three.jsはHTML5のcanvas要素を利用します。canvas要素はコンテンツを表示する描画エリアとなります。

```html
<body>
  <canvas id="myCanvas"></canvas>
</body>
```
canvas要素の大きさはJavaScriptを使って設定します。

これで準備完了なのでThree.jsを書いていきましょう。

# レンダラー
WebGLのレンダリングをするためのレンダラーを作成します。
*THREE.WebGLRendererクラス*のコンストラクターには引数として、HTMLに配置したcanvas要素を指定し、連携させます。

```js
const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector('#myCanvas')
});
```
## レンダーサイズの設定
画面に表示するサイズを指定します。

```js
renderer.setSize(960, 540);
```
**?画面いっぱいに表示させたいとき?**

```js
renderer.setSize(window.innerWidth, window.innerHeight);
```
bodyに余白ができるのでcssで余白を取ります。

```css
body {
  margin: 0;
  overflow: hidden;
}
```
``overflow: hidden``を指定すると、macOSのデスクトップブラウザのオーバースクロール<i>（たとえば画面上部にスクロールすると、画面上部を一瞬超えて表示される挙動）</i>を抑制できます。

# シーン
シーンを作成します。
シーンとは3D空間のことで、3Dオブジェクトや光源などの置き場となります。

```js
const scene = new THREE.Scene();
```

# カメラ
3Dではどの視点から空間を撮影するか、という実装をします。
この機能は「視点」や「カメラ」と呼ばれます。

- [PerspectiveCamera](https://threejs.org/docs/#api/en/cameras/PerspectiveCamera):遠近感が適用されるカメラ
- [OrthographicCamera](https://threejs.org/docs/#api/en/cameras/OrthographicCamera): 平行投影が適用されるカメラ

## PerspectiveCamera

```
new THREE.PerspectiveCamera(視野角, アスペクト比, near, far)
```

## OrthographicCamera

```
new THREE.OrthographicCamera(left, right, top, bottom, near, far)
```

- near : 区間の開始距離
- far : 区間の終了距離

Three.jsではnearのデフォルト値は1、farのデフォルト値は2000に設定されています。とくにfarの値はかなり小さく設定されていますので、必要に応じて大きな値に変更するとよいでしょう。

## カメラのポジション設定

```js
camera.position.set(0, 0, +1000);
```
# ジオメトリ(形状)
## 球体
[SphereGeometry](https://threejs.org/docs/#api/en/geometries/SphereGeometry)を使います。

```
new THREE.SphereGeometry(球の半径(1), 水平セグメントの数, 垂直セグメントの数, 水平方向の開始角度(0), 水平掃引角度サイズ(Math.PI * 2), 垂直方向の開始角度(0), 垂直掃引角度サイズ(Math.PI);
```

## 直方体
[BoxGeometry](https://threejs.org/docs/?q=BoxGeometry#api/en/geometries/BoxGeometry)を使います。

```
new THREE.BoxGeometry(幅(1), 高さ(1), 深さ(1), 辺の幅に沿ってセグメント化された長方形の面の数(1), 辺の高さに沿ってセグメント化された長方形の面の数(1), 辺の深さに沿ってセグメント化された長方形の面の数(1));
```

## 平面
[PlaneGeometry](https://threejs.org/docs/#api/geometries/PlaneGeometry)

```
new THREE.PlaneGeometry(幅(1), 高さ(1), widthSegments(1), heightSegments(1));
```

## 円錐
[ConeGeometry](https://threejs.org/docs/#api/geometries/ConeGeometry)

```
new THREE.ConeGeomerry(円錐の半径(1), 円錐の高さ(1), 円錐の円周の周りのセグメント化された面の数(8), 円錐の高さに沿った面の行数(1), 円錐の底面が開いているかキャップされているかを示すブール(false), 最初のセグメントの開始角度(0), 扇形の中心角(2*PI));
```

## 円柱
[CylinderGeometry](https://threejs.org/docs/#api/geometries/CylinderGeometry)

```
new THREE.CylinderGeometry(上部の円柱の半径(1), 下部の円柱の半径(1), 円柱の円周の周りのセグメント化された面の数(8), 円柱の高さに沿った面の行数(1), 円柱の端が開いているかキャップされているかを示すブール値(false), 最初のセグメントの開始角度(0), 扇形の中心角(2*PI));
```

## ドーナツ型
[TorusGeometry](https://threejs.org/docs/#api/geometries/TorusGeometry)

```
new THREE.TorusGeometry(トーラスの中心からチューブの中心までのトーラスの半径(1), チューブの半径(0.4), radialSegments(8), tubularSegment(1), 中心角(Math.PI*2));
```

# マテリアル(材質)
マテリアルは色や質感の情報を持っています。
ちなみに、このマテリアルと前章で作ったジオメトリを用いて後に登場するメッシュというものを作成します。

## MeshBasicMaterial

- 陰がつかないので均一な塗りつぶした状態
- ライトいらない

```
new THREE.MeshBasicMaterial(object(オプション));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/materials/MeshBasicMaterial)

## MeshNormalMaterial

- ノーマルのカラーをRGBで可視化するマテリアル
- ライトいらない

```
new THREE.MeshNomalMaterial(object(オプション));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/materials/MeshNomalMaterial)


## MeshLambertMaterial

- 光沢感のないマットな質感を表現できるマテリアル
- 陰影を必要とするマテリアルなので、ライトが必要

```
new THREE.MeshLambertMaterial(object(オプション));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/materials/MeshLambertMaterial)

## MeshPhongMaterial

- 光沢感のある質感を表現できるマテリアル
- ライトが必要

```
new THREE.MeshPhongMaterial(object(オプション));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/materials/MeshPhongMaterial)

## MeshToonMaterial

- アニメのようなトゥーンシェーディングを実現できるマテリアル
- ライトが必要

```
new THREE.MeshToonMaterial(object(オプション));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/materials/MeshToonMaterial)

## MeshStandardMaterial

- 物理ベースレンダリングのマテリアル
- ライトが必要

```
new THREE.MeshStandardMaterial(object(オプション));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/materials/MeshStandardMaterial)

# メッシュ

```js
const mesh = new THREE.Mesh(geometry, material);
```

# シーンに追加
**忘れるな！！**

```js
scene.add(mesh);
```

# ライト
マテリアルの中ではライトが必要なものもあるので、ここでまとめておきます。
## 環境光源

- 3D空間全体に均等に光を当てます
- 一律に明るくしたいときに使うといい

```
new THREE.AmbientLight(色(0xffffff), 光の強さ(1));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/lights/AmbientLight)

## 平行光源

- 特定の方向に放射される光
- 利用例としては、太陽の光です


```
new THREE.DirectionalLight(色(0xffffff), 光の強さ(	1));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/lights/DirectionalLight)

## 半球光源

- 上からの光の色と下からの光の色を分けられます
- 下からの光は反射光として、屋外での光の見え方に近くなります

```
new THREE.HemisphereLight(空の色(0xffffff), 地の色(0xffffff), 光の強さ(1));
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/lights/HemisphereLight)

## 点光源

- 単一点からあらゆる方向から放射される光源
- わかりやすい例としては、裸電球

```
new THREE.PointLight(色(0xffffff), 光の強さ(1), 距離(0), 光の減衰率(1))
```
[詳しくはドキュメント](https://threejs.org/docs/#api/en/lights/PointLight)
# リサイズ

```html
<meta name="viewport" content="width=device-width, initial-scale=1"/>
```
```js
// 初期化のために実行
onResize();
// リサイズイベント発生時に実行
window.addEventListener('resize', onResize);

function onResize() {
  // サイズを取得
  const width = window.innerWidth;
  const height = window.innerHeight;

  // レンダラーのサイズを調整する
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);

  // カメラのアスペクト比を正す
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
}
```
実装のポイントは次の通りです。

- リサイズ時にはレンダラーのサイズをsetSizeメソッドで画面幅に合わせること
- デスクトップでは、メインディスプレイ・サブディスプレイでPixelRatioの異なる可能性があるので、リサイズイベントでsetPixelRatioメソッドでを使って更新するべき
- リサイズ時にはカメラの縦横比が狂うので、リサイズ時に縦横比を正しく調整する
- 画面サイズの設定処理は、初期化時もリサイズ時も同じ
- onResize関数は初期化時とリサイズイベント発生の両方で呼び出す

# 地球儀を作る
![127_0_0_1_5501_earth_html_🔊.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116696/0c1fca44-c416-1273-961e-77d6b22e133d.png)
- [サンプルコード](https://github.com/watataku8911/ThreeJS-earth/blob/default-earth/earth.html)

## アニメーション
JavaScriptでアニメーションをさせるには、時間経過で関数を呼び続ける必要があります。そのためには、requestAnimationFrame()というグローバルメソッドを使用します。requestAnimationFrame()は引数として渡された関数を、毎フレーム実行します。

```js
// 初回実行
tick();

function tick() {
  requestAnimationFrame(tick);

  // アニメーション処理をここに書く
}
```
次に、Three.jsの表示結果を更新する命令を書きます。Three.jsでは自動的に画面が最新に切り替わらないので、明示的に画面が更新されるように命令を書く必要があります。renderer.render()という命令で更新を指示できます。

```js
// 初回実行
tick();

function tick() {
  requestAnimationFrame(tick);

  // アニメーション処理をここに書く

  renderer.render(scene, camera); // レンダリング
}
```
アニメーションの処理として、地球が自転するようにしてみましょう。時間経過で回転するようにrotation.yプロパティの数値を加算しています。

```js
// 初回実行
tick();

function tick() {
  requestAnimationFrame(tick);

  // アニメーション処理をここに書く
  earthMesh.rotation.y += 0.01;
  renderer.render(scene, camera); // レンダリング
}
```
## 星屑の生成(パーティクル)

```js
// 形状データを作成
const SIZE = 3000;
// 配置する個数
const LENGTH = 1000;
// 頂点情報を格納する配列
const vertices = [];
for (let i = 0; i < LENGTH; i++) {
  const x = SIZE * (Math.random() - 0.5);
  const y = SIZE * (Math.random() - 0.5);
  const z = SIZE * (Math.random() - 0.5);

  vertices.push(x, y, z);
}

// 形状データを作成
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));

// マテリアルを作成
const material = new THREE.PointsMaterial({
  // 一つ一つのサイズ
  size: 10,
  // 色
  color: 0xffffff,
});

// 物体を作成
const mesh = new THREE.Points(geometry, material);
scene.add(mesh); // シーンは任意の THREE.Scene インスタンス
```
コードの手順は次の通りです。

**①頂点を格納する配列を作成する**
```js
// 頂点情報を格納する配列
const vertices = [];
```
**②ジオメトリーに頂点座標を加えていく**
直方体エリア（一辺3000の距離）の中へランダムに1000個の粒子を配置します。SIZEとLENGTH変数で配置領域や個数をカスタマイズできるので、適宜調整ください。verticesは1次元の配列であり、頂点座標を[x0, y0, z0, x1, y1, z1, x2, y2, z2]と順番を登録していきます。

```js
// 頂点情報を格納する配列
const vertices = [];

// 形状データを作成
const SIZE = 3000;
// 配置する個数
const LENGTH = 1000;

const vertices = [];
for (let i = 0; i < LENGTH; i++) {
  const x = SIZE * (Math.random() - 0.5);
  const y = SIZE * (Math.random() - 0.5);
  const z = SIZE * (Math.random() - 0.5);

  vertices.push(x, y, z);
}
```
**③頂点からジオメトリーを作成する**
頂点座標を格納した配列verticesから、ジオメトリを作成します。setAttribute()メソッドを利用し登録します。
```js
// 形状データを作成
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
```

**④専用のマテリアルを作る**
``THREE.PointsMaterial``という専用のクラスを使って、粒子のサイズや色を指定します。``THREE.PointsMaterial``クラスは形状を持たない為、視点が変化しても常に正面を向いて表示されます。
```js
// マテリアルを作成
const material = new THREE.PointsMaterial({
  // 一つ一つのサイズ
  size: 10,
  // 色
  color: 0xFFFFFF,
});
```
[PointsMaterial](https://threejs.org/docs/#api/en/materials/PointsMaterial)
**④手順3と5で作成したジオメトリーとマテリアルから、メッシュを作り、3D空間に配置する**
```js
const mesh = new THREE.Points(geometry, material);
scene.add(mesh);
```
### TIPS：パーティクルをカラフルにする。
```js
const colors = [];
for (・・・) {
  //抜粋
  const colorX = Math.randam();
  const colorY = Math.randam();
  const colorZ = Math.randam();
  colors.push(color);
}
// ・・・
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
// ・・・

 const material = new THREE.PointsMaterial({
   size: 10,
   vertexColors:true
   blending: THREE.AdditiveBlending
 });
```

[Points](https://threejs.org/docs/#api/en/objects/Points):ポイントを表示するためのクラス。
# 最後に
次回はこの作った地球儀を使ってカメラの制御を学習していきます。

- [参考記事](https://ics.media/tutorial-three/quickstart/)