Another dimension

with three.js

(of problems)

React edition

What is three.js?

JavaScript 3D library

The aim of the project is to create an easy to use, lightweight, 3D library with a default WebGL renderer. The library also provides Canvas 2D, SVG and CSS3D renderers in the examples.

How does it look like?

And how's the code?

import * as THREE from 'three';

const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.PerspectiveCamera(70, aspect, 0.01, 10);
camera.position.z = 1;

const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);

function animation(time) {
  mesh.rotation.x = time / 2000;
  mesh.rotation.y = time / 1000;
  renderer.render(scene, camera);
}

IMPERATIVE

Where is my JSX?

import React from 'react';
import ReactDOM from 'react-dom';

class Example extends React.Component {
  componentDidMount() {
    this.mount.appendChild(renderer.domElement);

    // FIXME: Stop animating on unmount!
    frame(0);
    function frame(time) {
      requestAnimationFrame(frame);
      animation(time);
    }
  }

  render() {
    return <div ref={ref => (this.mount = ref)} />;
  }
}

ReactDOM.render(<Example />, document.getElementById('example'));

MEH :/

Can we do better?

react-three-fiber!

import React, { useRef, useState } from 'react';
import { useFrame } from 'react-three-fiber';

function Box(props) {
  // This reference will give us direct access to the mesh
  const mesh = useRef();

  // Set up state for the hovered and active state
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);

  // Rotate mesh every frame, this is outside of React without overhead
  useFrame(() => {
    mesh.current.rotation.x = mesh.current.rotation.y += 0.01;
  });

  return (
    <mesh
      {...props}
      ref={mesh}
      scale={active ? [1.5, 1.5, 1.5] : [1, 1, 1]}
      onClick={() => setActive(!active)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}
    >
      <boxBufferGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  );
}

DECLARATIVE

import ReactDOM from 'react-dom';
import React from 'react';
import { Canvas } from 'react-three-fiber';

import { Box } from './Box';

ReactDOM.render(
  <Canvas>
    <ambientLight />
    <pointLight position={[10, 10, 10]} />
    <Box position={[-1.2, 0, 0]} />
    <Box position={[1.2, 0, 0]} />
  </Canvas>,
  document.getElementById('root'),
);

REACT-ISH!

Is it slower than raw three.js?

No. Rendering performance is up to threejs and the GPU. Components participate in the renderloop outside of React, without any additional overhead. React is otherwise very efficient in building and managing component-trees, it could potentially outperform manual/imperative apps at scale.

Is Vazco 3D yet?

Meet *project name*!

Repository:​ *repo*

DEMO

But wait, there's more!

drei

A growing collection of useful helpers and abstractions for react-three-fiber.

Repository:​ github.com/pmndrs/drei

Basically a lot of declarative three.js wrappers and addons

Animations in 60FPS

  1. Never, ever, setState animations!

  2. Never let React anywhere near animated updates!

  3. Never bind often occuring reactive state to a component!

More in pitfalls.md and recipes.md

Where's the catch?

If something is not covered by react-three-fiber or drei, you're doomed forced to work with three.js on your own.

const gltf = useGLTFLoader(url);
const colorMap = useImageLoader(colorTextureLink);
const normalMap = useImageLoader(normalTextureLink);

// Meshes cannot be shared therefore we clone the objects.
const elements = gltf.scene.children;
const elementsClone = useMemo(
  () =>
    elements.map(object => {
      const cover = object.clone() as Mesh;
      cover.castShadow = true;
      cover.receiveShadow = true;

      const material = cover.material.clone() as MeshPhysicalMaterial;
      cover.material = material;
      if (material.map) {
        if (supportsTextureClone())
          material.map = material.map.clone();
        material.map.image = colorMap;
        material.map.needsUpdate = true;
      }

      // Same for normalMap.
      material.envMapIntensity = isFocused ? 1.5 : 0.2;
      material.needsUpdate = true;

      return cover;
    }),
  [elements, isFocused, normalMap, colorMap],
);

Takeaways

  1. three.js is very powerful
  2. three.js is quite low-level
  3. react-three-fiber is great
  4. drei is even better
  5. Prepare for stepping out of your declarative comfort zone

Questions?

PS It works with React Native as well.

Another dimension (of problems) with three.js, React edition

By Radosław Miernik

Another dimension (of problems) with three.js, React edition

  • 762