Get Started With ThreeJS
Tags: threejs, react, webdev, tutorial
Published: 2021-11-23 19:33:25

Recently, I used ThreeJS and it was really fun. Today, I'll teach you how to get started with it, with a fun (and simple) tutorial.

As [stated by Wikipedia](https://en.wikipedia.org/wiki/Three.js),

> Three.js is a cross-browser JavaScript library and application programming interface (API) used to create and display animated 3D computer graphics in a web browser using WebGL.

Here's is the finished product:
![rotating plane](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59oimfog3q4k2q5u26kh.gif)

I also made a [demo](https://threejs-tutorial-demo.vercel.app/) of the finished product.

## Table of Contents

1. [React Setup](#react)
2. [ThreeJS Setup](#threejs)

I'm going to do this in React, but most of this stuff should apply for plain HTML CSS and JS. The ThreeJS docs have a really nice [starter guide](https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene) to get you up and running with vanilla JS, so do check it out. If you haven't done React before, I would suggest watching [this video](https://www.youtube.com/watch?v=MRIMT0xPXFI) by Aaron Jack to get you started as fast as possible.

## React Setup <a name="react"></a>

Anyways, let's initialize a React project. If you want, you could also use something like [NextJS](https://nextjs.org/), but I'm sticking to [Create React App](https://create-react-app.dev/) for now.

I'm using `yarn` to initialize my project, so here are the two commands (one with `npm` and the other with `yarn`) to create a React project.

npm: `npx create-react-app threejs-learning`
yarn `yarn create react-app threejs-learning`

And yes, as [explained by the React docs](https://reactjs.org/docs/create-a-new-react-app.html), `npx` is not a typo (it's something to run scripts that comes with `npm`).

When you initialize the project, you'll see something like this:
![New Project Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1emx3llnvhxvcbwgngng.png)

Disregard this for now (we'll deal with the unnecessary files later). What you should do is start the server so you can see what the boilerplate looks like.

To do that, run the command that corresponds to what you initalized the project with:
yarn: `yarn start`
npm: `npm run start`

This will open up a browser tab at `http://localhost:3000` and you'll see something like this:
![Starter template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w5cfnwhumgjikmn9wut.png)

Great job you now have a React project set up!

Back to your editor now. In the `src` folder, you'll see these files: ![src folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmqo9omlo6fi2z21xevg.png)

Here, you can delete `App.css`, `App.test.js`, `index.css`, `logo.svg`, `reportWebVitals.js`, and `setupTests.js`

Shoot! If you look back at the browser tab, we encounter an error:
![Files not found error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qn5xiujkvkl02ssjhs7d.png)

If you take a look back at the `App.js` and `index.js` files in your editor, you'll see that they are importing some of the files we deleted, thus resulting in an error:

`App.js`:

```js
import logo from "./logo.svg";
import "./App.css";
```

`index.js`

```js
import "./index.css";
import reportWebVitals from "./reportWebVitals";
```

The solution is simple and requires only a couple keys. Just delete those lines from each file 🤪.

But some further works still needs to be done. Our code is still using the stuff we imported.

In `index.js`, after deleting the imports, your file should look like this:

```jsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

We need to delete the `reportWebVitals();` and the `<React.StrictMode>` since we removed the imports for that.

This is `index.js` after those changes:

```js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));
```

Now, let's fix `App.js`. This is how it should look like right now:

```jsx
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
```

Just delete everything in the `return();` function and replace it with a simple `<h1>I love React!</h1>`. This is how it should look:

```jsx
function App() {
  return <h1>I love React!</h1>;
}

export default App;
```

Ok great, now we have all the bloat out of our way. Note that this stuff we deleted _can_ be important if you have a big project, but for now it can be discarded since this a learning project.

Now, if you save it, you should see this in the browser: ![Result after deleting files](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0rsifm8wwbpv7cfa43xa.png)

## ThreeJS Setup <a name="threejs"></a>

So now we get to interact with ThreeJS. To get started, install it:

yarn: `yarn add three`
npm: `npm install three`

Okay, now go into your `App.js` file and import ThreeJS like this:

```js
import * as THREE from "three";
```

Then, change your `<h1>I love React!</h1>` to `<canvas id="bg"></canvas>` in the `return` function of the component. This is so that ThreeJS has something to attach itself on and do its work.

At this point, we're going to have to do a clever "hack" if you'll call it that. Since the JavaScript loads before the JSX (JSX is the code that looks like HTML), our code is unable to reference the `canvas` element if placed before the return statement.

We're going to have to use something called `useEffect` so that ThreeJS runs after the first render and we can access the canvas element.

Import `useEffect` with

```jsx
import { useEffect } from "react";
```

and insert

```jsx
useEffect(() => {}, []);
```

above the `return` statement. Here, the empty array as the second argument indicates for the `useEffect` hook to only run on the first render, not repeatedly after each one. Traditionally, you would put variable name(s) there so that `useEffect` would run after those variable(s) changed, but we only want it to run after the first render.

Now, in the `useEffect`, create a new ThreeJS scene (this scene is where everything will show up):

```jsx
useEffect(() => {
  const scene = new THREE.Scene();
}, []);
```

Now we have to create a camera. Add a camera with

```jsx
useEffect(() => {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
}, []);
```

This might be a bit overwhelming, but let me break it down. The first parameter (the `75`) is the FOV of the camera. FOV (aka Field of View) is basically how much the camera can see.

Think of it like this pizza:
![Pizza without slice](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oqkde1axh3m3ze7mju79.png)

The angle of the missing slice is how much the camera can see. The higher the angle, the more it can see. However, if it's too high, you can get results that don't look right.

The second parameter is for the aspect ratio of the view. This is basically the ratio of the width : height, and I've done so with the space of the page using the `window` object. The next two parameters are how near and far the camera can view objects.

Next, we have to create a renderer. Below the camera, create a renderer and set the background of the scene:

```jsx
useEffect(() => {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );

  const renderer = new THREE.WebGL1Renderer({
    canvas: document.querySelector("#bg"),
  });
  scene.background = new THREE.Color(0x4e9fe5);
}, []);
```

The `canvas` option allows ThreeJS to latch itself on an element in the DOM. The `scene.background` enables us to create a color with the `#4e9fe5` hex code (which will be our sky).

Next, add the following code:

```jsx
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
camera.position.set(10, 2, 0);
renderer.render(scene, camera);
```

The first line sets the pixel ratio, while the second sets the dimensions for the renderer. The third line sets the position for the camera (with the `x`, `y`, and `z` axes, respectively). The last line renders the scene with the camera we made above.

Now, let's make some lights:

```jsx
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
hemiLight.position.set(0, 20, 0);
scene.add(hemiLight);

const dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(-3, 10, -10);
scene.add(dirLight);

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
ambientLight.position.set(0, 0, 0);
scene.add(ambientLight);
```

The first chunk of code creates a new light, creates a gradient (from colors white to grey) of light from the top to the bottom of the scene. To wrap your head around this concept, I would suggest [this interactive playground](https://threejsfundamentals.org/threejs/threejs-lights-hemisphere.html) online) We then set the position of the light (with the `xyz` axes) and add it to the scene.

The second chunk sets up a directional light, which is like a traditional light source (it illuminates from a point). We set its color to white, set its position, and add it to the scene.

The ambient light is basically a light that is illuminating from everywhere in your scene. Think of your scene being put in a ball, which is then illuminated from the inside. We then set it's position to the center of the scene and add it.

Ok so now we have a basic scene set up. It should look like this:
![Scene without anything](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifdpckuxeiysm9nd5qdg.png)

We need to get a model in the scene now, to make it interesting. I would suggest going to [poly.pizza](https://poly.pizza/) and getting a model. I am using [this airplane](https://poly.pizza/m/7cvx6ex-xfL), (Attribution for model: Small Airplane by Vojtěch Balák [CC-BY](https://creativecommons.org/licenses/by/3.0/) via [Poly Pizza](https://poly.pizza/m/7cvx6ex-xfL)) but I highly recommend you to use any model you want. Download the `.glb` format of the object with this button:
![Download Button with .obj and .glb files](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kqej6rokvn9avwt2oxq.png)

Once downloaded, at this `.glb` file to the `public/` folder in the root of your project.

At the top of your code, add this to import the `GLTFLoader`:

```jsx
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
```

Next, add this code under the ambient light code to import the model:

```jsx
const loader = new GLTFLoader();

loader.load("/NAME_OF_FILE.glb", function (gltf) {
  gltf.scene.scale.set(0.8, 0.8, 0.8);
  scene.add(gltf.scene);
});
```

The first line creates a new loader object, so we can load the file. The `loader.load` part loads the actual model. The first argument is the `.glb` file. The `/NAME_OF_FILE.glb` accesses it from the `public/` folder, which is replaced by the name of your `.glb` file. The second argument is a function that has the resulted model as a varible. We can access the proper model with `gltf.scene`, hence why we are adding _that_ to our scene instead of just `gltf`.

Inside the function, I am scaling the model down to 80% of its original size since it was way too big for the viewport. Note that this is optional based on how good your model looks. It might even be small, so you can scale it up in that case.

Next, we have to add an `animate()` function. This basically just re-renders our ThreeJS scene constantly. To do that, just create a function like so:

```jsx
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

animate();
```

The first line inside of the function acts like a loop (the actual term is recursion). It calls the animate function again inside of itself, so it keeps re-rendering. The next line renders the scene and the camera again. We call the function outside of itself so it can start.

But wait a minute, nothing shows up in the browser! It's just a blue background! That's because we didn't add controls for the scene. ThreeJS doesn't work without these controls, hence why they are necessary.

To put them in the scene, import:

```js
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
```

and add this above the `function animate()` stuff:

```jsx
const controls = new OrbitControls(camera, renderer.domElement);
```

This creates a new object called `controls`, which is made from the `OrbitControls` class. The constructor of the `OrbitControls` has a `camera` (which we previously defined), and the domElement to put the controls in (which we set in the creation of the renderer with `canvas: document.querySelector("#bg");`

Now, you should see this in the browser!:
![Plane in the scene](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iyw4j4uq6q312iuzo6gm.png)

You can even interact with it by dragging using your left mouse button, scrolling to zoom in, and using right click to move the camera.

The only problem with this is that when you resize the window, it becomes really, _REALLY_ distorted:
![distorted plane](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lyixu21310j27g9r7p2.png)

This is definitely not what we want, so let's change that. Above the place where you defined your `animate` function, create a function like so:

```js
const resizeWindow = () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.render(scene, camera);
};
```

Here, we are updating the renderer dimension data. First, we set the new width and height. Then we set the new pixel ratio (this probably will not change but we're setting it just in case). Next, we change the aspect ratio of the camera to the new width and height. We then update the camera's view and re-render the scene.

If you check the browser again and resize it, you'll see that now this happens:
![width and height not changing after function creation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hoazpku9qh7rti319o9p.png)

This is happening because we haven't added called the function at all yet. To do so, add this after your `resizeWindow` function:

```js
window.addEventListener("resize", resizeWindow);
```

This line of code adds an event listener to the window object, and calls the `resizeWindow` function whenever the window is resized.

Now the plane is not distorted anymore!
![Undistorted plane](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qwbu69388twsp3g0ufm0.png)

We have the model loaded, but we should add some auto-rotation to make it look cool. To do that, add this in the function:

```js
controls.autoRotate = true;
controls.autoRotateSpeed = 4.0;
controls.update();
```

This essentially enables auto rotation, multiplies the speed by 4, and updates the controls to make it spin. If you want a laugh, change the `autoRotateSpeed` to something like `1000.0` and watch it go crazy 🤣.

In the end, your `App.js` should look something like this:

```jsx
import * as THREE from "three";
import { useEffect } from "react";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

function App() {
  useEffect(() => {
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );

    const renderer = new THREE.WebGL1Renderer({
      canvas: document.querySelector("#bg"),
    });

    scene.background = new THREE.Color(0x4e9fe5);

    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.position.set(10, 2, 0);
    renderer.render(scene, camera);

    const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
    hemiLight.position.set(0, 20, 0);
    scene.add(hemiLight);

    const dirLight = new THREE.DirectionalLight(0xffffff);
    dirLight.position.set(-3, 10, -10);
    scene.add(dirLight);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    ambientLight.position.set(0, 0, 0);
    scene.add(ambientLight);

    const controls = new OrbitControls(camera, renderer.domElement);

    const loader = new GLTFLoader();

    loader.load("/small-airplane-v3.glb", function (gltf) {
      gltf.scene.scale.set(0.8, 0.8, 0.8);
      scene.add(gltf.scene);
    });

    const resizeWindow = () => {
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(window.devicePixelRatio);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.render(scene, camera);
    };

    window.addEventListener("resize", resizeWindow);

    function animate() {
      requestAnimationFrame(animate);

      controls.autoRotate = true;
      controls.autoRotateSpeed = 4.0;

      controls.update();

      renderer.render(scene, camera);
    }

    animate();
  }, []);

  return <canvas id="bg"></canvas>;
}

export default App;
```

That's it! Now you're up and running with ThreeJS. This is a beginner tutorial and there is a bunch of stuff I didn't cover, so check out the [ThreeJS docs and examples](https://threejs.org/docs/). If you've followed along with this tutorial, choose another model and send a picture in the comments section so you can share your work!

The full code is in a repository on GitHub: {% github ShubhamPatilsd/threejs-learning no-readme %}

If you liked this post, the three shiny buttons on the left are waiting to be clicked, and if you didn't like the post, they still are open to clicking.

Oh yeah, and I also have a Twitter now (very exciting stuff). If you enjoy my blogs, do follow me as I share my thoughts about programming there as well (but more frequently). Follow me at: https://twitter.com/ShubhamPatilsd