First of all I want to say... "Yes , I know That demotivating feeling you have as a React or NextJS developer when you try to make 3JS (threeJS) play well with react".
Assuming you are a javaScript programmer with background in React or NextJS and are exploring ThreeJS, but all you want is to create declarative and reusable 3D components.Well now you can 😁😁😁!! This is all thanks to this library called react-three/fiber.
what is it? It is react library that expresses threeJS in JSX and this allows you to create scenes declaratively with I quote "re-usable, self-contained components that react to state, are readily interactive and can tap into React's ecosystem". If you want to learn more about it you can just simple jump into their documentation, well after this article of course.
Let's get started
Start your react or nextJS project, in this tutorial I am going to be using nextJS, but you can follow along with React as well.
step 1 - Start a new project :
npx create-next-app@latest
step 2 - Install threejs and react-three/fiber :
- Navigate to your project root folder
- run :
npm install three @react-three/fiber
step 3 - Lets Create our scene :
Before you can create any scene you first have to think of all the components that will exist in your scene, In our scene we are going to create a floor ,Light bulb , a box on top of the floor , our users should be able to interact with the the scene and our box should be movable.
Creating a Scene
In our index Page let's create a scene wrapper ,it's just going to be a simple div JSX element that will take up the document's view height and width your code should look like this :
// index.jsx
import css from "../styles/Home.module.css";
export default function Home() {
return (
<div className={css.scene}>
</div>
);
}
//Home.module.css
.scene{
width:100vw;
height:100vh;
}
.canvas{
background: #000;
}
To get rid of the default margin being applied to the body in the document you can add the following css styles to the global.css file
body{
margin: 0;
}
Adding the Canvas
The Next step is, just like in threejs or any drawing tool, We need a canvas to draw everything in. react-three/fiber provides a very special Canvas component, that you can import into your scene you code should look like this :
import { Canvas } from "@react-three/fiber";
import css from "../styles/Home.module.css";
export default function Home() {
return (
<div className={css.scene}>
<Canvas
shadows
className={css.canvas}
camera={{
position: [-6, 7, 7],
}}
>
</Canvas>
</div>
);
}
In the code above we. imported our react fiber canvas, and moved the default camera from its default position.
Creating a Floor Component
The next step we want to do is to create a floor component. In our project's root directory create a folder called components and create a new function component called Floor.jsx in the folder. The floor component in our case will be a made up of a box mesh object and just like in threejs a mesh component is made up of a Geometry and a Mesh material (you can choose which material you want to use in threejs), our floor will be made up of a Box buffer geometry and the material we are going to use is the mesh physical material. The box buffer geometry will get it's constructor arguments through the args JSX property (note : we need to pass-in constructor arguments as in an array).
Finally Your floor component will look like this.
//components/Floor.jsx
import React from "react";
function Floor(props) {
return (
<mesh {...props} recieveShadow>
<boxBufferGeometry args={[20,1,10]} />
<meshPhysicalMaterial color='white' />
</mesh>
);
}
export default Floor;
Then we need to import this floor into our canvas (Just like in react).
you index page should look like like this :
import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Floor from "../components/Floor";
export default function Home() {
return (
<div className={css.scene}>
<Canvas
shadows
className={css.canvas}
camera={{
position: [-6, 7, 7],
}}
>
<Floor/>
</Canvas>
</div>
);
}
Adding Ambient Lighting
as soon as you run you server you notice that your scene is still black. thats because we used a physicalMaterial material in our floor and the physical material is affected by light (therefor it requires light to be visible) and our scene does not have any light to light up.
So the next step we are going to do is add lighting. Our first light will be the ambient light to make our objects visible (This illuminates all objects in the scene equally so we will not crank up up the intensity) .
To add the light, we need to add the
<ambientLight color='white' intensity={0.3}/>
component to our scene (ambientLight is a Threejs object ).
The component is self explanatory we just added a white ambient light with an intensity set to 0.3.
your index page should look like this :
import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Floor from "../components/Floor";
export default function Home() {
return (
<div className={css.scene}>
<Canvas
shadows
className={css.canvas}
camera={{
position: [-6, 7, 7],
}}
>
<ambientLight color={"white"} intensity={0.3} />
<Floor position={[0, -1, 0]} />
</Canvas>
</div>
);
}
Creating a Box
Next we need to add the famous box you see in every threeJS tutorial out there.
To do this, just like the floor we are going to add a new component called Box.jsx with the following code :
import React from "react";
function Box(props) {
return (
<mesh {...props} recieveShadow={true} castShadow>
<boxBufferGeometry />
<meshPhysicalMaterial color={"white"} />
</mesh>
);
}
export default Box;
Once we done creating our box we can add it to our scene, you might have noticed that we are passing our components props into our mesh ,the reason I am doing this is to make my component more reusable, This will allow us to have multiple boxes 📦 positioned in different areas or with different dimensions when we want that.
More Lighting - Adding point Light
Once you add the box to the canvas. We are now going to improve the lighting again. This time we are going to create a bulb 💡. To do this we are going to create another new component Called LightBulb.jsx in the components folder, the component will look like this :
import React from "react";
function LightBulb(props) {
return (
<mesh {...props} >
<pointLight castShadow />
<sphereBufferGeometry args={[0.2, 30, 10]} />
<meshPhongMaterial emissive={"yellow"} />
</mesh>
);
}
export default LightBulb;
you going to need to position it a little higher in the scene, your index page (scene) should look like like this :
import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import LightBulb from "../components/Light";
import Floor from "../components/Floor";
export default function Home() {
return (
<div className={css.scene}>
<Canvas
shadows
className={css.canvas}
camera={{
position: [-6, 7, 7],
}}
>
<ambientLight color={"white"} intensity={0.2} />
<LightBulb position={[0, 3, 0]} />
<Box rotateX={3} rotateY={0.2} />
<Floor position={[0, -1, 0]} />
</Canvas>
</div>
);
}
Interacting with our Scene - Adding Orbit Controls
We are doing great, but the problem we have is that we cannot interact with our scene , we cannot not move around the scene's orbit. To be able to do this, we are going to need to add OrbitControls to our scene.
Let's get started, create a new Component in our components folder called OrbitControls.jsx
The component component should look like this :
// components/OrbitControls.jsx
import React from "react";
import { extend, useThree } from "@react-three/fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
extend({ OrbitControls });
function Controls(props) {
const { camera, gl } = useThree();
return <orbitControls attach={"orbitControls"} args={[camera, gl.domElement]} />;
}
export default Controls;
Just like in threeJS OrbitControls need a reference to the camera and the renderers domElement, we get the scene's camera and renderer by using the useThree() hook which react-fiber provides, when working with controls in react fiber we need to first call the extend({OrbitControls})
function, this extends/adds additional features to react fiber in this case OrbitControls. Then the we need to add orbitControls component and use the attach property in it to basically attach our OrbitControls to the scene (this will allow us to access the orbit Controls in any component), this is going to be very handy when we want to make our box draggable, we will need to disable orbit controls when we drag an the box in our scene.
Once we done we need to import our OrbitControls to our scene... you index page should look like this
//index.jsx
import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import OrbitControls from "../components/OrbitControls";
import Light from "../components/Light";
import Floor from "../components/Floor";
export default function Home() {
return (
<div className={css.scene}>
<Canvas
shadows
className={css.canvas}
camera={{
position: [-6, 7, 7],
}}
>
<ambientLight color={"white"} intensity={0.2} />
<Light position={[0, 3, 0]} />
<Box rotateX={3} rotateY={0.2} />
<OrbitControls />
<Floor position={[0, -1, 0]} />
</Canvas>
</div>
);
}
If adding controls like this seems dificult ,I have great news 📰 the creators of react three fiber were very to kind to provide us with other useful libraries we which can extend react three fiber one of which is drei ... Drei enables you to add Controls easily by abstracting some of the details we had to implement, In this article I won't talk about using Drei you can have a look at it later.
Adding Drag Controls
We are almost there, we can move the main camera around our scene using orbitControls but sill cannot move our box. To do this we need to make our box draggable.
We are going to create a new component called Draggable.jsx that will look like this :
import React, { useEffect, useRef, useState } from "react";
import { extend, useThree } from "@react-three/fiber";
import { DragControls } from "three/examples/jsm/controls/DragControls";
extend({ DragControls });
function Draggable(props) {
const groupRef = useRef();
const controlsRef = useRef();
const [objects, setObjects] = useState();
const { camera, gl, scene } = useThree();
useEffect(() => {
setObjects(groupRef.current.children);
}, [groupRef]);
useEffect(() => {
controlsRef.current.addEventListener("hoveron", () => {
scene.orbitControls.enabled = false;
});
controlsRef.current.addEventListener("hoveroff", () => {
scene.orbitControls.enabled = true;
});
}, [objects]);
return (
<group ref={groupRef}>
<dragControls ref={controlsRef} args={[objects, camera, gl.domElement]} />
{props.children}
</group>
);
}
export default Draggable;
You will notice that the steps to working with controls are essentially the same, just like before our drag controls need a reference to the camera, renderer's dom element and in this case the children *3D objects * it's going to apply dragging capabilities to, since the children we pass in react props are react components, we are going to need to wrap in the react prop children with a group JSX element that fiber provides, then create a reference to this group and extract the 3D object children from this group. We used a useEffect because we need to do this only when the groupRef has been set or changed. Lastly in the second use Effect we disable the Orbit controls when you hover over a draggable item and re-enable it when you hover away, forgive me for not removing the eventListeners when the component unmounts you should do that as good practice to avoid memory leaks.
Then we need to wrap our box with this draggable component. in our Index Page, our code should look like this :
import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import OrbitControls from "../components/OrbitControls";
import Light from "../components/Light";
import Floor from "../components/Floor";
import Draggable from "../components/Draggable";
export default function Home() {
return (
<div className={css.scene}>
<Canvas
shadows
className={css.canvas}
camera={{
position: [-6, 7, 7],
}}
>
<ambientLight color={"white"} intensity={0.2} />
<Light position={[0, 3, 0]} />
<Draggable>
<Box rotateX={3} rotateY={0.2} />
</Draggable>
<OrbitControls />
<Floor position={[0, -1, 0]} />
</Canvas>
</div>
);
}
Adding Texture to materials
AS a bonus let's add a texture map to our mesh.
In our Box components we need to import import { useLoader } from "@react-three/fiber";
and import { TextureLoader } from "three";
The first parameter of useLoader takes in a loader, in this example we are using a TextureLoader, and the second parameter takes in the path to the texture we want to load. We then create a textureMap and load it into our material our code will look like this :
import React from "react";
import { useLoader } from "@react-three/fiber";
import { TextureLoader } from "three";
function Box(props) {
const texture = useLoader(TextureLoader, "/texture.jpg");
return (
<mesh {...props} recieveShadow castShadow>
<boxBufferGeometry />
<meshPhysicalMaterial map={texture} color={"white"} />
</mesh>
);
}
export default Box;
Since we adding a texture which is an external resource file that needs to be loaded, we need to wrap our box component with Suspense so that the component is only rendered when the texture file loading is complete.
our indexPage will look like this:
import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import OrbitControls from "../components/OrbitControls";
import Light from "../components/Light";
import Floor from "../components/Floor";
import Draggable from "../components/Draggable";
import {Suspense} from "react";
export default function Home() {
return (
<div className={css.scene}>
<Canvas
shadows
className={css.canvas}
camera={{
position: [-6, 7, 7],
}}
>
<ambientLight color={"white"} intensity={0.2} />
<Light position={[0, 3, 0]} />
<Draggable>
<Suspense fallback={null}>
<Box rotateX={3} rotateY={0.2} />
</Suspense>
</Draggable>
<OrbitControls />
<Floor position={[0, -1, 0]} />
</Canvas>
</div>
);
}
That's it We just created a scene in a React/NextJS project using react fiber, there is a lot to explorer. What I love about react fiber is that it's easy and intuitive to work with but what I hope can be improved is it’s documentation (maybe you will be up for this tasks after reading and playing around with it long enough).
I hope you enjoyed the article ,and I would like to hear your comments.
Top comments (6)
thx man for this great and clear tutorial.
there is a typo on recieveShadow (receiveShadow) so the shadow wont cast otherwise.
also when using draggable, an error if i dont set a min-width and min-height to the scene, otherwise it cannot compute from vw and vh.
thanks again
Thanks for the post!
You could use markdown to color the snippets ;)
dev.to/hoverbaum/how-to-add-code-h...
Awesome! I am stuck trying to Load GLTF, OBJ or FBX 3d models. I've read the react-three-fiber documentation several times but still can't make it to load any 3d model. Do you have any clue about this? From oslavdev.medium.com/load-animated-... I understand there is some work to do with next-transpile-modules? Thanks, great article btw.
If you head over the the three/fiber docs (ECO-system) you should see the gltfjsx library. Hopefully, this is a point in the right direction for you!
Thanks for that !
I think you could add some screenshots of your results.