DEV Community 👩‍💻👨‍💻

Cover image for Crash course in interactive 3d animation with React-three-fiber and React-spring
Keerthi
Keerthi

Posted on • Updated on

Crash course in interactive 3d animation with React-three-fiber and React-spring

Introduction

There are a growing number of sites out there that utilises interactive 3d animations. The interaction experience of these sites gives you a feeling that you are interacting with real world objects. This trend is only going to grow because of the huge investment that companies are pledging so that they can get a foot in door in the Metaverse world.

Here are two great industry recognised examples of the usage of interactive 3d animation:
1] Bruno Simons award winning website - This site is designed like a 3D game, and you can navigate it using the car.

Bruno Simons wes bsite

2] Github home page - Github has incorporated this interactive 3d globe that you can interact with to see live Github commits.

Github landing page

Both these sites are built with three.js. The problem with three.js is that it has a very steep learning curve. You have to write a lot of code to do simple things. Luckily in react we have a solution in the form of react-three-fiber. React-three-fiber has simplified much of the complex coding by taking advantage of component based patterns which have simplified apis. Under the hood it is still three.js without compromising anything.

If you are intending to use interactive 3D animation in a future React.js project and would like to learn more about react-three-fiber, then this post will be perfect to start that journey.

What will be covered in this post are :

  • Quick intro to three.js, React-three-fiber and interactivity
  • A basic introduction to 3D concepts like the 3d co-ordinate system
  • Some basic concepts about how 3d projects are structured in three.js
  • A walk through on how to build your first 3d interactive app with react-three-fiber and react-spring

If you want a Video version on building the demo app you can skip the article and watch this Youtube video :

I also have an advanced video on building a product customizer

If you enjoy the theory part then enjoy reading the rest of this article.

Quick intro to three.js, React-three-fiber and interactivity

Three.js is the defacto 3d animation library that has become popular among Javascript developers. React-three-fiber is the three.js renderer for React.js. Everything that you can do with three.js can be done with react-three-fiber. Additionally 3d objects can also be made interactive relatavily easily. For example you can attach event handlers to handle hover and click events. In React.js you can manage the state of the 3d objects with state management in turn change its properties. In the demo we are going to build , we will change the color and size of a 3d Cube when hovered and clicked respectively.

React-three-fiber has a great ecosystem around it too. It even includes integration of the popular animation library react-spring. In this post you will learn how to integrate react-spring with react-three-fiber. You can check here for more libraries that work with react-three-fiber . You can also see that it has an accessibility module that is well documented too.

..and if you want to see more great examples made by react-three-fiber, you can go here

A bit about 3d coordinate system

When you work with CSS and Html, you position things with relative or absolute positions, Where you would set values for left, right, top and bottom properties of a div to absolute position that div. When you work with 3D, you would need to know about positioning things in 3d space, which is a whole new concept to grasp. Imagine you are in a room and there is a chair floating in thin air between the floor and the ceiling, in 3d you have a way of pin pointing its location using three values, this is the x, y and Z value. These values will be relative to some origin, lets just say for this the origin will be a chosen corner of the room. Lets look at another example of a transparent cube positioned in the corner of a room.

3D coordinate system

The cube has dimension of 2 units for height, width and depth. In the diagram I have marked the 8 corners of the cube with a circle and the associated co-ordinate, the corner of room is the origin (0,0,0). I have also marked the X,Y and Z axis with red arrows. We are used to dealing with X and Y axis, this is seen as 2D. But here we have an extra Z axis which can be seen as depth. As you can see in the example. If you look at the cube corner with (2,2,2) value, you see that this is the only point of the cube that does not touch the left wall, right wall or the floor but is elevated from the ground. This is the only point that has no zero value as well. So the important thing here is that the first 2 numbers are the x, y axis positions as in 2D, and the third number deals with depth. This is a basic explanation but please note that all of the axis can be negative numbers as well, this will mean that the points will fall outside of our visible room. You can think of it as absolute positioned div where you would give negative left or negative top value to move it outside of its parent div.

As an exercise you can go to three.js official playground where you can try out things. The link is https://threejs.org/editor/

Three.js playground

In the above screen shot all I did was add a box, drag the handle and observed the positional values. You can drag all handles and experiment with this. The playground can do much more complex things, but that's for another day when you progress from the basics.

Structure of a very basic 3D project in three.js

Before we dive into React-three-fiber, you need to have a basic idea of how a project is structured in three.js. Below is a diagram outlining this.

3d App structure

To summarise I have listed the the items shown in the above diagram.:

  • Scene - A scene will contain all of the 3D objects. Each object is also referred to as a Mesh
  • Mesh - This is a basic scene object , and it's used to hold the geometry and the material needed to represent a shape in 3D space.
  • Geometry - Geometry defines the shape, You can think of it as a skeleton structure without the graphical details
  • Material - Defines how the surface of the shape looks, this would be the graphical details.
  • Camera - This will capture everything in the scene and it also has a position value. Light - You need a light source to see your object. If you don't have light source then you wont see the colour and shadows as you would in real life.

and a basic code to render a cube with out light or color would look like this:

//The renderer will have access to the Canvas DOM element to
//update the display to show our 3d cube
const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)

// Create Scene and camera
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

//Mesh  - Holds the 3d object that will be added to scene
const mesh = new THREE.Mesh()

//Geometry  is a property of Mesh
// Our mesh will have a geometry for a box or cube
mesh.geometry = new THREE.BoxGeometry()
//Material  is also a property of Mesh
mesh.material = new THREE.MeshStandardMaterial()

//Add our Mesh to the scene
scene.add(mesh)

// After you have composed your scene with your mesh 
// and added the camera, you can render the whole thing
// with animation loop
function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}

animate()
Enter fullscreen mode Exit fullscreen mode

If you read the comments in the code, you will have a rough idea of the structure. I will not go deep into this as its quiet complex already. This is where React and React-three-fiber comes to the rescue. React-three-fiber abstracts out the above complexities and allows us to create 3D animation declaratively. In the rest of this post I will show you how to build our interactive cube with smooth animation using react-three-fiber and spring animation.

Part2 : How to build your first 3d interactive app with react-three-fiber and react-spring

In this section we will build an interactive cube that will be rotating, change colour when you hover over it and it will grow bigger when you mouse click on it. We will be using react-spring for smooth animation too.

Step 1: Project setup

I will be assuming that you have already setup a basic react app already using create-react-app . Then you can run

npm install three @react-three/fiber

, this will install three.js and react-three-fiber. Afterwards install the Drei dependency

npm install @react-three/drei

. Note the drei component gives react-three.fiber some super powers, but we will only be using it for the light source.

Step 2: Add some basic styling

Then in your app.css copy the following basic styles:

//app.css
html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Basic app.js structure to set the canvas, scene and light.

This is a skeleton structure of our app.js. We will be filling in the blansk as we go along.


import {useState,useRef} from "react"
import {Canvas, useFrame} from "@react-three/fiber"
import  "./app.css"

function Cube(props) {

      // Code for our 3d cube  goes here. In other words Our mesh

}


function App() {
  return (
   <Canvas>
     <ambientLight />
     <Cube />
   </Canvas>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

At the top we have the dependencies. We are going to use useState and useRef.

import {useState,useRef} from "react"
import {Canvas, useFrame} from "@react-three/fiber"
import  "./app.css"
Enter fullscreen mode Exit fullscreen mode

We will be letting react-three-fiber deal with the rendering, thats why we are use useRef, this allows us to access the DOM directly.

We import Canvas from react-three-fiber, this allows us to create a WebGl container to render our 3D. The useFrame is standard animation hook for react-three-fiber.

We are also attaching the app.css that we wrote in the last step.

Lets look at our App function:

 function App() {
  return (
   <Canvas>
     <ambientLight />
     <Cube />
   </Canvas>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the JSX part of our App function, we are using the react-three-fiber Canvas component as the wrapper. This has two child elements, one is a light source <ambientLight />. Second element is <Cube /> , The component will be rendering our mesh that defines our 3d cube. We have yet to write code for this as you saw in our skeleton code previously.

Note that typically you would also add a camera, but for our example we can leave it because React-three-fiber adds a camera automatically with a default position. So we will just go along with the default.

Step 4: Lets write code to define our 3d Cube

Our cube function will look like this:

function Cube(props) {
  // Use useRef hook to access the mesh element
  const mesh=useRef()

  // Jsx to rnder our cube
  return (
          <mesh ref={mesh}>
             <boxGeometry args={[2,2,2]}/>
             <meshStandardMaterial /> 
          </mesh>

  )
}
Enter fullscreen mode Exit fullscreen mode

All we are doing here is creating a <mesh /> element that has a ref={mesh} attribute, we are using this so that react-three-fiber can access the mesh element directly. Hence we have the line const mesh=useRef() to declare this ref value. The <mesh /> element has a child element and <meshStandardMaterial />. Remeber In three.js mesh elements have a geometry and material and thats what these elements are for.

The args in <boxGeometry args={[2,2,2]}/> element is for the dimensions. We are using na array with three values to create a cube with height, width and depth all equal to units.

Our code for app.js looks like this now :

import { useState, useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import "./app.css";

function Cube(props) {
  // Use useRef hook to access the mesh element
  const mesh=useRef()

  // Jsx to render our 3d cube. Our cube will have height
  // width and depth equal 2 units. 
  // You also need a material so that you can add color
  // and show shadows. We are using the standard
  // material <<meshStandardMaterial /> 
  return (
          <mesh ref={mesh}>
             <boxGeometry args={[2,2,2]}/>
             <meshStandardMaterial /> 
          </mesh>

  )
}

// Basic app structure to render a 3d cube
//<ambientLight /> is the standard light to use, otherwise
// everything comes out as black
function App() {
  return (
    <Canvas>
      <ambientLight />
      <Cube />
    </Canvas>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the browser you would see a grey box , as shown below. But its actually a cube. We only see the front face at the moment. In the next section we will add some colour and rotation.

3D gray cube

Step 5: Add some fancy lights, colour and animation

To give our cube realistic shadows, we need to add a specific point light element with a position, which looks like this <pointLight position={[10,10,10]} />. This is to be added our App function just after the <ambientLight />.

Our App function looks like this now :

function App() {
  return (
    <Canvas>
      <ambientLight />
      <pointLight position={[10,10,10]} />
      <Cube />
    </Canvas>
  );
}

Enter fullscreen mode Exit fullscreen mode

Now lets turn our attention to our Cube function to add colour and basic animation. To add colour to our cube we add an attribute to the meshStandardMaterial element, so it becomes <meshStandardMaterial color={"orange"}/> . Here we are setting color to orange.

To add a basic rotation animation we use the animation frame hook, and it will look like this useFrame ( ()=> (mesh.current.rotation.x += 0.01)) . Here we are accessing the mesh.current.rotation.x value to increase it by 0.01 unit. Its basically rotating on the x axis.

Our code looks like this:

import { useState, useRef } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import "./app.css";

function Cube(props) {
  // Use useRef hook to access the mesh element
  const mesh = useRef();

  //Basic animation to rotate our cube using animation frame
  useFrame ( ()=> (mesh.current.rotation.x += 0.01))

  // Jsx to render our 3d cube. Our cube will have height
  // width and depth equal 2 units.
  // You also need a material so that you can add color
  // and show shadows. We are using the standard
  // material <<meshStandardMaterial />   

  return (
    <mesh ref={mesh}>
      <boxGeometry args={[2, 2, 2]} />
      <meshStandardMaterial />
      <meshStandardMaterial color={"orange"}/> 
    </mesh>
  );
}

// Basic app structure to render a 3d cube
//<ambientLight /> is the standard light to use, otherwise
// everything comes out as black
function App() {
  return (
    <Canvas>
      <ambientLight />
      <pointLight position={[10,10,10]} />
      <Cube />
    </Canvas>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Hooray, our 3D cube is alive with colour, shadows and its moving in 3d space.

3D cube with colour

Step 6: Make cube interactively change colour on hover

What we aim to do here is to have the cube change colour when you hover over it. As you know in react if you are going to change some property in the display then you would need to use state variables and event handlers.

In the Cube function, lets introduce a state variable to store the hover state :
const [hovered,setHover] = useState(false)

Now all we have to do is bind an event handler to the <mesh /> element. Luckily mesh component has its event handlers for hover event , they are : onPointerOver and onPointerOut. This allows us to toggle the value on hovering in and out. So our mesh element opening tag would look like this :

<mesh ref={mesh} 
               onPointerOver={ (event)=> setHover(true)} 
               onPointerOut={(event)=> setHover(false)} >
Enter fullscreen mode Exit fullscreen mode

The last part is to change the colour to hotpink when the state changes to hovered. This can be done with a ternary expression on color property of meshStandardMaterial element. So that becomes :

<meshStandardMaterial color={hovered ? "hotpink" : "orange"}/>
Enter fullscreen mode Exit fullscreen mode

Our cube function looks like this now :

function Cube(props) {
  // Use useRef hook to access the mesh element
  const mesh = useRef();

  // State values for hover
  const [hovered, setHover] = useState(false);

  //Basic animation to rotate our cube using animation frame
  useFrame(() => (mesh.current.rotation.x += 0.01));

  // Jsx to render our 3d cube. Our cube will have height
  // width and depth equal 2 units.
  // You also need a material so that you can add color
  // and show shadows. We are using the standard
  // material <<meshStandardMaterial />

  return (
    <mesh
      ref={mesh}
      onPointerOver={(event) => setHover(true)}
      onPointerOut={(event) => setHover(false)}
    >
      <boxGeometry args={[2, 2, 2]} />
      <meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
    </mesh>
  );
}
Enter fullscreen mode Exit fullscreen mode

Thats all there is to it to change colour on hover.

Step 7: Add smooth animation to resize cube on click event

Three.js has its own animation hooks, but what three.js cant do we can achieve with react-spring animation. To resize our cube smoothly, we can use the react-spring hook this time.

Just a reminder, to install react-spring so you can use it with react-three-fiber, you need to run the following : npm install three @react-spring/three. Then you need to import it into the app.js, like :
import { useSpring, animated } from '@react-spring/three'

The import section at the top of our app.js looks like this now :

import {useState,useRef} from "react"
import {Canvas, useFrame} from "@react-three/fiber"
import { useSpring, animated } from "@react-spring/three";
Enter fullscreen mode Exit fullscreen mode

Going back to our task in hand, we need to resize the cube on the click event. Again like like previous task of changing colour, we need a state variable to store active state for our mesh when clicked. So we add the following line to our cube function:

const [active,setActive] = useState(false)

Enter fullscreen mode Exit fullscreen mode

Now we add a on click handler to the mesh, as before we have an arrow function to change the state on click: onClick = {(event)=> setActive(!active)}, so our mesh element opening tag looks like:

<mesh ref={mesh}
            onPointerOver={ (event)=> setHover(true)}
            onPointerOut={(event)=> setHover(false)}
            onClick = {(event)=> setActive(!active)}
          >
Enter fullscreen mode Exit fullscreen mode

Next we need to increase the scale of our cube by 1.5 if the active state is true. Now the tricky part is that react-spring is going to handle the size change. We are going to apply this to the mesh element, so what we need to do is first rename mesh element to animated.mesh. so <mesh>....</mesh> will become <animated.mesh>....</animated.mesh>. We are also going to set a scale property, this scale property will be handled by a react-spring hook so we simply say something like <animated.mesh scale={scale}>....</animated.mesh> so our mesh opening and closing tags will look like this now:

          <animated.mesh ref={mesh}
            onPointerOver={ (event)=> setHover(true)}
            onPointerOut={(event)=> setHover(false)}
            onClick = {(event)=> setActive(!active)}
            scale = { scale}
          >  .......

            ....
          </animated.mesh>

Enter fullscreen mode Exit fullscreen mode

now we simply use the react-spring hook to to set the size and deal with the animation. The following line of code does the trick

 const { scale } = useSpring({ scale: active ? 1.5 : 1 })
Enter fullscreen mode Exit fullscreen mode

What is happening here is , we are passing a ternary expression to check if the active state is true or false. and react-spring will deal with the animation.

Thats it your done!. The final code for your app.js looks like:

import {useState,useRef} from "react"
import {Canvas, useFrame} from "@react-three/fiber"
import { useSpring, animated } from "@react-spring/three"
import "./app.css"



function Cube(props) {
  // Use useRef hook to access the mesh element
  const mesh = useRef();

  // State values for hover and active state
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);

  //Basic animation to rotate our cube using animation frame
  useFrame(() => (mesh.current.rotation.x += 0.01));

  //Spring animation hook that scales size based on active state
  const { scale } = useSpring({ scale: active ? 1.5 : 1 });

  // Jsx to render our 3d cube. Our cube will have height
  // width and depth equal 2 units.
  // You also need a material so that you can add color
  // and show shadows. We are using the standard
  // material <<meshStandardMaterial />

  return (
    <animated.mesh
      ref={mesh}
      onPointerOver={(event) => setHover(true)}
      onPointerOut={(event) => setHover(false)}
      onClick={(event) => setActive(!active)}
      scale={scale}
    >
      <boxGeometry args={[2, 2, 2]} />
      <meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
    </animated.mesh>
  );
}

function App() {
  return (
    <Canvas>
      <ambientLight />
      <pointLight position={[10, 10, 10]} />
      <Cube />
    </Canvas>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

You can see the final code here on Codesandbox

Top comments (0)

12 APIs That You Will Love

>> Check out this classic DEV post <<