Adding OrbitControls to React Three Fiber

The author
Stephen Castle3 years ago

Orbit Controls

Three.JS has an import we can use called OrbitControls that adds a lot of free camera movement functionality. It is very configurable and can do some cool stuff like restrict the maximum angle of rotation, allow or disallow zooming, and lock focus on to a specific object. Let's implement it in our scene now to use as a basis for later camera controls in our game.

Importing OrbitControls and Using React Three Fiber Extend

In react-three-fiber we use Three.js objects as if they were React components. Many of those components are already available to us automatically. Like the sphereGeometry and meshStandardMaterial we have used previously. However, there are many objects in Three.js that are not automatically available as a component. Luckily the react-three-fiber API provides us with a way to add them via a function called extend. In the react-three-fiber docs, it describes it like this.

The extend function extends three-fibers catalog of known native JSX elements.

This is exactly what we want to do with OrbitControls, and to do so is pretty straightforward. Just import OrbitControls directly from Three.js, then import extend from react-three-fiber. And then call extend by passing in an object literal containing OrbitControls. You could pass more than one thing in to extend, and we will do so later.

import React, { Suspense, useRef } from "react";
import { Canvas, useLoader, extend } from "react-three-fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import "./styles.css";

// highlight-start
// Extend will make OrbitControls available as a JSX element called orbitControls for us to use.
extend({ OrbitControls });
// highlight-end

function Loading() {
  return (
    <mesh visible position={[0, 0, 0]} rotation={[0, 0, 0]}>
      <sphereGeometry attach="geometry" args={[1, 16, 16]} />
      <meshStandardMaterial
        attach="material"
        color="white"
        transparent
        opacity={0.6}
        roughness={1}
        metalness={0}
      />
    </mesh>
  );
}

function ArWing() {
  const group = useRef();
  const { nodes } = useLoader(GLTFLoader, "models/arwing.glb");
  return (
    <group ref={group}>
      <mesh visible geometry={nodes.Default.geometry}>
        <meshStandardMaterial
          attach="material"
          color="white"
          roughness={0.3}
          metalness={0.3}
        />
      </mesh>
    </group>
  );
}

export default function App() {
  return (
    <>
      <Canvas style={{ background: "white" }}>
        <directionalLight intensity={0.5} />
        <Suspense fallback={<Loading />}>
          <ArWing />
        </Suspense>
      </Canvas>
      <a
        href="https://codeworkshop.dev/blog/2020-04-04-adding-orbit-controls-to-react-three-fiber/"
        className="blog-link"
        target="_blank"
        rel="noopener noreferrer"
      >
        Blog Post
      </a>
    </>
  );
}

Initializing OrbitControls

Once we have imported and extended OrbitControls, we can use it in a component like this. And then add it to our scene.

import React, { Suspense, useRef } from "react";
import {
  Canvas,
  useLoader,
  useFrame, // highlight-line
  extend,
  useThree, // highlight-line
} from "react-three-fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import "./styles.css";

// Extend will make OrbitControls available as a JSX element called orbitControls for us to use.
extend({ OrbitControls });

function Loading() {
  return (
    <mesh visible position={[0, 0, 0]} rotation={[0, 0, 0]}>
      <sphereGeometry attach="geometry" args={[1, 16, 16]} />
      <meshStandardMaterial
        attach="material"
        color="white"
        transparent
        opacity={0.6}
        roughness={1}
        metalness={0}
      />
    </mesh>
  );
}

function ArWing() {
  const group = useRef();
  const { nodes } = useLoader(GLTFLoader, "models/arwing.glb");
  return (
    <group ref={group}>
      <mesh visible geometry={nodes.Default.geometry}>
        <meshStandardMaterial
          attach="material"
          color="white"
          roughness={0.3}
          metalness={0.3}
        />
      </mesh>
    </group>
  );
}

// highlight-start
const CameraControls = () => {
  // Get a reference to the Three.js Camera, and the canvas html element.
  // We need these to setup the OrbitControls component.
  // https://threejs.org/docs/#examples/en/controls/OrbitControls

  const {
    camera,
    gl: { domElement },
  } = useThree();

  // Ref to the controls, so that we can update them on every frame using useFrame
  const controls = useRef();

  useFrame((state) => controls.current.update());
  return <orbitControls ref={controls} args={[camera, domElement]} />;
};
// highlight-end

export default function App() {
  return (
    <>
      <Canvas style={{ background: "white" }}>
        <CameraControls /> // highlight-line
        <directionalLight intensity={0.5} />
        <Suspense fallback={<Loading />}>
          <ArWing />
        </Suspense>
      </Canvas>
      <a
        href="https://codeworkshop.dev/blog/2020-04-04-adding-orbit-controls-to-react-three-fiber/"
        className="blog-link"
        target="_blank"
        rel="noopener noreferrer"
      >
        Blog Post
      </a>
    </>
  );
}

Let's break down what we just did in that last step. There are three important parts.

Using the useThree Hook to get a reference to the Three.JS Camera and Canvas Element

To add OrbitControls we need a reference to the Three.js camera and canvas element when creating the component. To get these react-three-fiber provides the useThree hook, this is an escape hatch into getting access to core Three.js elements.

const {
  camera,
  gl: { domElement },
} = useThree();

Initializing the Orbit Controls

Once we have those, we can create the OrbitControls using orbitControls JSX element, which has been made available to us from earlier when we called extend(). You could try removing the line with extend and check the error to see what would have happened if we hadn't done that first. Something worth noticing here is that the args array provided to the JSX elements in react-three-fiber are always the parameters required to initialize a native object in Three.JS. [OrbitControls](https://threejs.org/docs/#examples/en/> controls/OrbitControls)

Getting used to the mapping between Three.js docs and their equivalent component API happens over time as you work with it.

<orbitControls args={[camera, domElement]} />

Plugging The Orbit Controls into the render loop with useFrame

In order for our orbit controls to be updated on every animation frame, we need to call controls.current.update() in the render loop. Any time you need some code to run in the render loop in react-three-fiber we use the useFrame hook. > In this case, since we want to call a method on OrbitControls, we also need to add a ref, and then we can call the update method.

// Ref to the controls, so that we can update them on every frame using useFrame
const controls = useRef();

useFrame((state) => controls.current.update());

Configuring OrbitControls

At this point you should be able to click and drag your mouse to rotate around the ship. But what if we want to restrict the rotation so that we stay behind the ship, and also turn off zooming with the scroll wheel. To access all the options of OrbitControls you can set them as props on the orbitControls component like this.

import React, { Suspense, useRef } from "react";
import {
  Canvas,
  useLoader,
  useFrame,
  extend,
  useThree,
} from "react-three-fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import "./styles.css";

// Extend will make OrbitControls available as a JSX element called orbitControls for us to use.
extend({ OrbitControls });

function Loading() {
  return (
    <mesh visible position={[0, 0, 0]} rotation={[0, 0, 0]}>
      <sphereGeometry attach="geometry" args={[1, 16, 16]} />
      <meshStandardMaterial
        attach="material"
        color="white"
        transparent
        opacity={0.6}
        roughness={1}
        metalness={0}
      />
    </mesh>
  );
}

function ArWing() {
  const group = useRef();
  const { nodes } = useLoader(GLTFLoader, "models/arwing.glb");
  return (
    <group ref={group}>
      <mesh visible geometry={nodes.Default.geometry}>
        <meshStandardMaterial
          attach="material"
          color="white"
          roughness={0.3}
          metalness={0.3}
        />
      </mesh>
    </group>
  );
}

const CameraControls = () => {
  // Get a reference to the Three.js Camera, and the canvas html element.
  // We need these to setup the OrbitControls class.
  // https://threejs.org/docs/#examples/en/controls/OrbitControls

  const {
    camera,
    gl: { domElement },
  } = useThree();

  // Ref to the controls, so that we can update them on every frame using useFrame
  const controls = useRef();
  useFrame((state) => controls.current.update());
  return (
    <orbitControls
      ref={controls}
      args={[camera, domElement]}
      enableZoom={false} // highlight-line
      maxAzimuthAngle={Math.PI / 4} // highlight-line
      maxPolarAngle={Math.PI} // highlight-line
      minAzimuthAngle={-Math.PI / 4} // highlight-line
      minPolarAngle={0} // highlight-line
    />
  );
};

export default function App() {
  return (
    <>
      <Canvas style={{ background: "white" }}>
        <CameraControls />
        <directionalLight intensity={0.5} />
        <Suspense fallback={<Loading />}>
          <ArWing />
        </Suspense>
      </Canvas>
      <a
        href="https://codeworkshop.dev/blog/2020-04-03-adding-orbit-controls-to-react-three-fiber/"
        className="blog-link"
        target="_blank"
        rel="noopener noreferrer"
      >
        Blog Post
      </a>
    </>
  );
}

Coming Up in Part Three

In Part Three we will start building something that really feels like a game. We will add movement controls to our ship, and create some procedurally generated terrain to fly through.