LoginSignup
99
64

More than 3 years have passed since last update.

three.js with Nuxt.js

Last updated at Posted at 2019-12-15

対象読者

nuxtの中でthree.jsをやることになった(やってみたい)フロントエンドエンジニア

概要

nuxtの中でthree.jsを使う方法を説明していきます。
基本的に自由にやっていいと思うんですが個人的にこれが良いと思う書き方でやっていきます。

ページ遷移するとオブジェクトが変形するサンプルです。
ソースコード
デモ
sammm.jpg

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の名前は何でもいいです。

Welcome - threejs-nuxt-sample - Visual Studio Code 2019_12_11 17_30_51 (2).png

そのあと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処理をするときの(見やすさの)邪魔になってしまうので、別ファイルに書いていきます。

components/Artwork/index.vue
<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>
components/Artwork/js/ArtworkGL.js
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のシーンを切り替える方法でやっていきます。

layout/default.vue
<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を置きます。

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);
     }
  }

ArtworkGL.js
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側で表示するという処理してます。

ArtworkGL.js
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使うことを書きます。

nuxt.config.js

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ですると便利!

おわりです。

99
64
0

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
  3. You can use dark theme
What you can do with signing up
99
64