Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン（導入編）

今回から[Three.js](https://threejs.org/)を使って3Dモデルを表示できるアプリを作成していきます。

Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン（導入編）[今回]
[Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン（ファイルインポート編）](https://qiita.com/0taUtan/items/e71436beaf612e7bb93f)
[Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン（投球編）](https://qiita.com/0taUtan/items/b1d7565585b72562ec20)
[Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン（Raycaster編）](https://qiita.com/0taUtan/items/7959c00da37068dd210e)

# ゴール
このハンズオンをやると次のようなアプリが作れちゃいます。
![一通り.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2103236/be6ae221-7b58-d029-6d1a-d0eb68ab8b57.gif)


# 今回のゴール
次のようなアニメーションが作れるようになるのが今回のゴールです。([GitHub](https://github.com/mininjin/display-3d-models/tree/install))

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2103236/34f2d9b8-2f30-d244-7e6e-8567453e5933.gif)


# 導入
### リポジトリの作成
とりあえずまずはvue projectの作成です。(vue-cliが入っていない人は[公式](https://cli.vuejs.org/)を参考にnpmインストールしてください。)とりあえずTypeScriptを指定してプロジェクトを作成します。（vueのセットアップが面倒な人はThree.jsの導入までスキップしてください。[レポジトリ](https://github.com/mininjin/display-3d-models/tree/init)をクローンすれば`init`ブランチからすぐに始められます。）

````
vue create app
cd app
````
ここからはコードを書き換えていきましょう。

まず、`Helloworld.vue`を`Three.vue`という名前に書き換えます。（便宜上）

次に、`App.vue`の`App.vue`内のアイコンを削除し、`Three.vue`をインポートします。

````@/App.vue
<template>
  <Three />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import Three from "./components/Three.vue";

export default defineComponent({
  name: "App",
  components: {
    Three,
  },
});
</script>
````

最後に`Three.vue`を次のように書き換えます。なお、cssのスタイリングは[tailwindcss](https://tailwindcss.com/)で行なっていきます。（適宜`vue add tailwind`してください。）

````@/components/Three.vue
<template>
  <div></div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({});
</script>
````

### Three.jsの導入
まず、[Three.js](https://threejs.org/)をnpmインストールします。TypeScriptなので`@types/three`もお忘れなく。

````
npm install -D three @types/three 
````

次に、`Three.vue`を次のように書き換えます。各要素の解説は後ほど。

````@/components/Three.vue
<template>
  <div ref="container" class="fixed w-full h-full top-0 left-0"></div>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import { GridHelper, PerspectiveCamera, Scene, WebGLRenderer } from "three";

export default defineComponent({
  setup() {
    // 描画するDOMの指定
    const container = ref();
    // Three.js
    const scene = new Scene();
    const camera = new PerspectiveCamera();
    const renderer = new WebGLRenderer();
    // 初期化
    const init = () => {
      if (container.value instanceof HTMLElement) {
        // DOMのサイズを取得
        const { clientWidth, clientHeight } = container.value;
        // 背景のグリッドの追加
        scene.add(new GridHelper());
        // カメラの設定
        camera.aspect = clientWidth / clientHeight;
        camera.updateProjectionMatrix();
        camera.position.set(10, 10, 0);
        camera.lookAt(0, 0, 0);
        // rendererの設定
        renderer.setSize(clientWidth, clientHeight);
        renderer.setPixelRatio(clientWidth / clientHeight);
        container.value.appendChild(renderer.domElement);
        // 描画
        animate();
      }
    };
    // 描画
    const animate = () => {
      const frame = () => {
        // 描画
        renderer.render(scene, camera);
        // 画面を更新
        requestAnimationFrame(frame);
      };
      frame();
    };

    // マウント時に初期化して描画
    onMounted(() => {
      init();
    });

    return {
      container,
    };
  },
});
</script>
````
ここで、おそらくTSのエラーが出るのでtsconfig.tsのコンパイラオプションに`"ES2020"`を追記します。

````tsconfig.ts
{
  "compilerOptions": {
   ...
   "lib": [
      "ES2020",
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
 ...
}
````
ここまでできたら、`npm run serve`で起動し、`localhost:8080`にアクセスしてみましょう。以下のような感じでグリッドが表示されているだけの真っ暗な画面が表示されればOKです。
![スクリーンショット 2022-01-22 14.40.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2103236/23bff75b-1d32-b925-1d45-bee90c6a81f0.png)


# 解説
`Three.vue`内のTSコードを見ていきましょう。まずは変数の定義から。

````ts
    // Three.js
    const scene = new Scene();
    const camera = new PerspectiveCamera();
    const renderer = new WebGLRenderer();
````
[Blender](https://blender.jp/)などの3Dモデルソフトを使ったことがある方なら馴染み深いかもしれません。これらはそれぞれ次のようなものです。(詳しくは[公式](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene)を参照)

- `Scene` : 描画する3D空間のオブジェクトが入るクラス
- `Camera` : 3D空間のビューポイント
- `Renderer` : ３D空間をcanvasに描画するためのクラス

次に初期化を見ていきましょう。

````ts
    // 初期化
    const init = () => {
      if (container.value instanceof HTMLElement) {
        // DOMのサイズを取得
        const { clientWidth, clientHeight } = container.value;
        // 背景のグリッドの追加
        scene.add(new GridHelper());
        // カメラの設定
        camera.aspect = clientWidth / clientHeight;
        camera.updateProjectionMatrix();
        camera.position.set(10, 10, 0);
        camera.lookAt(0, 0, 0);
        // rendererの設定
        renderer.setSize(clientWidth, clientHeight);
        renderer.setPixelRatio(clientWidth / clientHeight);
        container.value.appendChild(renderer.domElement);
        // 描画
        animate();
      }
    };
````
コメントに書いてある通りですが、ここで行なったのは以下の3つです。

- シーンにグリッドを追加
- カメラのアスペクト比・位置・視線の変更
- 描画のサイズ・アスペクト比調整とDOMへのアペンド

最後に、`animate`関数でアニメーションとして描画を行なっています。`animate`関数を見てみましょう。

````ts
    // 描画
    const animate = () => {
      const frame = () => {
        // 描画
        renderer.render(scene, camera);
        // 画面を更新
        requestAnimationFrame(frame);
      };
      frame();
    };
````
これもコメントに書いてある通りですが、アニメーションの各フレームで描画を行なっています。

# オブジェクトの挿入と視点の変更
### 球体の追加
このままでは何もないので、空間に球体を追加しましょう。次のような関数を作成します。

````ts
    // Sphereの作成
    const createSphere = (): Mesh => {
      const geometry = new SphereGeometry(3);
      const material = new MeshBasicMaterial({ color: 0xffffff });
      return new Mesh(geometry, material);
    };
````
基本的なオブジェクトの作成方法は、作りたいオブジェクトの形状である**Geometry**とその色などの性質を決める**Material**を作成し、そこからMeshというオブジェクトを作成するという手順です。ここでは、半径3で白色の球体を作成しました。

あとは、次のようにしてシーンに追加します。

````@/components/Three.ts
    const init = () => {
      if (container.value instanceof HTMLElement) {
        ...
        // 球体の追加
        const sphere = createSphere();
        scene.add(sphere);
        ...
      }
    };
````
実際に表示してみると、次のように見えます。
![スクリーンショット 2022-01-22 14.48.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2103236/4e2b7230-d2d1-6974-647a-9d90050f9473.png)

このままだと球体に見えないので、Materialを変更して、光源を追加して影をつけてみましょう。

````ts
    const light = new PointLight();
    // 初期化
    const init = () => {
      if (container.value instanceof HTMLElement) {
        ...
        // ライトの設定
        light.color.setHex(0xffffff);
        light.position.set(10, 10, 0);
        scene.add(light);
        ...
      }
    };
    ...
    // Sphereの作成
    const createSphere = (): Mesh => {
      const geometry = new SphereGeometry(3);
      const material = new MeshLambertMaterial();
      return new Mesh(geometry, material);
    };
````
今回はLamberMaterialにしてみました。次のように見えるはずです。
![スクリーンショット 2022-01-22 15.05.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2103236/c8cd559e-9eb9-beae-af9d-f075d7e5cd4f.png)

### カメラの視点変更
最後に、カメラの視点を変更してみます。

````ts
    // 描画
    const animate = () => {
      // カメラの位置パラメータ
      let phi = 0;
      const frame = () => {
        ...
        // カメラの視点変更
        phi += 0.003 * Math.PI;
        camera.position.set(10 * Math.cos(phi), 10, 10 * Math.sin(phi));
        camera.lookAt(0, 0, 0);
        ...
      };
      frame();
    };
````
若干数学チックですが、こんな感じでアニメーションフレームを修正します。単純にx,z座標を極座標を使って回転させるだけですね。`0.003`のレートを変えれば回転スピードが制御できます。

ここまでできればゴールです！冒頭で見せたGIFのようになれば完了です。

#まとめ
最終的なソースコードは以下のようになりました。

````@/components/Three.vue
<template>
  <div ref="container" class="fixed w-full h-full top-0 left-0"></div>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import {
  GridHelper,
  Mesh,
  MeshLambertMaterial,
  PerspectiveCamera,
  PointLight,
  Scene,
  SphereGeometry,
  WebGLRenderer,
} from "three";

export default defineComponent({
  setup() {
    // 描画するDOMの指定
    const container = ref();
    // Three.js
    const scene = new Scene();
    const camera = new PerspectiveCamera();
    const renderer = new WebGLRenderer();
    const light = new PointLight();
    // 初期化
    const init = () => {
      if (container.value instanceof HTMLElement) {
        // DOMのサイズを取得
        const { clientWidth, clientHeight } = container.value;
        // 背景のグリッドの追加
        scene.add(new GridHelper());
        // ライトの設定
        light.color.setHex(0xffffff);
        light.position.set(10, 10, 0);
        scene.add(light);
        // 球体の追加
        const sphere = createSphere();
        scene.add(sphere);
        // カメラの設定
        camera.aspect = clientWidth / clientHeight;
        camera.updateProjectionMatrix();
        camera.position.set(10, 10, 0);
        camera.lookAt(0, 0, 0);
        // rendererの設定
        renderer.setSize(clientWidth, clientHeight);
        renderer.setPixelRatio(clientWidth / clientHeight);
        container.value.appendChild(renderer.domElement);
        // 描画
        animate();
      }
    };
    // 描画
    const animate = () => {
      // カメラの位置パラメータ
      let phi = 0;
      const frame = () => {
        // 描画
        renderer.render(scene, camera);
        // カメラの視点変更
        phi += 0.002 * Math.PI;
        camera.position.set(10 * Math.cos(phi), 10, 10 * Math.sin(phi));
        camera.lookAt(0, 0, 0);
        // 画面を更新
        requestAnimationFrame(frame);
      };
      frame();
    };

    // マウント時に初期化して描画
    onMounted(() => {
      init();
    });

    // Sphereの作成
    const createSphere = (): Mesh => {
      const geometry = new SphereGeometry(3, 100, 100);
      const material = new MeshLambertMaterial();
      return new Mesh(geometry, material);
    };

    return {
      container,
    };
  },
});
</script>
````

今回は、単純にオブジェクトを作成して視点を回転しながら表示させるだけでした。次回は実際にオブジェクトをファイルから取り込んでみようと思います。