DEV Community

Cover image for Creating a rudimentary pool table game using React, Three JS and react-three-fiber: Part 1
Manan Joshi
Manan Joshi

Posted on • Edited on

Creating a rudimentary pool table game using React, Three JS and react-three-fiber: Part 1

For a long time, I wanted to learn and get started with WebGL. Having done some work with OpenGL I thought WebGL would be a great addition to know and learn. This is when I came across three.js.

Three.js is an awesome 3D library for working with graphics on the web. It is written in JavaScript but does not have native support for React. Having worked with React a lot I wanted to use the expressiveness of React combined with the power of three js. This is when I found react-three-fiber. It is a lightweight React renderer for three.js and works well.

This is a three-part series of articles where we will see how we can use React, three.js, and react-three-fiber to create a game of pool table.

  • Part 1: Getting started with React, three.js, and react-three-fiber.
  • Part 2: Setting up the basic scene (Coming soon).
  • Part 3: Adding physics and finishing up(Coming soon).

First things first

  • Create a React project. The best way to do this is by using create-react-app
  • The next step is to install the three.js and react-three-fiber modules. Depending on the package manager of your choice go ahead and install them. npm i three react-three-fiber or yarn add three react-three-fiber

Now that our project is set up let's do the fun part and jump into coding.

Organizing the project

This is a project structure that I like to follow and by no means, you've to do this. This is how I like to organize but feel free to move things around.

Inside the src directory let's create different directories for components, views, utils, and assets. Your directory structure should look something like this

project
│   README.md
└───src
│   │   index.js
│   │   App.js
│   │
│   └───assets
│   └───components
│   └───utils
│   └───views
Enter fullscreen mode Exit fullscreen mode

Creating a basic scene

  • Go ahead and create a file called Scene.js inside the views directory.
  • Just copy and paste the code below in the Scene.js file.
import React from 'react';

function Scene() {
  return (
    <mesh>
      <boxBufferGeometry attach='geometry' args={[1, 1, 1]} />
      <meshNormalMaterial attach='material' />
    </mesh>
  );
}

export default Scene;

Enter fullscreen mode Exit fullscreen mode

This will create a cube mesh for us.

Let's go and see what each line does.

All the jsx tags that you see are react-three-fiber wrappers around three.js objects

  • The mesh component is the Mesh object from the three js library. The same is true for boxBufferGeometry and meshNormalMaterial.
  • If you check out the docs for the components on the three js website you'll see BoxBufferGeometry has a constructor with a bunch of parameters.
  • The way you can create a new instance in React with react-three-fiber is by using the args prop for that component and passing in the parameters as an array.
  • So in the above example, <boxBufferGeometry attach='geometry' args={[1, 1, 1]} /> will create a new BoxBufferGeometry (aka cube) with params 1, 1, and 1 for width, height and depth respectively. The attach prop tells the renderer what kind of object the given component is. You can use all the properties for this given object and from its superclass as props to the component. You can find all the properties in the docs for three js.
  • Similarly, the meshNormalMaterial can be used to color the geometry among many other uses which we will see later.

Congratulations you just created a cube and added it to the scene. The next step is to render the scene inside of a canvas element. You all know how to do this, so bye-bye and happy coding.

I was just kidding. So now, let's create a canvas.

Creating the Canvas

  • Open up the App.js file and copy and paste the code given below.
import React from 'react';
import { Canvas } from 'react-three-fiber';

import Scene from './views/Scene';

function App() {
  return (
    <Canvas>
      <Scene />
    </Canvas>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode
  • Here the Canvas component will add a canvas element to the dom and render the scene as part of the HTML canvas element.

We are done at this point. Just go and run npm start and you'll be able to see your beautiful cube in the browser.

Your output should look something like this

shot1

  • Just one last thing to do here is the canvas doesn't take the entire height of the screen.
  • So in your index.css just add the following lines
body {
  margin: 0;
  height: 100vh;
  width: 100vw;
  background-color: black;
}

#root {
  height: 100%;
}
Enter fullscreen mode Exit fullscreen mode
  • And finally, you'll see a cube that is in the center of your screen.

I hope you're happy with the effort you just put in but as you can see the cube looks more like a square. Don't panic, believe me, it is a cube. To see it as a 3D object let's add mouse/track-pad controls so that we can perform pan, rotate and zoom(aka orbit controls).

Adding Orbit Controls

  • Let's go ahead and create a file called Controls.js and copy and paste in the code given below.
import React, { useRef } from 'react';
import { extend, useThree, useFrame } from 'react-three-fiber';
import OrbitControls from 'three/examples/jsm/controls/OrbitControls';

extend({ OrbitControls });

function Controls() {
  const controlsRef = useRef();
  const { camera, gl } = useThree();

  useFrame(() => controlsRef.current && controlsRef.current.update());

  return (
    <orbitControls
      ref={controlsRef}
      args={[camera, gl.domElement]}
      enableRotate
      enablePan={false}
      maxDistance={100}
      minDistance={5}
      minPolarAngle={Math.PI / 6}
      maxPolarAngle={Math.PI / 2}
    />
  );
}

export default Controls;
Enter fullscreen mode Exit fullscreen mode
  • The first thing to understand here is, OrbitControls is not part of the main three module, hence you cannot use it directly as we saw in the previous mesh and geometry code for the cube.
  • To deal with this react-three-fiber provides an extend function which can be used for modules outside of the main three js codebase. Remember to call the extend function in the beginning before the component function and after that, you'll be able to use the extended module like any other three js module.
  • So, now as we saw earlier while using mesh and geometry we can use orbit controls in the same way along with all its properties.
  • Let's talk about the hooks used above as well useRef, useThree, and useFrame.
  • useRef among other things is Reacts' way of giving us access to the underlying dom node. You can read more about it here
  • useThree is a react-three-fiber hook that essentially gives us access to everything that is added to the scene. This is going to be super helpful to us later on.
  • useFrame also a react-three-fiber hook is called for every frame that is drawn. If you have used RequestAnimationFrame API provided by the browser, this hook is similar to that. It will be formulating the basics of our physics calculation later on in the example.
  • And the final step is adding the newly created controls to the canvas. To do this open the App.js file and replace the current code with the code below.
import React from 'react';
import { Canvas } from 'react-three-fiber';

import Scene from './views/Scene';
import Controls from './components/Controls';

function App() {
  return (
    <>
      <Canvas>
        <Scene />
        <Controls />
      </Canvas>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Start the app and nothing will have changed but now you'll be able to use your mouse-wheel/track-pad to zoom in and out while holding on to the left click will allow you to rotate and inspect the cube from all sides as well as allow you to pan. You should be able to do something like this shown below.

shot2

Before we jump into modeling out our pool table there is just one last thing that we want to do. Let us just adjust our camera a little bit so that we can see how to change the default settings on the camera.

Editing the camera settings

  • Go ahead and open your Scene.js file and replace the contents with the code below.
import React from 'react';
import { useThree } from 'react-three-fiber';

function Scene() {
  const { camera } = useThree();

  camera.fov = 45;
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.near = 0.1;
  camera.far = 1000;

  camera.up.set(0, 0, 1);
  camera.position.set(-5, 7, 5);

  return (
    <mesh>
      <boxBufferGeometry attach='geometry' args={[1, 1, 1]} />
      <meshNormalMaterial attach='material' />
    </mesh>
  );
}

export default Scene;
Enter fullscreen mode Exit fullscreen mode
  • As we had seen earlier, useThree gives up access to the default camera. We are just tweaking some settings on that so that we can see the scene better.
  • You will be able to find all the properties and functions the camera object has here.

This concludes part 1 of the three-part series. I'll be posting the upcoming parts in the following days.

Also, this is my first blog post ever. It will be great if you could leave some feedback in the comments so that I can understand and improve my content delivery. You can follow me on Twitter and Instagram.

Peace out and happy coding!!!

Top comments (11)

Collapse
 
jonasroessum profile image
Jonas Røssum • Edited

Great article!
I really needed it two days ago, since I started experimenting with react-three-fiber and OrtbitControls!

Feedback:

This part felt a bit out of place:

You all know how to do this, so bye-bye and happy coding.

I was just kidding. So now, let's create a canvas.

Also, maybe you could rephrase this part:

Just one last thing to do here is the canvas doesn't take the entire height of the screen.
So in your index.css just add the following lines

For the part about Editing the camera settings, do you intend to only do this on initial render further in the article series? If not, I would recommend putting it in a useEffect call, to prevent the camera from resetting in each render

const { camera } = useThree()

useEffect(() => {
  camera.fov = 45
  camera.aspect = window.innerWidth / window.innerHeight
  camera.near = 0.1
  camera.far = 1000

  camera.up.set(0, 0, 1)
  camera.position.set(-5, 7, 5)
}, [])
Enter fullscreen mode Exit fullscreen mode

Other than a few missing commas here and there, really nice article!

Collapse
 
manan30 profile image
Manan Joshi

Thanks for the feedback. The camera is used for a couple of other things further down the line but this is a really good suggestion to wrap the camera in an useEffect call.

Collapse
 
controlplusb profile image
Sean Matheson

This is amazing. Thanks for posting!

Collapse
 
manan30 profile image
Manan Joshi

Thank you!!

Collapse
 
seanmclem profile image
Seanmclem

Your first code sample appears to be missing some imports

Collapse
 
manan30 profile image
Manan Joshi

Could you please point me to that? As far as I remember the code sample will work well.

Collapse
 
seanmclem profile image
Seanmclem • Edited

Creating a basic scene:

First code sample uses mesh, boxBufferGeometry, and meshNormalMaterial, but only imports react. If that's accurate - how does that work?

Thread Thread
 
manan30 profile image
Manan Joshi

Sorry for the late reply. So the way this works is if you are aware that the jsx we write in React is not valid HTML but there is a plugin that transpiles those calls into React.createElement call. The same way when you write <mesh /> or <boxBufferGeometry> or any of the components in the three.js catalog react-three-fiber will go and convert those calls to three js calls. For starters check out this and this

Collapse
 
thisispete profile image
pete

I'm getting errors when I get to the orbit controls.. "The above error occurred in the component:" "Uncaught TypeError: target is not a constructor" ...

Collapse
 
thisispete profile image
pete

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

looks like this line changed and now needs to be destructured from OrbitControls

Collapse
 
abdelrahmanlotfy profile image
AbdElRahmanLotfy

Thanks , great tutorial .