Write three.js in React Using react-three-fiber
source link: https://alligator.io/react/react-with-threejs/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
So you want to write some 3D graphics/animations in your React apps using three.js? Let’s see how we can do just that, leveraging the react-three-fiber
library.
This article supposes you are familiar with React (with hooks)
and three.js
. If not, you can get a quick starton React hooks here and on three.js here .
Getting started & Examples
We’re going to use react-three-fiber , (for now-on called R3F
), which is essentially a powerful React renderer for three.js, both for the web and with React Native.
We started using this library for this simple reason: if you’re usually working with React and you want to create a three.js experience, the standard approach can be painful to scale -> Some examples here , here and here .
Working live examples : If you want to have an idea on what you can create using react-three-fiber you can check our last projects here , here and here .
This affiliate banner helps support the site :pray:
But First, Why react-three-fiber?
- Component-based Scene : This is actually the main reason why I love this library. It allows us to write three.js objects in a
declarative
way, so we can build-up our scene creating re-usable React components, leveragingprops
,states
andhooks
. Keep in mind that you can write essentially three.js entire object catalogue and all its properties. - Built-in helpers : It’s delivered with some useful functions like
raycaster
and it gives you access on eachmesh
to all the useful pointer-events likeonClick
,onPointerOver
,onPointerOut
,… Exactly like any DOM element . - Hooks : It comes with a lot of hooks already, like
useFrame
that allows us to attach functions into the raf loop (or even override the default one), anduseThree
from where we can get useful objects likerenderer
,scene
,camera
,… - Resize : If you don’t need special custom resize logic, it already handles the camera changes for you, and the canvas resizes itself independently. Besides you can even get access to the
size
of theviewport
(the size of the quad you are rendering based on 3D coordinates). - Dependency-free : Since it’s “just” a renderer it doesn’t rely on the three.js version, so you are free to choose your preferred version.
- Re-render only when needed : It works as any React component, it updates itself on a dependency change (state, store, etc).
Let’s Write Some three.js
So, it’s time to give you a quick and simple example. First of all we need to define our Canvas
component. Everything inside of it will be added to the main scene
(defined by react-three-fiber for us).
In this example I'll emphasize the code splitting in multiple files to show how cool is to work in components. It's not performance-optimized, because it's not the goal of this introduction. You can find the complete example with the final code here.
//... import { Canvas } from "react-three-fiber"; //... function App() { return ( <Canvas> // here you can pass a lot of options as prop <Children/> // any Threejs object (mesh, group or whatever) <Children/> // any Threejs object (mesh, group or whatever) </Canvas> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
The first step is done. With just these few lines we’ve already created the canvas
, the camera
(a perspective one but you can customize it) and the scene
and we don’t need to care about the resize. Awesome !
Add a group with just a mesh
Let’s compare the two approaches on how to create a basic mesh and add it into a group:
// plain JS const group = new Group(); const geo = new BoxBufferGeometry(2,2,2); const mat = new MeshStandardMaterial({color: 0x1fbeca}); const mesh = new Mesh(geo, mat); group.position.set(0,0.1,0.1); group.add(mesh); scene.add(group); // declarative <group position={[0,0.1,0.1]}> <mesh> <boxBufferGeometry attach="geometry" args={[0.047, 0.5, 0.29]} /> <meshStandardMaterial attach="material" color={0xf95b3c} /> </mesh> </group> // we don't need to add to the scene because since it's a child of the canvas it's automatically added
As you can see it’s pretty straight-forward. You just need to pass the required arguments as args
property and then you can set all the other properties as props.
Since it is supposed that you're already familiar with three.js, I won't compare the two approaches anymore :wink:.
Create multiple cubes
Suppose now that you want to create multiple cubes and add them into the group. In plain vanilla JavaScript you need to create a class
(or not, depends on your preferred approach) to create and handle them, and push them into an array and so on. With R3F
you can just add an array of components into what the function returns, as any DOM-element. You can even pass a prop and apply it internally.
export default () => { const nodesCubes = map(new Array(30), (el, i) => { return <Cube key={i} prop1={..} prop2={...}/>; }); return ( <group> { nodesCubes } </group> ); };
Animate them
Let’s now animate them. react-three-fiber
provides you a great way to attach your logic into the raf
loop, using the useFrame
hook.
// ... import {useFrame} from 'react-three-fiber' // ... const mesh = useRef() useFrame( ({gl,scene,camera....}) => { mesh.current.rotation.y += 0.1 })
Handle properties with states
Let’s now suppose you want to change some properties on hover
or on click
. You don’t need to set any raycaster
because it’s already defined for you. Since we are using React we can leverage the useState
hook.
//... const mesh = useRef() const [isHovered, setIsHovered] = useState(false); const color = isHovered ? 0xe5d54d : 0xf95b3c; const onHover = useCallback((e, value) => { e.stopPropagation(); // stop it at the first intersection setIsHovered(value); }, [setIsHovered]); //... <mesh ref={mesh} position={position} onPointerOver={e => onHover(e, true)} onPointerOut={e => onHover(e, false)} > <boxBufferGeometry attach="geometry" args={[0.047, 0.5, 0.29]} /> <meshStandardMaterial color={color} attach="material" /> </mesh>
Let’s now change the color and the animation too, modifying the state of the cube from “active” to “inactive” based on the user’s click. As before we can play with state
and use the built-in function onClick
.
//... const [isHovered, setIsHovered] = useState(false); const [isActive, setIsActive] = useState(false); const color = isHovered ? 0xe5d54d : (isActive ? 0xf7e7e5 : 0xf95b3c); const onHover = useCallback((e, value) => { e.stopPropagation(); setIsHovered(value); }, [setIsHovered]); const onClick = useCallback( e => { e.stopPropagation(); setIsActive(v => !v); }, [setIsActive] ); // raf loop useFrame(() => { mesh.current.rotation.y += 0.01 * timeMod; if (isActiveRef.current) { // a ref is needed because useFrame creates a "closure" on the state time.current += 0.03; mesh.current.position.y = position[1] + Math.sin(time.current) * 0.4; } }); //... return ( <mesh ref={mesh} position={position} onClick={e => onClick(e)} onPointerOver={e => onHover(e, true)} onPointerOut={e => onHover(e, false)} > <boxBufferGeometry attach="geometry" args={[0.047, 0.5, 0.29]} /> <meshStandardMaterial color={color} attach="material" /> </mesh> );
That’s it. Easy-peasy.
Add some lights
As before if we want to add some lights we just need to create a function (or component) and add it into the scene
graph.
return ( <> <ambientLight intensity={0.9} /> <pointLight intensity={1.12} position={[0, 0, 0]} /> </> )
Last step
We’ve already created a basic scene in three.js, but let’s do our last step just to show you how easy it is to add custom logic into the render-loop.
Let’s add a rotation on the container group of all the cubes. You can just simply go to the group
component, use the useFrame
hook and set the rotation there, just like before.
useFrame(() => { group.current.rotation.y += 0.005; });
That’s all, looks easy and straight-forward right?
Bonus
Since R3F
is brought to you by Paul Henschel it’s totally compatible withreact-spring. This means you can use react-spring
to animate all your three.js
stuff, accessing directly to the element, and to be honest it’s totally mind blowing .
Last Points to Clarify
Since I’ve discovered react-three-fiber , I’ve used it for all my React three.js projects, from the simplest demo to the most complex one. I strongly suggest you to read all the documentation online and overview all the features because here we just covered some of them.
By the way, if you are used to working with three.js, you’ll have to switch a little how you interact / create stuff. It has, as everything, a learning curve and you’ll need some time to change the approach if you want to keep performance at its best, like for example using a shared material, cloning a mesh, etc, but based on my experience it’s absolutely worth the time invested.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK