Nuxt3でthree.jsを使いたい!
はじめに
最近Nuxt3とthree.jsについて学んでいて、両方いっぺんに学びたい!と思いこの記事を書きました。
この記事のゴール
Nuxt3とthree.jsを使ってSPAで3Dオブジェクトを描画する。
バージョン
node -> v16.11.0
Nuxt -> v3.0.0-rc.6
three.js -> v^0.142.0
サンプル
ソースコード
環境構築
Nuxt3 インストール
npx nuxi init three-nuxt3
cd three-nuxt3
npm i
npm run dev
http://localhost:3000/
でページが表示されれば完了。
three.js インストール
npm i --save three
npm i @types/three
Windi CSS インストール (しなくても良い)
Windi CSS
を使用しない方はスキップしてください。
npm i nuxt-windicss -D
nuxt.config.ts 修正
nuxt-windicss
を追加
import { defineNuxtConfig } from 'nuxt'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
+ buildModules: [
+ 'nuxt-windicss',
+ ],
})
実装
今回は2ページ作成してSPAでページ遷移できるようにしました。
app.vue
+ <script setup lang="ts">
+ useWindowSize()
+ </script>
+
<template>
+ <div id="app" class="fixed top-0 left-0 w-full h-full">
+ <NuxtPage />
</div>
</template>
composables/window-size.ts
新規作成
画面サイズの状態管理
import { Ref } from 'vue'
const clientWidth: Ref<number> = ref(0)
const clientHeight: Ref<number> = ref(0)
const targetDom: Ref<HTMLElement | null> = ref(null)
export const useWindowSize = () => {
onMounted(() => {
targetDom.value = document.getElementById('app')
clientWidth.value = targetDom.value.clientWidth
clientHeight.value = targetDom.value.clientHeight
window.addEventListener('resize', updateWindowSize)
})
const updateWindowSize = () => {
clientWidth.value = targetDom.value.clientWidth
clientHeight.value = targetDom.value.clientHeight
}
return {
clientWidth,
clientHeight
}
}
pages/index.vue
新規作成
<script setup lang="ts">
import { Ref } from 'vue'
const container: Ref<HTMLElement | null> = ref(null)
const { clientWidth, clientHeight } = useWindowSize()
onMounted(() => {
const { init } = useSphere(container, clientWidth, clientHeight)
init()
})
</script>
<template>
<div class="fixed top-0 left-0 w-full h-full" ref="container">
<div class="absolute top-0 left-0">
<NuxtLink to="box" class="block py-4 px-12 bg-white font-bold hover:underline">Box ></NuxtLink>
</div>
</div>
</template>
composables/sphere.ts
新規作成
import { Line, LineBasicMaterial, PerspectiveCamera, Scene, SphereGeometry, Vector3, WebGLRenderer } from 'three'
import { Ref } from 'vue'
export const useSphere = (container: Ref<HTMLElement>, clientWidth: Ref<number>, clientHeight: Ref<number>) => {
const init = () => {
// レンダラー作成
const renderer = new WebGLRenderer()
renderer.setSize(clientWidth.value, clientHeight.value)
renderer.setPixelRatio(clientWidth.value / clientHeight.value)
container.value.appendChild(renderer.domElement)
// シーン追加
const scene = new Scene()
// カメラ作成
const camera = new PerspectiveCamera(45, clientWidth.value / clientHeight.value)
camera.position.set(20, 20, 20)
camera.lookAt(new Vector3(0, 0, 0))
// 球体作成
const geometry = new SphereGeometry(10, 32, 32)
const material = new LineBasicMaterial({ color: 0x6699ff, linewidth: 1 })
const sphere = new Line(geometry, material)
scene.add(sphere)
// 毎フレーム時に実行されるループイベント
const tick = () => {
// 球体を回転
sphere.rotation.x += .01
sphere.rotation.y += .01
// レンダリング
renderer.render(scene, camera)
requestAnimationFrame(tick)
}
tick()
// コンテキスト削除
onUnmounted(() => {
renderer.dispose()
renderer.forceContextLoss()
})
}
return { init }
}
pages/box.vue
新規作成
<script setup lang="ts">
import { Ref } from 'vue'
const container: Ref<HTMLElement | null> = ref(null)
const { clientWidth, clientHeight } = useWindowSize()
onMounted(() => {
const { init } = useBox(container, clientWidth, clientHeight)
init()
})
</script>
<template>
<div class="fixed top-0 left-0 w-full h-full" ref="container">
<div class="absolute top-0 left-0">
<NuxtLink to="/" class="block py-4 px-12 bg-white font-bold hover:underline">Sphere ></NuxtLink>
</div>
</div>
</template>
composabels/box.ts
import { BoxGeometry, Mesh, MeshNormalMaterial, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from 'three'
import { Ref } from 'vue'
export const useBox = (container: Ref<HTMLElement>, clientWidth: Ref<number>, clientHeight: Ref<number>) => {
const init = () => {
// レンダラー作成
const renderer = new WebGLRenderer()
renderer.setSize(clientWidth.value, clientHeight.value)
renderer.setPixelRatio(clientWidth.value / clientHeight.value)
container.value.appendChild(renderer.domElement)
// シーン作成
const scene = new Scene()
// カメラ作成
const camera = new PerspectiveCamera(45, clientWidth.value / clientHeight.value)
camera.position.set(20, 20, 20)
camera.lookAt(new Vector3(0, 0, 0))
// 箱作成
const geometry = new BoxGeometry(10, 10, 10)
const material = new MeshNormalMaterial()
const box = new Mesh(geometry, material)
scene.add(box)
// 毎フレーム時に実行されるループイベント
const tick = () => {
// 箱を回転
box.rotation.x += .01
box.rotation.y += .01
// レンダリング
renderer.render(scene, camera)
requestAnimationFrame(tick)
}
tick()
// コンテキスト削除
onUnmounted(() => {
renderer.dispose()
renderer.forceContextLoss()
})
}
return { init }
}
コード上で行っていることはコメントとして入れておきました。
詳しく知りたい方は公式ドキュメントを参照してください。
Nuxt3
three.js
詰まった箇所
作ってみて詰まった箇所を共有します。
SPAで遷移先で描画されていない
3DオブジェクトをonMounted
時にdomに描画しているのですが、ページ遷移すると3Dオブジェクトが描画されない問題。
ページ遷移先の画面サイズが取得できていないのが原因っぽい。
解決方法
composabels/window-size.ts
で画面サイズを状態管理して解決しました。
(多分もっと良い方法があると思う)
ページ遷移しすぎるとErrorが出る
three.module.js:26455 WARNING: Too many active WebGL contexts. Oldest context will be lost.
最大で15個のWebGLコンテキストが有効でそれを超えるとErrorが出る。
解決方法
// コンテキスト削除
onUnmounted(() => {
renderer.dispose()
renderer.forceContextLoss()
})
onUnmounted
時にコンテキストを削除したら解決しました。
ただ、ページ遷移時に一瞬だけ画面が白くなってしまうのでそれもなんとかしたい。
さいごに
Nuxt3 + three.js で3Dオブジェクトを描画する方法と詰まった箇所をまとめました。
この記事に書いたコードは僕の試行錯誤の結果なのでより良いコードがある場合は教えてもらえるとありがたいです🙇♂️
参考文献
Discussion