Table of Contents

Nuxt 3 - Add 3D with Three.js.

nuxt-three

Tutorial on how to add 3D to a Nuxt 3 application with Three.js.

Tags: Nuxt 3, Three.js

Time to read: 6 min

Prerequisites

  • Familiarity with the command line
  • Install Node.js version 16.0 or higher

Setup a Nuxt 3 app

Incase you already have a Nuxt 3 app that you wish to add 3D to, you can skip this step and move on to the next.

Otherwise run below commands to create a nuxt 3 app.

npx nuxi init nuxt3-app
cd nuxt3-app
npm i

View app at http://localhost:3000/

Add three.js

Now that we have a Nuxt app, we will add three.js which is a 3D library that tries to make it as easy as possible to get 3D content on a webpage. It uses WebGL to draw 3D to a canvas element.

run below command

npm i three @types/three

Generate .nuxt folder/files

By running 'npm run dev' we will generate the .nuxt folder and files.

npm run dev

Create a component which renders a 3D box

Now we will create a component that will render a 3D box on to a Canvas element. We begin by creating a new folder with a new file:

./components/Box.vue

add the following code to it:

<script setup lang="ts">
import { useThree } from '@/composables/useThree';
import { BoxGeometry, Mesh, MeshLambertMaterial, PerspectiveCamera, Scene, WebGLRenderer } from 'three';
let _scene: Scene;
let _camera: PerspectiveCamera;
let _renderer: WebGLRenderer;
let _renderLoopId: number;
let _box: Mesh;
const { initThree, cleanUpThree } = useThree();
const canvas = computed(() => document.getElementById('mountId') as HTMLCanvasElement);
function animateObject() {
  //rotate object
  _box.rotation.x += 0.01;
  _box.rotation.y += 0.01;
}
function renderLoop() {
  // will keep running for every frame since
  // we keep recreate a new requestAnimationFrame at the end of the function.
  _renderer.render(_scene, _camera);
  animateObject();
  _renderLoopId = requestAnimationFrame(renderLoop);
}
function setupScene() {
  //initialize
  const { scene, camera, renderer } = initThree('mountId');
  _scene = scene;
  _camera = camera;
  _renderer = renderer;
  //create a box and add to scene
  const boxGeometry = new BoxGeometry(1, 1, 1);
  const boxMaterial = new MeshLambertMaterial({ color: 0x00ff00 });
  _box = new Mesh(boxGeometry, boxMaterial);
  _scene.add(_box);
  // start the renderLoop
  _renderLoopId = requestAnimationFrame(renderLoop);
}
onMounted(() => {
  if (canvas.value) {
    setupScene();
  }
});
onBeforeUnmount(() => {
  cancelAnimationFrame(_renderLoopId);
  cleanUpThree(_scene, _renderer);
});
</script>
<template>
  <canvas id="mountId" width="700" height="500" class="m-auto h-[500px] w-[700px] rounded-md" />
</template>

Create a composable for managing three.js

To improve readability a make the app more reusable we will move separate the foundational logic of three.js to a composable. Create a new folder with a new file called:

./composables/useThree.ts

add the following code to the file:

import { PerspectiveCamera, Scene, WebGLRenderer, SpotLight } from 'three';
import { disposeObject } from '@/utils/disposeUtils';
export function useThree() {
  function initThree(canvasMountId: string) {
    const canvas = document.getElementById(canvasMountId)! as HTMLCanvasElement;
    const camera = new PerspectiveCamera(75, 200 / 200, 0.1, 1000);
    camera.position.set(1, 0, 1.7);
    camera.lookAt(0, 0, 0);
    const spotLight = new SpotLight('white', 0.2);
    spotLight.position.set(0.1, -1, 3);
    const scene = new Scene();
    scene.add(spotLight);
    const renderer = new WebGLRenderer({
      canvas,
      antialias: true,
      alpha: true,
    });
    return { scene, camera, renderer };
  }
  function cleanUpThree(scene: Scene, renderer: WebGLRenderer) {
    disposeObject(scene);
    renderer.dispose();
  }
  return {
    initThree,
    cleanUpThree,
  };
}

Add cleanup util

When using three.js its important to remember to clean up when leaving the current route to view something else. Three.js will not do this automatically for you so we will also add a clean up util.

Create a new folder with a new file:

./utils/disposeUtils.ts

add the following code to the file

import type { BufferGeometry, Material, Texture, Mesh, Group, Object3D } from 'three';
export function disposeObject(object: Group | Object3D) {
  if (!object) return;
  const geometries = new Map<string, BufferGeometry>();
  const materials = new Map<string, Material>();
  const textures = new Map<string, Texture>();
  object.traverse((object) => {
    const mesh = object as Mesh;
    if (mesh.isMesh) {
      const geometry = mesh.geometry as BufferGeometry;
      if (geometry) {
        geometries.set(geometry.uuid, geometry);
      }
      const material = mesh.material as any;
      if (material) {
        materials.set(material.uuid, material);
        for (const key in material) {
          const texture = material[key];
          if (texture && texture.isTexture) {
            textures.set(texture.uuid, texture);
          }
        }
      }
    }
  });
  for (const entry of textures) {
    entry[1].dispose();
  }
  for (const entry of materials) {
    entry[1].dispose();
  }
  for (const entry of geometries) {
    entry[1].dispose();
  }
}

Add the Box component where you want to show it.

Now we are finished, just add the Box component where you want to display it. for example, replace the app.vue file with:

<template>
  <Box />
</template>
and now you got yourself a spinning cube in a nuxt application! Enjoy!

Did you like this tutorial?

You can support me, so that i can continue to make more tutorials like this one.

buy-me-a-coffee