Building a 3D Scene in Nuxt with TresJS

Adding 3D graphics and animations to your web applications can be intimidating at first, but what if I told you that you can easily start your journey building 3D experiences with something as familiar to you as Vue Components?

In this tutorial, we’ll be exploring the features and benefits of TresJS, a Vue custom renderer for ThreeJS. Whether you’re a seasoned developer with 3D experience, or a total beginner, this library is definitely worth checking out.

In this guide, we’ll be exploring how to build a 3D scene in a Nuxt 3 app using TresJS using the official Nuxt Module.


Setting up Nuxt.js for 3D

Nuxt is a progressive web framework on-top of VueJS designed to make the developer’s life easier. It can be used for creating server-side rendered (SSR) web applications, statically generated sites (SSG), and single-page applications (SPA).

To initialize a new project, open your terminal of choice and run the following command:

npx nuxi@latest init tres-nuxt

Once the boilerplate is finished, open your project folder in your IDE and install the dependencies:

pnpm install

Run the local server by:

pnpm run dev

You should be able to see the Welcome Page of Nuxt in your localhost:3000

Screenshot 2023-07-07 at 09.36.07.png


Installing the TresJS Module

Now, let’s install the TresJS official module for Nuxt:

pnpm add three @tresjs/nuxt

If you want to get all the benefits of autocomplete and TresJS component IntelliSense in your IDE make sure you install TypeScript and the types definitions for ThreeJS using :

pnpm add @types/three -D

Using the TresJS Nuxt Module

Add @tresjs/nuxt to the modules section of nuxt.config.ts

export default defineNuxtConfig({
  modules: ['@tresjs/nuxt'],
});

That’s it! You can now use @tresjs/nuxt in your Nuxt app. ✨


Adding the 3D Canvas

Jump to your app.vue and replace the default code on the template with this:

<template>
  <TresCanvas window-size>

  </TresCanvas>
</template>

The TresCanvas component is responsible for creating the canvas element in the DOM and injecting ThreeJS into it. By default, it takes the width and the height of the parent component, but for simplicity we will set window-size to true to force the canvas to take the size of the window.

If you open the DOM inspector on your browser you should be able to see the following html generated:

Screenshot 2023-07-07 at 09.51.09.png

But the page is currently blank. That’s because we haven’t added any objects to the scene. If you want to test that everything works fine until this point, you can set another property to the <TresCanvas /> component called clear-color

<TresCanvas window-size clear-color="#82dbc5">
</TresCanvas>

That should set the background-color of the scene like this:

Screenshot 2023-07-07 at 09.55.01.png

Congratulations, you successfully render ThreeJS in your browser. Now, let’s go to the fun part.


Adding a 3D Object

In the context of ThreeJS and 3D in general, an object is the combination of two elements:

  • A Geometry which defines the structure of the object, where the vertices are located (could be a cube, a sphere, a cone, etc).
  • A Material which ****determines the visual properties of the object, such as the color, the opacity and even more complex attributes as metalness and roughness

Combined together they form something we call, a Mesh.

Object 3D meshe.png

To do this with TresJS, create a component called <TresMesh /> and pass both geometry and material component as slots.

<TresCanvas window-size clear-color="#82dbc5">
  <TresMesh>
    <TresBoxGeometry :args="[2,1,1]" />
    <TresMeshBasicMaterial color="yellow" />
  </TresMesh>
</TresCanvas>

This will render something like this:

Screenshot 2023-07-11 at 11.57.27.png

Let’s take a moment to analyze the code above. 🤔

Arguments

First we added a TresBoxGeometry component to create a box structure and we passed a special prop called args with an array of parameters.

These arguments represent the ThreeJS instance constructor parameters:

const geometry = new THREE.BoxGeometry(2,1,1) // [width, height, depth]

Every time you want to initialize a ThreeJS instance, you can do it with the args props.

Props

You can also modify the instances by passing props as you would do in regular Vue components. For example, we added a TresMeshBasicMaterial and we set the color to be yellow.

Under the hood, what the library is doing is this:

const material = new THREE.MeshBasicMaterial()
material.color = new Color('yellow')

Set Props

All properties whose underlying object has a .set() method have a shortcut to receive the value as an array.

For example, the TresMesh has a position property, which is a Vector3 object.

You can pass it to the component like this:

 <TresMesh :position=[2,-1,2]>
    <TresBoxGeometry :args="[2,1,1]" />
    <TresMeshBasicMaterial color="yellow" />
  </TresMesh>

TresJS will automagically 🪄 convert it to this:

mesh.position.set(new Vector(2,-1,2))

Colors

You can pass colors to the components using the color prop, which accepts a string with the color name, hex value or an RGB vector:

<TresAmbientLight color="teal" /><TresAmbientLight color="#008080" /><TresAmbientLight :color="[0.3, 0.5, 0.2]" /> ✅ /// [red, green, blue]

Transforming the Object

Properties can also be use to transform a 3D object’s:

  • Position
  • Rotation
  • Scale

Since we already learned how to change the position of a mesh in the previous section, let’s make some changes to the cube so that we can better understand other transformations. Copy the following code:

<TresCanvas window-size clear-color="#82dbc5">
  <TresMesh>
    <TresBoxGeometry />
    <TresMeshNormalMaterial />
  </TresMesh>
</TresCanvas>

Here, we use the MeshNormalMaterial because it makes it easy to see the different parts of the geometry with RGB colors, which is really useful for demonstration purposes.

<TresMesh>
  <TresBoxGeometry />
  <TresMeshNormalMaterial />
</TresMesh>


Rotation

Rotation is simply changing the orientation of an object around an axis. This is defined by a set of three angles, one for each of the X, Y, and Z axes.

This is typically done using Euler angles in the x, y, and z dimensions. The rotation values are given in radians.

For instance, if you want to rotate an object 90 degrees about the Y axis, you will use Math.PI/2 as the value. You may specify the rotation of the object with the rotation prop, which also takes an array of three values or a Vector3, representing rotations along the x, y, and z axis respectively.

Here’s an example:

<TresMesh :rotation="[Math.PI/4, Math.PI/2, 0]">
  <TresBoxGeometry  />
  <TresMeshNormalMaterial />
</TresMesh>

2phkcgq46o3z8br0t13w.png

In this example, the mesh is rotated 90 degrees around the Y axis and 45 degrees on the X axis. Notice that we did not rotate it around Z (values are 0).


Scale

The scale of an object determines its size in the 3D world. It’s specified in terms of the scale prop. By default, the scaling factor of an object is 1 in all directions.

You can use the scale prop to make an object larger or smaller. For instance, if you want to make an object twice as wide and half as tall, you could pass [2,0.5,1] as the value to the scale prop, like so:

<TresMesh :scale="[2, 0.5, 1]">
   <TresBoxGeometry/>
   <TresMeshNormalMaterial />
</TresMesh>

d16if6h1hv6miu2hs3c1.png


Animating TresJS Objects

The next part is really fun because we are going to bring our box to life. Up until this point, our box has only been rendered “once”, and I say “once” because under the hood ThreeJS has already started a loop using window requestAnimationFrame.

gifpal-example.gif

Similar to how animation works in cinema, to convey the illusion of movement for a 3D object, we need to trick the human eye by rendering several frames (typically 60 per second). This makes our brain believe that the object is being animated. This is where the concept of FPS (frames per second) comes from in video games.

ThreeJS handles rendering for you and provides a composable called useRenderLoop that allows you to register a callback triggered at the native browser refresh rate.

const { onLoop } = useRenderLoop()

onLoop(({ delta, elapsed, clock, dt }) => {
  // I will run at every frame ~ 60FPS (depending of your monitor)
})

The onLoop callback receives an object with the following properties based on the THREE clock:

  • delta: The delta time between the current and the last frame. This is the time in seconds since the last frame.
  • elapsed: The elapsed time since the start of the render loop.

Now that we have the tool, we need to reference our Mesh in the script tag to use it inside of the composable. However, how can we do that if the Mesh is only declared in the template?

To obtain a reference to a DOM element using Vue, we can use Vue’s Template Refs in the same way we would with plain Vue.

const boxRef = ref()

And then in the template:

<TresMesh ref="boxRef">

Once mounted, the boxRef.value will contain our ThreeJS Mesh instance object. We can modify it as needed inside the callback.

const { onLoop } = useRenderLoop()

onLoop(({ delta, elapsed, clock, dt }) => {
  // I will run at every frame ~ 60FPS (depending of your monitor)
  if(boxRef.value) {
     boxRef.value.rotation.y += delta
     boxRef.value.rotation.z = elapsed * 0.2
  }
})

Notice how you can use both delta and elapsed to modify a certain property.

Voilá, our cube is now animated and full of life:


Adding User Interactivity

One of the main benefits of adding 3D to your website is the boost it gives to end-user interactivity. A great example of this is allowing the user to move freely around the scene and use zoom to get a better view of the objects in detail.

We can achieve this with an abstraction component called <OrbitControls /> available in the @tresjs/cientos package.

But why use another package? TresJS is meant to be an ecosystem where you can achieve pretty much everything you could do in plain ThreeJS by using the core package of TresJS. However, this can also result in a lot of complex lines of code and logic. The cientos package aims to reduce the effort required and provide cool abstractions in the form of components and composable features to improve the developer experience.

To use the @tresjs/cientos package you just need to install it locally:

pnpm add @tresjs/cientos

And then re-start the Nuxt app so the @tresjs/nuxt module auto-imports all the necessary code.

The <OrbitControls /> abstraction seamlessly implements the ThreeJS OrbitControls, eliminating the need for manual configuration or updating of the camera.

<template>
  <TresCanvas window-size clear-color="#82dbc5">
    <OrbitControls />
    <TresMesh ref="boxRef">
      <TresBoxGeometry />
      <TresMeshNormalMaterial />
    </TresMesh>
  </TresCanvas>
</template>

But what about changing the color of the background of the scene, for example?


useTweakpane

Sometimes, you may want to give users control over customizing aspects of the 3D scene based on front-end inputs.

The cientos package offers a handy composable called useTweakpane around Tweakpane, a compact pane library used to fine-tune parameters and monitor value changes.

Let’s say we define our canvas props on a reactive state:

<script setup lang="ts">
  // Rest of code

  const gl = reactive({
    clearColor: '#82dbc5',
  })
</script>

<template>
  <TresCanvas window-size v-bind="gl">
    <OrbitControls />
    <TresMesh ref="boxRef">
      <TresBoxGeometry />
      <TresMeshNormalMaterial  />
    </TresMesh>
  </TresCanvas>
</template>

To modify the clearColor of the canvas, simply use the pane from the useTweakPane composable and add an input on the desired property, as shown below:

<script setup lang="ts">
  // Rest of code

  const gl = reactive({
    clearColor: '#82dbc5',
  })

  const { pane } = useTweakPane()

  pane.addInput(gl, 'clearColor', { label: 'Background' })
</script>

And voilá, now you have control over the canvas background color with a color picker out-of-the-box .😊

Screenshot 2023-07-24 at 11.57.51.png


Wrapping Up

Congratulations! You have successfully implemented your very first 3D experience on Nuxt using TresJS. Wasn’t that hard, was it 😉?

You learned how to configure and use the @tresjs/nuxt package in your Nuxt app. You can add a TresCanvas component to render the scene, create a 3D box using geometries and materials, use props and args, and even animate your cube using transforms and the render loop useRenderloop composable.

You even earned how to extend the core package functionality by installing @tresjs/cientos. This allows you to add user interactivity to the scene and save time while improving the development experience.

Here are some handy resources if you want to continue your journey with TresJS:

Download the cheatsheets

Save time and energy with our cheat sheets.