React Three Fiber Tutorial for Beginners

Published on 24 Aug, 2024 | ~34 min read

If you're a React enthusiast looking to start your creative 3D journey, or even if you know just the basics, you're in the right place! By the end of this article, you'll have all the essential knowledge needed to create 3D experiences on your web pages using React Three Fiber.

By the way, don’t worry if you don’t have any prior experience with the standard Three.js library. As long as you know the basics of React, you're ready for this guide.

Before we begin, I suggest checking out this short read for a quick insight into WebGL/WebGPU and how these technologies relate to libraries like Three.js, React Three Fiber, Babylon.js, and Pixi.js.

And without further ado, let’s get started!

Installations

Create a new folder and open it in your editor (I’m assuming you're using VSCode), then open a new terminal.

Open the project folder, then open a new terminal
Open the project folder, then open a new terminal

In the terminal, type the following command: npm create vite@latest r3f-tutorial. Then, you'll be prompted with two lists: in the first, choose React; in the second, choose JavaScript.

This will create a new folder named r3f-tutorial. Now, open this folder in your editor the same way you did with its parent. Then, open a new terminal and type the following command: npm install to install the necessary dependencies.

Next, we need to install Three.js and React Three Fiber. To do this, type the following command: npm install three @react-three/fiber.

Now, run the command npm run dev to start the development server.

Once that's done, Vite will generate a link in the command line. Ctrl + Left-Click on it to open the app's page.

Vite starting page
Vite starting page

Now, make sure your main.jsx file looks exactly like this:

function App() {
    return (
      <div id='canvas-container'>
      {/* The magic happens here */}
      </div>
    )
}
  
export default App

Finally, open the index.css file and delete everything from it.

And with that done, we’re ready for the real fun!

The Pillars of Every React Three Fiber Application

Can you imagine life on Earth without water? The same principle applies to React Three Fiber applications and the following three components.

Coordinate System

We can’t discuss 3D—whether it's React Three Fiber or any other 3D software—without talking about the coordinate system.

A coordinate system represents space using three axes: the x-axis for left and right, the y-axis for up and down, and the z-axis for forward and backward.

Coordinate system
Coordinate system

Although this concept is easy to understand and not something we implement ourselves, it is crucial to grasp before moving on.

Scene

The scene is essentially a container for the visible components of the application.

Scene
Scene

Camera

A scene without a camera is like a box with no way to look inside. It may contain interesting things, but they’re meaningless if we can’t see them, right?

Essentially, the camera is the component that allows us to see parts of a scene.

Camera
Camera

Creating Our First Scene

Can’t wait to start creating, huh? Let’s satisfy that urge by importing and using the <Canvas> component.

import { Canvas } from '@react-three/fiber';

function App() {
  return (
    <div id='canvas-container'>
      <Canvas>
        {/* The magic happens here actually */}
      </Canvas>
    </div>
  );
}

export default App;

After saving, if you look at your page, you should see your first piece of art created with React Three Fiber. Congratulations!

But… are you kidding me? I only see a blank page!

No need to worry—the canvas is right there, ready for you to start adding elements. To confirm, press Ctrl + Shift + I to open the developer tools, go to the Elements tab, and hover over the canvas-container div. This will highlight the canvas.

The canvas
The canvas

To make the application take up the full window size, you should know that the canvas element, by default, takes up the full size of its container.

This means we need to use CSS to make the canvas-container div occupy the full space of the page. As a result, the canvas will take up the full size of that div, and thus the full size of the window. In fact, this is why we wrapped the Canvas component within a div in the first place.

In the index.css file, add the following snippet.

#canvas-container {
  width: 100vw;
  height: 100vh;
}

Even though we've made our canvas take up the full space on the page, you might still notice some unwanted extra scrolling space.

Full window canvas
Full window canvas

To get rid of that extra scrolling space, remove the margins from the <body> element.

body {
    margin: 0;
}

#canvas-container {
  width: 100vw;
  height: 100vh;
}

Meshes

A mesh is a 3D object, whether it’s a simple shape like a box or sphere, or a complex model like a car or character created in Blender and loaded into the scene.

That said, a mesh is composed of two main elements: geometry and material.

In simple terms, the geometry defines the shape of an object, while the material determines what the object is made of.

Geometries and materials
Geometries and materials

Geometries

We have various geometries available that we can easily use to create meshes. In this tutorial, we’ll use two of them, starting with the sphere geometry.

Since a sphere is a mesh, we'll need to create a <mesh /> component. Within this component, we’ll use two other components to represent the geometry and the material.

function App() {
    return (
      <div id='canvas-container'>
        <Canvas>
          <mesh>
            <sphereGeometry />
            <meshBasicMaterial />
          </mesh>
        </Canvas>
      </div>
    );
  }
Sphere
Sphere

Creating a sphere doesn't stop there—we have a few properties we can tweak, like the size. To adjust the size, we use the args prop, which takes an array of constructor arguments.

In this case, the first argument/array element represents the size.

<sphereGeometry args={[3]} />

This results in a large sphere. However, you’ll notice that it isn’t smoothly rounded.

The sphere is not perfectly rounded
The sphere is not perfectly rounded

To fix this, we need to add a couple more elements to the args array.

<sphereGeometry args={[3, 80, 80]} />

Now your sphere looks smoothly rounded, but you might be wondering about those two 80s we just added, right?

In reality, every 3D object is made up of triangles. These triangles are formed by connecting segments.

The more segments a mesh has, the smoother it will appear. However, keep in mind that the more segments your objects have, the heavier the impact on your app's performance.

So, back to our sphere—the first 80 represents the number of width segments, and the second 80 represents the number of height segments.

To visualize the segments, add the wireframe prop to the material.

<meshBasicMaterial wireframe />
Width and height segments
Width and height segments

To create a box, we use the <boxGeometry /> component. The arguments are width, height, and depth, in that order.

<mesh>
    <boxGeometry args={[2, 3, 2]} />
    <meshBasicMaterial />
</mesh>

Materials

To change how our box is presented, we need to adjust its material properties, such as enabling wireframe mode (as we did with the sphere earlier) or changing the color of the box.

<meshBasicMaterial color='deepskyblue' />

Or

<meshBasicMaterial color={0x00bfff} />

There are many materials for different use cases, but a common factor among the majority of them is how they interact with light.

In the previous examples, we used the <meshBasicMaterial /> component, which is unique because it isn’t affected by light. Actually, I chose this material on purpose since we don’t have a light source in our scene yet.

That being said, let’s use the <meshPhongMaterial /> this time.

<mesh>
    <boxGeometry args={[2, 3, 2]} />
    <meshPhongMaterial />
</mesh>

With that done, you should now see a black rectangle regardless of the color you pass to the color prop. This is expected—after all, who can see the color of an object without any light?

Let’s add a light source. Add the line below, but don’t worry about the details just yet; there’s a dedicated section on lighting later in this article.

<Canvas>
    <mesh>
        <boxGeometry args={[2, 3, 2]} />
        <meshPhongMaterial color={0x00bfff} />
    </mesh>
    <directionalLight position={[2, 5, 1]} />
</Canvas>

Now the color of the box should be visible but darker than expected. This is because light affects how colors are perceived through shading and shadows.

To better see this effect, let’s change the camera’s position to adjust our view of the box.

In the <Canvas /> component, add the camera prop like this:

<Canvas camera={{ position: [2, 2, 2] }}>
Material and light
Material and light

As mentioned, there are many materials, each suited for specific use cases. For example, <meshStandardMaterial /> is designed to look more realistic than <meshPhongMaterial />. There’s also <meshPhysicalMaterial />, which offers even greater realism with its reflection properties.

Note: more realistic materials require more computation and resources from the device running the application. Keep this in mind when choosing materials for your objects.

Looking for something special rather than realistic? Try using <meshToonMaterial />, which gives your objects a cartoonish look.

<Canvas>
    <mesh>
      <torusKnotGeometry args={[1.7, 0.3, 256, 256]} />
      <meshToonMaterial color={0x00bfff} />
    </mesh>
    <directionalLight position={[4, 2, 3]} />
</Canvas>
meshtoonmaterial
torusKnotGeometry with meshToonMaterial

Geometric Transformations

Translation

When we talk about translation, we mean moving objects to a specific position. To achieve this, set the position prop on the mesh you want to move and pass an array of three values: the x, y, and z coordinates of the target position.

<mesh position={[-2, 2, -3]}>
    <torusKnotGeometry args={[1.7, 0.3, 256, 256]} />
    <meshToonMaterial color={0x00bfff} />
</mesh>

This moves the mesh two units to the left, two units up, and three units forward.

Scale

To change the size of a mesh, use the scale prop and pass an array of three values that define how much to scale the mesh along each axis.

<mesh position={[-2, 2, -3]} scale={[2, 0.5, 2]}>

This doubles the size of the mesh along the x and z axes while halving its height.

Rotation

To rotate objects, there are several options available, but as a beginner, you'll mainly need the rotation prop. Like the previous props, this one takes an array of three values, each representing the amount of rotation along the x, y, and z axes.

Note: rotation values are in radians, not degrees

<mesh rotation={[Math.PI / 2, 0, 0]}>

This rotates the mesh half a circle along the x-axis.

The Render Loop

The render loop, or animation loop, is one of the most fundamental concepts to understand if you want to animate objects in your scenes.

An animation is essentially a sequence of images, or frames, displayed at a specific frequency. This applies to any kind of animation you can think of, whether it’s a 3D application, a 2D game, a video, or a CSS animation.

That said, if we have a mesh and we want to animate it moving from one position to another, we need to display the mesh at various positions between the starting point and the endpoint over time.

This animation, like every animation, is achieved through the render loop.

Render loop
Render loop

Now, let’s see how this works by implementing an example.

First, instead of creating the <mesh /> directly inside the <Canvas />, we'll move it into a separate function.

import { Canvas } from '@react-three/fiber';

function AnimatedBox() {
  return (
    <mesh>
      <boxGeometry args={[2, 2, 2]} />
      <meshStandardMaterial color={0x00bfff} />
    </mesh>
  );
}

function App() {
  return (
    <div id='canvas-container'>
      <Canvas>
        <AnimatedBox />
        <directionalLight position={[4, 2, 3]} />
      </Canvas>
    </div>
  );
}

export default App;

Next, within this function, we’ll integrate the box animation into the render loop using a hook called useFrame().

First, we need to import the hook.

import { Canvas, useFrame } from '@react-three/fiber';

Next, we'll use it within our component.

function AnimatedBox() {
    useFrame(() => {
        // Animation code
    });

    return (
        <mesh>
        <boxGeometry args={[2, 2, 2]} />
        <meshStandardMaterial color={0x00bfff} />
        </mesh>
    );
}

The useFrame() hook runs the code within the function we pass to it multiple times per second, depending on the device running the application. For example, on a 60Hz monitor, useFrame() will call the function 60 times per second.

For this animation, we’ll rotate the box over time. This means we’ll gradually increase the rotation values to animate the rotation, or decrease them for a reverse animation.

To achieve this, we need to access the box mesh within the function passed to the useFrame() hook. We'll use the useRef() hook to create a reference to the mesh.

First, we need to import the useRef() hook.

import { useRef } from 'react';

Next, we’ll create a reference and set it to the mesh.

function AnimatedBox() {
    const boxRef = useRef();
  
    useFrame(() => {
      // Animation code
    });
  
    return (
      <mesh ref={boxRef}>
        <boxGeometry args={[2, 2, 2]} />
        <meshStandardMaterial color={0x00bfff} />
      </mesh>
    );
}

Finally, we can access the properties of our mesh through that reference. In our case, we'll increment the rotation values.

useFrame(() => {
    boxRef.current.rotation.x += 0.005;
    boxRef.current.rotation.y += 0.005;
    boxRef.current.rotation.z += 0.005;
});

Animated Demo.

Camera Controls

Up until now, we've been adding objects to our scene, but we haven't had control over the view or the angles to observe these objects and the scene as a whole.

That being said, various modules are specifically designed for this, so we don't have to code them manually. And this brings us to Drei, a vast collection of ready-to-use components that can be effortlessly integrated into your applications.

So before we continue, click on the terminal, press Ctrl + C to stop the dev server, and then run the following command: npm install @react-three/drei

Next, run the server again, and let’s explore a couple of solutions for camera control.

FirstPersonControls

FirstPersonControls work like the camera controls in first-person shooter games, where the camera's perspective matches your own, and you move using the WASD or arrow keys. You can also use the mouse buttons to navigate the scene with FirstPersonControls.

FirstPersonControls
FirstPersonControls

First, import the FirstPersonControls module.

import { FirstPersonControls } from '@react-three/drei';

Then, use the component inside the <Canvas />. And that’s literally it!

<Canvas>
  <FirstPersonControls />
  <AnimatedBox />
  <directionalLight position={[4, 2, 3]} />
</Canvas>

We have several options to tweak, such as adjusting the camera movement speed.

<FirstPersonControls movementSpeed={3} />

You can also set the camera to move forward automatically. I’m not sure how useful that would be, but it’s there if you ever need it.

<FirstPersonControls movementSpeed={0.3} autoForward={true} />

You can find the rest of the options on the FirstPersonControls documentation page.

Orbit Controls

The OrbitControls, as its name suggests, allows you to orbit the camera around a certain point. It is actually one of the most commonly used camera controls.

OrbitControls
OrbitControls

So first, you have to import the module.

import { OrbitControls } from '@react-three/drei';

Next, use the component inside the <Canvas />.

<Canvas>
  <OrbitControls />
  <AnimatedBox />
  <directionalLight position={[4, 2, 3]} />
</Canvas>

Then, you have various options to customize, such as enabling or disabling zooming, panning, or damping.

For more information about these and other types of camera controls, check out the documentation page on the official Drei documentation page.

Helpers and Gizmos

Helpers and gizmos are primarily debugging tools, but they can also be integral to the final product.

Helpers

There are various helpers to choose from, and in this article, I'll focus on just a couple. The first one is the <axesHelper />, which displays the axes of the scene's coordinate system.

<Canvas>
  <axesHelper />
  <OrbitControls />
  <AnimatedBox />
  <directionalLight position={[4, 2, 3]} />
</Canvas>

If your setup is similar to mine, you might not see the helper because it's covered by the box. You can either remove the box or increase the size of the axes by passing the new length as an array to the args prop.

<axesHelper args={[10]} />

The second helper we'll explore is the <gridHelper />, which is incredibly useful for guiding the placement of planes. It can also serve as a grid, which I'll demonstrate in a future article.

The <gridHelper /> component offers several options that can be passed to its args prop, such as adjusting the size to increase the grid's dimensions.

<gridHelper args={[20]} />

The second option we can adjust is the number of divisions or squares that make up the grid.

<gridHelper args={[20, 20]} />

The third element is the color of the main crossing axes, and the fourth is the color of the remaining segments.

<gridHelper args={[20, 20, 0xff22aa, 0x55ccff]} />
gridHelper
gridHelper

Gizmos

A gizmo acts like a compass for your application, helping you understand orientation and direction.

Drei provides two types of gizmos. If you haven’t installed Drei yet, do so now. Then, import the following modules:

import {
  OrbitControls,
  GizmoHelper,
  GizmoViewcube,
  GizmoViewport,
} from '@react-three/drei';

First, we need to use the <GizmoHelper>, which serves as a wrapper for the gizmo. This component allows us to specify the gizmo's placement using the alignment and margin props.

<Canvas>
  // Gizmo Wrapper
  <GizmoHelper alignment='bottom-right' margin={[80, 80]}>
    // The gizmo here
  </GizmoHelper>
  <gridHelper args={[20, 20, 0xff22aa, 0x55ccff]} />
  <OrbitControls />
  <AnimatedBox />
  <directionalLight position={[4, 2, 3]} />
</Canvas>

Inside the <GizmoHelper>, you can set one of the two types of gizmos. That’s all there is to it!

<GizmoHelper alignment='bottom-right' margin={[80, 80]}>
  <GizmoViewport />
</GizmoHelper>

Or

<GizmoHelper alignment='bottom-right' margin={[80, 80]}>
  <GizmoViewcube />
</GizmoHelper>
Gizmos
Gizmos

GUI

Debugging and testing values can be quite tedious and time-consuming. For this reason, having a graphical user interface (GUI) can be incredibly helpful.

That being said, there are a few excellent libraries available for this task, so there's no need to create one from scratch. The most common ones are dat.gui, Tweakpane, and Leva, which is the choice for this tutorial.

First things first, install the library using the following command: npm install leva@0.9.34

Note: I didn't install the latest version because it has an issue with the color picker.

Next, import the useControls hook.

import { useControls } from 'leva';

First, we’ll add a slider to our GUI to control the rotation speed of the box.

In the AnimatedBox() function, we'll use the useControls() hook to create a variable that holds the speed value.

const { speed } = useControls({
  speed: {
    value: 0.005,
    min: 0.0,
    max: 0.03,
    step: 0.001,
  },
});

The speed object with its four properties represents the slider. The keys are self-explanatory: value is the initial value, min and max define the minimum and maximum values of the slider, and step specifies the increment between values.

Leva slider
Leva slider

Now, while the GUI will be displayed in the application, it won't affect the box's animation speed because we haven't linked the GUI values to the rotation values. So let's do that.

const { speed } = useControls({
  speed: {
    value: 0.005,
    min: 0.0,
    max: 0.03,
    step: 0.001,
  },
});

useFrame(() => {
  boxRef.current.rotation.x += speed;
  boxRef.current.rotation.y += speed;
  boxRef.current.rotation.z += speed;
});

Now, if you try to adjust the speed, it should work as expected.

We can also add a color picker to the GUI to change the color of the box.

To do this, we'll create a color variable using the useControls() hook and pass an initial value as the hexadecimal representation of the color.

const { color, speed } = useControls({
  color: '#00bfff',
  speed: {
    value: 0.005,
    min: 0.0,
    max: 0.03,
    step: 0.001,
  },
});

Next, we'll pass the color variable to the color prop of the material used to create the box mesh.

<meshStandardMaterial color={color} />

Full Example.

Lights

Just like in real life, React Three Fiber offers different types of lights. In this tutorial, we're going to explore four of the most commonly used ones.

AmbientLight

AmbientLight simulates the light that comes from the environment, such as sunlight filtering into a room, the soft glow of streetlights, or even the diffused light from overcast skies.

AmbientLight
AmbientLight

What I want you to do now is replace the <directionalLight /> we've been using since the beginning of the article with <ambientLight />.

Note: you can have multiple light sources of different types in one scene. I just want you to see the effect of <ambientLight /> without any other light sources.

<ambientLight />

When you do that, you’ll notice the scene becomes overly bright. To fix this, we need to reduce the light intensity and slightly darken its color.

<ambientLight color={0xfcfcfc} intensity={0.2} />

DirectionalLight

Directional light is a light source that emits a broad stream of light, similar to sunlight or the illumination in a room on a sunny day where the only light source is a window.

DirectionalLight
DirectionalLight

To add a directional light we can do it the same way we did it earlier.

Additionally, we can control both the intensity and color of the light.

<directionalLight
  position={[4, 2, 3]}
  color={0xffea00}
  intensity={0.8}
/>

SpotLight

This light source is defined by the cone shape it forms. Examples from real life include stage lights in a theatre, spotlights in architectural designs, and light emitted from UFOs to abduct people... cough cough.

Spotlight
Spotlight

To add a spotlight, simply use the <spotLight /> component. Note that the intensity prop is essential; without it, the spotlight won’t be visible!

<spotLight intensity={50} position={[4, 2, 3]} />

We can adjust the light properties, but doing so without a helper can be quite challenging.

So to add a light helper, follow these steps.

First, create a new component that returns a light component. Do not add the light directly to the <Canvas>.

function LightWithHelper() {
  return <spotLight intensity={50} position={[4, 2, 3]} />;
}

When you add the <LightWithHelper /> component to the <Canvas>, nothing should change visually.

So, what we're going to do now is import the useHelper() hook from Drei and the SpotLightHelper from Three.js.

import {
  OrbitControls,
  GizmoHelper,
  GizmoViewport,
  useHelper,
} from '@react-three/drei';
import { SpotLightHelper } from 'three';

Next, we'll create a reference to the light within the component using the useRef() hook.

function LightWithHelper() {
  const light = useRef();

  return (
    <spotLight
      ref={light}
      intensity={50}
      position={[4, 2, 3]}
    />
  );
}

The last step is to use the useHelper() hook we just imported. It takes three arguments: the reference to the light, the SpotLightHelper class, and a color. And with this, you should be able to see the helper.

function LightWithHelper() {
  const light = useRef();

  useHelper(light, SpotLightHelper, 'orange');
  
  return (
    <spotLight
      ref={light}
      intensity={50}
      position={[4, 2, 3]}
    />
  );
}
Spotlight helper
Spotlight helper

Now you can, for example, change the radius of the spot created by the light using the angle prop.

<spotLight
  ref={light}
  intensity={50}
  position={[4, 2, 3]}
  angle={Math.PI / 8}
/>
Spotlight angle
Spotlight angle

As you can see, the circle created by the helper is a great indicator to help you adjust the right radius. However, we can make the task even easier by using the GUI to set the angle values!

function LightWithHelper() {
  const light = useRef();

  useHelper(light, SpotLightHelper, 'orange');

  const { angle } = useControls({
    angle: Math.PI / 8,
  });

  return (
    <spotLight
    ref={light}
    intensity={50}
    position={[4, 2, 3]}
    angle={angle}
    />
  );
}
Setting the spotlight angle values with GUI
Setting the spotlight angle values with GUI

Another property we can tweak is the penumbra, which controls the blurriness of the edges of the spotlight.

function LightWithHelper() {
  const light = useRef();

  useHelper(light, SpotLightHelper, 'orange');

  const { angle, penumbra } = useControls({
    angle: Math.PI / 8,
    penumbra: {
      value: 0.0,
      min: 0.0,
      max: 1.0,
      step: 0.1,
    },
  });

  return (
    <spotLight
      ref={light}
      intensity={50}
      position={[4, 2, 3]}
      angle={angle}
      penumbra={penumbra}
    />
  );
}
Spotlight penumbra
Spotlight penumbra

Full Example.

PointLight

PointLight is a source of light that emits light uniformly in all directions. Examples include campfires, light bulbs, and candlelight.

PointLight
PointLight

To create a PointLight, add a <pointLight /> component within the <Canvas> and be sure to set the intensity.

<pointLight intensity={50} position={[4, 2, 3]} />

Shadows

We can't talk about light without mentioning shadows.

Adding shadows in React Three Fiber is quite simple. We just need to enable shadows in the <Canvas> and specify which components are shadow casters and which are shadow receivers.

To activate shadows, we need to add the shadows prop to the <Canvas>.

<Canvas shadows>

The shadow casters are the light and the box, so we'll add the castShadow prop to both.

<spotLight
  ref={light}
  intensity={50}
  position={[5, 8, 0]}
  angle={angle}
  penumbra={penumbra}
  castShadow
>
<mesh ref={boxRef} position={[0, 3, 0]} castShadow>

Next, we'll create a plane to visualize the shadow cast by the box. And, of course, we'll set the receiveShadow prop on the mesh.

By the way, create the plane directly in the <Canvas>; there's no need to create a new function.

<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
  <planeGeometry args={[20, 20]} />
  <meshStandardMaterial />
</mesh>

And with that done, you should be able to see the shadow of the box on the plan

Now, there's a problem you're likely to encounter with shadows during your journey as a React Three Fiber developer.

To demonstrate this, we'll first replace the spotlight with a directional light. We'll create a new component that returns the directional light along with a helper.

Just as we did with the spotlight earlier, we'll import the helper specifically designed for the directional light.

import { SpotLightHelper, DirectionalLightHelper } from 'three';

Next, we'll create a function named DirectionalLightWithHelper that returns a <directionalLight /> component.

function DirectionalLightWithHelper() {
  const light = useRef();
  useHelper(light, DirectionalLightHelper, 2, 'crimson');

  return (
    <directionalLight
      ref={light}
      position={[5, 8, 0]}
      castShadow
    />
  );
}

Then add it to the <Canvas> instead of the <LightWithHelper /> component we created in the previous section.

<DirectionalLightWithHelper />

Notice how I passed four arguments this time to the useHelper() hook. Most importantly, the new argument, which specifies the size of the helper, is added before the color. Don't make the same mistake I did by forgetting this argument—it literally took me hours to figure out where the error message was coming from!

The size of the helper
The size of the helper

Now, set the position of the light to [-5, 8, 0] and the position of the box to [5, 8, 0].

<directionalLight
  ref={light}
  position={[-5, 8, 0]}
  castShadow
/>
<mesh ref={boxRef} position={[5, 3, 0]} castShadow>
  <boxGeometry args={[2, 2, 2]} />
  <meshStandardMaterial color={color} />
</mesh>
Incomplete shadow
Incomplete shadow

As you can see, most of the box shadow is now missing! But why?

Well, lights actually have defined areas where their shadows appear, and sometimes these shadows extend beyond these areas.

By changing the positions of the light and the box, we found ourselves in a situation where the shadow area was not large enough to encompass the entire shadow.

That being said, to visualize the area of the shadow cast by our light, we need to use a different helper.

So the first thing we're going to do is import the CameraHelper class from Three.js

import { SpotLightHelper, CameraHelper } from 'three';

Next, we need to transform the <directionalLight /> component returned by the DirectionalLightWithHelper() function into a wrapper component.

<directionalLight
  ref={light}
  position={[-5, 8, 0]}
  castShadow
>
</directionalLight>

Next, within the <directionalLight> component, we’re going to create an orthographic camera.

In React Three Fiber, there are several types of cameras. One of them is the orthographic camera, which is defined by four edges: top, right, bottom, and left.

<directionalLight
  ref={light}
  position={[-5, 8, 0]}
  castShadow
>
  <orthographicCamera attach='shadow-camera' />
</directionalLight>

You not only added a new camera for an unknown reason but also a new prop with an unknown value. Like, come on, man!

Yes, I know it’s a lot all of a sudden, but here’s the explanation.

Cameras in React Three Fiber are not only used to display the scene but also to calculate the areas where shadows cast by lights should appear. For example, DirectionalLight uses an orthographic camera for this purpose.

With that said, an orthographic camera has a helper that displays the area it captures. Consequently, we leveraged this by attaching the camera to the light’s shadow and using the helper to show the area covered by the shadow.

To better understand this, imagine the shadow as a person. We’ve given this person a camera or a laser tool to show us the area they can see. This area is where the shadow will appear.

That’s the first part. Now, in standard Three.js, we typically create an object and then attach it to a parent object. Similarly, to use a camera, we need to create it and then attach it to the scene.

In React Three Fiber, this kind of setup is handled automatically. If you recall from the second section, I mentioned that the camera is one of the most essential parts of a React Three Fiber application, yet we didn’t create one. Why? Because it was created and attached to the scene automatically!

In our case, instead of adding the orthographic camera directly to the scene, we want to attach it to the shadow. We do this using the attach prop and setting its value to 'shadow-camera'.

Now, to display the helper, we’ll create a new reference, call the useHelper() hook while passing the reference and CameraHelper as arguments, and then set the reference to the camera. All of this should be familiar to you at this point.

function DirectionalLightWithHelper() {
  const light = useRef();
  useHelper(light, DirectionalLightHelper, 2, 'crimson');

  const shadow = useRef();
  useHelper(shadow, CameraHelper);

  return (
    <directionalLight
    ref={light}
    position={[-5, 8, 0]}
    castShadow>
      <orthographicCamera
        attach='shadow-camera'
        ref={shadow}
        args={[-2, 2, 2, -2]}
      />
    </directionalLight>
  );
}

Full Example.

Once that’s done, you should be able to see the helper. However, the shadow is completely disappeared because attaching the camera reset the shadow area.

Shadow area
Shadow area

Now we have full control over the shadow area. So to adjust its size so it encompasses the box, you can pass the edge values as an array to the args prop or set each edge individually using its respective prop.

<directionalLight
  ref={light}
  position={[-5, 8, 0]}
  castShadow
>
  <orthographicCamera
    attach='shadow-camera'
    ref={shadow}
    left={-2}
    right={2}
    top={8}
    bottom={4}
  />
</directionalLight>

Same as:

<directionalLight
ref={light}
position={[-5, 8, 0]}
castShadow
>
  <orthographicCamera
    attach='shadow-camera'
    ref={shadow}
    args={[-2, 2, 8, 4]}
  />
</directionalLight>
After adjusting the shadow area
After adjusting the shadow area

Loading Models

They say a picture is worth a thousand words. In the context of 3D, a model is worth just as much.

That said, loading models into a scene in React Three Fiber is a piece of cake. Just follow the next few steps.

First and foremost, download a .glb or .gltf file and place it in the public folder of your project directory.

Next, we have two methods for loading our file. The first is using the useLoader() hook from React Three Fiber along with the GLTFLoader from Three.js examples.

import { Canvas, useFrame, useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

The next step is to create a new component in which we'll load the model. Inside this component, we'll call the useLoader() hook, which takes two arguments: the first is the type of file loader we want to use (in our case, the GLTFLoader), and the second is the path to the file.

function Model() {
  const result = useLoader(GLTFLoader, '/Cthulhu.gltf');
}

The second method involves using only the useGLTF() hook from Drei. To do this, import the hook, call it, and pass the path to the file as an argument.

import {
  OrbitControls,
  GizmoHelper,
  GizmoViewport,
  useHelper,
  useGLTF,
} from '@react-three/drei';
function Model2() {
const result = useGLTF('/Chicken.gltf');
}

The remaining steps are identical for both methods.

Before we proceed to the next step, we need to understand something important: the <primitive /> component.

Most objects in React Three Fiber are represented by corresponding components. For example, a mesh is represented by the <mesh> component, a geometry like a box is represented by the <boxGeometry /> component, and similarly for materials, lights, helpers, and so on.

However, some objects don't fall under this rule, and models are one of these exceptions.

For these types of objects, we need to use the <primitive /> component. So, you create a component and pass the object as a value to the obejct prop, and React Three Fiber will handle rendering it in the scene. Pretty cool, huh?

function Model() {
  const result = useLoader(GLTFLoader, '/Cthulhu.gltf');
  return <primitive object={result.scene} />;
}
function Model2() {
  const result = useGLTF('/Chicken.gltf');
  return <primitive object={result.scene} />;
}

Now, we simply add the <Model /> component to the <Canvas>.

// In the Canvas
<Model />
// In the Canvas
<Model2 />

You can apply geometric transformations to your model through the corresponding props in the <primitive /> component, not in the <Model /> component.

return <primitive object={result.scene} position={[0, 2, 0]} />;

Textures

Applying Textures to Meshes

To apply textures to your objects, there are two methods. The first is using the useLoader() hook that we imported earlier, along with the TextureLoader module from Three.js.

So, let's start by importing the TextureLoader.

By the way, make sure you have the textures you want to use in the public folder.

import {
  SpotLightHelper,
  DirectionalLightHelper,
  CameraHelper,
  TextureLoader,
} from 'three';

Next, we'll create a new component that returns a sphere to which we'll apply the texture. Within this component, we'll call the useLoader() hook, passing the TextureLoader and the path to the image file as arguments.

function SphereWithTexture() {
  const texture = useLoader(TextureLoader, '/texture 1.jpg');

  return (
    <mesh position={[-2, 3, 2]}>
      <sphereGeometry />
      <meshStandardMaterial />
    </mesh>
  );
}

Then, we'll pass the texture to the map prop of the material. And of course, don't forget to add the component to the <Canvas>.

<meshStandardMaterial map={texture} />
// In the Canvas
<SphereWithTexture />

The second method involves using the useTexture() hook from Drei, which eliminates the need to import the TextureLoader module.

import {
  OrbitControls,
  GizmoHelper,
  GizmoViewport,
  useHelper,
  useGLTF,
  useTexture,
} from '@react-three/drei';
function SphereWithTexture2() {
  const texture = useTexture('/texture 2.jpg');

  return (
    <mesh position={[0, 1, -2]}>
      <sphereGeometry />
      <meshStandardMaterial map={texture} />
    </mesh>
  );
}
// In the Canvas
<SphereWithTexture2 />

We can apply different materials with different textures to boxes. Instead of loading just one texture, we'll load six. Similarly, instead of using a single material component within the mesh, we'll use six.

function BoxWithTexture() {
  const texture1 = useTexture('/texture 1.jpg');
  const texture2 = useTexture('/texture 2.jpg');
  const texture3 = useTexture('/texture 3.jpg');
  const texture4 = useTexture('/texture 4.jpg');
  const texture5 = useTexture('/texture 5.jpg');
  const texture6 = useTexture('/texture 6.jpg');

  return (
    <mesh position={[0, 2, -4]}>
      <boxGeometry />
      <meshBasicMaterial attach='material-0' map={texture1} />
      <meshBasicMaterial attach='material-1' map={texture2} />
      <meshBasicMaterial attach='material-2' map={texture3} />
      <meshBasicMaterial attach='material-3' map={texture4} />
      <meshBasicMaterial attach='material-4' map={texture5} />
      <meshBasicMaterial attach='material-5' map={texture6} />
    </mesh>
  );
}
// In the Canvas
<BoxWithTexture />

As you can see, I added the attach prop to attach the material components to the specific materials of the mesh object. We did something similar earlier in the shadows section—remember?

Box with different texture on each face
Box with different texture on each face

Applying Textures to the Scene

To change the properties of the scene in general, we need to use the useThree() hook. So, let’s start by importing it.

import {
  Canvas,
  useFrame,
  useLoader,
  useThree
} from '@react-three/fiber';

Next, we’ll create a component focused on changing the background of the scene.

function UpdateSceneBackground() {
  // This component doesn't return anything
  return null;
}
// Don't forget to add the component to the Canvas
<UpdateSceneBackground />

The useThree() hook actually returns more than just the scene. However, in our case, we’ll focus on extracting and using only the scene object.

function UpdateSceneBackground() {
  const { scene } = useThree();

  return null;
}

Before applying a texture, let’s start by changing the background color of the scene.

To do that, we need to import the Color class from the core of Three.js. Then, pass an instance of it with the desired color name to the background property of the scene.

import {
  SpotLightHelper,
  DirectionalLightHelper,
  CameraHelper,
  TextureLoader,
  Color,
} from 'three';
function UpdateSceneBackground() {
  const { scene } = useThree();

  scene.background = new Color('black');

  return null;
}

And boom, your background is now darker than the night sky!

Now it's time for some textures. The background property doesn't just accept Color instances; it also accepts textures.

function UpdateSceneBackground() {
  const { scene } = useThree();

  const texture = useLoader(TextureLoader, '/stars.jpg');

  scene.background = texture;

  return null;
}

With that done, you'll see the texture applied, but it will likely appear brighter than expected. This isn’t due to the lighting but because the color space hasn’t been set.

To set the color space of the texture, import the SRGBColorSpace module from the core of Three.js. Then, assign it to the colorSpace property.

import {
  SpotLightHelper,
  DirectionalLightHelper,
  CameraHelper,
  TextureLoader,
  Color,
  SRGBColorSpace,
} from 'three';
const texture = useLoader(TextureLoader, '/stars.jpg');
texture.colorSpace = SRGBColorSpace;
Before and after setting the color space of the texture
Before and after setting the color space of the texture

Hold on, yes, the texture now looks better, but I have no idea what this color space is. Any insights on this?

Well, this topic is explained in detail in this article. So open it in another tab, and let's continue our work, shall we?

The fun doesn't stop here. The scene is actually a cube, which has six faces, meaning we can apply a different texture to each face.

To do this, we need a special type of texture called a cube texture. It consists of six textures, one for each face of the cube.

Again, we have two methods to create this type of texture. The first is to use the useLoader() hook and import the CubeTextureLoader module from Three.js. Then, pass the loaded texture to the background property of the scene.

import {
  SpotLightHelper,
  DirectionalLightHelper,
  CameraHelper,
  TextureLoader,
  Color,
  SRGBColorSpace,
  CubeTextureLoader,
} from 'three';
function UpdateSceneBackground() {
  const { scene } = useThree();

  const [texture] = useLoader(CubeTextureLoader, [
    [
      '/texture 1.jpg',
      '/texture 2.jpg',
      '/texture 3.jpg',
      '/texture 4.jpg',
      '/texture 5.jpg',
      '/texture 6.jpg',
    ],
  ]);

  scene.background = texture;

  return null;
}

The second option is to import and use the useCubeTexture() hook from Drei.

useCubeTexture() takes two arguments: the first is an array of file names, and the second is an object specifying the path to these files.

function UpdateSceneBackground() {
  const { scene } = useThree();

  const texture = useCubeTexture(
    [
      'texture 1.jpg',
      'texture 2.jpg',
      'texture 3.jpg',
      'texture 4.jpg',
      'texture 5.jpg',
      'texture 6.jpg',
    ],
    { path: '' }
  );

  scene.background = texture;

  return null;
}
Cube texture set as a scene background
Cube texture set as a scene background

Events

Raycasting, or picking objects from the scene, is one of the easiest tasks in React Three Fiber. In fact, you might not fully appreciate how easy it is until you compare it to doing the same in standard Three.js!

In React Three Fiber, we have a wide range of events, from mouse click and hover to double-click and more. All we need to do is attach these events to objects and specify what should happen when the event is triggered. It's very similar to handling events in Vanilla JavaScript.

Here's an example. Let’s say we want to toggle the wireframe mode of the box when it gets clicked.

First, we’ll use state to check whether the wireframe mode is active or not.

import { useRef, useState } from 'react';
// Within AnimatedBox()

const [wireframe, setWireframe] = useState(false);

Next, we’ll define a handler function to toggle the state and pass this state to the wireframe prop of the material.

// Within AnimatedBox()

const [wireframe, setWireframe] = useState(false);

// Handle click event to toggle the wireframe mode
const handleClick = () => {
  setWireframe(wireframe === false ? true : false);
};

//...

<meshStandardMaterial color={color} wireframe={wireframe} />

Finally, we’ll assign the onClick event to the mesh.

<mesh
  ref={boxRef}
  position={[5, 3, 0]}
  castShadow
  onClick={handleClick}
>

Full Example.

Conclusion

And that wraps up this article! I hope I was able to smooth out your initial experience with React Three Fiber and, most importantly, make it both enjoyable and fun.

As you continue to explore and create with React Three Fiber, remember that experimentation is key. The world of 3D graphics is vast and exciting, so keep pushing your boundaries and have fun with your projects!

Resources

Related Content