Web-Based 3D Scene:
React rendering

|
12 min read
Explore how to bring 3D models to life in a React app with react-three/fiber, making your 3D scenes fully web-integrated and interactive.

Introduction

In this tutorial, we'll explore how to render a 3D scene of a room and interact with in React.

We'll be leveraging two powerful React libraries:

  • @react-three/fiber: A React renderer for Three.js. This library allows you to work with Three.js in the React way, ensuring smooth integration and simpler code patterns.
  • @react-three/drei: A collection of reusable components, helpers, and primitives to speed up your Three.js projects in React. It complements @react-three/fiber by providing a set of higher-level tools and additions, further easing the development process.

Before we dive in lets takea look at the final result:

Feel free to toggle the hanging lamp between light and dark mode, or rotate the scene with your mouse by clicking and dragging the scene.

Prerequisites and Libraries

I've prepared a relatively simple 3D scene of a room using Blender which I then exported to glTF format. The zip file provided below contains the glTF file and two distinct textures — one for light mode and the other for dark mode:

Setting Up the Basic Scene

Lets start off by rendering a basic cube using react-three/fiber. To achieve this we can use the canvas component from the react-three-fiber library. Unlike in traditional threejs app canvas component takes care of couple things for us:

  • settingup the scene and the camera
  • re-rendering the scene on every frame
  • automatically resizing the scene to fit parent container and update on resize

To render a simple cube we can use a mesh and boxGeometry components. Lower case components directly corespond to threejs classes which in our case would be THREE.Mesh and THREE.BoxGeometry.

You can read more about this example on the official react-three-fiber documentation page.

With our basic scene in place, let's move on to loading provided 3d model and textures which would replace the cube.

Loading the Model and Textures

To load our scene we can utilize the useGLTF and useTexture hooks from the @react-three/drei library.

jsx
1import { Canvas } from "@react-three/fiber";
2import { useGLTF, useTexture } from "@react-three/drei";
3
4function Scene() {
5 const { nodes, cameras } = useGLTF(ModelUrl);
6 const texture =useTexture(LightTextureUrl);
7 const { Main, Lamp, Plant } = nodes;
8 const meshes = [Main, Lamp,Plant];
9 return (
10 <>
11 {meshes.map((mesh) => (
12 <mesh
13 geometry={mesh.geometry}
14 rotation={mesh.rotation}
15 position={mesh.position}
16 >
17 <meshBasicMaterial map={texture} map-flipY={false} />
18 </mesh>
19 ))}
20 </>
21 );
22}
23
24// ...
25
26

Notice the nodes object from the useGLTF hook is destructure it into Main, Lamp, and Plant variables. I specifically named those meshes this way in blender during the modeling process to ensure easy identification in our project. While merging meshes can enhance performance, I chose to separate certain ones, making it more convenient to apply interactions and animations.

Make sure to you replace ModelUrl and LightTextureUrl with the appropriate URLs.

Adjusting the Camera

If the scene seems distant, it's due to the default camera position. We can improve it by either manually settingup a camera or integrating the camera from the glTF file:

jsx
1import { Canvas, useThree } from "@react-three/fiber";
2import { useGLTF, useTexture } from "@react-three/drei";
3
4function Scene() {
5 const set = useThree((state) => state.set);
6
7 const { nodes, cameras } = useGLTF(ModelUrl);
8 const texture = useTexture(LightTextureUrl);
9 const { Main, Lamp, Plant } = nodes;
10 const meshes = [Main, Lamp, Plant];
11
12 set({ camera: cameras[0] });
13 return (
14 <>
15 {/* ... */}
16 </>
17 );
18}
19
20// ...
21

Taking it one step further. We can laverage react-three/drei and make your scene more dynamic by enabling the camera to follow mouse movements using the OrbitControls. Its one of many usefull components provided by the drei library that can be used to interact with the scene or a camera. You can explore various settings and controls on the official drei documentation page or in the three js docs which is what drei is relying on

jsx
1// ...
2import {OrbitControls} from '@react-three/drei';
3import {Vector3} from 'three';
4
5function CameraControls() {
6 const lookAt = new Vector3(0, 0.7, 0);
7
8 return (
9 <>
10 <OrbitControls
11 target={lookAt}
12 maxPolarAngle={1.7}
13 minPolarAngle={1}
14 minAzimuthAngle={-0.7}
15 maxAzimuthAngle={0.7}
16 rotateSpeed={0.2}
17 enableZoom={false}
18 dampingFactor={0.02}
19 enablePan={false}
20 enableDamping
21 enableRotate
22 />
23 </>
24 );
25}
26
27function Scene() {
28 const set = useThree((state) => state.set);
29
30 const { nodes, cameras } = useGLTF(ModelUrl);
31 const texture = useTexture(LightTextureUrl);
32 const { Main, Lamp, Plant } = nodes;
33 const meshes = [Main, Lamp, Plant];
34
35 set({ camera: cameras[0] });
36 return (
37 <>
38 <CameraControls/>
39 {/* ... */}
40 </>
41 );
42}
43
44// ...
45

Lets review all the changes we've made so far. For clarity, I've refactored some components into separate files:

Implementing Dark Mode

Every modern UI needs a dark mode so lets add it to our scene! One effective way to achieve this dark/light mode toggle is by baking textures under two distinct lighting conditions. By creating separate textures for each mode, we can simply swap between them to achieve the desired theme effect.

Lets start by implementing the theme state in App.js file:

App.js
jsx
1import { Canvas, useThree } from "@react-three/fiber";
2import Scene from "./Scene";
3
4export default function App() {
5 const [theme, setTheme] = useState('dark');
6 const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark');
7
8 return (
9 <div id="app">
10 <Canvas>
11 <Scene toggleTheme={toggleTheme} theme={theme} />
12 </Canvas>
13 </div>
14 );
15}
16

Next, modify the scene to utilize our theme state:

Scene.js
js
1/* ... */
2
3export default function Scene({ theme, toggleTheme }) {
4 const { nodes, cameras } = useGLTF(ModelUrl);
5 const texureUrl = theme === "dark" ? DarkTextureUrl : LightTextureUrl;
6 const texture = useTexture(texureUrl);
7 const { Main, Lamp, Plant } = nodes;
8 const meshes = [Main, Lamp, Plant];
9
10 return (
11 <>
12 {/* ... */}
13 </>
14 );
15}
16
17

Now we need a way to toggle the theme. You could build some UI for it, but a cooler way would be to toggle the theme by interacting with the scene. Lets make the lamp clickable and toggle the theme when its clicked. Thanks to react-three-fiber, it's as simple as adding an onClick prop to the mesh element:

Mesh.js
jsx
1export default function Mesh({mesh, texture, onClick}) {
2 return (
3 <mesh
4 onClick={onClick}
5 geometry={mesh.geometry}
6 rotation={mesh.rotation}
7 position={mesh.position}
8 >
9 <meshBasicMaterial map={texture} map-flipY={false} />
10 </mesh>
11 );
12}
13

Lets now change the way we render scene elements so we can append the onClick prop only to the lamp:

Scene.js
js
1/* ... */
2
3export default function Scene({ theme, toggleTheme }) {
4 const { nodes, cameras } = useGLTF(ModelUrl);
5 const texureUrl = theme === "dark" ? DarkTextureUrl : LightTextureUrl;
6 const texture = useTexture(texureUrl);
7 const { Main, Lamp, Plant } = nodes;
8 const meshes = [Main, Lamp, Plant];
9
10 return (
11 <>
12 <CameraControls camera={cameras[0]}/>
13 {meshes.map((mesh) => (<Mesh key={mesh.uuid} mesh={mesh} texture={texture} />))}
14 <Mesh onClick={toggleTheme} mesh={Lamp} texture={texture} />
15 <Mesh mesh={Plant} texture={texture} />
16 <Mesh mesh={Main} texture={texture} />
17 </>
18 );
19}
20
21

To impove the look of the scene we can also change background color depending on the theme:

App.js
jsx
1import { Canvas, useThree } from "@react-three/fiber";
2import Scene from "./Scene";
3
4export default function App() {
5 const [theme, setTheme] = useState('dark');
6 const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark');
7
8 return (
9 <div id="app" style={{background: theme === 'dark' ? 'black' : 'white'}}>
10 <Canvas>
11 <Scene toggleTheme={toggleTheme} theme={theme} />
12 </Canvas>
13 </div>
14 );
15}
16

You might notice that when you click the lamp it takes a while for the theme to change. Thats because the texture needs to be loaded first which can take a second depending on your connection. We can fix that by using the useTexture.preload function from the drei library which preloads both textures making sure they're available when needed.

Scene.js
js
1import {useTexture} from '@react-three/drei';
2
3useTexture.preload(DarkTextureUrl);
4useTexture.preload(LightTextureUrl);
5
6

Final Result

Here's the compiled version of our interactive 3D scene:

I hope you've learned something from this tutorial. Please reach out if you have any feedback or questions!

Leave feedback