LoginSignup
6

More than 1 year has passed since last update.

posted at

updated at

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

今回からThree.jsを使って3Dモデルを表示できるアプリを作成していきます。

Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン(導入編)[今回]
Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン(ファイルインポート編)
Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン(投球編)
Three.js with Vue3 Typescript で作る3Dモデル表示アプリハンズオン(Raycaster編)

ゴール

このハンズオンをやると次のようなアプリが作れちゃいます。
一通り.gif

今回のゴール

次のようなアニメーションが作れるようになるのが今回のゴールです。(GitHub)

タイトルなし.gif

導入

リポジトリの作成

とりあえずまずはvue projectの作成です。(vue-cliが入っていない人は公式を参考にnpmインストールしてください。)とりあえずTypeScriptを指定してプロジェクトを作成します。(vueのセットアップが面倒な人はThree.jsの導入までスキップしてください。レポジトリをクローンすればinitブランチからすぐに始められます。)

vue create app
cd app

ここからはコードを書き換えていきましょう。

まず、Helloworld.vueThree.vueという名前に書き換えます。(便宜上)

次に、App.vueApp.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で行なっていきます。(適宜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を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

解説

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

    // Three.js
    const scene = new Scene();
    const camera = new PerspectiveCamera();
    const renderer = new WebGLRenderer();

Blenderなどの3Dモデルソフトを使ったことがある方なら馴染み深いかもしれません。これらはそれぞれ次のようなものです。(詳しくは公式を参照)

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

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

    // 初期化
    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関数を見てみましょう。

    // 描画
    const animate = () => {
      const frame = () => {
        // 描画
        renderer.render(scene, camera);
        // 画面を更新
        requestAnimationFrame(frame);
      };
      frame();
    };

これもコメントに書いてある通りですが、アニメーションの各フレームで描画を行なっています。

オブジェクトの挿入と視点の変更

球体の追加

このままでは何もないので、空間に球体を追加しましょう。次のような関数を作成します。

    // 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

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

    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

カメラの視点変更

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

    // 描画
    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>

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6