Creating a 3D Table Configurator with React Three Fiber
Tags: webdev, beginners, tutorial, threejs
Published: 2023-02-21 01:00:25

Let's create a 3D Table Configurator using the following libraries:

- Vite
- React
- Tailwind
- Three.js
- React Three Fiber
- Material UI

🔥 This tutorial is a good starting point to create a product configurator for an exciting shopping experience. 

The main topics covered are:
- how to load a 3D model
- how to modify it using a user interface
- how to scale and move items smoothly


A video version is also available where you can watch the final render:

{% embed https://www.youtube.com/watch?v=wW0XwNhrDFQ %}

## Project Setup

I prepared you a [starter pack](https://github.com/wass08/table-configurator-three-js-r3F-tutorial-starter) including:

- a React app created with Vite
- a MUI User Interface
- three.js/React Three Fiber installed including a Canvas displaying a cube to get started
- the 3D model of the table in `public/models`


![3D Table model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uubbyeodpwwxaiiglppa.png)

_The table model contains all the different legs layout_

![Table model hierarchy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2dp6mgczg2kxbui70gt.png)

I separated in different meshes the left and right legs to make it simple when we will expand the table width.


**Clone the repo and run **

```
yarn
yarn dev
```


![A simple 3D cube](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f5b92z5muklvlhtk66z0.png)

You should see a cube and be able to rotate around it thanks to `<OrbitControls />`

## Staging

Now let’s create a better staging environment. I rarely start from scratch, I recommend you to go through [React Three Fiber examples](https://docs.pmnd.rs/react-three-fiber/getting-started/examples) to find a good starting point.

I chose one named [stage presets, gltfjsx](https://codesandbox.io/s/if9crg)


![3D car](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2eyo25ss8ce30wx69zjj.png)

_It contains nice lighting and shadows settings._

We start by copying the canvas with the camera settings and enabling shadows.

```
<Canvas shadows camera={{ position: [4, 4, -12], fov: 35 }}>
...
</Canvas>
```
_I set `y position` of the camera to `4`_

Then we copy the `stage` component and `orbitcontrols` and instead of using their model, we use the cube for now.

```
<Stage intensity={1.5} environment="city" shadows={{
  type: "accumulative",
  color: "#85ffbd",
  colorBlend: 2,
  opacity: 2,
}}
  adjustCamera={2}
>
...
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2} />
```

I assigned the color to the one of the gradient in the CSS index file. 
It helps to build good looking shadows on the floor. I also changed adjustCamera to `2` to zoom out a little bit.

`minPolarAngle` and `maxPolarAngle` are limits you can define on `OrbitControls` to avoid going below or over the model.

Adjust those parameters to what you prefer.

We can’t see shadows yet even if the canvas and stage have shadows enabled. 

We need to tell our mesh to castShadows.

![Adding cast shadow to our mesh](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4vwxdchf127350mw8dj.png)


 Now we can see the very smooth shadow generated by the stage component.


![Cube with shadows](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf07vqmd79bfv57ddzdn.png)




## Load the table

Let’s render our table instead of the default cube.

To do so we use the `gltfjsx` client with 

```npx gltjfs public/models/Table.gltf``` 

It generates a `Table.js` file containing a React component with the extracted meshes from the table model file.

Let’s copy everything, delete the file, and create a new one in the `components` folder named `Table.jsx`

By default it’s named `Model`, rename it to `Table`. We need to fix the path adding `./models` for both `useGltf` and the `preload` call.


![Table component code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/klqkn4y1owgfjguum87r.png)
_The final component code should looks like this_


Now let’s replace the cube with the table in `Experience.jsx`.


![Experience code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9ehfeieajsn69vjrwgw.png)

There’s no shadows yet. So let’s add the `castShadow` prop on every meshes on the `Table` component.

```
<mesh casthShadows />
```


![Table model with shadows enabled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2bxsqrqr9dfgtrm65bpn.png)

Now our table renders shadows correctly, but all the legs layouts are displayed at the same time.

## Legs layout

To be able to display only one layout at a time let’s create a folder named `contexts` and a file named `Configurator.jsx`

Let's create a context Boilerplate with `createContext` `ConfiguratorProvider` and `useConfigurator`.


![Context boilerplate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/malr6y6l28k6ayr4u0zc.png)


Ok, now we want to have the choice between our legs layout, so we define `legs` and `setLegs` with useState. 
Our layouts will be 0, 1 and 2. so let’s default to 0.

```
const [legs, setLegs] = useState(0);
```

Go back to our table and get the legs from useConfigurator.

```
const { legs } = useConfigurator();
```

And we will simply do conditional rendering for our legs.

If it’s 0, we render the first one, if it’s 1 we render the second layout, and if it’s 2 we render the last one.

```
{legs === 0 && (
        <>
          <mesh
            castShadow
            geometry={nodes.Legs01Left.geometry}
            material={materials.Metal}
            position={[-1.5, 0, 0]}
          />
          <mesh
            geometry={nodes.Legs01Right.geometry}
            material={materials.Metal}
            position={[1.5, 0, 0]}
            castShadow
          />
        </>
      )}
      {legs === 1 && (
        <>
          <mesh
            geometry={nodes.Legs02Left.geometry}
            material={materials.Metal}
            position={[-1.5, 0, 0]}
            castShadow
          />
          <mesh
            geometry={nodes.Legs02Right.geometry}
            material={materials.Metal}
            position={[1.5, 0, 0]}
            castShadow
          />
        </>
      )}
      {legs === 2 && (
        <>
          <mesh
            geometry={nodes.Legs03Left.geometry}
            material={materials.Metal}
            position={[-1.5, 0, 0]}
            castShadow
          />
          <mesh
            geometry={nodes.Legs03Right.geometry}
            material={materials.Metal}
            position={[1.5, 0, 0]}
            castShadow
          />
        </>
      )}
```

_In a real project you could refactor it more nicely._

Now jump into `main.jsx` and wrap the app in our `ConfiguratorProvider` to make our context available everywhere.


![main source code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snvjwsr256xmqf5eujp0.png)
_Wrapping the App in the Configurator provider_


![3D Table with first layout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ae9xds3yxbap6xkvu637.png)

_It works, now we only see the first legs layout!_

Let’s add the `Interface` component I prepared for you next to the canvas. 

```
<Canvas shadows camera={{ position: [4, 4, -12], fov: 35 }}>
  <Experience />
</Canvas>
<Interface />
```



![Interface enabled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ruv8sgh5djzqldo9ey2m.png)
_You now can see the different options available._

Now let's go to our `Interface` code.

I commented the full settings so we don’t mess up with the naming.

Let’s grab legs and setLegs from our configurator.

```
const [legs, setLegs] = useConfigurator();
```

...and let’s uncomment the value and onChange on our layout radio buttons.


![Code uncomment on the interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nq8wofojpdgdcw5up867.png)


Now when we switch, the legs change correctly...

...but the shadows are not re-rendered because the scene doesn’t know something changes.

A simple way to tell it is to get the legs value in the experience component to force the re-rendering.

```
const { legs } = useConfigurator();
```
_Add this line to the `Experience` component._

Now the shadows are re-generated when we switch.


## Legs color

Let’s change the legs color. Go to the `Configurator` and create `legsColor` and `setLegsColor` with the same default value we have in the interface. Feel free to change it.
Don't forget to add it to the exposed values from the context.


![Context code with legs colors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4y81syr8ypjqxwn0chxi.png)

Let's apply the color to the `Table`.

```
const { legs, legsColor, setLegsColor } = useConfigurator();
```

We add a useEffect with legsColor so every time it changes, this function will get called.

```
useEffect(() => {
  materials.Metal.color = new Three.Color(legsColor);
}, [legsColor]);
```

We need to add this import line manually:
```
import * as Three from "three";
```

On the interface let’s add the `legsColor` and `setLegsColor` and uncomment the value and onChange.

```
<FormControl>
  <FormLabel>Legs Color</FormLabel>
  <RadioGroup
    value={legsColor}
    onChange={(e) => setLegsColor(e.target.value)}
>
```



![Table with gold legs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5v4jjxpiy6hg4xl43xx2.png)


Now the color of our legs changes every time we apply it.


## Table width

Last step, let’s change the width from our table!

In our `Configurator` context create a `tableWidth` state with a default value of `100` (centimeters)


![Context code with table width](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7sgzlcx99x91cy8her16.png)

_Our final context should looks like this_


Get it into the `Table` component, and let’s calculate a `scalingPercentage` by diving the tableWidth per one hundred. 

```
const tableWidthScale = tableWidth / 100;
```

On the plate, change the `scale` with the tableWidthScale on the `x` axis, and keep the `y` and `z` to `1`.


![Apply table width scale](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc0ofkmmmzjodlezm0jg.png)


In the `Interface` let’s uncomment the slider `value` and `onChange`.

```
const { tableWidth, setTableWidth, legs, setLegs, legsColor, setLegsColor } =
    useConfigurator();
...
<FormControl>
  <FormLabel>Table width</FormLabel>
  <Slider
  sx={{
  width: "200px",
  }}
  min={50}
  max={200}
  value={tableWidth}
  onChange={(e) => setTableWidth(e.target.value)}
  valueLabelDisplay="auto"
  />
</FormControl>
```

Now our table width change but we need to move the legs accordingly, we can do it simply by multiplying the `x` position by the tableWidthScale.


![Code apply x position of the table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rvu686pvesjo1m7073sz.png)

_Adjusting the `x` position of the table legs_



![3D table with a longer width](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pzd97vcecuza3gqt9i8z.png)

_Now it works correctly, but still the movement is not very smooth_



## Smooth animation

To make a smooth animation when we changes the table width, let’s store references of our plate, left legs and right legs.

```
const plate = useRef();
const leftLegs = useRef();
const rightLegs = useRef();
```

Because our `leftLegs` and `rightlegs` are never rendered at the same time, we can save the 3 legs layouts in the same references.

Let’s remove the tableWidthScale multiplier as we will do it another way.

```
{legs === 0 && (
        <>
          <mesh
            castShadow
            geometry={nodes.Legs01Left.geometry}
            material={materials.Metal}
            position={[-1.5, 0, 0]}
            ref={leftLegs}
          />
          <mesh
            geometry={nodes.Legs01Right.geometry}
            material={materials.Metal}
            position={[1.5, 0, 0]}
            castShadow
            ref={rightLegs}
          />
        </>
      )}
      {legs === 1 && (
        <>
          <mesh
            geometry={nodes.Legs02Left.geometry}
            material={materials.Metal}
            position={[-1.5, 0, 0]}
            castShadow
            ref={leftLegs}
          />
          <mesh
            geometry={nodes.Legs02Right.geometry}
            material={materials.Metal}
            position={[1.5, 0, 0]}
            castShadow
            ref={rightLegs}
          />
        </>
      )}
      {legs === 2 && (
        <>
          <mesh
            geometry={nodes.Legs03Left.geometry}
            material={materials.Metal}
            position={[-1.5, 0, 0]}
            castShadow
            ref={leftLegs}
          />
          <mesh
            geometry={nodes.Legs03Right.geometry}
            material={materials.Metal}
            position={[1.5, 0, 0]}
            castShadow
            ref={rightLegs}
          />
        </>
      )}
```

We use the `useFrame` hook which will be called at each frame.

It provides the `state`, and the `delta` time. which is the time between elapsed from the last frame.

We declare a targetScale Vector3 with tableWidthScale on the `x` axis. And on the plate scale we use the lerp function to transition smoothly from our currentScale into our targetScale.

```
import { useFrame } from "@react-three/fiber"; 
...
useFrame((_state, delta) => {
  const tableWidthScale = tableWidth / 100;
  const targetScale = new Vector3(tableWidthScale, 1, 1);

  plate.current.scale.lerp(targetScale, delta);
});
```

Now it animates smoothly, but it’s too slow, let’s define an `ANIM_SPEED` constant of `12` and multiply the delta by it.

```
const ANIM_SPEED = 12;
...
plate.current.scale.lerp(targetScale, delta * ANIM_SPEED);
```

Let’s do the same process for both legs impacting the `position` instead of the `scale`:

```

const ANIM_SPEED = 12;

export function Table(props) {
  const { nodes, materials } = useGLTF("./models/Table.gltf");

  const { legs, legsColor, tableWidth } = useConfigurator();

  const plate = useRef();
  const leftLegs = useRef();
  const rightLegs = useRef();

  useEffect(() => {
    materials.Metal.color = new Three.Color(legsColor);
  }, [legsColor]);

  useFrame((_state, delta) => {
    const tableWidthScale = tableWidth / 100;
    const targetScale = new Vector3(tableWidthScale, 1, 1);

    plate.current.scale.lerp(targetScale, delta * ANIM_SPEED);

    const targetLeftPosition = new Vector3(-1.5 * tableWidthScale, 0, 0);
    leftLegs.current.position.lerp(targetLeftPosition, delta * ANIM_SPEED);

    const targetRightPosition = new Vector3(1.5 * tableWidthScale, 0, 0);
    rightLegs.current.position.lerp(targetRightPosition, delta * ANIM_SPEED);
  });
```


![Final result of the 3D table configurator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mrwurc0e887cy8hecgyi.png)

_Yes! Our table configurator now works perfectly!_



## Conclusion

Congratulations you now have a great starting point to build a 3D product configurator using React Three Fiber and Material UI.

[Live preview](https://codesandbox.io/p/github/wass08/table-configurator-three-js-r3F-tutorial-final/main?workspaceId=5cd54208-3423-4f91-a7e8-14ca07bdc9af&file=%2Fsrc%2FApp.jsx)

The code is available here:
[https://github.com/wass08/table-configurator-three-js-r3F-tutorial-final](https://github.com/wass08/table-configurator-three-js-r3F-tutorial-final)

I highly recommend you to read [React Three Fiber documentation](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) and check their [examples](https://docs.pmnd.rs/react-three-fiber/getting-started/examples) to discover what you can achieve and how to do it.

For more React Three Fiber tutorials you can check my [Three.js/React Three Fiber](https://www.youtube.com/watch?v=LNvn66zJyKs&list=PLpepLKamtPjiUF6PvVUbIFhx9HaS0qJs_) playlist on YouTube.

Thank you, don't hesitate to ask your questions in the comments section 🙏