【Vuetify + Three.js】Vuetifyのダイアログ（モーダル）上でThree.jsのシーンを表示する

# What`s the problem?
以下のようなコードを書いてVuetifyの[Dialog component](https://vuetifyjs.com/ja/components/dialogs/)の中でThree.jsのシーンを表示させようとすると上手くいかなかった。
##### コード
```vue:App.vue
<template>
  <v-app>
    <v-row align="center">
      <v-col align="center">
        <v-dialog v-model="dialog">
          <template v-slot:activator="{ on, attrs }">
            <v-btn color="primary" dark v-bind="attrs" v-on="on">
              Open Dialog
            </v-btn>
          </template>

          <v-card>
            <v-card-title>Three.js in dialog</v-card-title>
            <!-- ここにシーンを表示したい -->
            <v-container>
              <canvas ref="canvasRef" class="canvas"></canvas>
            </v-container>
          </v-card>
        </v-dialog>
      </v-col>
    </v-row>
  </v-app>
</template>

<script>
import * as THREE from "three";

export default {
  name: "App",

  data() {
    this.scene = null;
    this.camera = null;
    this.renderer = null;
    this.cube = null;

    return {
      dialog: false,
    };
  },

  methods: {
    // 必要なインスタンスを初期化
    init() {
      this.scene = new THREE.Scene();
      this.camera = new THREE.PerspectiveCamera(75, 2, 0.1, 1000);
      this.renderer = new THREE.WebGLRenderer({ canvas: this.$refs.canvasRef });
      this.renderer.setSize(1000, 500);

      const geometry = new THREE.BoxGeometry();
      const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
      this.cube = new THREE.Mesh(geometry, material);
      this.scene.add(this.cube);

      this.camera.position.z = 5;
    },

    // アニメーション
    animate() {
      requestAnimationFrame(this.animate);
      console.log(this.cube.rotation.x);
      this.cube.rotation.x += 0.01;
      this.cube.rotation.y += 0.01;
      this.renderer.render(this.scene, this.camera);
    },
  },

  watch: {
    // ダイアログが開いたら初期化してアニメーションを開始
    dialog: function (val) {
      if (val) {
        this.init();
        this.animate();
      }
    },
  },
};
</script>

<style scoped>
.canvas {
  display: block;
  margin-left: auto;
  margin-right: auto;
}
</style>
```
##### 結果
![qiita1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2667735/d8e45619-733e-8291-381a-e123a1332e09.gif)

# Solution
シーンの初期化とアニメーションを行う際に、`setTimeout()`関数を利用すると上手く表示することができた。
##### コード
`````vue:App.js
<template>
  // 略
</template>

<script>
import * as THREE from "three";

export default {
  // 一部略

  watch: {
    // ダイアログが開いたら初期化してアニメーションを開始
    dialog: function (val) {
      if (val) {
        // 変更点はここ！
        setTimeout(() => {
          this.init();
          this.animate();
        });
      }
    },
  },
};
</script>

<style scoped>
// 略
</style>
`````
##### 結果
![qiita2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2667735/6f8044b1-0d70-6628-72e2-726ae0d37e04.gif)
# 仕組み
そもそもの話、ダイアログの中になぜシーンを表示できないかと言うと、Vuetifyのダイアログを開く処理と並行してThree.jsのシーン等を初期化する処理を走らせていることが原因らしい。（詳細な仕組みについては分からないが、なんとなくダメそうな雰囲気は確かにある）

`setTimeout()`は通常第二引数に時間を与えることで指定した秒数だけ待機した後に関数を実行してくれる。しかし、第二引数を与えない or 0を与えると[できるだけ早く実行](https://developer.mozilla.org/ja/docs/Web/API/setTimeout#:~:text=%E3%81%97%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82-,delay,-%E7%9C%81%E7%95%A5%E5%8F%AF)する、つまりダイアログを開く処理が完了するまで待機してくれるようになるため、ダイアログを開く処理とThree.jsの初期化処理が競合せずに済むようになる。

# 余談
筆者はこの問題を解決するために1週間無駄にしました()

「setTimeoutで無理やり描画を遅延させれば何とかなるんじゃね！？」という浅はかな考えの元0.1秒程度遅延させてみた結果偶然にも上手くいき、その後第二引数に0を与えても同様の結果が得られることに気付き、そこで初めて`setTimeout()`の上記の仕様を知り「なるほどなぁ」という気持ちになりました。そもそも第二引数を省略可能だなんて知らなかった...

本記事は[東京高専プロコンゼミ② Advent Calendar 2022](https://adventar.org/calendars/8371)の25日目の記事です。昨日の記事と合わせて2日連続で個人的なトラブルシューティングをアドベントカレンダーに投稿するという暴挙となりましたが、将来これらの記事が誰かの役に立てればと思います。