David Omotayo Frontend developer and indie game enthusiast.

Configure 3D models with react-three-fiber

13 min read 3660

react-three-fiber

Loading 3D models into a WebGL scene is one of the most complicated aspects of 3D rendering in the browser. While libraries like Three.js and Babylon.js provide powerful APIs that help ease the tension of working with WebGL, they are not immune from their own tedious processes, like creating shaders from scratch and reusing scene.add() when adding objects to the scene.

react-three-fiber is a React renderer for Three.js that eases working with 3D models on the web by handling imperative Three.js functionalities under the hood and providing access to primitive Three.js objects through Hooks.

In this article, we’ll cover how to render and configure 3D assets created in a 3D software program like Blender or Maya in a React project using react-three-fiber. By the end of this article, you’ll be able to render 3D models on your website.

Prerequisites

To follow along with this tutorial, you’ll need a fundamental knowledge of React and Three.js’ architecture, including the lighting, geometry, and canvas. You’ll also need to have Node.js installed on your machine. Let’s get started!

Creating and preparing a 3D model for the web

Out of the box, Three.js supports several 3D model formats. Due to the complexity and difficulty of most of these file formats, it’s recommended to use the glTF (GL Transmission Format) whenever possible.

Focused on runtime asset delivery, glTF assets are fast to load and can be compressed into a compact size for transmission. glTF assets are provided in both JSON .gltf and binary .glb formats. Each supported file format has a respective loader in Three.js. For example, you’d use the glTF loader to load a glTF or GLB file into a react-three-fiber scene.

To create your model, you can use the 3D modeling software of your choice, but in my experience, most people prefer Blender. If you’re like me, and you don’t have any experience with creating 3D models, you can outsource public-domain glTF files from a site like sketchfab.com.

Most of the free assets on these sites fall under the Creative Commons license, meaning you can use them in your project as long as you include attribution to the original creators. It’s wise to check for permission to use an asset before including it in your project.

If you plan to add actions like animation or event mapping to your model, you should design your asset in such a way that each part, or mesh, is properly grouped and separated. Unfortunately, few of the free assets available online have this implemented, so you’d need to purchase one or build your own.

In this tutorial, we’ll use the shoe asset as an example project. If you’re finding it hard to build or find a model, you can get one from the Three.js GitHub repo.

We made a custom demo for .
No really. Click here to check it out.

Converting glTF to GLB

glTF is based on JSON, meaning some of its data is stored externally, for example, geometry, shaders, textures, and animation data. However, GLB stores this data internally, making its size considerably smaller than glTF. Therefore, the .glb binary format is best to use in your projects. Now, let’s walk through converting glTF files to GLB.

There are several tools you could use for this operation, however, the glTF-pipeline is one of the best choices. It is a flexible tool for optimizing glTF assets that has several extensions for performing common operations right from your command line tool, like GLB to glTF conversion, glTF compression, and more.

Alternately, you can use the official glTF tool extension for VS Code to preview, debug, and convert models right from the editor. You can also use this extension to edit and tweak glTF files. For example, if your model’s meshes aren’t named properly, you can edit the file and rename it however you want.

To begin the conversion process with glTF-pipeline, first, we need to install it globally on our machine with the following command:

npm install -g gltf-pipeline

Next, place the glTF file in an empty folder and open your command line tool. cd into the folder and run the command below:

gltf-pipeline -i <source file> -o <output file>

Replace the first placeholder in the command above with the name of the source file. Replace the second placeholder with your preferred output file name:

gltf-pipeline -i shoe.gltf -o shoe.glb

After running the command above, you should see a new GLB file in the folder alongside the previous glTF file. This process also works the other way around for converting GLB to glTF format.

Compressing the model

To avoid degrading our site’s performance with the size of our 3D asset, we’ll compress it before loading it into our scene. It’s recommended that your file size not exceed one to two megabytes. We’ll use a glTF pipeline compression extension called Draco:

gltf-pipeline -i <source file> -o <output file> --draco.compressionLevel=10

The command above is similar to the one we used earlier during the conversion process, with the only difference being the compression level flag. Draco compression has a maximum value of ten and a minimum value of zero. Passing 10 into the compression level provides the maximum possible compression.

Now, we can set up a project, create a scene, and load our model.

Setting up our react-three-fiber project

First, let’s create a new React project with Create React App:

npx create-react-app react-three

Afterwards, install react-three-fiber with the command below:

npm install three @react-three/fiber

Once you’re done, go ahead and run the command below to install all the dependencies we need for the project:

npm i @react-three/drei react-colorful valtio

The command above will install the following dependencies:

  • react-colorful: color picker component for React
  • drei: provides useful add ons to react-three-fiber, like cameras, plane, and controls
  • Valtio: a lightweight, proxy-based state management tool for React

Next, we’ll create a scene in an empty component. First, import and create an empty canvas as follows:

import React from "react";
import { Canvas } from "react-three-fiber";
import "./styles.css";
export default function App() {
  return <Canvas style={{ background: "#171717" }}></Canvas>;
}

Now, our project and scene are set up and ready for us to start loading our models! Before we can load our asset into the scene and configure it, we need to find an easy way to convert our model into a component.

Converting the model into a reusable React component

Loading glTF models into a Three.js scene is a lot of work. Before we can configure or animate our model’s meshes, we need to iterate through each part of our model’s meshes and save them separately.

Luckily for us, react-three-fiber has a remarkable utility package called gltfjsx that breaks down the model and compiles it into a declarative and reusable JSX component.

To begin, open your command line tool, cd into the folder where your compressed glTF model is, and run the command below:

npx gltfjsx <glTF model source file>

Run the command above on the shoe model we downloaded earlier. Now, the code above will look like the following:

npx gltfjsx shoe-draco.gltf

The command above will return a JavaScript file that schemes out all the asset’s content in the format of a React functional component. The file’s content will look similar to the following code:

import React, { useRef } from 'react'
import { useGLTF } from '@react-three/drei/useGLTF'

function Shoe({ ...props }) {
 const group = useRef()
 const { nodes, materials } = useGLTF('/shoe-draco.glb')
 const snap = useSnapshot(state);
 return (
<group ref={group} {...props} dispose={null}>
  <group ref={group} {...props} dispose={null}>
    <mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
    <mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
    <mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
    <mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
    <mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
    <mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
    <mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
    <mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
   </group>
 </group>
)}

The nodes’ and materials’ values are being destructured by the useGLTF Hook, while the path to our model is being passed into the Hook as a parameter.

The destructured node is an object that contains all the information in our model, including the animation, texture, and geometry. In this case, the node is using the model’s restructured geometry and is passed into the mesh components as props.

Adding the model component to the scene

You can either drop the newly created file into your project’s src folder as is, or you can copy and paste the code from inside the file into an existing component in your project.

To add the model to the scene, import it as you would any other React component:

import Shoe from './Shoe.js'

Next, move the glTF file you compressed earlier to the /public folder. Finally, add the component inside your canvas:

import React from "react";
import { Canvas } from "react-three-fiber";
import Shoe from './Shoe.js'
import "./styles.css";

export default function App() {
  return(
<Canvas style={{ background: "#171717" }}>
   <Shoe />
</Canvas>;
)};

You could also add it to your component as follows:

import React, { useRef } from "react"
import { Canvas } from "react-three-fiber"
import { useGLTF } from '@react-three/drei/useGLTF'
import "./styles.css";

function Shoe({ ...props }) {
 const group = useRef()
 const { nodes, materials } = useGLTF('/shoe-draco.glb')
 return (
<group ref={group} {...props} dispose={null}>
  <group ref={group} {...props} dispose={null}>
    <mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
    <mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
    <mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
    <mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
    <mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
    <mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
    <mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
    <mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
   </group>
 </group>
)}

export default function App() {
  return(
<Canvas style={{ background: "#171717" }}>
   <Shoe />
</Canvas>;
)};

At this point, if you start the development server, React will throw a compile error. The model component is asynchronous, therefore, we have to nest it within the <Suspense>component in the canvas, giving us control over intermediate loading fallbacks and error handling.

First, we need to import the model component from React, then wrap it around the model in the canvas:

 import React, { Suspense, useRef } from "react";
import { Canvas } from "react-three-fiber";
import { useGLTF } from '@react-three/drei/useGLTF'
import "./styles.css";

function Shoe({ ...props }) {
 const group = useRef()
 const { nodes, materials } = useGLTF('/shoe-draco.glb')
 return (
<group ref={group} {...props} dispose={null}>
  <group ref={group} {...props} dispose={null}>
    <mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
    <mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
    <mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
    <mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
    <mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
    <mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
    <mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
    <mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
   </group>
 </group>
)}

export default function App() {
  return(
<Canvas style={{ background: "#171717" }}>
  <Suspense fallback={null}>
     <Shoe />
  </Suspense>
</Canvas>;
)};

The code above will clear the error, and our model will be successfully displayed on the browser. However, whatever we load into our scene will appear only as a silhouette figure of the model.

React Three Fiber Model Silhouette

Currently, our scene doesn’t have a lighting source. A common solution is to simply add ambientLight and spotLight components just before <Suspense> in the canvas:

<Canvas style={{ background: "#171717" }}>
<ambientLight intensity={1} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
  <Suspense fallback={null}>
      <Shoe />
   </Suspense>
</Canvas>;

Afterwards, the model will display properly:

React Three Fiber Scene Lighting Source

Go ahead and fiddle with the intensity, angle, and position of the lights. If you’re not satisfied with the result, you could also position the light by adding a positioning prop.

Configuring the model

Now that our model is declarative, we have complete access to its meshes, meaning we can add, remove, and alter parts of our model.

For example, if the first mesh component in the code represents the shoe’s laces, and we delete or comment out that mesh, the shoe’s laces will disappear in the scene. You could decide to animate, add events, and even perform conditions on your model.

To alter the model, simply change the props. If we want to change the color of the first mesh to red, add material-color="Red" as a prop. In the scene, that part of the model will change to red:

<mesh material={materials.White} geometry={nodes['buffer-0-mesh-0'].geometry} material-color="Red"/>

We can do the same thing dynamically with React’s useState Hook, as well as any state management library that supports Suspense. In this case, we’ll use Valtio, which we installed earlier.

Configuring our model with Valtio

Go ahead and import proxy and useSnapshot from Valtio:

import {proxy, useSnapshot} from 'valtio'

Next, copy the code below and paste it just before your model component:

const state = proxy({
  current: null,
  items: {
    laces: "#ff3",
    mesh: "#3f3",
    caps: "#3f3",
    inner: "#3f3",
    sole: "#3f3",
    stripes: "#3f3",
    band: "#3f3",
    patch: "#3f3",
  },
})

We created an object with two keys, items and current. current has a value of null, while items has an object value with keys representing each part of our model and a hex color value. The object is then wrapped with the proxy.

To use the state inside the component, we’ll create a constant snap variable inside the model component and assign it useSnapshot. Afterwards, we’ll pass the state into the snapshot as a parameter:

import React, { useRef, Suspense} from 'react'
import { Canvas } from "react-three-fiber";
import "./styles.css";
import { useGLTF } from '@react-three/drei/useGLTF'
import {proxy, useSnapshot} from 'valtio';

const state = proxy({
  current: null,
  items: {
    laces: "#ff3",
    mesh: "#3f3",
    caps: "#3f3",
    inner: "#3f3",
    sole: "#3f3",
    stripes: "#3f3",
    band: "#3f3",
    patch: "#3f3",
  },
})

function Shoe({ ...props }) {
 const group = useRef()
 const { nodes, materials } = useGLTF('/shoe-draco.glb')
 const snap = useSnapshot(state);
 return (
<group ref={group} {...props} dispose={null}>
  <group ref={group} {...props} dispose={null}>
    <mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
    <mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
    <mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
    <mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
    &lt;mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
    <mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
    <mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
    <mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
   </group>
 </group>
)}

export default function App() {
  return <Canvas style={{ background: "#171717" }}>
<ambientLight intensity={1} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
  <Suspense fallback={null}>
      <Shoe />
  </Suspense>
</Canvas>;
}

Before we can use the state’s snapshots in the component, we have to set the material color of each mesh to the state variable color. To do so, we’ll add the material-color props to each mesh as we did previously. This time, we’ll pass it our state’s snapshot:

const state = proxy({
  current: null,
  items: {
    laces: "#ff3",
    mesh: "#3f3",
    caps: "#3f3",
    inner: "#3f3",
    sole: "#3f3",
    stripes: "#3f3",
    band: "#3f3",
    patch: "#3f3",
  },
})

function Shoe({ ...props }) {
 const group = useRef()
 const { nodes, materials } = useGLTF('/shoe-draco.glb')
 const snap = useSnapshot(state);
 return (
<group ref={group} {...props} dispose={null}>
  <group ref={group} {...props} dispose={null}>
    <mesh geometry={nodes.shoe.geometry} material={materials.laces} material-color={snap.items.laces}/>
    <mesh geometry={nodes.shoe_1.geometry} material={materials.mesh} material-color={snap.items.mesh}/>
    <mesh geometry={nodes.shoe_2.geometry} material={materials.caps} material-color={snap.items.caps}/>
    <mesh geometry={nodes.shoe_3.geometry} material={materials.inner} material-color={snap.items.inners}/>
    <mesh geometry={nodes.shoe_4.geometry} material={materials.sole} material-color={snap.items.sole}/>
    <mesh geometry={nodes.shoe_5.geometry} material={materials.stripes} material-color={snap.items.stripes}/>
    <mesh geometry={nodes.shoe_6.geometry} material={materials.band} material-color={snap.items.band}/>
    <mesh geometry={nodes.shoe_7.geometry} material={materials.patch} material-color={snap.items.patch}/>
   </group>
 </group>
  )
  }

Now, if we change any of the colors in our state items, that part of the model will update its color in the scene.

Adding events

Instead of changing the state manually, we can add events to our model so that users can select each part of the shoe and change the color with a color picker.

First, we’ll add onPointerDown and onPointerMissed events to the model’s group component. We’ll create DOM event functions that set state to the part of the model that was clicked and null when nothing is selected:

<group ref={group} {...props} dispose={null}
 onPointerDown={(e) => {e.stopPropagation(); state.current = e.object.material.name }}
onPointerMissed={(e) =>{state.current = null} }
>

Now, we’ll import HexColorPicker from react-colorful. We’ll create a new component that will house the HexColorPicker component and a header tag that displays the name of the part of the model being clicked:

import {HexColorPicker} from 'react-colorful'


 function ColorPicker(){
 const snap = useSnapshot(state);
  return(
   <div>
     <HexColorPicker color={snap.items[snap.current]} onChange={(color)=>(state.items[state.current] = color)}/>
     <h1>{snap.current}</h1>
   </div>
    )
  }

Immediately after we save our code, a color picker GUI should appear on the browser that looks like the image below:

React Three Fiber Color Picker GUI

Now, to change the color of our model’s mesh, we can click it and select a color with the color picker. You can also add classNames to the color picker and an <H1> tag.

React Three Fiber Custom Styling Color GUI

Animating the model

Right now, the model doesn’t feel like a 3D object. Specifically, it lacks depth and movement like a static image. We can fix this by adding an orbitControls component to the canvas:

 export default function App() {
      return (
<Canvas style={{ background: "#171717" }}>
 <spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
 &lt;ambientLight intensity={1} />
 <Suspense fallback={null}>
          <Shoe />
 </Suspense>
       <orbitControls />
</Canvas>;
    )}

In react-three-fiber, models are animated with the useFrame Hook, which is similar to the JavaScript requestAnimationFrame() method. It returns a callback 60 times or more per second, depending on the display refresh rate.

If we set the value of our model’s rotation on the y-axis to "5.09" inside the useFrame callback, the model will translate up and down along the y-axis 60 times every second, creating the illusion of a floating object.

For an even more realistic animation, you can alter the rotation value of the z-axis and the x-axis in the useFrame Hook. To save time, we’ll use the code below:

useFrame( (state) => {
 const t = state.clock.getElapsedTime()
 ref.current.rotation.z = -0.2 - (1 + Math.sin(t / 1.5)) / 20
 ref.current.rotation.x = Math.cos(t / 4) / 8
 ref.current.rotation.y = Math.sin(t / 4) / 8
 ref.current.position.y = (1 + Math.sin(t / 1.5)) / 10
  })

Remember to import useFrame from react-three-fiber.

The complete code for our project is as follows:

import {HexColorPicker} from 'react-colorful'
import React, { useRef, Suspense} from 'react'
import { Canvas, useFrame } from "react-three-fiber";
import "./styles.css";
import { useGLTF } from '@react-three/drei/useGLTF'
import {proxy, useSnapshot} from 'valtio';

const state = proxy({
  current: null,
  items: {
    laces: "#ff3",
    mesh: "#3f3",
    caps: "#3f3",
    inner: "#3f3",
    sole: "#3f3",
    stripes: "#3f3",
    band: "#3f3",
    patch: "#3f3",
  },
})

function Shoe({ ...props }) {
 const group = useRef()

useFrame( (state) => {
 const t = state.clock.getElapsedTime()
 ref.current.rotation.z = -0.2 - (1 + Math.sin(t / 1.5)) / 20
 ref.current.rotation.x = Math.cos(t / 4) / 8
 ref.current.rotation.y = Math.sin(t / 4) / 8
 ref.current.position.y = (1 + Math.sin(t / 1.5)) / 10
  })

 const { nodes, materials } = useGLTF('/shoe-draco.glb')
 const snap = useSnapshot(state);
 return (
<group 
ref={group} 
{...props} 
dispose={null}
onPointerDown={(e) => {e.stopPropagation(); state.current = e.object.material.name }}
onPointerMissed={(e) =>{state.current = null} }
>
  <group ref={group} {...props} dispose={null}>
    <mesh geometry={nodes.shoe.geometry} material={materials.laces}/>
    <mesh geometry={nodes.shoe_1.geometry} material={materials.mesh}/>
    <mesh geometry={nodes.shoe_2.geometry} material={materials.caps} />
    <mesh geometry={nodes.shoe_3.geometry} material={materials.inner} />
    <mesh geometry={nodes.shoe_4.geometry} material={materials.sole}/>
    <mesh geometry={nodes.shoe_5.geometry} material={materials.stripes}/>
    <mesh geometry={nodes.shoe_6.geometry} material={materials.band}/>
    <mesh geometry={nodes.shoe_7.geometry} material={materials.patch} />
   </group>
 </group>
)}

function ColorPicker(){
 const snap = useSnapshot(state);
  return(
   <div>
     <HexColorPicker color={snap.items[snap.current]} onChange={(color)=>(state.items[state.current] = color)}/>
     <h1>{snap.current}</h1>
   </div>
    )
  }

export default function App() {
  return (
  <>
<Canvas style={{ background: "#171717" }}>
<ambientLight intensity={1} />
<spotLight intensity={0.5} angle={0.1} penumbra={1} position={[10, 15, 10]} castShadow />
  <Suspense fallback={null}>
      <Shoe />
  </Suspense>
</Canvas>;
<ColorPicker />
</>
)}

Conclusion

In this article, we covered how to configure 3D models for the web and how to map events to models with Valtio state management. We clarified the difference between glTF and GLB files, determining which was the best to use in our project.

We added events to our model, allowing us to create customization tools like a color picker and a lighting source. You can challenge yourself to add more elements to your model, like a shadow caster or a hover event.

Hopefully, this article will serve as a useful resource for incorporating 3D models into your website. Happy coding!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

David Omotayo Frontend developer and indie game enthusiast.

Leave a Reply