Getting started with React Three Fiber.

Dhrumil Limbad
6 min readJan 29, 2022

--

React-three-fiber is a React renderer for three.js.

We will scratch the surface of React Three Fiber and cover concepts like Texture, Loading 3D models, Node Selection, Interacting with objects and Instances.

Prerequisite

Working knowledge of Three.js.

React.js and Hooks.

GitHub Code.

Live Demo.

Goal

Aim of this tutorial.

Step 1: Installation

Please refer to the pmnd docs and install for React.

Step 2: Setting up the scene.

Navigate to App.js and paste the following code.

import React, { useState, useRef } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { OrbitControls, Html } from '@react-three/drei'
function Box() {
const [hidden, setVisible] = useState(false)
return (
<mesh scale={0.6}>
<boxGeometry />
<meshStandardMaterial />
<Html
style={{
transition: 'all 0.2s',
opacity: hidden ? 0 : 1,
transform: `scale(${hidden ? 0.5 : 1})`
}}
distanceFactor={1.5}
position={[0, 0, 0.51]}
transform
occlude
onOcclude={setVisible}>
<h1>NANI??!! </h1>
</Html>
</mesh>
)
}
export default function App() {
return (
<Canvas dpr={[1, 2]} camera={{ fov: 25 }}>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 5]} />
<pointLight position={[-10, -10, -10]} />
<Box />
<OrbitControls />
</Canvas>
)
}

We always render our scene under <Canvas> component. All geometries will be defined outside the return statement. Here we will define a <Box /> geometry which is typically a cube.

Unlike the generic Three.js you wont need to add everything to the scene manually, Canvas component will handle everything as long as it is a parent to your geometry.

Html from ‘@react-three/drei’ package is used to mix HTML with WebGL. Here we have used it to embed some text on the geometry. It comes with many options, one of the useful feature that comes in handy when using text is occlusion. On occlusion it hides the text for better UX.

Here’s an example of bad UX.

When the geometry is rotated the text is still visible on the back of it’s face.

As long as we use below mentioned options we wont suffer from bad UX.

occlude
onOcclude={setVisible}
The output should look like this for now.

Step 3: Using Textures.

Let’s create a football!

We will start by creating a sphere geometry and then we will provide it with a texture map.

Firstly, past the following code just after the Box function and before export statement. Add <Sphere position={[0, 0, 1]}/> inside the <Canvas> under <Box> like so …

.
.
.
function Sphere(props) {
const ref = useRef();
useFrame((state) => {
ref.current.position.set(
Math.sin(state.clock.getElapsedTime() / 1.5) / 2,
Math.cos(state.clock.getElapsedTime() / 1.5) / 2,
Math.cos(state.clock.getElapsedTime() / 1.5) / 2 + 0.5
);
ref.current.rotation.set(
Math.sin(state.clock.getElapsedTime() / 1.5) / 2 * 5,
Math.cos(state.clock.getElapsedTime() / 1.5) / 2,
Math.tan(state.clock.getElapsedTime() / 10.5) / 2
);
});
return (
<mesh ref={ref} {...props}>
<sphereGeometry args={[0.2, 30, 30]} />
<meshStandardMaterial color='hotpink' />
</mesh>
);
}
export default function App() {
return (
<Canvas dpr={[1, 2]} camera={{ fov: 25 }}>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 5]} />
<pointLight position={[-10, -10, -10]} />
<Box />
<Sphere position={[0, 0, 1]}/>
<OrbitControls />
</Canvas>
)
}

Here, we don't use generic way of animating objects like requestAnimationFrame but useFrame hook. The useFrame hook does the job of animating and displaying objects on very frame.

Within the useFrame hook we are defining it’s motion and rotation property. I have used .set function to set for x, y and z dir/axis.

Let’s see how it looks now without texture.

Sphere without texture.

Now let’s add texture to the Sphere. Add a texture file to your public folder. I have named my texture as football_texture.jpg . The texture is available on the GitHub repo.

Add/modify for the commented lines.

import { useTexture } from "@react-three/drei";function Sphere(props) {
const colorMap = useTexture("football_texture.jpg"); //ADD THIS
const ref = useRef();
useFrame((state) => {
...
...
});
return (
<mesh ref={ref} {...props}>
<sphereGeometry args={[0.2, 30, 30]} />
<meshStandardMaterial map={colorMap} /> //ADD THIS
</mesh>
);
}

Here, meshStandardMaterial will accept our defined colorMap as a prop and will set texture of the sphere.

Sphere with a texture.

Step 4: Loading a 3D model, Creating Instances and Playing with nodes.

Now we will play with Instances and Node of 3D model objects.

Instances are copy of objects, instead of writing <SomeObject /> every time we can simply have a mechanism that can allow us to pass a number of our own desire and create instances.

Nodes are the segments for any whole object. Here we will be using a shoe 3D model where there are two nodes namely the sole of the shoe and the rest of the part.

Before we create anything related to Step4 we will first create random noise/data so that we can set out instances in random vector space.

So copy the following code and past right below your imports. Like so

import React, { Suspense, useState, useRef } from "react";
import { Canvas, useFrame, useLoader } from "@react-three/fiber";
import {
Html,
Instances,
Instance,
OrbitControls,
Environment,
useProgress,
useGLTF,
useTexture
} from "@react-three/drei";
import * as THREE from "three";
const color = new THREE.Color();
const randomVector = (r) => [
r / 2 - Math.random() * r,
r / 2 - Math.random() * r,
r / 2 - Math.random() * r,
];
const randomEuler = () => [
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI,
];
const randomData = Array.from({ length: 50 }, (r = 3) => ({
random: Math.random(),
position: randomVector(r),
rotation: randomEuler(),
}));
function Loader() {
const { progress } = useProgress()
return <Html center>{progress} % loaded</Html>
}

Now moving on to the a single Shoe instance. Copy the following code and past it before App()

function Shoe({ random, ...props }) {
const ref = useRef();
const [hovered, setHover] = useState(false);
useFrame((state) => {
const t = state.clock.getElapsedTime() + random * 10000;
ref.current.rotation.set(
Math.cos(t / 4) / 2,
Math.sin(t / 4) / 2,
Math.cos(t / 1.5) / 2
);
ref.current.position.y = Math.sin(t / 1.5) / 2;
ref.current.scale.x =
ref.current.scale.y =
ref.current.scale.z =
THREE.MathUtils.lerp(ref.current.scale.z, hovered ? 0.7 : 0.3, 0.1);
ref.current.color.lerp(
color.set(hovered ? "red" : "white"),
hovered ? 1 : 0.1
);
});
return (
<group {...props}>
<Instance
ref={ref}
onPointerOver={(e) => (e.stopPropagation(), setHover(true))}
onPointerOut={() => setHover(false)}
/>
</group>
);
}

Here we are setting hover animation on our instance where if hovered it will scale up in x, y and z direction along with change in color.

event.stopPropagation()  doesn't just stop this event from bubbling  up, it also stops it from  being delivered to farther objects (objects behind this one). All other  objects, nearer or farther, no longer count as being hit while the  pointer is over this object. If they were previously delivered  pointerover events, they will immediately be delivered pointerout  events.

Now we will create a handle that will help us manage the range of our instances. Copy the following code above the Shoe(). Make sure you have shoe.glb model in your public folder (Check GitHub).

function Shoes({ range }) {
const { nodes, materials } = useGLTF("/shoe.glb");
return (
<>
<Instances
range={range}
material={materials.phong1SG}
geometry={nodes.Shoe.geometry}
>
{randomData.map((props, i) => (
<Shoe key={i} {...props} />
))}
</Instances>
</>
);
}

Here, Shoes() will take range as prop and de-structure the nodes and send it to the Shoe for instancing. We have use useGLTF hook to load the 3D model.

We are all done, now add the following in your App() return.

export default function App() {
return (
<Canvas concurrent dpr={[1, 2]} camera={{ fov: 25 }}>
...
...
<Shoes range="1" />
<Environment preset="city" />
{/* ADD ABOVE TWO LINES */}
</Suspense>
<OrbitControls />
</Canvas>
);
}

Here, the range specifies the copies of Shoe. Change the range to 2 and you shall have 2 instances of shoes. But for this tutorial I’ll keep it to 1.

Note, we have added concurrent which will defer the expensive operation taking place, visit https://docs.pmnd.rs/react-three-fiber/advanced/pitfalls for more performance related guide. And we have used a <Loading /> fallback just to give our scene to load fully.

Hope you find this useful.

Full-Stack Web Developer Intern at Websmith Solution.

--

--

Dhrumil Limbad
Dhrumil Limbad

Written by Dhrumil Limbad

Loves to talk about Shopify, Frontend Frameworks, AR/VR, Node, JS, TS and Animations.

No responses yet