Three.js の世界へようこそ！ ３歩でわかる お手軽3Dプログラミング

Qiita の [Advent Calendar](https://qiita.com/advent-calendar/2019) も盛り上がっているようですが，本記事は社内の Advent Calendar 用に書きました。
1人でも多くの方に3Dプログラミングの魅力について知って頂けたら嬉しいです。とはいえ，私も専門家ではないので初心者ながら頑張って書いていきたいと思います。

3Dに馴染みのない方にもなるべく抵抗がないよう，行列やベクトル，内積・外積のような数学的な話は省いています。というか，私もあんまり分かってません（汗
そういった知識がなくてもある程度なら3Dプログラミングができてしまうのが Three.js の魅力でもあります。


# Three.js とは

ブラウザ上での 3D 描画を簡単に扱えるようにしてくれる JS ライブラリです。

- 公式サイト
https://threejs.org/

- 入門サイト（おすすめ）
https://ics.media/tutorial-three/


# 登場人物たちを見てみよう！

～ Three.js の世界に登場する人たちをご紹介するコーナー ～

<details>
<summary>見てみる</summary>
<div>

## [Scene](https://threejs.org/docs/#api/en/scenes/Scene) / シーン
- 世界に存在するオブジェクトを格納するところ
- 基準となる座標系を持つ（ワールド座標）

## [Mesh](https://threejs.org/docs/#api/en/objects/Mesh) / メッシュ
- 世界に存在するモノ
- シーンが舞台ならメッシュは役者
- Geometry（形状）と Material（質感）で出来ている
- オブジェクト個々の座標を持つ（ローカル座標）

## [Geometry](https://threejs.org/docs/#api/en/core/Geometry) / ジオメトリー
- モノの形状を表現するもの
- Box, Circle, Ring, Sphere など様々な形状が用意されている
- vertices（頂点）, faces（面）の配列から自由な形状を作ることもできる

## [Material](https://threejs.org/docs/#api/en/materials/Material) / マテリアル
- モノの質感を表現するもの
- ライトの影響あり or なし，光沢感のあり or なし，などの種類がある
- 面に Texture（画像）を貼ることもできる

## [Light](https://threejs.org/docs/#api/en/lights/Light) / ライト
- 世界を照らす光
- 面の反射や影などに影響を与える
- 平行光源や点光源など，種類によってオブジェクトへの影響が異なる

## [Camera](https://threejs.org/docs/#api/en/cameras/Camera) / カメラ
- 世界を観る者
- 透視投影や平行投影など，種類によって見え方が異なる
    - メッシュとライトとカメラの向きや位置関係などでどんな絵になるか決まる
- カメラを動かすことで世界の中での移動や向きの変更を表現できる

## [Renderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) / レンダラー
- 世界を描きだす者
- カメラから見たシーンを1枚の写真のように画像にしてくれる
- アニメーションしたい場合は一定間隔でレンダーする
- WebGLRenderer を使えば，WebGL で GPU を使ったレンダリングができる

## [Canvas](https://developer.mozilla.org/ja/docs/Web/API/Canvas_API) / キャンバス
- 世界の外に結果を伝える者
- HTML要素
- Renderer が描画した画像は Canvas を通して HTML の上に表示される
- HTML側にも座標系がある（スクリーン座標系）

## その他
- 全てのオブジェクトは [Object3D](https://threejs.org/docs/#api/en/core/Object3D) を継承している
    - 位置や向き，表示 or 非表示などの基本情報を持つ
    - Object3D の親子を持て，階層構造を表現できる
- 他にも，[Fog](https://threejs.org/docs/#api/en/scenes/Fog) / フォグ のようなオブジェクトもある
- プロはシェーダーをカスタマイズする（私には無理・・・）

</div>
</details>


# 簡単 3 Step で体験してみよう！

～ サンプルを使って Three.js の世界を体験できるコーナー ～

## Step1: 物体を表示する

まずは，シンプルな立方体を表示してみましょう。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="dsudo" data-slug-hash="PowZKWx" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="three.js sample step 1">
  <span>See the Pen <a href="https://codepen.io/dsudo/pen/PowZKWx">
  three.js sample step 1</a> by dsudo (<a href="https://codepen.io/dsudo">@dsudo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

※ スマホの場合は，Result をクリックしたあと，右下の Rerun をクリックすると結果が表示されます。

[こちら](https://daisukesudo.github.io/samples/threejs/step1/) からもご覧いただけます。

### 解説

０．事前に Three.js ライブラリを読み込んでおく必要があります。HTML の Script タグに追加すれば OK です。今回は CDN から取得することにします。

``` html
<script src="https://unpkg.com/three@0.111.0/build/three.js"></script>
```

１．HTML に Canvas要素を追加しておき，JavaScript側で取得します。ここに Three.js で描いた結果を表示していきます。ついでに Canvas の幅と高さも取っておきます。これも後ほど使います。

``` html
<canvas id="main"></canvas>
```

``` js
const canvas = document.querySelector("#main");
const width = canvas.clientWidth;
const height = canvas.clientHeight;
```

２．立方体の設置場所としてシーンを作成します。ここから Three.js のオブジェクトを扱っていきます。Three.js のオブジェクトは `THREE` というモジュールに入っています。

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

３．次に，表示する立方体（メッシュ）を作成します。立方体を表現する `BoxGeometry` と ライトを使わずに発色できる `MeshNormalMaterial` を使っていきます。作成したメッシュに位置を設定してシーンに追加します。※今回は話を簡単にするためにライトは使用しません。

``` js
const box = new THREE.Mesh(
  new THREE.BoxGeometry(64, 64, 64),
  new THREE.MeshNormalMaterial()
);
box.position.set(0, 0, 0);
scene.add(box);
```

４．続いて，カメラを作成します。今回は `PerspectiveCamera` を使います。これは透視投影と言って，人間の目で 3D 世界を見た時と近い遠近感のある見た目にしたいときに使用します。ほかに平行投影（正投影）といってどこからみても同じ大きさに見えるカメラもあります。
視野角，アスペクト比，手前の閾値，奥の閾値を設定します。アスペクト比の計算に Canvas の幅と高さを使います。位置を適当に設定し， `lookAt` で注視点を指定します。注視点はカメラがどこを向いているかです。立方体のポジションを指定していて，カメラが立方体の中心に向いている状態になります。（`BoxGeometry` の場合，ローカル座標の原点が図形の中心になります。どこが原点になるかは図形によって異なります）

``` js
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
camera.position.set(200, 100, 300);
camera.lookAt(scene.position);
```

５．とうとうレンダラーの登場です。もちろん `WebGLRenderer` を使用します。レンダリング結果を表示する Canvas を指定します。指定せずに内部で生成させることもできますが，レンダラーに問い合わせせずに扱えるようにしておいた方が何かと便利なので明示的に指定しています。
`antialias: true` を設定しておくと物体の輪郭が滑らかに表示されます。サイズは Canvas の幅と高さを設定します。`clearColor` は背景色です。透過させることもできます。`pixelRatio` は HiDPI（高画素密度モニター）のときにぼやけるの防ぐらしいです。たしかに指定しないとぼやけました。

``` js
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(width, height);
renderer.setClearColor(0xd0f0d0);
renderer.setPixelRatio(window.devicePixelRatio);
```

６．いよいよレンダリングです。シーンとカメラを指定して，レンダラーの `render` メソッドを実行します。

``` js
renderer.render(scene, camera);
```

無事に立方体が表示されました👏


## Step2: 物体を回転する

静止画を表示するだけでは面白くないので，続いて先ほどの立方体を回転させてみましょう。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="dsudo" data-slug-hash="dyPGzQj" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="three.js sample step 2">
  <span>See the Pen <a href="https://codepen.io/dsudo/pen/dyPGzQj">
  three.js sample step 2</a> by dsudo (<a href="https://codepen.io/dsudo">@dsudo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

[こちら](https://daisukesudo.github.io/samples/threejs/step2/) からもご覧いただけます。

### 解説

レンダラーの設定までは先ほどと同じです。（あ，微妙に背景色だけ変えてあります）

先ほどは１度レンダリングだけで終わってしまいましたが，アニメーションするためには一定間隔でレンダリングし続けなければなりません。そこで，`renderer.render(scene, camera);` の部分を次のように変更します。

``` js
const animate = () => {
  // next frame
  requestAnimationFrame(animate);

  // rotate
  const sec = Date.now() / 1000;
  box.rotation.x = sec * (Math.PI / 4);
  box.rotation.y = sec * (Math.PI / 4);
  box.rotation.z = sec * (Math.PI / 4);

  // render
  renderer.render(scene, camera);
};

animate();
```

まずは `animate` 関数を定義し，`requestAnimationFrame` を使って，自分を呼び出すことで永遠に再帰処理をしています。その中で `renderer.render` を呼び出すことで，レンダリングを繰り返し行うことを実現しています。
`requestAnimationFrame` はブラウザでアニメーションするときに使う関数で，タブがフォアグラウンドの場合は，およそ 60回/秒 ですが，バックグラウンドの場合は，リソースの消費を抑えるためにより少ない呼び出しになります。

物体を動かさないとアニメーションしているか分からないので，`Date.now()` から取得した値を使って `box.rotation.x/y/z` を設定することで少しずつ向きを変化させています。この辺の設定をいじると色んな動きが試せて


## Step3: 物体を自在に回転する

クルクル回ったのはいいけど，ただ見てるだけというのも退屈ですね。
Three.js の魅力の１つはインタラクティブな操作に対応させることができるところです。
最後はユーザーの入力（マウスやタッチパッド）で立方体を動かしてみましょう。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="dsudo" data-slug-hash="JjoGrGB" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="three.js sample step 3">
  <span>See the Pen <a href="https://codepen.io/dsudo/pen/JjoGrGB">
  three.js sample step 3</a> by dsudo (<a href="https://codepen.io/dsudo">@dsudo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

[こちら](https://daisukesudo.github.io/samples/threejs/step3/) からもご覧いただけます。

### 使い方

#### マウス
ドラッグ：回転
ホイール：拡大・縮小
CTRL+ドラッグ：移動

#### タッチパッド
１本指で動かす：回転
２本指で広げる：拡大
２本指で狭める：縮小
２本指でスライド：移動

※ 右下の Rerun ボタン：リセット（CodePen の機能）

### 解説

まず，立方体を動かしているというのはウソですｗ
実際にはカメラが動いています。カメラが立方体の周りを回っているので，見ている人からは立方体が回転しているように見えます。コペルニクス的な何かですね。

`OrbitControls` というカメラ制御を使用します。こちらは Three.js の本体のライブラリに含まれないため，追加でライブラリを読み込む必要があります。

``` html
<script src="https://unpkg.com/three@0.111.0/examples/js/controls/OrbitControls.js"></script>
```

これまでのオブジェクトに加えて，`OrbitControls` を追加します。`enableDamping ` はデフォルト false ですが，true を設定すると，カメラの移動や回転がマウスを離したあとにピタっとは止まらず，なだらかに余韻を残したような動きになります。[^1]

[^1]: カメラが真上と真下を向いたときにそれ以上いかなくなるのは，ジンバルロックという現象を防ぐために制御してるためだと思われます。この言葉を覚えておくと，どっちを向いたらいいか分からなくなったときなどに「ジンバルロックが起きたんだな」といった 3D ギャグが使えるようになります。

``` js
var controls = new THREE.OrbitControls(camera, canvas);
controls.enableDamping = true;
```

先ほどの `animate` との違いですが，物体を自動的に動かす必要はないので，`box.rotation.x/y/z` を設定していた部分を削除しています。代わりに `controls.update` を呼び出していますが，こちらは `enableDamping` か `autoRotate` を true に設定している場合は必要になります。デフォルト (false) のままであれば不要です。

``` js
const animate = () => {
  // next frame
  requestAnimationFrame(animate);

  // required if controls.enableDamping or controls.autoRotate are set to true
  controls.update();

  // render
  renderer.render(scene, camera);
};

animate();
```

あと，地味に Canvas がアクティブになって枠線が強調されてしまうので，CSS に `outline: none;` を追加してエフェクトを無効化します。

ということで，簡単に3Dプログラミングができましたね😉


# おしまい

どうでしたか？3Dの魅力を体験して頂けたでしょうか？
私も５年ぶりに Three.js を触ってみましたが，やっぱり3Dは楽しいですね！

もっと色んなことを知りたい方は公式のドキュメントやサンプルをご覧頂くとさらに奥深い Threejs の世界へと誘われること請け合いです。
https://threejs.org/examples/#webgl_animation_cloth

本記事を書くにあたってコチラのサイトにお世話になりました。とても分かりやすくて参考になります。
https://ics.media/tutorial-three/

それから，Three.js Advent Calendar もあるので要チェックですね。
https://qiita.com/advent-calendar/2019/threejs