Creating Three-dimensional React Components!

Creating Three-dimensional React Components!

🌞Introduction

In case you are not aware, there is a JavaScript library that provides an API to create and display animated 3D computer graphics in a web browser. Three.js uses JavaScript to create GPU-accelerated 3D animations that can be included in a website without the need for proprietary browser plugins. This is made possible by WebGL, a low-level graphics API built exclusively for the web.

When I was first introduced to three.js, I was fascinated by the examples shown on the library's website and the dozens of tutorials for beginners ( check them) that enabled me to create really beautiful 3D objects in my browser with merely the knowledge I had acquired from a computer graphics class. Well, this sounds great, but seems to leave the article's title vague. What does this have to do with React?

React and three would be two completely different worlds that would be difficult to reconcile on a large scale. React was built from the ground up to work with imperative systems like three, and this is the same reason you use it for the dom. In other words, copying a three.js code snippet into your react component will not work. Many bugs will come up in the process of integrating the two. In this article, I will walk thru the whole process of integrating three.js into a react component without the use of any react packages. (Fundametnals of Three.js is required tho 🤞)

😈Pure Three.js

We will try to integrate Red Stapler's star diving 3D animation into a cinema reservation react app. The results will be something like this:

149400381-b6247f7a-b4af-4316-9650-1a2b3f1e8a01.gif

The code snippet is quite straightforward. Initially, a three-scene is created with a camera and a WebGL renderer that is appended to the body of the DOM. Then 6000 Vector3 objects were created to represent the stars (X, Y, and Z), and an initial speed of 0 was given to every star. Lastly, a white image was added as a texture for every object. This code will just spread 6000 dots in random locations across the screen, but to make the objects move, we need to alter the values of the speed property.

    <script src="three.min.js"></script>
    <script>
    let scene, camera, renderer, stars, starGeo;

    function init() {

      scene = new THREE.Scene();

      camera = new THREE.PerspectiveCamera(60,window.innerWidth / window.innerHeight, 1, 1000);
      camera.position.z = 1;
      camera.rotation.x = Math.PI/2;

      renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.body.appendChild(renderer.domElement);

      starGeo = new THREE.Geometry();
      for(let i=0;i<6000;i++) {
        star = new THREE.Vector3(
          Math.random() * 600 - 300,
          Math.random() * 600 - 300,
          Math.random() * 600 - 300
        );
        star.velocity = 0;
        star.acceleration = 0.02;
        starGeo.vertices.push(star);
      }

      let sprite = new THREE.TextureLoader().load( 'star.png' );
      let starMaterial = new THREE.PointsMaterial({
        color: 0xaaaaaa,
        size: 0.7,
        map: sprite
      });

      stars = new THREE.Points(starGeo,starMaterial);
      scene.add(stars);

      window.addEventListener("resize", onWindowResize, false);

      animate(); 
    }
    init();

    </script>

To animate the stars, loop over the star geometry, changing the velocity of every star by the acceleration value and changing the value of any of the 3 dimensions of the star depending on the direction of motion you want. To avoid losing the star into the horizon, reinitialize its initial location and velocity after a certain threshold. The requestAnimationFrame() method instructs the browser to perform an animation and to run a given function to update the animation before the next repaint. The method accepts a callback as an argument, which will be executed before the repaint.

    function animate() {
      starGeo.vertices.forEach(p => {
        p.velocity += p.acceleration
        p.y -= p.velocity;

        if (p.y < -200) {
          p.y = 200;
          p.velocity = 0;
        }
      });
      starGeo.verticesNeedUpdate = true;
      stars.rotation.y +=0.002;

      renderer.render(scene, camera);
      requestAnimationFrame(animate);
    }

😇React + Three.js

For me, the most challenging part is not related to threejs at all, but primarily the way React tries to mount and unmount items from the DOM. Each React component has a lifecycle that you can track and alter throughout its three phases. Recall that mounting is putting elements into the DOM. When mounting a component, React calls the following four built-in functions in this order:

  1. constructor()
  2. getDerivedStateFromProps()
  3. render()
  4. componentDidMount()

Recall also that React refs provide a way to access DOM nodes or React elements created in the render method. When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument. In the render method shown below, this.mount will hold a reference to the actual< div> the component is mounted to, and accordingly, appendChild(this.renderer.domElement) will be able to append the WebGL renderer to the div node in the DOM. It's worth noting that this. mount won't exist until the component has been mounted, and that's why we are putting our 3D object in a componentDidMount method. This might be tricky make sure you read it again!

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

Note that Three.Geometry() is no longer supported in Three.js, and BufferGeometry() is used instead. To migrate between the two, set the position attribute of the buffer geometry using the setAttribute() method. The final code will look something like this:

import React, { Component } from "react";
import * as THREE from "three";

class ThreeScene extends Component {
  componentDidMount() {
    this.scene = new THREE.Scene();

    this.renderer = new THREE.WebGLRenderer();
    this.renderer.setSize(window.innerWidth, window.innerHeight);

    this.mount.appendChild(this.renderer.domElement);

    this.camera = new THREE.PerspectiveCamera(
      60,
      window.innerWidth / window.innerHeight,
      1,
      1000
    );
    this.camera.position.z = 1;
    this.camera.position.x = Math.PI / 2;
    var vert = [];
    this.velocities = [];
    this.acceleration = [];
    for (let i = 0; i < 10000; i++) {
      vert.push(
        Math.random() * 600 - 300,
        Math.random() * 600 - 300,
        Math.random() * 600 - 300
      );
      this.velocities.push(0);
      this.acceleration.push(0.01);
    }

    this.stars_geometry = new THREE.BufferGeometry();

    var verticies = new Float32Array(vert);
    this.stars_geometry.setAttribute(
      "position",
      new THREE.BufferAttribute(verticies, 3)
    );
    var photo = new THREE.TextureLoader().load("images/star.png");
    var material = new THREE.PointsMaterial({
      color: 0xaaaaaa,
      size: 0.9,
      map: photo,
    });
    this.stars = new THREE.Points(this.stars_geometry, material);

    this.scene.add(this.stars);
    this.animation();
    this.renderer.render(this.scene, this.camera);
  }
    animation = () => {
    var positions = this.stars_geometry.getAttribute("position");
    for (let i = 0; i < positions.count; i++) {
      var z = positions.getZ(i);

      var vel = this.velocities[i];
      const accel = this.acceleration[i];
      vel += accel;
      z -= vel;
      if (z < -250) {
        z = 250;
        vel = 0;
      }
      this.velocities[i] = vel;
      positions.setZ(i, z);
      positions.needsUpdate = true;
    }
    this.stars.rotation.z += 0.01;
    this.renderer.render(this.scene, this.camera);

    requestAnimationFrame(this.animation);
  };
  render() {
    return (
      <div
        ref={(mount) => {
          this.mount = mount;
        }}
      ></div>
    );
  }
}
export default ThreeScene;

🥱Final Words

Well, it is great to integrate your three.js objects into a react app and now you know how but as stated before React is all about dealing with imperative systems like three and this seems to make things more complex and hard to scale. If you are new to React this method would be okay especially if you are not scaling up your app with multiple 3D objects but in real life react-three-fiber will be a better option to deal with threejs while wearing React glasses. In the next article I will introduce how to create models using react three fiber stay tuned😍.

Github repo