Your First three.js Scene: Hello, Cube! 1.1

YOUR FIRST three.js SCENE: HELLO, CUBE!

In just a couple of lines, we’ll have a basic three.js app running

// Get a reference to the container element that will hold our scene
const container = document.querySelector( '#container' );

// create a Scene
const scene = new THREE.Scene();
// Set the background color
scene.background = new THREE.Color( 'skyblue' );

// Create a Camera
const fov = 35; // AKA Field of View
const aspect = container.clientWidth / container.clientHeight;

const near = 0.1; // the near clipping plane
const far = 100; // the far clipping plane

const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );

// every object is initially created at ( 0, 0, 0 )
// we'll move the camera back a bit so that we can view the scene
camera.position.set( 0, 0, 10 );

// create a geometry
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

// create a default (white) Basic material
const material = new THREE.MeshBasicMaterial();

// create a Mesh containing the geometry and material
const mesh = new THREE.Mesh( geometry, material );

// add the mesh to the scene
scene.add( mesh );

// create the renderer
const renderer = new THREE.WebGLRenderer();

renderer.setSize( container.clientWidth, container.clientHeight );
renderer.setPixelRatio( window.devicePixelRatio );

// add the automatically created <canvas> element to the page
container.appendChild( renderer.domElement );

// render, or 'create a still image', of the scene
renderer.render( scene, camera );

Open up the empty template on Codesandbox.io (or your local equivalent) and we’ll get started.

In this chapter, we’ll create a bare-bones three.js app. We’ll introduce quite a bit of theory along the way, but the actual code is short - just 18 lines without the comments.

The Components of a Real-Time 3D App

A basic scene

Every three.js app (in fact, nearly every real-time 3D app) will have the following basic components:

Together, the Scene, Camera, <canvas>, and Renderer give us the scaffolding of an app, however, none of them can actually be seen. We’ll also need to add some kind of visible object, and in this chapter, we’ll add a simple box-shaped Mesh, which has three components.

Mesh Details

Our First three.js App

Now we are finally ready to write some code! We’ll divide our app up into 6 steps:

  1. Initial Setup
  2. The Scene
  3. The Camera
  4. Visible Objects
  5. The Renderer
  6. Render The Scene

Every app that you write will always contain at least these basic steps, so you should take some time to make sure that you fully understand them.

1. Initial Setup

  1. Initial Setup
  2. The Scene
  3. The Camera
  4. Visible Objects
  5. The Renderer
  6. Render The Scene

Inside index.html we’ve created a container to hold our scene

<div id="container">
  <!-- This div will hold our scene-->
</div>

… and here is one way of getting a reference to this container in JavaScript

// Get a reference to the container element that will hold our scene
const container = document.querySelector( '#container' );

Even the most basic of apps requires some setup. We already covered the index.html and main.css files in the previous chapter, so we just need to add the JavaScript here. Turn your attention to app.js, which should currently be empty.

In this simple app, our JavaScript for the setup step is a single line. We’ve created an HTML container element to hold our scene and we’ll need to know its width and height when we’re setting up the Camera and Renderer below, so we’ll store a reference to it in the variable container at the top of the file. Once we’ve done so we’ll be able to use it further down in our code.

2. The Scene

  1. Initial setup
  2. The Scene
  3. The Camera
  4. Visible Objects
  5. The Renderer
  6. Render The Scene
The scene

2.1 Create the Scene

Create a scene

const container = document.querySelector( '#container' );

// create a Scene
const scene = new THREE.Scene();
// Set the background color
scene.background = new THREE.Color( 'skyblue' );

First, we’ll create the scene. We’re calling the Scene constructor to create a scene instance, and storing that instance in a variable also called scene.

2.2 Set the Background Color

Set the scene’s background color

const container = document.querySelector( '#container' );

// create a Scene
const scene = new THREE.Scene();
// Set the background color
scene.background = new THREE.Color( 'skyblue' );

We’ve also set the background color to a light blue color. As with the scene, this color was also created using a constructor, in this case, the Color constructor, and we’ve passed in as a parameter the word ‘skyblue’, which can be any of the CSS color names.

Aside from writing the name of the color, there are quite a few other ways of defining colors, as we’ll see soon.

The scene instance that we’ve just created is used to make a data structure called a scene graph. We’ll go over the technical details in of this in Chapter 2.5, but for now, just think of the scene object you have created as a holder for all the visible objects you want to display.

Once we have created an object, if we want to see it we’ll need to add it to the scene using scene.add( object ) and if we later want to remove it from the scene, we can just do scene.remove( object ). Simple!

We’ll need a camera to actually view the scene though. Let’s make one now.

3. The Camera

  1. Initial Setup
  2. The Scene
  3. The Camera
  4. Visible Objects
  5. The Renderer
  6. Render The Scene
The camera

We’ll make the camera in two steps. First, we’ll call the constructor to create a camera instance, just as we did with the scene in step 2. The only difference is that the camera’s constructor does take several parameters, as we’ll see in just a moment. Next, we’ll need to move the camera back a bit to view the scene, and we’ll take the opportunity to briefly introduce positioning objects in 3D space and the three.js coordinate system.

There are a couple of different cameras available, but we’ll generally stick with the PerspectiveCamera which uses perspective projection to set up a view of the scene.

Without going into any great detail here, perspective projection renders the scene in the way you expect a 3D scene to look - in other words, it mimics the way the human eye sees the world, with objects getting smaller the further away they are from the camera. Nearly all 3D games and special effects in films use a perspective camera.

The other important type of camera is the OrthographicCamera. This uses orthographic projection, which means that objects don’t get smaller as they get further away. This is often used for 2D games, or for user interfaces drawn on top of a 3D game (or 3D website). We’ll investigate both of these camera types in detail in Section 3: Cameras.

Orthographic and perspective projection

3.1 Create the Camera

Add the following code to create a camera

scene.background = new THREE.Color( 'skyblue' );

// Create a Camera
const fov = 35; // AKA Field of View
const aspect = container.clientWidth / container.clientHeight;

const near = 0.1; // the near clipping plane
const far = 100; // the far clipping plane

const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );

// every object is initially created at ( 0, 0, 0 )
// we'll move the camera back a bit so that we can view the scene
camera.position.set( 0, 0, 10 );

The PerspectiveCamera’s constructor takes four parameters, which together define its viewing frustum.

The Camera’s Viewing Frustum

A frustum shape

A frustum is a mathematical term meaning a four-sided rectangular pyramid with the top cut off. When we view the scene with our camera, everything inside the frustum is visible, everything outside it is not. In the following diagram, the area in between the Near Clipping Plane and the Far Clipping Plane is the frustum.

Perspective camera frustum

The four parameters that we passed into the constructor were used to create this shape. Let’s take a look at each of them now.

Field of View (FOV)

The Field of View angle

The .fov or Field of View parameter defines the angle of the viewing frustum, that is, how much of the world can be seen through the camera. It’s specified in degrees (as we’ll see soon, this is an exception; most angles in three.js are expressed in radians). Another way to think of this is that the .fov parameter defines how much bigger the far clipping plane will be than the near clipping plane. The valid range for the FOV is from 1 to 179 degrees.

Aspect Ratio

Aspect ratio examples

The .aspect ratio is the width divided by the height of the viewing rectangle. An aspect ratio of 1 will be a square, while window.innerWidth / window.innerHeight will be a rectangle with the same proportions as your current browser window (minus the portion at the top with the URL and browser controls). We’ve created a <container> element to hold our scene, and we’re using the width and height from that to calculate the aspect ratio.

Clipping Planes

Clipping Planes

The .near parameter defines the near clipping plane. Objects closer to the camera than .near will not be visible. For a PerspectiveCamera, the near plane must be greater than 0, and less than the .far plane. This defines the smaller end of the frustum pyramid shape in the diagram above.

The .far parameter defines the far clipping plane. Objects further away from the camera than this will not be visible. For a PerspectiveCamera, this can be anything bigger than the .near plane. In practice, to make your scene efficient you will want this to be as small as possible. This defines the larger “base” of the frustum.

3.2 Position the Camera

We need to move the camera back a bit so that we can see the scene

// Create a Camera
const fov = 35; // AKA Field of View
const aspect = container.clientWidth / container.clientHeight;

const near = 0.1; // the near clipping plane
const far = 100; // the far clipping plane

const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );

// every object is initially created at ( 0, 0, 0 )
// we'll move the camera back a bit so that we can view the scene
camera.position.set( 0, 0, 10 );

By default everything that we create is put at the point (0,0,0). This is called the origin and it’s the point where:

$$ \begin{aligned} x=0 & \cr y=0 & \cr z=0 & \end{aligned} $$

The camera that we created just now is positioned at (0,0,0), and any objects that we add to the scene will also be positioned at (0,0,0), by default.

This means that we need to move the camera back a bit to let us see the scene. You can also think of the point that the camera is placed at to be the dividing line between your screen, and the 3D scene that you are creating.

Since this is the first time that we’ve had to take the position of an object into account, let’s take a few moments to understand the three.js coordinate system.

The three.js Coordinate System

The three.js coordinate system

The above diagram shows the three.js coordinate system, along with the camera we just created, and your screen. The point where the x-axis, y-axis, and z-axis meet is (0, 0, 0). As we just mentioned, this is called the origin, and we’ve moved our camera to the point (0, 0, 10) - ten units towards us.

Take note of the direction of the axes relative to the camera and your screen.

The three.js coordinate system, simple

With the line camera.position.set( 0, 0, 10 ), we’ve left the camera in the middle of the x-axis and y-axis, but moved it 10 units towards us along the z-axis.

4. Visible Objects

  1. Initial Setup
  2. The Scene
  3. The Camera
  4. Visible Objects
  5. The Renderer
  6. Render The Scene
A visible object

We’ve created a camera to see things with, and a scene to put them in. The next step is to create something to see! We’ll start by making a simple white cube.

To do this, we’ll use a kind of object called a Mesh, which consists of three components - a Geometry (or rather a BufferGeometry, but more on that below), a Material, and the Mesh itself, which is a holder for the geometry and material and is used to set the position of the object inside the scene. We’ll make the geometry and material first, and then create the mesh to hold them.

4.1 Create a Geometry

Add the following line after the camera to create a box-shaped buffer geometry:

camera.position.set( 0, 0, 10 );

// create a geometry
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

// create a default (white) Basic material
const material = new THREE.MeshBasicMaterial();

// create a Mesh containing the geometry and material
const mesh = new THREE.Mesh( geometry, material );

// add the mesh to the scene
scene.add( mesh );

The geometry of an object defines its shape. If we create a box-shaped geometry (like we are doing here), our mesh will be shaped like a box. If we create a sphere shaped geometry, our mesh will be shaped like a sphere. If we create a cat-shaped geometry, our mesh will be shaped like a cat, and so on.

We’re creating our cube using a kind of geometry called a BoxBufferGeometry. The three numbers that we have passed in as parameters define the width, height, and depth of the box, respectively.

Most objects in three.js have built-in defaults, so even though the docs say that BoxBufferGeometry should take 6 parameters, we can get away with ignoring most or all of them and three.js will fill in the blanks with default values.

In this case, if we were to not pass in any parameters: const geometry = new THREE.BoxBufferGeometry() - we would get default box that is 1 x 1 x 1 - one unit high, one unit wide, and one unit deep. That’s a bit small for us now though, so we’re passing in bigger size parameters to create a 2 x 2 x 2 box.

Now that we’ve created a geometry… ahem, buffer geometry, that is… we’ve given our object a shape.

The next thing we need to do is describe how it looks, and for that, we need a material.

4.2 Create a Material

Add the following line to create the material

// create a geometry
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

// create a default (white) Basic material
const material = new THREE.MeshBasicMaterial();

// create a Mesh containing the geometry and material
const mesh = new THREE.Mesh( geometry, material );

// add the mesh to the scene
scene.add( mesh );

Materials define the surface properties of objects - that is, which material that the object looks like it is made from. Where the geometry tells us that the mesh is a box, or a car, or a cat, the material tells us that it’s a metal box, or a stone car, or a red-painted cat.

There are quite a few materials in three.js. To start with, we’ll create a MeshBasicMaterial, which is the simplest (and fastest) material available in three.js. It completely ignores any lights in the scene and just shades a mesh based on its color or any texture maps (which we’ll explain in Chapter 1.4). This is useful since we have not yet added any lights.

Remember what I said about three.js assigning sensible default values if you don’t provide them yourself? Well, we haven’t passed in any parameters to the material so everything is set to default, which in this case means that the material will be white. We’ll see how to change the material’s color in the next chapter.

4.3 Create a Mesh

A mesh consists of a geometry and a material

Create a mesh, passing in the geometry and material as parameters

// create a geometry
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

// create a default (white) Basic material
const material = new THREE.MeshBasicMaterial();

// create a Mesh containing the geometry and material
const mesh = new THREE.Mesh( geometry, material );

// add the mesh to the scene
scene.add( mesh );

Next, we need to create a mesh to hold our geometry and material.

The Mesh object is one of the most important and basic objects in three.js and you will be using it a lot. Thankfully, just like the Scene object, it’s very simple and its main function is to hold a geometry and a material and tell us where in the scene they are located.

4.4 Add the Mesh to the Scene

Use the scene’s add() method to attach the Mesh to the scene graph

// create a geometry
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

// create a default (white) Basic material
const material = new THREE.MeshBasicMaterial();

// create a Mesh containing the geometry and material
const mesh = new THREE.Mesh( geometry, material );

// add the mesh to the scene
scene.add( mesh );

We’ll add the mesh to the scene using the scene.add( mesh ) method. If we want to remove it later, we can use scene.remove( mesh ) method.

Once we’ve added the mesh to the scene, the mesh is called a child of the scene, and the scene is called the parent of the mesh.

We can access the geometry and material at any time using mesh.geometry and mesh.material.

5. The Renderer

  1. Initial Setup
  2. The Scene
  3. The Camera
  4. Visible Objects
  5. The Renderer
  6. Render The Scene
The rendered scene outputs to a canvas element

The next step is to create the renderer, which is a machine that takes your scene and camera as inputs and outputs pretty pictures onto your <canvas>.

Although a number of renderers are in available in three.js, we’ll be concentrating on the WebGLRenderer throughout this book since it’s the most full-featured and powerful.

5.1 Create the Renderer

Create a renderer with default settings

scene.add( mesh );

// create the renderer
const renderer = new THREE.WebGLRenderer();

renderer.setSize( container.clientWidth, container.clientHeight );
renderer.setPixelRatio( window.devicePixelRatio );

// add the automatically created <canvas> element to the page
container.appendChild( renderer.domElement );

We’re creating a WebGLRenderer with default settings for now since we’re not passing in any parameters to the constructor. As usual, three.js will take care of setting up sensible defaults for any of the parameters that we do not specify.

5.2 Set the Renderer’s Width and Height

We previously created a container DIV to hold our canvas

<div id="container">
  <!-- This div will hold our scene-->
</div>

..and we set the width and height of the container in CSS, so that it fills the full window.

#container {
  position: absolute;
  width: 100%;
  height: 100%;
}

Once we’ve done that, we can tell the renderer to set the canvas to the same size as the container

// create the renderer
const renderer = new THREE.WebGLRenderer();

renderer.setSize( container.clientWidth, container.clientHeight );
renderer.setPixelRatio( window.devicePixelRatio );

// add the automatically created <canvas> element to the page
container.appendChild( renderer.domElement );

The renderer has automatically created a `<canvas>` element to draw the scene onto. We’ll add this to our page in a moment, but first, let’s make sure that everything is set up the way that we need it.

The automatically created <canvas> has a default size, although this time it’s set by the browser rather than three.js. Currently on Chrome that default size is 150 x 300 pixels, which is rather small, so let’s tell the renderer the size that we do want.

The approach we’re taking to set up the canvas here is:

The renderer.setSize() method takes care of the final step for us. We just need to pass in the correct width and height (in pixels). container.clientWidth and container.clientHeight give us these values.

5.3 Set The Renderer’s Pixel Ratio

We need to set the correct pixel ratio for the device our app is running on

// create the renderer
const renderer = new THREE.WebGLRenderer();

renderer.setSize( container.clientWidth, container.clientHeight );
renderer.setPixelRatio( window.devicePixelRatio );

// add the automatically created <canvas> element to the page
container.appendChild( renderer.domElement );

We also need to tell our renderer what the pixel ratio of our device is. This specifies the ratio between the physical pixels in your device’s screen and the CSS pixels. Generally, the ratio is one - that is, one CSS pixel is equal to one pixel in the screen. However, for some devices, notably Apple Retina displays, the ratio may be two - that is, one CSS pixel equals two pixels in the screen, allowing for sharper images.

In any case, it’s not something that you need to think about too much for now. Just remember to add this line.

5.4 Add the Canvas Element to Our Page

We’ll append the canvas element as a child of the container

// create the renderer
const renderer = new THREE.WebGLRenderer();

renderer.setSize( container.clientWidth, container.clientHeight );
renderer.setPixelRatio( window.devicePixelRatio );

// add the automatically created <canvas> element to the page
container.appendChild( renderer.domElement );

Once you’ve appended the canvas to the container, if you look at the HTML in the browser console you should see this, assuming a browser window size of 1080 x 600 pixels

<div id="container">
  <!-- This div will hold our scene-->
  <canvas width="1080" height="600" style="width: 1080px; height: 600px;"></canvas>
</div>

Now we’re finally ready to add the <canvas> to our page.

As we mentioned above, it was created automatically when we created our renderer, and it’s stored in renderer.domElement, where DOM stands for Document Object Model. We’ll add it as a child of the container element using container.appendChild(), a built-in browser method.

Once you added the canvas, take a look at the structure of your HTML in the browser console. You can right click on an HTML element and then click “Select Element” to jump straight to it in most browsers.

Once you’ve done that, you should see that the renderer has indeed appended the <canvas> as a child of the container.

Everything is now set up and in place. There remains just one last thing to do, and that is:

6. Render the Scene

  1. Initial Setup
  2. The Scene
  3. The Camera
  4. Visible Objects
  5. The Renderer
  6. Render the Scene
A rendered scene

Add the following and final line to your code to render the scene

container.appendChild( renderer.domElement );

// render, or 'create a still image', of the scene
renderer.render( scene, camera );

With this single line, we’re telling the renderer to take a still picture of the scene using the camera and output that picture into the <canvas> element.

You will now see your white cube being rendered against a blue background. It’s a bit hard to see that it’s actually a cube so far since we’re looking at it head-on. But we’ll improve that a lot in the next chapter.

Well done! You’ve completed the first chapter and taken your first giant leap in your career as a three.js developer.