【React/mapboxの地図上で3Dモデルを動かす】(1) 3DモデルをThree.jsを使ってブラウザに表示する

# はじめに

タイトルにある通り、ReactWebアプリに描画されたmapboxの地図上で3Dモデルを動かすことをゴールに記事にしていきます。
長いので4つの記事に分けて書きます。

- (1) 3DモデルをThree.jsを使ってブラウザ表示する
- (2) mapboxの地図上に3Dモデルを表示する
- (3) mapboxの地図上で3Dモデルをアニメーションさせる
- (4) mapboxの地形データ付きの地図上で地形に沿って3Dモデルをアニメーションさせる

今回は **(1) 3DモデルをThree.jsを使ってブラウザ表示する** です。

## 最終的な完成形

mapboxの地形付きの地図上で、3Dモデルが地形に沿って移動していきます。

![完成形.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/7fa6f549-cde8-334a-e072-0a4b2138352d.gif)


# 3Dモデルの準備

今回はこちらのドローンのモデルを使おうと思います。
![drone.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/306d1716-5182-3ba6-3b9f-eb3c5796faef.png)

[https://free3d.com/3d-model/drone-costume-411845.html](https://free3d.com/3d-model/drone-costume-411845.html)

# 環境構築

今回はThree.jsでモデルを表示することに集中するため、Reactと言っておきながらReactは使いません。
ピュアなJavascriptでまず3Dモデル表示の流れを行っていこうと思います。

## vite

```bash
yarn create vite

✔ Select a framework: › Vanilla
✔ Select a variant: › JavaScript
```

## package install

```bash
yarn add three
```

## 3D描画用のcanvasを設定

描画先の`canvas`を設定します。

```diff_html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
-   <div id="app"></div>
+   <canvas id="canvas"></canvas>
    <script type="module" src="/main.js"></script>
  </body>
</html>
```

## css調整

ブラウザの余白を無くします。
直接関係は無いのでやらなくても大丈夫です。

```diff_css:style.css
+ * {
+   margin: 0;
+   padding: 0;
+ }

:root {
  font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 24px;
  font-weight: 400;
  ...
  ...
```

## Js初期セットアップ

```diff_javascript:main.js
+ import './style.css'

+ const canvas = document.getElementById("canvas");
```

## DLした3Dモデル配置

`/models`というディレクトリを作ってそこに放り込んでおきます。

![modelパス.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/ff89c599-b0df-3302-d647-254c56a8b438.png)


## 試しに起動

```
yarn dev
```

[http://localhost:5173](http://localhost:5173)

画面は真っ黒な状態。コンソールにエラーとか出てなければOK。
![初期確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/8c559366-5dd8-b5ce-1656-7c361aaef2e4.png)


# Three.jsについて

## 概要

3Dコンテンツを作れるJavascriptライブラリです。
[https://threejs.org/](https://threejs.org/)

簡単な3Dオブジェクトは勿論、3Dモデル用のファイルなどを読み込んで使うこともできます。

## ブラウザでの描画の流れ

ものすごく大雑把な自分の中でのイメージです。
![threejsのイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/73d086cb-652f-1665-3326-b32a4a21ff5d.png)


- **scene**・・・3Dオブジェクトを表示する場所。ここに3Dオブジェクトをどんどん配置していく。
- **camera**・・・sceneをどのように見るか。
- **renderer**・・・cameraで見ている状態をブラウザで描画できるようにする。ここでは**canvas**を指定する。

# 3Dモデルを表示させる実装

## 描画するための土台部分を作る

まずイメージ図のような、ブラウザに描画するための土台部分を作っていきます。

```js:main.js
import "./style.css";
import * as THREE from "three";

const canvas = document.getElementById("canvas");

const scene = new THREE.Scene();
// 背景をわかりやすいように白にしておく
scene.background = new THREE.Color(0xffffff);

const camera = new THREE.PerspectiveCamera(
  35,
  innerWidth / innerHeight,
  0.1,
  2000
);
camera.position.z = 80;
camera.position.y - 7.2;

// カメラでsceneを見れるように
scene.add(camera);

// 3Dの世界を投影する対象の設定
const renderer = new THREE.WebGLRenderer({ canvas });
// ブラウザの画面幅目一杯まで描画するサイズ設定
renderer.setSize(innerWidth, innerHeight);

// 3Dの世界をブラウザに描画する
renderer.render(scene, camera);
```

真っ白な画面が表示されていればひとまず土台部分はOKです

![土台.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/dc567fc2-eed0-110d-f628-4fb1e4f228d0.png)

## モデルを配置する

DLした3Dモデルを配置していきます。
Materialの中にファイルが色々と入っているのですが、今回は`drone_costum.fbx`を使いたいと思います。

```diff_javascript:main.js
import "./style.css";
import * as THREE from "three";
+ import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";

const canvas = document.getElementById("canvas");

const scene = new THREE.Scene();
// 背景をわかりやすいように白にしておく
scene.background = new THREE.Color(0xffffff);

const camera = new THREE.PerspectiveCamera(
  35,
  innerWidth / innerHeight,
  0.1,
  2000
);
camera.position.z = 80;
camera.position.y - 7.2;

// カメラでsceneを見れるように
scene.add(camera);

+ // 3Dモデルの読み込みと配置
+ const fbxLoader = new FBXLoader();
+ // テクスチャの読み込み先の設定
+ fbxLoader.setResourcePath("./models/Drone_Costum/Teturizer/");
+ fbxLoader.load("./models/Drone_Costum/Material/drone_costum.fbx", (obj) => {
+   // 3Dモデルが大きすぎたので縮小
+   obj.scale.set(0.025, 0.025, 0.025);
+   // 3Dモデルをsceneに配置
+   scene.add(obj);
+ });

// 3Dの世界を投影する対象の設定
const renderer = new THREE.WebGLRenderer({ canvas });
// ブラウザの画面幅目一杯まで描画するサイズ設定
renderer.setSize(innerWidth, innerHeight);

- // 3Dの世界をブラウザに描画する
- renderer.render(scene, camera);

+ // モデルは読み込み処理により遅延してsceneにセットされるため、再描画が必要
+ // なので、renderを繰り返し行うように設定する
+ const animate = () => {
+   // 3Dの世界をブラウザに描画する
+   renderer.render(scene, camera);
+   window.requestAnimationFrame(animate);
+ };
+ animate();

```

モデルの配置は終わり、描画はできているのですがモデルが真っ黒です。
これは光源が設定されていないので光が無い状態だからです。

![モデル追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/15becc17-1af6-8faf-891e-1e712bcde27c.png)


## 光源を追加

ライトを追加していきます。

```diff_javascript:main.js
import "./style.css";
import * as THREE from "three";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";

const canvas = document.getElementById("canvas");

const scene = new THREE.Scene();
// 背景をわかりやすいように白にしておく
scene.background = new THREE.Color(0xffffff);

const camera = new THREE.PerspectiveCamera(
  35,
  innerWidth / innerHeight,
  0.1,
  2000
);
camera.position.z = 80;
camera.position.y - 7.2;

// カメラでsceneを見れるように
scene.add(camera);

+ // ライトの設定 今回は2種類のライトを追加します
+ const ambientLight = new THREE.AmbientLight();
+ ambientLight.color.set(0xffffff);
+ ambientLight.intensity = 0.5;
+ scene.add(ambientLight)
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff,0.4);
+ directionalLight.position.set(1,0.55,5);
+ scene.add(directionalLight);

// 3Dモデルの読み込みと配置
const fbxLoader = new FBXLoader();
// テクスチャの読み込み先の設定
fbxLoader.setResourcePath("./models/Drone_Costum/Teturizer/");
fbxLoader.load("./models/Drone_Costum/Material/drone_costum.fbx", (obj) => {
  // 3Dモデルが大きすぎたので縮小
  obj.scale.set(0.025, 0.025, 0.025);
  // 3Dモデルをsceneに配置
  scene.add(obj);
});

// 3Dの世界を投影する対象の設定
const renderer = new THREE.WebGLRenderer({ canvas });
// ブラウザの画面幅目一杯まで描画するサイズ設定
renderer.setSize(innerWidth, innerHeight);

// モデルは読み込み処理により遅延してsceneにセットされるため、再描画が必要
// なので、renderを繰り返し行うように設定する
const animate = () => {
  // 3Dの世界をブラウザに描画する
  renderer.render(scene, camera);
  window.requestAnimationFrame(animate);
};
animate();

```

光源が追加され、モデルの外観が見えるようになりました。

![ライト追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/b7303d8e-3cba-07d7-ab71-ce8acdb04b12.png)


## 浮遊感を出す(アニメーションを追加)

ついでに少しモデルを上下させてドローンに浮遊感を出してみようと思います。
アニメーションは`animate`の中で表現していくことになります。

```diff_javascript:main.js
import "./style.css";
import * as THREE from "three";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";

const canvas = document.getElementById("canvas");

const scene = new THREE.Scene();
// 背景をわかりやすいように白にしておく
scene.background = new THREE.Color(0xffffff);

const camera = new THREE.PerspectiveCamera(
  35,
  innerWidth / innerHeight,
  0.1,
  2000
);
camera.position.z = 80;
camera.position.y - 7.2;

// カメラでsceneを見れるように
scene.add(camera);

// ライトの設定 今回は2種類のライトを追加します
const ambientLight = new THREE.AmbientLight();
ambientLight.color.set(0xffffff);
ambientLight.intensity = 0.5;
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
directionalLight.position.set(1, 0.55, 5);
scene.add(directionalLight);

// 3Dモデルの読み込みと配置
+ let drone;
const fbxLoader = new FBXLoader();
// テクスチャの読み込み先の設定
fbxLoader.setResourcePath("./models/Drone_Costum/Teturizer/");
fbxLoader.load("./models/Drone_Costum/Material/drone_costum.fbx", (obj) => {
  // 3Dモデルが大きすぎたので縮小
  obj.scale.set(0.025, 0.025, 0.025);
  // 3Dモデルをsceneに配置
  scene.add(obj);
+   drone = obj;
});

// 3Dの世界を投影する対象の設定
const renderer = new THREE.WebGLRenderer({ canvas });
// ブラウザの画面幅目一杯まで描画するサイズ設定
renderer.setSize(innerWidth, innerHeight);

// モデルは読み込み処理により遅延してsceneにセットされるため、再描画が必要
// なので、renderを繰り返し行うように設定する
- const animate = () => {
+ const animate = (frame) => {

+   // 3Dモデルの読み込みが終わっていたら
+   if (drone) {
+     // 上下に3Dモデルを動かす
+     let elevation = Math.sin(frame * 0.01) * 0.5;
+     drone.position.y = elevation;
+   }

  // 3Dの世界をブラウザに描画する
  renderer.render(scene, camera);
  window.requestAnimationFrame(animate);
};
animate();

```

ドローンに浮遊感を持たせることができました。

![step1完成形.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/e2bd888c-7cd3-d81b-04c7-50a23010e883.gif)


# まとめ

Three.jsについての基本的な描画の流れを追っていきました。
Three.jsライブラリのクラスや細かいプロパティなどは公式ドキュメントで詳細に説明されています。

次回はこれをmapboxの地図の世界へ持っていきます。

# 参考

[Three.js 公式ドキュメント](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene)
[お借りした3Dモデル](https://free3d.com/3d-model/drone-costume-411845.html)

# 連載

(1) 3D モデルを Three.js を使ってブラウザ表示する
[(2) mapbox の地図上に 3D モデルを表示する](https://qiita.com/itouoti/items/90550c8d529d63596d96)
[(3) mapbox の地図上で 3D モデルをアニメーションさせる](https://qiita.com/itouoti/items/5a0e077ae492bb378017)
[(4) mapbox の地形データ付きの地図上で地形に沿って 3D モデルをアニメーションさせる](https://qiita.com/itouoti/items/40541ea11a66e385cdd8)