Introduction to React Three Fiber

Introduction

React-Three-Fiber or R3F is a powerful library used to build WebGL scenes for the Web and with React Native. R3F uses Three.js under the background to render 3d objects and shapes.

Prerequisites

This article requires a basic understanding of React and the basics of Three.js.

Setting up R3F

When we begin developing 3D objects, we will make use of the Canvas object. These styles ensure the Canvas takes up the full width and height of the browser viewport.

* {
  box-sizing: border-box;
}

html,
body,
#root {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
}

Building Blocks of R3F

The starting point of R3F is the Canvas component. Canvas sets the scene for us to render our 3D objects. It’s our portal into Three.js. Canvas renders Three.js elements, not DOM elements. We cannot write HTML elements in Canvas, only Three.js elements. By default it sets up the following for us:

  • Translucent WebGL-renderer
  • Perspective camera
  • Orthographic camera
  • A scene

If we were working with Three.js, we would have to set these up ourselves. However, R3F handles the “heavy-lifting” for us.

Making Basic Shapes and Adding Colors

In Three.js, we can build shapes, also called geometries. Shapes begin in the mesh component which is a native component R3F provides. In the mesh, we define a Material and the Geometry for that material. The geometry defines the shape we want to render and the material defines the color and look of that shape.

To create a circle, use the circleBufferGeometry, and define the args. The first value in the array is the radius of the circle, and the second is the number of segments or angles, the circle has. There are 3 segments by default. We can adjust the color of the material using the color prop.

import {Canvas} from 'react-three-fiber'

function App() {
  return (
    <>
      <Canvas>
        <mesh>
          <ambientLight />
          <circleBufferGeometry attach="geometry" args={[2, 30]}/>
          <meshBasicMaterial attach="material" color="red" />
        </mesh> 
      </Canvas>
    </>
  );
}

To create a Cylinder, all you have to do is use a different Geometry. Here, we use cylinderBufferGeometry. The values in the args of cylinderBufferGeometry control the height, width, and depth of the cylinder.

<Canvas>
  <ambientLight/>
  <mesh>
    <cylinderBufferGeometry attach="geometry" args={[1, 1,3]}/>
    <meshBasicMaterial attach="material" color="red" />
  </mesh> 
</Canvas>

We can control the position of a mesh using the position prop. This prop takes an array of 3 values that control the x, y, and z axes respectively.

<Canvas>
  <ambientLight intensity={0.3}/>
  <mesh position={[-1,0,-2]}>
  <cylinderBufferGeometry attach="geometry" args={[1, 1,3]}/>
  <meshStandardMaterial attach="material" color="red" />
  </mesh> 
</Canvas>

There are several types of lights used to create different lighting effects. In this section we used ambientLight. This light globally illuminates all objects in the scene equally. This light cannot be used to cast shadows as it does not have a direction. We can control the intensity of the light the ambientLight provides using the intensity prop. The values of this prop range from 0 to 1. Without adding ambientLight to our canvas, the default color of the shapes will be set to black and the color defined in the color prop will be ignored. This is because there cannot be colors without a light source.

Rotating Shapes

We import and use the useFrame hook. It allows us to create loop effects when the frame re-renders. That’s why you have to call it where it’s being used. We rotate the y-axis and update the value. As each frame re-renders, we update the value of the y-axis rotation by 0.5.

You’ll notice we moved the cylinder into its Cylinder component. The reason is we’re not allowed to use useFrame in the main component. We only use it in the component whose values we want to update with each re-render.

import React, {useRef} from 'react';
import {Canvas, useFrame} from 'react-three-fiber'

const Cylinder = () => {
  const cylinderMesh = useRef();
  useFrame(() => (cylinderMesh.current.rotation.y = cylinderMesh.current.ro   tation.y += .5))
  return (
    <mesh ref={cylinderMesh}>
      <cylinderBufferGeometry attach="geometry" args={[1,1,2]} />
      <meshStandardMaterial attach="material" color="red" />
    </mesh>
  )
}

  return (
    <>
      <Canvas>
        <ambientLight/>
         <Cylinder/>
      </Canvas>
    </>
  );

Controlling the Camera

We can adjust the camera angle of the Canvas using the position attribute in the camera prop. position takes an array of 3 values that control the x, y, and z axes respectively. You should be able to click and drag your mouse to rotate around the ship. We can define the level of zoom with the fov, field-of-view, attribute. The smaller the fov, the closer the object.

<Canvas camera={{position: [-5,5,5], fov: 30}}>
  <ambientLight/>
  <Cylinder/>
</Canvas> 

We can also control how the camera orbits around the canvas using OrbitControls. OrbitControls add a lot of free camera movement functionality It’s that easy. We import OrbitControls from drei. Drei is a library of useful helpers and abstractions for R3F.

import {OrbitControls} from 'drei'

function App() {
  return (
    <>
      <Canvas camera={{position: [-5,5,5], fov: 30}}>
        <ambientLight/>
        <directionalLight />
        <spotLight position={[0,5,10]}/>
        <Cylinder/>
        <OrbitControls/>
      </Canvas>
    </> 
  );
} 

Adding 3D Depth to Shapes

We can define reflective lighting for the shapes that enhance their 3d effects. Three.js provides us with several light options. We’ll use the spotLight. This is a light that gets emitted from a single point in one direction, along a cone that increases in size the further from the light it gets. The position prop of the light works the same way as the camera’s position attribute.

<Canvas camera={{position: [-5,5,5], fov: 30}}>
  <ambientLight/>
  <directionalLight />
  <spotLight position={[0,5,10]}/>
  <Cylinder/>
  <OrbitControls/> 
</Canvas>

Adding Shadows

To add shadows to shapes, we have to add directionalLight to them, as this is the only light that casts shadows. directionalLight is a light that gets emitted in a specific direction. This light will behave as though it is infinitely far away and the rays produced from it are all parallel. The common use case for this is to simulate daylight. Even with directionalLight added, a plane is needed for the shadow to appear. Think of the plane as the surface the shadow is cast on. Let’s first define a plane.

Steps to cast shadows:

Shadows on the Canvas Element.

To have shadows in your scene you must first turn on shadows in the Canvas by adding the shadowMap prop to the component.

<Canvas shadowMap camera={{position: [-5,5,5], fov: 30}}>
  <ambientLight/>
  <directionalLight 
 .....
/>

Enable the Cylinder component’s mesh to cast a shadow.

We add a castShadow prop on to the mesh since this is the object that will cast the shadow. This enables it to cast the shadow.

<mesh castShadow>
  <cylinderBufferGeometry attach="geometry" args={[1,1,2]} />
  <shadowMaterial attach="material" color="blue"/>
</mesh>

Enabling Shadows on the directionalLight.

To tell a light that it should cast shadows you need to set the castShadow prop on the light component to true as well. By default, directionalLight is the only light that casts shadows. However, that functionality can be added to the others. shadow-mapSize-height and shadow-mapSize-width are used to control the intensity of the shadow. There are several other options we can use to control a shadow’s properties.

<directionalLight 
  castShadow
  position={[10,10,0]}
  intensity={1.5}
  shadow-mapSize-width={1024}
  shadow-mapSize-height={1024} 
/>

Set the Plane to receive the shadows that will be cast.

The last step is to add a receiveShadow prop to any object that you want to have a shadow cast upon it by another object. For shadows, we add the shadowMaterial as the type of material for the plane. We can define the shadow’s color using the color prop. We rotate the plane using the rotation prop. This ensures that the plane is aligned perfectly to the floor. I initially used a meshStandardMaterial to simulate how the plane looks. However, to display shadows, we have to use shadowMaterial.

<mesh receiveShadow rotation={[-Math.PI / 2,0,0]} position={[0,-3,0]}>
  <planeBufferGeometry attach='geometry' args={[100,100]}/>
  <shadowMaterial attach="material" color="blue"/>
</mesh>

Following the steps correctly, you should have a blue shadow rendering under the cylinder.

Conclusion

In this article, we’ve seen how to create 3d objects using R3F, control their rotation, add shadows, define their colors, and introduce lighting effects to 3d objects. We can do even more with R3F, especially when combined with other tools in its ecosystems like useSpring, cannon.js, and drei.