Three.JS + React: Exploring 3D graphics on the Web

Three.JS + React: Exploring 3D graphics on the Web

I have to confess that I have always been fascinated by animations, especially 3D graphics. Maybe the complexity of the tools at the time kept me somewhat away.

Fortunately, all this has been changing. WebGL, among other technologies, has been making way for this interesting world, and the best part of it, it is in the web browser.

For my first steps in this experience, I chose the following technologies:

  • Three.js for everything related to 3D graphics since it is sufficiently documented and has very useful examples for references.
  • React.js,  it is my favorite development library. I like the order that you can maintain in a project following an ecosystem of components and React stands out very well achieving this. However, other JS technologies should work without further problems.
  • React-three-fiber, It will save us a lot of time for our Scene creations, some hooks integrations, etc.
  • React-spring, to smooth out animations (scaling, colorization changes, etc.)
  • Gatsby.js, Empty React project boilerplate.

My goal for this mini-project was to achieve the interaction of a React.js component with some 3D Objects generated with Three.js; encapsulated in reusable components. I was beyond satisfied by the results.

Three + React - Testing Component state

These are the more important entities in Three.js:

  • Renderer: Like the drawer(WebGL, etc)
  • Camera: To control perspective aspects
  • Scene: The space or universe where objects are created
  • Meshes: They are our Objects constituted by Geometries and Materials
    - Geometries: Vectors / Triangles that define the object (shape)
    - Materials: What Geometry looks like when drawn (texture, color)
  • Lights: Lights that act on our objects. Also capable of casting shadows

If you think about it, entities are very attached to our reality, which is great since gives you a perception of familiarity to the world that we are about to explore.

Since we initially have an empty space (scene), let's start with a simple element: a cube.

NOTE: Complete source available at the end of the post.

// Box Component const Box = (props) => { ... return ( <mesh {...props} onClick={onClick} onPointerOver={e => setHover(true)} onPointerOut={e => setHover(false)} castShadow > <boxBufferGeometry attach="geometry" args={[1, 1, 1]} /> <meshPhysicalMaterial attach="material" map={texture} roughness={0} /> </mesh> ) }

Great! We already have our reusable Box component. In the code above, my intention is to show how to generate a basic 3D Object (Mesh) generated by a reusable component. Note that this object has both Geometry and Material (In the documentation you will surely find more options for both).

Now, all we need to do is render it and for this, a normal scenario for Three.js would be to define the other entities: Renderer, Camera, Scene. But fortunately, React-three-fiber provides us with a Canvas component that already creates these entities with default values.

// Main Component export default () => ( <Canvas camera={{ position: [10, 10, 10]}} onCreated={({ gl }) => gl.shadowMap.enabled = true } > <ambientLight intensity={0.5} /> <spotLight position={[10, 10, 20]} penumbra={1} /> <Box position={[-3, 3, 0]} /> </Canvas> )

And just like that this all we need to render our Box component into our main component!

3D Cube Testing

Autorotate

Obviously there are complex properties you can use to make complex calculations and achieve different results in your Objects.

The following source code is the complete use case of the mini-project experience I was working on for this proof of concept. This is what it does:

  • Renders seven cubes rotating with some images textures attached to them. The useFrame() hook handles the frame by frame rotation.
  • You can click on a cube and this will change its component state (active). This modifies the rotation speed and scale of the cube.
  • Hover in and out will highlight the cube
  • The main component handles the cube selection
  • A plain JS React component handles the list of the selected cubes
// index.js import { List, Avatar } from 'antd'; import React, { Suspense, useRef, useState, useEffect } from 'react'; import { Canvas, extend, useFrame, useLoader, useThree } from 'react-three-fiber'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { useSpring, a } from 'react-spring/three'; import './styles.css' import "antd/dist/antd.css"; const selectedHexColor = 0xffd5c3; // Not neccesary but this provides free control on rotation, zooming, etc. extend({ OrbitControls }); const Controls = (props) => { const { autoRotate } = props; const orbitRef = useRef(); const { camera, gl } = useThree(); useFrame(() => orbitRef.current.update()); return ( <orbitControls autoRotate={!!autoRotate} args={[camera, gl.domElement]} ref={orbitRef} /> ) } // Plane Component const Plane = () => ( <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} receiveShadow> <planeBufferGeometry attach="geometry" args={[100, 100]} /> <meshPhysicalMaterial attach="material" color="orange" /> </mesh> ) // Box Component const Box = (props) => { const { title, icon, selectToogle } = props; // This reference const mesh = useRef() const [hovered, setHover] = useState(false) const [active, setActive] = useState(false) const [originalHex, setOriginalHex] = useState(); // mesh every frame, this is outside of React without overhead useFrame(() => active ? mesh.current.rotation.y += 0.1 : mesh.current.rotation.y += 0.01); // Loading icon texture const texture = useLoader(THREE.TextureLoader, icon); // init and hover color handling useEffect(() => { !originalHex && setOriginalHex(mesh.current.material.color.getHex()); hovered ? mesh.current.material.color.setHex(selectedHexColor) : (originalHex && mesh.current.material.color.setHex(originalHex)); }, [hovered, active, originalHex]) // react spring is not necesary. Using to provide a smooth animation on scaling const springProps = useSpring({ scale: active ? [1.5, 1.5, 1.5] : [1, 1, 1], }); return ( <a.mesh {...props} ref={mesh} scale={springProps.scale} onClick={e => { setActive(!active) selectToogle({title, icon}) }} onPointerOver={e => setHover(true)} onPointerOut={e => setHover(false)} castShadow > <boxBufferGeometry attach="geometry" args={[1, 1, 1]} /> <meshPhysicalMaterial attach="material" map={texture} roughness={0} /> </a.mesh> ) } // React Component to list the cube selection const CubeList = (props) => { const { selection } = props; return !!selection && ( <div className="CubeList"> <List itemLayout="horizontal" dataSource={selection} renderItem={item => ( <List.Item> <List.Item.Meta avatar={<Avatar src={item.icon} />} title={<span className="text">{item.title}</span>} /> </List.Item> )} /> </div> ) } // Main Component export default () => { const [selection, setSelection] = useState([]); const selectToogle = ({title, icon}) => { const newSelection = [ ...selection ]; const itemIndex = newSelection.findIndex(item => item.title === title) if (itemIndex > -1) { newSelection.splice(itemIndex, 1); } else { newSelection.push({title, icon}); } setSelection(newSelection); } return ( <> { !!selection.length && (<CubeList selection={selection} />) } <Canvas camera={{ position: [10, 10, 10]}} onCreated={({ gl }) => { gl.shadowMap.enabled = true; gl.shadowMap.type = THREE.PCFSoftShadowMap; }} > <Controls /> <ambientLight intensity={0.5} /> <spotLight position={[10, 10, 20]} penumbra={1} castShadow /> <fog attach="fog" args={['black', 15, 50]} /> <Plane /> <Suspense fallback={null}> <Box position={[-9, 3, 0]} selectToogle={selectToogle} title="GraphQL" icon="/gql.png" /> <Box position={[-6, 3, 0]} selectToogle={selectToogle} title="Ruby on Rails" icon="/rails.png" /> <Box position={[-3, 3, 0]} selectToogle={selectToogle} title="React" icon="/react.png" /> <Box position={[0, 3, 0]} selectToogle={selectToogle} title="Angular" icon="/angular.png" /> <Box position={[3, 3, 0]} selectToogle={selectToogle} title="Vue" icon="/vue.png" /> <Box position={[6, 3, 0]} selectToogle={selectToogle} title="Docker" icon="/docker.jpeg" /> <Box position={[9, 3, 0]} selectToogle={selectToogle} title="Chicho" icon="/mario_question.png" /> </Suspense> </Canvas> </> ) } /* styles.css */ html, body, canvas, #___gatsby, #gatsby-focus-wrapper { width: 100%; height: 100%; margin: 0; padding: 0; background-color: #000; } .CubeList { position: absolute; z-index: 10; top: 0; padding: 20px; } .text { color:white; }

I had a blast messing around with 3d graphics, I loved it!  I like the power of abstraction that Three.js provides us and the possibilities to connect all this with the technologies we love. There are so many components to play around with, there is also VR support! I am definitely trying that as well. I hope you  enjoyed the content of this post and that it makes you want to give it a try. Once you start creating 3D scenes you just don’t want to stop... Happy Coding!

Links:

A Brief Intro to JVM's Language Landscape
Writing good Javascript: Let's not forget about performance

Suscribe to our newsletter

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.