DEV Community

Richard Haines
Richard Haines

Posted on • Edited on

Notes on react-three-fiber

Originally published on my blog

This is a collection of notes im putting together to better understand how to work with 3D stuff in React. Im writing it as to myself, as that's who it is for, but if others find it useful then that's cool too. But it's not written as a tutorial or anything. Fair warning!

Setup

Install:

yarn add react-three-fiber three @react-three/drei

Enter fullscreen mode Exit fullscreen mode

Set whole page to fill 100% width and height

* {
  box-sizing: border-box;
}

html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}
Enter fullscreen mode Exit fullscreen mode

What is all this stuff

The Canvas component renders threejs elements, not DOM elements. It takes the whole height and width of it container, so that is why, if you want to do a whole page 3D rendering of something then you need to set the containers (in our case the actual body, as we are only doing one page) width and height to 100%.

We cant directly use HTML elements within the Canvas though there is a way to do that which I will go over later.

Because the Canvas component renders threejs elements we can use stuff like mesh, sphereGeometry and meshStandardMaterial amongst many others. It provides all these via its context to any children rendered within it, translating the threejs API into React components that can be used as normal so we can also pass props to these elements/components too.

mesh, geometry and material

A mesh is like the base skeleton an object is made from, like a wire frame or modal.

The geometry is the shape the mesh will take and the material is what will cover the mesh, how it will look. You can use different types of materials which change the way you can interact with the object and how it behaves.

There are lots of different geometry and materials that can be used directly in Canvas like a HTML element. Such as meshStandardMaterial or sphereGeometry.

The attach prop on the geometry and material above attaches the element to its parent, in this case the mesh as the set value. So the sphereGeometry is attached to th4e mesh as the geometry and the meshStandardMaterial is attached to the mesh as the material.

Constructor arguments are passed with the args prop. The sphereGeometry takes a number of constructor args, the first three of which are the radius, width and height. See the SphereGeometry docs on threejs for more information. As an example, the constructor args for the sphere are:

  • radius — sphere radius. Default is 1.
  • widthSegments — number of horizontal segments. Minimum value is 3, and the default is 8.
  • heightSegments — number of vertical segments. Minimum value is 2, and the default is 6.
  • phiStart — specify horizontal starting angle. Default is 0.
  • phiLength — specify horizontal sweep angle size. Default is Math.PI * 2.
  • thetaStart — specify vertical starting angle. Default is 0.
  • thetaLength — specify vertical sweep angle size. Default is Math.PI.

Lights

If no sources of lights are provided then the object will be black. Which makes sense as without light we wouldn't be able to see ti in real life. A list of the light sources taken from a smashing magazine article:

  • Point. Possibly the most commonly used, the point light works much like a light bulb and affects all objects in the same way as long as they are within its predefined range. These can mimic the light cast by a ceiling light.
  • Spot. The spot light is similar to the point light but is focused, illuminating only the objects within its cone of light and its range. Because it doesn’t illuminate everything equally as the point light does, objects will cast a shadow and have a “dark” side.
  • Ambient. This adds a light source that affects all objects in the scene equally. Ambient lights, like sunlight, are used as a general light source. This allows objects in shadow to be viewable, because anything hidden from direct rays would otherwise be completely dark. Because of the general nature of ambient light, the source position does not change how the light affects the scene.
  • Hemisphere. This light source works much like a pool-table light, in that it is positioned directly above the scene and the light disperses from that point only.
  • Directional. The directional light is also fairly similar to the point and spot lights, in that it affects everything within its cone. The big difference is that the directional light does not have a range. It can be placed far away from the objects because the light persists infinitely.
  • Area. Emanating directly from an object in the scene with specific properties, area light is extremely useful for mimicking fixtures like overhanging florescent light and LCD backlight. When forming an area light, you must declare its shape (usually rectangular or circular) and dimension in order to determine the area that the light will cover.

Spheres with different materials.

The code for all three

<Canvas>
  <mesh
    visible // object gets render if true
    userData={{ test: "hello" }} // An object that can be used to store custom data about the Object3d
    position={[0, 0, 1]} // The position on the canvas of the object [x,y,x]
    rotation={[0, 0, 0]} // The rotation of the object
    castShadow // Sets whether or not the object cats a shadow
    // There are many more props.....
  >
    {/* A spherical shape*/}
    <sphereGeometry attach="geometry" args={[1, 16, 16]} />
    {/* A standard mesh material*/}
    <meshStandardMaterial
      attach="material" // How the element should attach itself to its parent
      color="#7222D3" // The color of the material
      transparent // Defines whether this material is transparent. This has an effect on rendering as transparent objects need special treatment and are rendered after non-transparent objects. When set to true, the extent to which the material is transparent is controlled by setting it's .opacity property.
      roughness={0.1} // The roughness of the material - Defaults to 1
      metalness={0.1} // The metalness of the material - Defaults to 0
    />
  </mesh>
  {/*An ambient light that creates a soft light against the object */}
  <ambientLight intensity={0.5} />
  {/*An directional light which aims form the given position */}
  <directionalLight position={[10, 10, 5]} intensity={1} />
  {/*An point light, basically the same as directional. This one points from under */}
  <pointLight position={[0, -10, 5]} intensity={1} />
  {/* We can use the drei Sphere which has a simple API. This sphere has a wobble material attached to it */}
  <Sphere visible position={[-3, 0, 1]} args={[1, 16, 16]}>
    <MeshWobbleMaterial
      attach="material"
      color="#EB1E99"
      factor={1} // Strength, 0 disables the effect (default=1)
      speed={2} // Speed (default=1)
      roughness={0}
    />
  </Sphere>
  {/* This sphere has a distort material attached to it */}
  <Sphere visible position={[3, 0, 1]} args={[1, 16, 16]}>
    <MeshDistortMaterial
      color="#00A38D"
      attach="material"
      distort={0.5} // Strength, 0 disables the effect (default=1)
      speed={2} // Speed (default=1)
      roughness={0}
    />
  </Sphere>
</Canvas>
Enter fullscreen mode Exit fullscreen mode

Drei

Drei (three in german) is a super helpful package that contains loads of helpers and abstractions for react-three-fiber. One thing that I noticed was that using drei with Nextjs (as this site does) will throw an error when trying to import stuff - cannot use import statement outside a module. This is because Nextjs uses common js modules on server side rendering. We can use withTM from next-transpile-modules in the next.config file to transpile the common js stuff in drei. We also need to dynamically import the component using the drei components and disable server side rendering. See this issue and this PR.

If you are using Nextjs....

import dynamic from "next/dynamic";
const ThreeBall = dynamic(
  () => import("./../../src/components/post/three-ball"),
  { ssr: false }
);
Enter fullscreen mode Exit fullscreen mode

next.config.js

const withTM = require("next-transpile-modules")([
  "three",
  "drei",
  "postprocessing",
]);

module.exports = withTM();
Enter fullscreen mode Exit fullscreen mode

The main players

The Renderer in react-three-fiber(R3F) is the HTML canvas element which gets added dynamically to the DOM unless specifically added via an id or className.

The scene is the stage where everything plays out. The meshes encompass lights, groups, 3D positions and cameras. A scene can be thought of as a theater scene where the mesh or groups of meshes are the act that will play out, the lights give the mesh and its geometry and materials a purpose on the scene, they light them from angles and provide context to why they are there. The cameras and positions allow us to view them all from different angles.

A handy canvas component

I made a handy canvas component which i use to render any 3D stuff i want. It takes advantage of Chakra-ui but the styles can just as easily be used inline with a style tag or added from a CSS file.

import React, { Suspense } from 'react';
import { Canvas } from 'react-three-fiber';
import { Box } from '@chakra-ui/core';

/**
 * A container with a set width to hold the canvas.
 * @param {String} width - The width of containing element
 * @param {String} height - The height of containing element
 * @param {Array}  position - The position of the camera, [x, y, z] axis.
 * @param {Number} fov - The field of view of the camera. The higher the number, the further away the view
 * @param {Number} zoom - If the camera should be zoomed in. Provide a number. Defaults to 1.
 * ...rest = any styles you want to apply to the containing div
 */
const CanvasContainer = ({
  width,
  height,
  position,
  fov,
  zoom,
  children,
  ...rest
}) => {
  return (
    <Box height={height} width={width} {...rest}>
      <Canvas
        colorManagement
        camera={{
          position,
          fov,
          zoom,
        }}
      >
        <Suspense fallback={null}>{children}</Suspense>
      </Canvas>
    </Box>
  );
};

export default CanvasContainer;
Enter fullscreen mode Exit fullscreen mode

Models

Working with models is very fun, and fairly painless using the tools the the Poimandres team have put together. Generally speaking you are going to be working with GLTF which is a specification for loading 3D content. It accepts both JSON (.gltf) or binary (.glb) formats. Instead of storing a single texture or assets like .jgp or .png, gltf packages up all that is needed to show the 3D content. That could include everything from the mesh, geometry, materials and textures. For more information checkout the Three docs.

You can use a package called gltfjsx which will take a gltf file which has to be stored in your public folder and then read that and create a React component out of it for you. If your gltf model has animations then the package will handle that for you too, creating the animations logic right inside the component. We can take an example, one of the birds from the Three repository examples.

The code for this bird is auto generated for us. It looks like this:

/*
auto-generated by: https://github.com/pmndrs/gltfjsx
*/
import React, { useRef, useState, useEffect } from "react";
import { useFrame } from "react-three-fiber";
import { useGLTF } from "@react-three/drei/useGLTF";
import * as THREE from "three";
import { PerspectiveCamera } from "@react-three/drei";

export default function Stork(props) {
  const group = useRef();
  const { nodes, materials, animations } = useGLTF("/stork.glb");

  const actions = useRef();
  const [mixer] = useState(() => new THREE.AnimationMixer());
  useFrame((state, delta) => mixer.update(delta));
  useEffect(() => {
    actions.current = {
      storkFly_B_: mixer.clipAction(animations[0], group.current),
    };
    actions.current.storkFly_B_.play(); // To play the animation we have to call the play method
    return () => animations.forEach((clip) => mixer.uncacheClip(clip));
  }, [animations, mixer]);
  return (
    <group ref={group} {...props} dispose={null}>
      <PerspectiveCamera makeDefault position={[-10, 50, 250]} />
      <hemisphereLight args={["white", 2, 2]} />
      <spotLight
        intensity={0.2}
        position={[20, 25, 14]}
        angle={0.15}
        penumbra={1}
        castShadow
      />
      <mesh
        material={nodes.mesh_0.material}
        geometry={nodes.mesh_0.geometry}
        name="mesh_0"
        morphTargetDictionary={nodes.mesh_0.morphTargetDictionary}
        morphTargetInfluences={nodes.mesh_0.morphTargetInfluences}
      />
    </group>
  );
}

useGLTF.preload("/stork.glb");
Enter fullscreen mode Exit fullscreen mode

A handy model component

I made a handy gtlf model component too. This component accepts a number of props such as the path to the models .gltf file, the position the model should take up on the canvas, its rotation which is added in the Vector3 array format of [x, y, z] and any additional props to be passed onto the models mesh.

import React from 'react';
import { useGLTF } from '@react-three/drei';
import { useFrame } from 'react-three-fiber';

/**
 * A basic gltf modal renderer component.
 * @param {String} scenePath - The path to the scene file. Should be kept in the public folder
 * @param {Array} position - The position on the canvas the model should take
 * @param {Array} rotation - Optional rotation of the model. If provided [x, y, z] values are mapped to the useFrame hook which will rotate the model in the given direction(s)
 */
const GLTFModal = ({ scenePath, position, rotation, ...rest }) => {
  const gltf = useGLTF(scenePath, true);
  const mesh = React.useRef();
  useFrame(() =>
    rotation
      ? ((mesh.current.rotation.x += rotation[0]),
        (mesh.current.rotation.y += rotation[1]),
        (mesh.current.rotation.z += rotation[2]))
      : null,
  );
  return (
    <mesh castShadow ref={mesh} {...rest} position={position}>
      <primitive object={gltf.scene} dispose={null} />
    </mesh>
  );
};

export default GLTFModal;

Enter fullscreen mode Exit fullscreen mode

It's then used like this:

  <GLTFModal
    scenePath="/scene.gltf"
    position={[0, -1, 0]}
    rotation={[0, 0.01, 0]}
  />
Enter fullscreen mode Exit fullscreen mode

Top comments (0)