対象読者
nuxtの中でthree.jsをやることになった(やってみたい)フロントエンドエンジニア
概要
nuxtの中でthree.jsを使う方法を説明していきます。
基本的に自由にやっていいと思うんですが個人的にこれが良いと思う書き方でやっていきます。
ページ遷移するとオブジェクトが変形するサンプルです。
ソースコード
デモ

nuxt導入は省略します。
(Qiitaに入門記事がたくさんあってドキュメントにも説明があるので初めてnuxt触る方はそちらを参考にしてみてください。)
手順1. three.jsをnpm installまたはyarn addする
nuxtのもろもろのセットアップが終わったら、npmまたはyarnでthree.jsを追加します。
yarn add three --save
手順2. canvasのcomponentを作る
canvasのためのcomponentを作ります。
componentsの中にArtworkという階層を作ってその中にindex.vueを作ります。
componentの名前は何でもいいです。
そのあとthree.jsで書かれたjsモジュール(Artwork/js/ArtworkGL)を作って読み込みます。
ここの名前もなんでもいいです。
最初は空のclassだけ書いておいて、あとで具体的に書きます。
three.jsとdomの処理を住み分けさせる。
注意することといえば、
data()の中にthree.jsのオブジェクトを入れないことです。
基本的にdata()に登録するパラメーターはdomのレンダリングのためのもので、値が変化したかどうか常に監視されています。
three.jsのひとつひとつのオブジェクトは階層が深いです。
そのため末端まで監視されていることになります。かなり無駄です。
なにかthree.jsのパラメータをdomに表示させたいなどのことをがない限り、ここに登録することはお勧めしないです。
そしてmethodsにthree.jsの処理をばりばり書かないことです。
methodsは主にdomのイベントや処理を書くためのものです。
three.jsで書くコードは長すぎてdom処理をするときの(見やすさの)邪魔になってしまうので、別ファイルに書いていきます。
<template>
  <section class="artwork">
    <canvas class="artwork__canvas" ref="canvas"></canvas>
  </section>
</template>
<script>
import ArtworkGL from "./js/ArtworkGL";
export default {
  name: 'artwork',
  components: {},
  props: [],
  data () {
    // 基本的にはここにthree.jsのオブジェクトを追加しない。
    return {
    }
  },
  computed: {
  },
  mounted () {
     // canvas要素を渡す。
     this.artworkGL = new ArtworkGL({
       $canvas: this.$refs.canvas
     });
  },
  destroyed() {
    // canvasを作ったり壊したりする前提の場合はここに処理停止する処理を書く(今回省略)。
  },
  methods: {
    // この中にthree.jsの処理をばりばり書かない。
  }
}
</script>
<style>
  /* スタイルなどお好みで */
  .artwork{
    position: fixed;
  }
</style>
import * as THREE from "three";
// three.jsの処理を書いていく
export default class ArtworkGL{
  constructor(props){
  }
}
手順3. default.vueにcomponentをimportする
どこに表示させるかにもよりますが、canvasをdefault.vueに配置して、すべてのページでdefault.vueを使うのがベストだと思います。1ページのうちの一部でしか使わないというときはそのページに配置するしかないですが、複数のページで共通で使う場合は一個のcanvasを使うほうがいいのでdefault.vueに置きます。
ページ遷移するごとにcanvas要素やWebGLコンテクストやthree.jsのオブジェクトなどを壊したり作り直したりするよりも、内部でオブジェクトの非表示・表示を切り替えたりレンダリングを停止・再開させる処理をしたほうがエコです。
なので、一個のcanvasをdefault.vueに配置させ、routeの監視などでWebGLのシーンを切り替える方法でやっていきます。
<template>
  <div>
    <Artwork />
    <nuxt />
  </div>
</template>
<script>
import Artwork from "~/components/Artwork";
export default {
  components: {
    Artwork
  },
  watch: {
     // routeが変わるときにシーンを変えるなどなにか処理する
    '$route.name': function(_new, _old){
     }
  }
}
</script>
手順4. あとはthree.jsを書いてく
さきほど作ったArtworkGL.jsに書いていきます。
ここからはnuxtの支配外なのでwebpackで開発しているときと同じように書いていきます。
デモのthree.js部分ソースコード
手順は以上です。あとはyarn run devをしてブラウザを見ながら開発をしていきます。
おまけ1. domとthree.jsのオブジェクトを連動させたいとき
vueはdomのレンダリングのためのファイルなので、domとthree.jsは住み分けさせるということを言いました。
しかしdomが変化したときにthree.js側も変化させたり、逆の場合もあったりすると思います。
そういうときはEventBusが便利です。
自前のEventBusでもいいですしVueを使ってもいいと思います。
VueをEventBusにする
ルートディレクトリにutilsを作ってその中にevent-bus.jsを置きます。
import Vue from 'vue';
const EventBus = new Vue();
export default EventBus;
ちょっと雑ですが使い方の例を置いておきます。
domからthree.jsへのイベント発火
例えばdom側で変化したときにthree.js側でcameraの向きを変えたいときは、three.js側にイベント登録をしておいてdom側でイベント発火をさせます。
<template>
  <section class="artwork">
    <div @click="onChangeCamera('left')">toLeft</div>
    <div @click="onChangeCamera('right')">toRight</div>
  </section>
</template>
<script>
import ArtworkGL from "./js/ArtworkGL";
import EventBus from "~/utils/event-bus";
export default {
  name: 'artwork',
  components: {},
  props: [],
  data () {
    return {
    }
  },
  methods: {
     onChangeCamera(direction){
         // $emit()でイベント発火。第一引数にイベント名(任意)。それ以降の引数には渡したい値を入れる。
         EventBus.$emit("CHANGE_CAMERA", direction);
     }
  }
import * as THREE from "three";
import EventBus from "~/utils/event-bus";
export default class ArtworkGL{
  constructor(){
     // $on()でイベント登録。
     // $emit()でいれた名前と同じものをいれる。第二引数に発火させたい関数を登録する。
     EventBus.$on("CHANGE_CAMERA", this.onChange.bind(this));
  }
  // 省略
  onChange(direction){
     switch(direction){
        case "left": 
             this.camera.position(-4, 0, 0); 
             this.camera.lookAt(this.scene.position);
             break;
        case "right": 
             this.camera.position(4, 0, 0); 
             this.camera.lookAt(this.scene.position);
             break;
     }
  }
three.jsからdomへのイベント発火
逆の場合でも同じような使い方です。
下のコード例は、three.jsでオブジェクトが動いてるか動いてないかをdom側で表示するという処理してます。
import * as THREE from "three";
import EventBus from "~/utils/event-bus";
export default class ArtworkGL{
  constructor(){
     this.obj = new THREE.Object3D();
     this.render();
  }
  render(){
     // 省略(何らかの処理)
   
     if(this.obj.userData.moving !== this.obj.userData.moving_old){
        // 任意のパラメーターが変わったら発火させる。
        EventBus.$emit("TOGGLE_OBJ", this.obj.userData.moving)
     }
     requestAnimationFrame(this.render.bind(this));
  }
<template>
  <section class="artwork">
    <div>
      Moving: {{ moving }}
    </div>
  </section>
</template>
<script>
import ArtworkGL from "./js/ArtworkGL";
import EventBus from "~/utils/event-bus";
export default {
  name: 'artwork',
  components: {},
  props: [],
  data () {
    return {
       moving: false
    }
  },
  mounted(){
     // イベント登録
     EventBus.$on("TOGGLE_OBJ", this.onToggle);
  },
  destroyed(){
     // イベント解除
     EventBus.$off("TOGGLE_OBJ", this.onToggle);
  },
  methods: {
     onToggle(moving){
        this.moving = moving;
     }
  }
おまけ2. シェーダをインポートさせたいとき
自作シェーダをインポートして使いたいときは、raw-loaderを使います。
まずyarn addまたはnpm installで追加します。
yarn add raw-loader --save-dev
nuxt.config.jsに.vertと.fragの拡張子のときだけraw-loader使うことを書きます。
export default {
  build: {
    extend (config, ctx) {
      if (!!config.module) {
        config.module.rules.push({ test: /\.(vert|frag)$/i, use: ["raw-loader"] });
      }
    }
  },
あとはシェーダを使いたいところでimportします。
import * as THREE from "three";
import vertexShader from "./glsl/shape.vert";
import fragmentShader from "./glsl/shape.frag";
export default class Shape{
  constructor(){
    // なにか処理
    this.init();
  }
  init(){
    this.material = new THREE.ShaderMaterial({
       vertexShader: vertexShader,
       fragmentShader: fragmentShader
    });
まとめ
以下伝えたかったことまとめです。
1. three.jsはひとつひとつのオブジェクトが数珠つながりでかなり大きいのでvueのdata()には入れない!
2. methodsに長ーいthree.jsコードを書こうとしない!
3. three.jsのオブジェクトが大きいということは、壊したり作り直したりを避けてなるべく使いまわししたほうがいい!
4. domとthree.jsのやりとりにはEventBusですると便利!
おわりです。
