Articles
- 2023 02
- 2022 11
- 2022 10
Nuxt 3 - Add 3D with Three.js.
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.