Kevin Beltrão
Posted on November 6, 2022
Introduction
The idea here is to introduce you to WebGL, Three.js, and React Three Fiber concepts, even if you don't know what either of them is. I hope I can get you to understand a little bit of what happens when developing 3D software on your browser.
First, I'll get you into some new concepts so you understand what I'll be talking about. How is the browser able to render 3D at all? Then we're coding a small demo with our first 3D view and animations.
The idea is not to get very deep into R3F but to show how few lines of code can deliver great results. So even though I'm not getting into things such as loading models, their docs are very easy and clear.
The result is available in this repository on Github, feel free to checkout.
WebGL
Making it simple, WebGL is a JavaScript API that renders triangles very fast. It uses GPU since its focus is on making many operations in parallel (while CPU would be generally much faster but more limited to parallel operations).
When building more complex objects, it'll be needed to place many different triangles in different places, sizes, and colors. The triangles are drawn inside a <canvas>
and the GPU is responsible for drawing them. The instructions to place points and draw pixels are written in shaders
, to which we provide info such as points positions, colors, camera position, and model transformations.
The main issue with WebGL is that it's very hard. You'd need over 100 lines to write a single triangle as you can check in this example, so imagine positioning every single one of them in a complex shape, making sure each one of them has a color compatible with the lightning, size according to the perspective, etc.
Also, WebGL has great compatibility across different browsers as you can check here and I advise you to check out Mozilla's docs on WebGL.
Three.js
Three.js is a JavaScript library under the MIT license that makes it easier for users to work with WebGL. It's still close enough to WebGL so you can interact with it directly, but will make our lives way easier.
It's important to understand the 4 main elements in Three.js:
Objects - Elements you're using in your Three.js project, such as lights, imported 3D models, geometries, etc. When creating an object we'll need a
Mesh
(combination of a geometry (shape) and a material (color/texture/etc)).Scene - The container of the objects, is what Three.js is asked to render.
Camera - Defines how we'll view the scene. Three.js provides many different camera types, the main two are the perspective camera and the orthographic camera, but another example is the stereo camera, which simulates human eyes by adding two different views side by side (if you wanna build VR software for example). You can check more about cameras in their documentation.
Renderer - What displays the scene in the canvas.
On their official website you can check many websites that use Three.js.
React Three Fiber
React Three Fiber is a React renderer for Three.js which promises to have no limitations when working with Three.js. It also abstracts a lot of code, which means will have to write way less to deliver the same thing.
Have a look at their repository and their docs.
Demo
Creating project
First of all, let's start creating a react app. I'll be using yarn
yarn create react-app react-three-fiber-demo
Then let's install the main dependencies
yarn add three @react-three/fiber
I'll also suggest installing three types because even though we won't be using TypeScript for this demo, it'll improve VS Code suggestions
yarn add -D @types/three
Now, let's delete the files we won't be using in our demo inside the src/ folder, leaving just App.js
, index.css
, and index.js
(remember to remove the imports of deleted files on index.js).
Canvas
The first thing you should know is that react has its own renderer. When we use JSX, React's renderer understands why should HTML tags, such as a <h1>
, become.
Just like React's renderer, we'll need one for Three.js. We'll be writing tags that don't belong to JSX's syntax and it'll understand its own tags.
For that, we'll import Canvas
from @react-three/fiber
, and what goes in it is R3F code, NOT JSX, just like we can't add R3F code outside the Canvas.
So, replace the App.js content to that:
import { Canvas } from '@react-three/fiber';
const App = () => {
return (
<Canvas>
{/* Code here */}
</Canvas>
);
};
export default App;
Object
So, let's start by adding an object to our project. We'll do it by adding a mesh and informing what kind of geometry we want. For this example, we'll use a boxGeometry.
import { Canvas } from '@react-three/fiber';
const App = () => {
return (
<Canvas>
<mesh>
<boxGeometry />
</mesh>
</Canvas>
);
};
export default App;
You should be seeing a very small gray square. The reason it's small is that the canvas will have its parent's size, so let's edit index.css to:
html,
body,
#root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
overflow: hidden;
}
And now your gray square should be bigger.
Orbit controls
What looks like to be a gray square is actually not a gray square, but a 3D box (even though you can't actually see its depth). If you'd like to be able to check your 3D object, let's install drei
, a module with utility and helper functions.
yarn add @react-three/drei
Now, if we import OrbitControls
and add this component to our code, we should be able to spin the Cube by dragging the page with the mouse.
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
const App = () => {
return (
<Canvas>
<OrbitControls />
<mesh>
<boxGeometry />
</mesh>
</Canvas>
);
};
export default App;
Material
We should also add a material to our mesh, so we can apply color to our geometry. Let's add a meshBasicMaterial with the red color and check the result.
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
const App = () => {
return (
<Canvas>
<OrbitControls />
<mesh>
<boxGeometry />
<meshBasicMaterial color="red" />
</mesh>
</Canvas>
);
};
export default App;
Now we have a red cube, but as you can tell the basic material only adds a flat color that doesn't look realistic. Let's change it to the standard material and check the results.
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
const App = () => {
return (
<Canvas>
<OrbitControls />
<mesh>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</Canvas>
);
};
export default App;
Now we have a black cube, but why? Because the standard material reacts to the light, which we didn't add.
Lights
So let's add a directional light and set its position:
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
const App = () => {
return (
<Canvas>
<OrbitControls />
<directionalLight position={[1, 2, 3]} />
<mesh>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</Canvas>
);
};
export default App;
Now, spinning the cube, we can see that the faces that receive light get red, but the faces that get no light are still completely black, which is also not realistic. So let's also add an ambient light with a 0.5 intensity:
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
const App = () => {
return (
<Canvas>
<OrbitControls />
<directionalLight position={[1, 2, 3]} />
<ambientLight intensity={0.5} />
<mesh>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</Canvas>
);
};
export default App;
PS: Remember all the arguments you can pass to the elements are available on the docs I linked before.
Movement
Alright, now we have a better cube reacting to light. Let's make it move!
We'll now import a hook called useFrame from React Three Fiber, but this kind of hook must be called inside the canvas, so we can't just call it in the App component.
So let's create a specific component for the box:
// Box.js
const Box = () => {
return (
<mesh>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
);
};
export default Box;
// App.js
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import Box from './Box';
const App = () => {
return (
<Canvas>
<OrbitControls />
<directionalLight position={[1, 2, 3]} />
<ambientLight intensity={0.5} />
<Box />
</Canvas>
);
};
export default App;
And now let's import the useFrame inside Box.js. This hook will call a function on every frame rendered. Know this box is already rerendering multiple times per second. Also, we'll use React's useRef hook to reference our mesh.
// Box.js
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
const Box = () => {
const boxRef = useRef();
useFrame(() => {
boxRef.current.position.x += 0.01;
});
return (
<mesh ref={boxRef}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
);
};
export default Box;
The callback function passed to useFrame is changing the box's position on the X axis on each frame, and now you can see your box moving.
Speed issue
The issue here is that different computers will have different powers and different frame rates, which means that this function will be called more or less times (depending on how often the computer can rerender the scene).
To prevent different results across different computers, we can use a built in solution. The first parameter passed by useFrame is the state, that contains the clock object. There, we'll be able to find a property called elapsedTime, which gets the time continuously (always increasing from the first time it was called on).
It means we can use the elapsedTime instead of the amounts of time the function was called to set the position of the box, assuring everyone will get the same output:
// Box.js
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
const Box = () => {
const boxRef = useRef();
useFrame((state) => {
const { elapsedTime } = state.clock;
boxRef.current.position.x = elapsedTime;
});
return (
<mesh ref={boxRef}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
);
};
export default Box;
Final touches
And just to keep our box in the view, let's change this function to make the cube comeback after a while. We can use sin/cos since they vary from -1 to 1. So I'll change the position x and y according to sin/cos to make the box move in a circle:
// Box.js
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
const Box = () => {
const boxRef = useRef();
useFrame((state) => {
const { elapsedTime } = state.clock;
boxRef.current.position.x = Math.sin(elapsedTime);
boxRef.current.position.y = Math.cos(elapsedTime);
});
return (
<mesh ref={boxRef}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
);
};
export default Box;
And now you should see this:
Conclusion
There's way more into R3F, I hope that was a good introduction. Content on Three.js is not as easy to find as other tools, but I assure the best way of learning is by practicing and making projects.
As a next step suggestion, try adding a texture to that cube. Also, try loading a GLTF model using drei's useGTLF hook. A good website to find free models is sketchfab.com.
Posted on November 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.