Your First ThreeJs Scene: Hello, Cube!
YOUR FIRST three.js SCENE: HELLO, CUBE!
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.
// Get a reference to the container element that will hold our scene
const container = document.querySelector( '#scene-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) now, and we’ll get started.
The Components of a Real-Time 3D App
Every three.js app (in fact, nearly every real-time 3D app) will have the following basic components:
- a
Scene
- this is a holder for everything else. You can think of it as a “tiny universe” in which all your 3D objects live. - a
Camera
- this is directly equivalent to the concept of a camera in the real world, and you’ll use this to view your scene. - a
<canvas>
- this is an HTML canvas element, and just like a canvas in the real world, it starts out as a blank rectangle, just waiting to hold your beautiful creations! - a
Renderer
- this is a machine that takes aCamera
and aScene
as input and outputs beautiful drawings (or renderings) onto your<canvas>
.
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.- a
Mesh
- this is just a holder for aGeometry
and aMaterial
and defines the position in 3D space. We can add this to ourScene
. - a
Geometry
- this defines the shape of ourMesh
. - a
Material
- and this defines the way that the surface of theMesh
looks.
Our First three.js App
Now we are finally ready to write some code! We’ll divide our app up into 6 steps:
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
Inside index.html we’ve created a container to hold our scene
<div id="scene-container">
<!-- This div will hold our scene-->
</div>
… and we’ll use querySelector to get a reference to that container in JavaScript
// Get a reference to the container element that will hold our scene
const container = document.querySelector( '#scene-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
2.1 Create the Scene
Create a scene
const container = document.querySelector( '#scene-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( '#scene-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
- Initial Setup
- The Scene
- The Camera
- 3.1 Create The Camera
- 3.2 Position The Camera
- Visible Objects
- The Renderer
- Render The Scene
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 usesorthographic 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.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
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.
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
.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
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
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:
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 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.
+X
points to the right of the screen-X
points to the left of the screen+Y
points to the top of the screen-Y
points to the bottom of the screen+Z
points out of the screen (towards you)-Z
points into the screen (away from you)
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
- Initial Setup
- The Scene
- The Camera
- Visible Objects
- 4.1 Create A Geometry
- 4.2 Create A Material
- 4.3 Create A Mesh
- 4.4 Add The Mesh To The Scene
- The Renderer
- Render The Scene
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 type 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 Ch 1.4: A Brief Introduction to Texture Mapping). 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
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
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 the <canvas>
element in your HTML page.
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 WebGLRenderer 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="scene-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<body>
element.
#scene-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:
- create an HTML
<div class="container">
element to hold the<canvas>
- set the size of the container using CSS (in this case, we’ve set it to the full window)
- get a reference to this element in JavaScript as the variable
container
- use the size of the
container
to set the size of the<canvas>
element
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 mentioned back in Ch 0.10: Functions Built-In to Your Browser: the Web APIthat mobile devices draw your web page onto a virtual viewport, which may not have the same resolution as the device’s physical screen. Then, once the page has been drawn, it gets scaled down to fit onto the device’s physical screen. The pixels on this virtual viewport are referred to as CSS pixels, while the real pixels on the actual screen are referred to as physical pixels.
It’s common for the virtual viewport to be much larger than the real viewport - common ratios are 2x, 3x, 4x or even 5x! This allows for much sharper images to be rendered, which is important for viewing text on small screens.
That’s great, but when it comes to rendering WebGL, if we forget to take this into account then our scenes may look great on our development laptops but will look blurry and low-resolution on mobile devices. So, we need to tell the renderer what the pixel ratio is, which is as simple as adding the above 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 );
Now we’re finally ready to add the
<canvas>
to our page.
As we mentioned above, when we created our
WebGLRenderer
, it automatically created an HTML <canvas>
element for us. This element only exists in memory at first and is stored in renderer.domElement
. To be able to view our scene, we’ll need to add this <canvas>
to our webpage, inside the #scene-container
div.
We’ll use a built-in browser method called
appendChild
to achieve this. Refer back to Ch 0.10: THE WEB API if this method is unfamiliar to you.
Once we’ve appended the
canvas
as a child of the scene container, if you take a look at the structure of the HTML in the browser console, you should see that the renderer has indeed appended the <canvas>
as a child of the container. It has also set the width
and height
CSS properties, and the width
and height
attributes on the canvas for us.
Assuming a browser window size of 800x600, it should look like this:
<div id="scene-container">
<!-- This div will hold our scene-->
<canvas width="1080" height="600" style="width: 1080px; height: 600px;"></canvas>
</div>
Everything is now set up and in place. There remains just one last thing to do, and that is:
6. Render the 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.
.
Comments
Post a Comment