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.