Fundamentals

Summary: An introductory article covering the basics of three.js, including the structure of a three.js application, building a 'Hello Cube' scene with renderer, camera, geometry, material, and mesh, adding animation and lighting, and setting up ES6 modules with import maps.

Introduction to Three.js

This is the first article in a series of articles about three.js. Three.js is a 3D library that tries to make it as easy as possible to get 3D content on a webpage.

Three.js is often confused with WebGL since more often than not, but not always, three.js uses WebGL to draw 3D. WebGL is a very low-level system that only draws points, lines, and triangles. To do anything useful with WebGL generally requires quite a bit of code and that is where three.js comes in. It handles stuff like scenes, lights, shadows, materials, textures, 3d math, all things that you'd have to write yourself if you were to use WebGL directly.

These tutorials assume you already know JavaScript and, for the most part they will use ES6 style. Most browsers that support three.js are auto-updated so most users should be able to run this code. If you'd like to make this code run on really old browsers look into a transpiler like Babel. Of course users running really old browsers probably have machines that can't run three.js.

When learning most programming languages the first thing people do is make the computer print "Hello World!". For 3D one of the most common first things to do is to make a 3D cube. So let's start with "Hello Cube!"

Structure of a Three.js App

Before we get started let's try to give you an idea of the structure of a three.js app. A three.js app requires you to create a bunch of objects and connect them together.

Things to notice about the diagram:

- There is a Renderer. This is arguably the main object of three.js. You pass a Scene and a Camera to a Renderer and it renders (draws) the portion of the 3D scene that is inside the frustum of the camera as a 2D image to a canvas.

- There is a scenegraph which is a tree like structure, consisting of various objects like a Scene object, multiple Mesh objects, Light objects, Group, Object3D, and Camera objects. A Scene object defines the root of the scenegraph and contains properties like the background color and fog. These objects define a hierarchical parent/child tree like structure and represent where objects appear and how they are oriented. Children are positioned and oriented relative to their parent. For example the wheels on a car might be children of the car so that moving and orienting the car's object automatically moves the wheels.

Note in the diagram Camera is half in half out of the scenegraph. This is to represent that in three.js, unlike the other objects, a Camera does not have to be in the scenegraph to function. Just like other objects, a Camera, as a child of some other object, will move and orient relative to its parent object.

- Mesh objects represent drawing a specific Geometry with a specific Material. Both Material objects and Geometry objects can be used by multiple Mesh objects. For example to draw two blue cubes in different locations we could need two Mesh objects to represent the position and orientation of each cube. We would only need one Geometry to hold the vertex data for a cube and we would only need one Material to specify the color blue. Both Mesh objects could reference the same Geometry object and the same Material object.

- Geometry objects represent the vertex data of some piece of geometry like a sphere, cube, plane, dog, cat, human, tree, building, etc... Three.js provides many kinds of built in geometry primitives. You can also create custom geometry as well as load geometry from files.

- Material objects represent the surface properties used to draw geometry including things like the color to use and how shiny it is. A Material can also reference one or more Texture objects which can be used, for example, to wrap an image onto the surface of a geometry.

- Texture objects generally represent images either loaded from image files, generated from a canvas, or rendered from another scene.

- Light objects represent different kinds of lights.

Hello Cube Setup

Given all of that we're going to make the smallest "Hello Cube" setup.

First let's load three.js. It's important you put `type="module"` in the script tag. This enables us to use the `import` keyword to load three.js. As of r147, this is the only way to load three.js properly. Modules have the advantage that they can easily import other modules they need. That saves us from having to manually load extra scripts they are dependent on.

Next we need is a `<canvas>` tag. We will ask three.js to draw into that canvas so we need to look it up.

After we look up the canvas we create a WebGLRenderer. The renderer is the thing responsible for actually taking all the data you provide and rendering it to the canvas.

Note there are some esoteric details here. If you don't pass a canvas into three.js it will create one for you but then you have to add it to your document. Where to add it may change depending on your use case and you'll have to change your code so I find that passing a canvas to three.js feels a little more flexible. I can put the canvas anywhere and the code will find it whereas if I had code to insert the canvas into to the document I'd likely have to change that code if my use case changed.

<script type="module">
import * as THREE from 'three';
</script>

<body>
  <canvas id="c"></canvas>
</body>

<script type="module">
import * as THREE from 'three';

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
  ...
</script>

Camera Setup

Next up we need a camera. We'll create a PerspectiveCamera.

`fov` is short for `field of view`. In this case 75 degrees in the vertical dimension. Note that most angles in three.js are in radians but for some reason the perspective camera takes degrees.

`aspect` is the display aspect of the canvas. By default a canvas is 300x150 pixels which makes the aspect 300/150 or 2.

`near` and `far` represent the space in front of the camera that will be rendered. Anything before that range or after that range will be clipped (not drawn).

Those four settings define a "frustum". A frustum is the name of a 3d shape that is like a pyramid with the tip sliced off. In other words think of the word "frustum" as another 3D shape like sphere, cube, prism, frustum.

The height of the near and far planes are determined by the field of view. The width of both planes is determined by the field of view and the aspect.

Anything inside the defined frustum will be drawn. Anything outside will not.

The camera defaults to looking down the -Z axis with +Y up. We'll put our cube at the origin so we need to move the camera back a little from the origin in order to see anything.

const fov = 75;
const aspect = 2;  // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

Scene, Geometry, Material, and Mesh

Next we make a Scene. A Scene in three.js is the root of a form of scene graph. Anything you want three.js to draw needs to be added to the scene.

Next up we create a BoxGeometry which contains the data for a box. Almost anything we want to display in Three.js needs geometry which defines the vertices that make up our 3D object.

We then create a basic material and set its color. Colors can be specified using standard CSS style 6 digit hex color values.

We then create a Mesh. A Mesh in three.js represents the combination of three things:

- A Geometry (the shape of the object)
- A Material (how to draw the object, shiny or flat, what color, what texture(s) to apply. Etc.)
- The position, orientation, and scale of that object in the scene relative to its parent. In the code below that parent is the scene.

And finally we add that mesh to the scene. We can then render the scene by calling the renderer's render function and passing it the scene and the camera.

It's kind of hard to tell that is a 3D cube since we're viewing it directly down the -Z axis and the cube itself is axis aligned so we're only seeing a single face.

const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);

const material = new THREE.MeshBasicMaterial({color: 0x44aa88});

Animating with requestAnimationFrame

Let's animate it spinning and hopefully that will make it clear it's being drawn in 3D. To animate it we'll render inside a render loop using requestAnimationFrame.

`requestAnimationFrame` is a request to the browser that you want to animate something. You pass it a function to be called. In our case that function is `render`. The browser will call your function and if you update anything related to the display of the page the browser will re-render the page. In our case we are calling three's `renderer.render` function which will draw our scene.

`requestAnimationFrame` passes the time since the page loaded to our function. That time is passed in milliseconds. I find it's much easier to work with seconds so here we're converting that to seconds.

We then set the cube's X and Y rotation to the current time. These rotations are in radians. There are 2 pi radians in a circle so our cube should turn around once on each axis in about 6.28 seconds.

We then render the scene and request another animation frame to continue our loop. Outside the loop we call `requestAnimationFrame` one time to start the loop.

It's a little better but it's still hard to see the 3d.

function render(time) {
  time *= 0.001;  // convert time to seconds

  cube.rotation.x = time;
  cube.rotation.y = time;

  renderer.render(scene, camera);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

Adding Lighting

What would help is to add some lighting so let's add a light. There are many kinds of lights in three.js which we'll go over in a future article. For now let's create a directional light.

Directional lights have a position and a target. Both default to 0, 0, 0. In our case we're setting the light's position to -1, 2, 4 so it's slightly on the left, above, and behind our camera. The target is still 0, 0, 0 so it will shine toward the origin.

We also need to change the material. The MeshBasicMaterial is not affected by lights. Let's change it to a MeshPhongMaterial which is affected by lights.

const color = 0xFFFFFF;
const intensity = 3;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);

const material = new THREE.MeshPhongMaterial({color: 0x44aa88});  // greenish blue

Multiple Cubes

Just for the fun of it let's add 2 more cubes.

We'll use the same geometry for each cube but make a different material so each cube can be a different color.

First we'll make a function that creates a new material with the specified color. Then it creates a mesh using the specified geometry and adds it to the scene and sets its X position.

Then we'll call it 3 times with 3 different colors and X positions saving the Mesh instances in an array.

Finally we'll spin all 3 cubes in our render function. We compute a slightly different rotation for each one.

If you compare it to the top down diagram above you can see it matches our expectations. With cubes at X = -2 and X = +2 they are partially outside our frustum. They are also somewhat exaggeratedly warped since the field of view across the canvas is so extreme.

As you can see we have 3 Mesh objects each referencing the same BoxGeometry. Each Mesh references a unique MeshPhongMaterial so that each cube can have a different color.

I hope this short intro helps to get things started. Next up we'll cover making our code responsive so it is adaptable to multiple situations.

function makeInstance(geometry, color, x) {
  const material = new THREE.MeshPhongMaterial({color});

  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  cube.position.x = x;

  return cube;
}

const cubes = [
  makeInstance(geometry, 0x44aa88,  0),
  makeInstance(geometry, 0x8844aa, -2),
  makeInstance(geometry, 0xaa8844,  2),
];

function render(time) {
  time *= 0.001;  // convert time to seconds

  cubes.forEach((cube, ndx) => {
    const speed = 1 + ndx * .1;
    const rot = time * speed;
    cube.rotation.x = rot;
    cube.rotation.y = rot;
  });

  ...

ES6 Modules, Three.js, and Folder Structure

As of version r147 the preferred way to use three.js is via es6 modules and import maps.

es6 modules can be loaded via the `import` keyword in a script or inline via a `<script type="module">` tag. Notice `'three'` specifier there. If you leave it as it is, it will likely produce an error. An import map should be used to tell the browser where to find three.js. Note that path specifier can start only with `./` or `../`.

To import addons like OrbitControls.js use the following. Don't forget to add addons to the import map. You can also use a CDN.

To conclude, the recommended way of using three.js is to set up an import map pointing to the three.js module file and addons, then use `<script type="module">` with import statements to load three.js and its addons.

<script type="module">
import * as THREE from 'three';

...
</script>

<script type="importmap">
{
  "imports": {
    "three": "./path/to/three.module.js"
  }
}
</script>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@<version>/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@<version>/examples/jsm/"
  }
}
</script>

<script type="importmap">
{
  "imports": {
    "three": "./path/to/three.module.js",
    "three/addons/": "./different/path/to/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

...
</script>


Responsive Design

Summary: This article explains how to make three.js applications responsive to different screen sizes and display situations. It covers CSS-based sizing, handling aspect ratios, managing canvas resolution, and dealing with HD-DPI displays.

Responsive Design

This is the second article in a series of articles about three.js. The first article was about fundamentals. If you haven't read that yet you might want to start there.

This article is about how to make your three.js app be responsive to any situation. Making a webpage responsive generally refers to the page displaying well on different sized displays from desktops to tablets to phones.

For three.js there are even more situations to consider. For example, a 3D editor with controls on the left, right, top, or bottom is something we might want to handle. A live diagram in the middle of a document is another example.

The last sample we had used a plain canvas with no CSS and no size

Making the Canvas Fill the Page

That canvas defaults to 300x150 CSS pixels in size.

In the web platform the recommended way to set the size of something is to use CSS.

Let's make the canvas fill the page by adding CSS

<style>
html, body {
   margin: 0;
   height: 100%;
}
#c {
   width: 100%;
   height: 100%;
   display: block;
}
</style>

Understanding the CSS Setup

In HTML the body has a margin of 5 pixels by default so setting the margin to 0 removes the margin. Setting the html and body height to 100% makes them fill the window. Otherwise they are only as large as the content that fills them.

Next we tell the `id=c` element to be 100% the size of its container which in this case is the body of the document.

Finally we set its `display` mode to `block`. A canvas's default display mode is `inline`. Inline elements can end up adding whitespace to what is displayed. By setting the canvas to `block` that issue goes away.

You can see the canvas is now filling the page but there are 2 problems. One our cubes are stretched. They are not cubes they are more like boxes. Too tall or too wide. Open the example in its own window and resize it. You'll see how the cubes get stretched wide and tall.

The second problem is they look low resolution or blocky and blurry. Stretch the window really large and you'll really see the issue.

Fixing the Stretchy Problem

Let's fix the stretchy problem first. To do that we need to set the aspect of the camera to the aspect of the canvas's display size. We can do that by looking at the canvas's `clientWidth` and `clientHeight` properties.

We'll update our render loop like this

function render(time) {
  time *= 0.001;

+  const canvas = renderer.domElement;
+  camera.aspect = canvas.clientWidth / canvas.clientHeight;
+  camera.updateProjectionMatrix();

  ...

Verifying the Fix

Now the cubes should stop being distorted.

Open the example in a separate window and resize the window and you should see the cubes are no longer stretched tall or wide. They stay the correct aspect regardless of window size.

Fixing the Blockiness

Canvas elements have 2 sizes. One size is the size the canvas is displayed on the page. That's what we set with CSS. The other size is the number of pixels in the canvas itself. This is no different than an image. For example we might have a 128x64 pixel image and using CSS we might display as 400x200 pixels.

<img src="some128x64image.jpg" style="width:400px; height:200px">

Understanding Drawing Buffer Size

A canvas's internal size, its resolution, is often called its drawingbuffer size. In three.js we can set the canvas's drawingbuffer size by calling `renderer.setSize`. What size should we pick? The most obvious answer is "the same size the canvas is displayed". Again, to do that we can look at the canvas's `clientWidth` and `clientHeight` properties.

Let's write a function that checks if the renderer's canvas is not already the size it is being displayed as and if so set its size.

Creating the resizeRendererToDisplaySize Function

Notice we check if the canvas actually needs to be resized. Resizing the canvas is an interesting part of the canvas spec and it's best not to set the same size if it's already the size we want.

Once we know if we need to resize or not we then call `renderer.setSize` and pass in the new width and height. It's important to pass `false` at the end. `renderer.setSize` by default sets the canvas's CSS size but doing so is not what we want. We want the browser to continue to work how it does for all other elements which is to use CSS to determine the display size of the element. We don't want canvases used by three to be different than other elements.

Note that our function returns true if the canvas was resized. We can use this to check if there are other things we should update. Let's modify our render loop to use the new function

function resizeRendererToDisplaySize(renderer) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    renderer.setSize(width, height, false);
  }
  return needResize;
}

Updating the Render Loop

Since the aspect is only going to change if the canvas's display size changed we only set the camera's aspect if `resizeRendererToDisplaySize` returns `true`.

function render(time) {
  time *= 0.001;

+  if (resizeRendererToDisplaySize(renderer)) {
+    const canvas = renderer.domElement;
+    camera.aspect = canvas.clientWidth / canvas.clientHeight;
+    camera.updateProjectionMatrix();
+  }

  ...

Final Result

It should now render with a resolution that matches the display size of the canvas.

To make the point about letting CSS handle the resizing let's take our code and put it in a separate `.js` file. Here then are a few more examples where we let CSS choose the size and notice we had to change zero code for them to work.

Let's put our cubes in the middle of a paragraph of text.

and here's our same code used in an editor style layout where the control area on the right can be resized.

The important part to notice is no code changed. Only our HTML and CSS changed.

Handling HD-DPI displays

HD-DPI stands for high-density dot per inch displays. That's most Macs nowadays and many Windows machines as well as pretty much all smartphones.

The way this works in the browser is they use CSS pixels to set the sizes which are supposed to be the same regardless of how high res the display is. The browser will just render text with more detail but the same physical size.

There are various ways to handle HD-DPI with three.js.

The first one is just not to do anything special. This is arguably the most common. Rendering 3D graphics takes a lot of GPU processing power. Mobile GPUs have less power than desktops, at least as of 2018, and yet mobile phones often have very high resolution displays. The current top of the line phones have an HD-DPI ratio of 3x meaning for every one pixel from a non-HD-DPI display those phones have 9 pixels. That means they have to do 9x the rendering.

Computing 9x the pixels is a lot of work so if we just leave the code as it is we'll compute 1x the pixels and the browser will just draw it at 3x the size (3x by 3x = 9x pixels).

For any heavy three.js app that's probably what you want otherwise you're likely to get a slow framerate.

That said if you actually do want to render at the resolution of the device there are a couple of ways to do this in three.js.

One is to tell three.js a resolution multiplier using `renderer.setPixelRatio`. You ask the browser what the multiplier is from CSS pixels to device pixels and pass that to three.js

Why setPixelRatio is Not Recommended

After that any calls to `renderer.setSize` will magically use the size you request multiplied by whatever pixel ratio you passed in.  This is strongly NOT RECOMMENDED . See below

The other way is to do it yourself when you resize the canvas.

function resizeRendererToDisplaySize(renderer) {
      const canvas = renderer.domElement;
      const pixelRatio = window.devicePixelRatio;
      const width  = Math.floor( canvas.clientWidth  * pixelRatio );
      const height = Math.floor( canvas.clientHeight * pixelRatio );
      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        renderer.setSize(width, height, false);
      }
      return needResize;
    }

Why Manual Pixel Ratio is Better

This second way is objectively better. Why? Because it means I get what I ask for. There are many cases when using three.js where we need to know the actual size of the canvas's drawingBuffer. For example when making a post processing filter, or if we are making a shader that accesses `gl_FragCoord`, if we are making a screenshot, or reading pixels for GPU picking, for drawing into a 2D canvas, etc... There are many cases where if we use `setPixelRatio` then our actual size will be different than the size we requested and we'll have to guess when to use the size we asked for and when to use the size three.js is actually using. By doing it ourselves we always know the size being used is the size we requested. There is no special case where magic is happening behind the scenes.

Here's an example using the code above.

It might be hard to see the difference but if you have an HD-DPI display and you compare this sample to those above you should notice the edges are more crisp.

HD-DPI: Limiting maximum drawing buffer size

When using a fractional UI scaling factor on some operating system (eg: 150% on OSX or Linux) the assumption that the real physical resolution equals `width * window.devicePixelRatio` and `height * window.devicePixelRatio` no longer holds. This may lead to excessive GPU load, lower frame rates and high power consumption.

A possible mitigation is to cap the maximum internal resolution (e.g. limit width × height) so the buffer remains within safe bounds.

function resizeRendererToDisplaySize(renderer, maxPixelCount=3840*2160) {
      const canvas = renderer.domElement;
      const pixelRatio = window.devicePixelRatio;
      let width  = Math.floor( canvas.clientWidth  * pixelRatio );
      let height = Math.floor( canvas.clientHeight * pixelRatio );
      const pixelCount = width * height;
      const renderScale = pixelCount > maxPixelCount ? Math.sqrt(maxPixelCount / pixelCount) : 1;
      width = Math.floor(width * renderScale);
      height = Math.floor(height * renderScale);

      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        renderer.setSize(width, height, false);
      }
      return needResize;
    }

Conclusion

This article covered a very basic but fundamental topic. Next up lets quickly go over the basic primitives that three.js provides.


Prerequisites

Summary: This page outlines the JavaScript and web development knowledge assumed by the three.js articles, including ES6 modules, CSS selectors, closures, ES5/ES6/ES7 features, and recommended tooling like Visual Studio Code with ESLint.

Prerequisites

These articles are meant to help you learn how to use three.js. They assume you know how to program in JavaScript. They assume you know what the DOM is, how to write HTML as well as create DOM elements in JavaScript. They assume you know how to use es6 modules via import and via `<script type="module">` tags. They assume you know how to use import maps. They assume you know some CSS and that you know what CSS selectors are. They also assume you know ES5, ES6 and maybe some ES7. They assume you know that the browser runs JavaScript only via events and callbacks. They assume you know what a closure is.

Here's some brief refreshers and notes

es6 modules

es6 modules can be loaded via the `import` keyword in a script or inline via a `<script type="module">` tag. Here's an example

See more details at the bottom of this article.

<script type="importmap">
{
  "imports": {
    "three": "./path/to/three.module.js",
    "three/addons/": "./different/path/to/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

...

</script>

`document.querySelector` and `document.querySelectorAll`

You can use `document.querySelector` to select the first element that matches a CSS selector. `document.querySelectorAll` returns all elements that match a CSS selector.

You don't need `onload`

Lots of 20yr old pages use HTML like

That style is deprecated. Put your scripts at the bottom of the page.

or use the `defer` property.

<html>
  <head>
    ...
  </head>
  <body>
     ...
  </body>
  <script>
    // inline javascript
  </script>
</html>

Know how closures work

In the code above the function `a` creates a new function every time it's called. That function *closes* over the variable `foo`. Here's more info.

function a(v) {
  const foo = v;
  return function() {
     return foo;
  };
}

const f = a(123);
const g = a(456);
console.log(f());  // prints 123
console.log(g());  // prints 456

Understand how `this` works

`this` is not magic. It's effectively a variable that is automatically passed to functions just like an argument is passed to function. The simple explanation is when you call a function directly like

`this` will be `null` (when in strict mode or in a module) where as when you call a function via the dot operator `.` like this

`this` will be set to `someobject`.

The parts where people get confused is with callbacks.

doesn't work as someone inexperienced might expect because when `loader.load` calls the callback it's not calling it with the dot `.` operator so by default `this` will be null (unless the loader explicitly sets it to something). If you want `this` to be `someobject` when the callback happens you need to tell JavaScript that by binding it to the function.

*this* article might help explain `this`.

const callback = someobject.somefunction;
 loader.load(callback);

const callback = someobject.somefunction.bind(someobject);
 loader.load(callback);

ES5/ES6/ES7 stuff

`var` is deprecated. Use `const` and/or `let`

There is no reason to use `var` **EVER** and at this point it's considered bad practice to use it at all. Use `const` if the variable will never be reassigned which is most of the time. Use `let` in those cases where the value changes. This will help avoid tons of bugs.

Use `for(elem of collection)` never `for(elem in collection)`

`for of` is new, `for in` is old. `for in` had issues that are solved by `for of`

As one example you can iterate over all the key/value pairs of an object with

for (const [key, value] of Object.entries(someObject)) {
  console.log(key, value);
}

Use `forEach`, `map`, and `filter` where useful

Arrays added the functions `forEach`, `map`, and `filter` and are used fairly extensively in modern JavaScript.

Use destructuring

Assume an object `const dims = {width: 300, height: 150}`

old code

new code

Destructuring works with arrays too. Assume an array `const position = [5, 6, 7, 1]`;

old code

new code

Destructuring also works in function arguments

const width = dims.width;
const height = dims.height;

const y = position[1];
const z = position[2];

const dims = {width: 300, height: 150};
const vector = [3, 4];

function lengthOfVector([x, y]) {
  return Math.sqrt(x * x + y * y);
}

const dist = lengthOfVector(vector);  // dist = 5

function area({width, height}) {
  return width * height;
}
const a = area(dims);  // a = 45000

Use object declaration short cuts

old code

new code

const width = 300;
 const height = 150;
 const obj = {
   width: width,
   height: height,
   area: function() {
     return this.width * this.height
   },
 };

const width = 300;
 const height = 150;
 const obj = {
   width,
   height,
   area() {
     return this.width * this.height;
   },
 };

Use the rest parameter and the spread operator `...`

The rest parameter can be used to consume any number of parameters. Example

The spread operator can be used to expand an iterable into arguments

or copy an array

or to merge objects

function log(className, ...args) {
   const elem = document.createElement('div');
   elem.className = className;
   elem.textContent = args.join(' ');
   document.body.appendChild(elem);
 }

const position = [1, 2, 3];
someMesh.position.set(...position);

const copiedPositionArray = [...position];
copiedPositionArray.push(4); // [1,2,3,4]
console.log(position); // [1,2,3] position is unaffected

Use `class`

The syntax for making class like objects pre ES5 was unfamiliar to most programmers. As of ES5 you can now use the `class` keyword which is closer to the style of C++/C#/Java.

Understand getters and setters

Getters and setters are common in most modern languages. The `class` syntax of ES5 makes them much easier than pre ES5.

Use arrow functions where appropriate

This is especially useful with callbacks and promises.

Arrow functions bind `this` to the context in which you create the arrow function.

is a shortcut for

See link above for more info on `this`.

loader.load((texture) => {
  // use texture
});

const foo = (function(args) {/* code */}).bind(this));

Promises as well as async/await

Promises help with asynchronous code. Async/await help use promises.

It's too big a topic to go into here but you can read up on promises here and async/await here.

Use Template Literals

Template literals are strings using backticks instead of quotes.

Template literals have basically 2 features. One is they can be multi-line

`foo` and `bar` above are the same.

The other is that you can pop out of string mode and insert snippets of JavaScript using `${javascript-expression}`. This is the template part. Example:

or

or

const foo = `this
is
a
template
literal`;
const bar = "this\nis\na\ntemplate\nliteral";

const r = 192;
const g = 255;
const b = 64;
const rgbCSSColor = `rgb(${r},${g},${b})`;

const color = [192, 255, 64];
const rgbCSSColor = `rgb(${color.join(',')})`;

const aWidth = 10;
const bWidth = 20;
someElement.style.width = `${aWidth + bWidth}px`;

Learn JavaScript coding conventions.

While you're welcome to format your code any way you chose there is at least one convention you should be aware of. Variables, function names, method names, in JavaScript are all lowerCasedCamelCase. Constructors, the names of classes are CapitalizedCamelCase. If you follow this rule your code will match most other JavaScript. Many linters, programs that check for obvious errors in your code, will point out errors if you use the wrong case since by following the convention above they can know when you're using something incorrectly.

const v = new vector(); // clearly an error if all classes start with a capital letter
const v = Vector();     // clearly an error if all functions start with a lowercase letter.

Consider using Visual Studio Code

Of course use whatever editor you want but if you haven't tried it consider using Visual Studio Code for JavaScript and after installing it setup eslint. It might take a few minutes to setup but it will help you immensely with finding bugs in your JavaScript.

Some examples

If you enable the `no-undef` rule then VSCode via ESLint will warn you of many undefined variables.

Above you can see I mis-spelled `doTheThing` as `doThing`. There's a red squiggle under `doThing` and hovering over it it tells me it's undefined. One error avoided.

If you're using `<script>` tags to include three.js you'll get warnings using `THREE` so add `/* global THREE */` at the top of your JavaScript files to tell eslint that `THREE` exists. (or better, use `import` 😉)

Above you can see eslint knows the rule that `UpperCaseNames` are constructors and so you should be using `new`. Another error caught and avoided. This is the `new-cap` rule.

There are 100s of rules you can turn on or off or customize. For example above I mentioned you should use `const` and `let` over `var`.

Here I used `var` and it warned me I should use `let` or `const`

Here I used `let` but it saw I never change the value so it suggested I use `const`.

Of course if you'd prefer to keep using `var` you can just turn off that rule. As I said above though I prefer to use `const` and `let` over `var` as they just work better and prevent bugs.

For those cases where you really need to override a rule you can add comments to disable them for a single line or a section of code.

If you really need to support legacy browsers use a transpiler

Most modern browsers are auto-updated so using all these features will help you be productive and avoid bugs. That said, if you're on a project that absolutely must support old browsers there are tools that will take your ES5/ES6/ES7 code and transpile the code back to pre ES5 Javascript.


Setup

Summary: A guide to setting up your computer as a development environment for three.js, including installing a local web server needed for WebGL development due to browser security restrictions.

Setup

This article is one in a series of articles about three.js. The first article was about three.js fundamentals. If you haven't read that yet you might want to start there.

Before we go any further we need to talk about setting up your computer as a development environment. In particular, for security reasons, WebGL cannot use images from your hard drive directly. That means in order to do development you need to use a web server. Fortunately development web servers are super easy to setup and use.

First off if you'd like you can download this entire site from this link. Once downloaded double click the zip file to unpack the files.

Next download one of these simple web servers.

Servez (GUI option)

If you'd prefer a web server with a user interface there's Servez.

Just point it at the folder where you unzipped the files, click "Start", then go to in your browser http://localhost:8080/ or if you'd like to browse the samples go to http://localhost:8080/threejs.

To stop serving click stop or quit Servez.

Node.js servez (command line option)

If you prefer the command line (I do), another way is to use node.js. Download it, install it, then open a command prompt / console / terminal window. If you're on Windows the installer will add a special "Node Command Prompt" so use that.

Then install the servez by typing

OSX installation

If you're on OSX use

Running servez

Once you've done that type

servez path/to/folder/where/you/unzipped/files

Or if you're like me

cd path/to/folder/where/you/unzipped/files
servez

It should print something like

Then in your browser go to http://localhost:8080/.

If you don't specify a path then servez will serve the current folder.

If either of those options are not to your liking there are many other simple servers to choose from.

Now that you have a server setup we can move on to textures.


Primitives

Summary: This article covers the built-in 3D primitive geometries available in three.js, including shapes like boxes, spheres, cones, and more advanced options like extrusion, text, and wireframes. It also walks through a complete example demonstrating how to instantiate these primitives with random colors in a scene.

Primitives

Three.js has a large number of primitives. Primitives are generally 3D shapes that are generated at runtime with a bunch of parameters.

It's common to use primitives for things like a sphere for a globe or a bunch of boxes to draw a 3D graph. It's especially common to use primitives to experiment and get started with 3D. For the majority of 3D apps it's more common to have an artist make 3D models in a 3D modeling program like Blender or Maya or Cinema 4D. Later in this series we'll cover making and loading data from several 3D modeling programs. For now let's go over some of the available primitives.

Many of the primitives below have defaults for some or all of their parameters so you can use more or less depending on your needs.

BoxGeometry

A Box

const width = 8;  // ui: width
const height = 8;  // ui: height
const depth = 8;  // ui: depth
const geometry = new THREE.BoxGeometry( width, height, depth );

const width = 8;  // ui: width
const height = 8;  // ui: height
const depth = 8;  // ui: depth
const widthSegments = 4;  // ui: widthSegments
const heightSegments = 4;  // ui: heightSegments
const depthSegments = 4;  // ui: depthSegments
const geometry = new THREE.BoxGeometry(
	width, height, depth,
	widthSegments, heightSegments, depthSegments );

CircleGeometry

A flat circle

const radius = 7;  // ui: radius
const segments = 24;  // ui: segments
const geometry = new THREE.CircleGeometry( radius, segments );

const radius = 7;  // ui: radius
const segments = 24;  // ui: segments
const thetaStart = Math.PI * 0.25;  // ui: thetaStart
const thetaLength = Math.PI * 1.5;  // ui: thetaLength
const geometry = new THREE.CircleGeometry(
	radius, segments, thetaStart, thetaLength );

ConeGeometry

A Cone

const radius = 6;  // ui: radius
const height = 8;  // ui: height
const radialSegments = 16;  // ui: radialSegments
const geometry = new THREE.ConeGeometry( radius, height, radialSegments );

const radius = 6;  // ui: radius
const height = 8;  // ui: height
const radialSegments = 16;  // ui: radialSegments
const heightSegments = 2;  // ui: heightSegments
const openEnded = true;  // ui: openEnded
const thetaStart = Math.PI * 0.25;  // ui: thetaStart
const thetaLength = Math.PI * 1.5;  // ui: thetaLength
const geometry = new THREE.ConeGeometry(
	radius, height,
	radialSegments, heightSegments,
	openEnded,
	thetaStart, thetaLength );

CylinderGeometry

A Cylinder

const radiusTop = 4;  // ui: radiusTop
const radiusBottom = 4;  // ui: radiusBottom
const height = 8;  // ui: height
const radialSegments = 12;  // ui: radialSegments
const geometry = new THREE.CylinderGeometry(
	radiusTop, radiusBottom, height, radialSegments );

const radiusTop = 4;  // ui: radiusTop
const radiusBottom = 4;  // ui: radiusBottom
const height = 8;  // ui: height
const radialSegments = 12;  // ui: radialSegments
const heightSegments = 2;  // ui: heightSegments
const openEnded = false;  // ui: openEnded
const thetaStart = Math.PI * 0.25;  // ui: thetaStart
const thetaLength = Math.PI * 1.5;  // ui: thetaLength
const geometry = new THREE.CylinderGeometry(
	radiusTop, radiusBottom, height,
	radialSegments, heightSegments,
	openEnded,
	thetaStart, thetaLength );

DodecahedronGeometry

A dodecahedron (12 sides)

const radius = 7;  // ui: radius
const geometry = new THREE.DodecahedronGeometry( radius );

const radius = 7;  // ui: radius
const detail = 2;  // ui: detail
const geometry = new THREE.DodecahedronGeometry( radius, detail );

ExtrudeGeometry

An extruded 2d shape with optional bevelling. Here we are extruding a heart shape. Note this is the basis for TextGeometry.

const shape = new THREE.Shape();
const x = -2.5;
const y = -5;
shape.moveTo(x + 2.5, y + 2.5);
shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);

const extrudeSettings = {
  steps: 2,  // ui: steps
  depth: 2,  // ui: depth
  bevelEnabled: true,  // ui: bevelEnabled
  bevelThickness: 1,  // ui: bevelThickness
  bevelSize: 1,  // ui: bevelSize
  bevelSegments: 2,  // ui: bevelSegments
};

const geometry = THREE.ExtrudeGeometry(shape, extrudeSettings);

const outline = new THREE.Shape([
  [ -2, -0.1], [  2, -0.1], [ 2,  0.6],
  [1.6,  0.6], [1.6,  0.1], [-2,  0.1],
].map(p => new THREE.Vector2(...p)));

const x = -2.5;
const y = -5;
const shape = new THREE.CurvePath();
const points = [
  [x + 2.5, y + 2.5],
  [x + 2.5, y + 2.5], [x + 2,   y      ], [x,       y      ],
  [x - 3,   y      ], [x - 3,   y + 3.5], [x - 3,   y + 3.5],
  [x - 3,   y + 5.5], [x - 1.5, y + 7.7], [x + 2.5, y + 9.5],
  [x + 6,   y + 7.7], [x + 8,   y + 4.5], [x + 8,   y + 3.5],
  [x + 8,   y + 3.5], [x + 8,   y      ], [x + 5,   y      ],
  [x + 3.5, y      ], [x + 2.5, y + 2.5], [x + 2.5, y + 2.5],
].map(p => new THREE.Vector3(...p, 0));
for (let i = 0; i < points.length; i += 3) {
  shape.add(new THREE.CubicBezierCurve3(...points.slice(i, i + 4)));
}

const extrudeSettings = {
  steps: 100,  // ui: steps
  bevelEnabled: false,
  extrudePath: shape,
};

const geometry =  new THREE.ExtrudeGeometry(outline, extrudeSettings);
return geometry;

IcosahedronGeometry

An icosahedron (20 sides)

const radius = 7;  // ui: radius
const geometry = new THREE.IcosahedronGeometry( radius );

const radius = 7;  // ui: radius
const detail = 2;  // ui: detail
const geometry = new THREE.IcosahedronGeometry( radius, detail );

LatheGeometry

A shape generated by spinning a line. Examples would be: lamps, bowling pins, candles, candle holders, wine glasses, drinking glasses, etc... You provide the 2d silhouette as series of points and then tell three.js how many subdivisions to make as it spins the silhouette around an axis.

const points = [];
for ( let i = 0; i < 10; ++ i ) {

	points.push( new THREE.Vector2( Math.sin( i * 0.2 ) * 3 + 3, ( i - 5 ) * .8 ) );

}

const geometry = new THREE.LatheGeometry( points );

const points = [];
for ( let i = 0; i < 10; ++ i ) {

	points.push( new THREE.Vector2( Math.sin( i * 0.2 ) * 3 + 3, ( i - 5 ) * .8 ) );

}

const segments = 12;  // ui: segments
const phiStart = Math.PI * 0.25;  // ui: phiStart
const phiLength = Math.PI * 1.5;  // ui: phiLength
const geometry = new THREE.LatheGeometry(
	points, segments, phiStart, phiLength );

OctahedronGeometry

An Octahedron (8 sides)

const radius = 7;  // ui: radius
const geometry = new THREE.OctahedronGeometry( radius );

const radius = 7;  // ui: radius
const detail = 2;  // ui: detail
const geometry = new THREE.OctahedronGeometry( radius, detail );

ParametricGeometry

A surface generated by providing a function that takes a 2D point from a grid and returns the corresponding 3d point.

const slices = 25;  // ui: slices
const stacks = 25;  // ui: stacks

// from: https://github.com/mrdoob/three.js/blob/b8d8a8625465bd634aa68e5846354d69f34d2ff5/examples/js/ParametricGeometries.js
function klein( v, u, target ) {

	u *= Math.PI;
	v *= 2 * Math.PI;
	u = u * 2;

	let x;
	let z;

	if ( u < Math.PI ) {

		x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( u ) * Math.cos( v );
		z = - 8 * Math.sin( u ) - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( u ) * Math.cos( v );

	} else {

		x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( v + Math.PI );
		z = - 8 * Math.sin( u );

	}

	const y = - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( v );

	target.set( x, y, z ).multiplyScalar( 0.75 );

}

return new ParametricGeometry(
	klein, slices, stacks );

PlaneGeometry

A 2D plane

const width = 9;  // ui: width
const height = 9;  // ui: height
const geometry = new THREE.PlaneGeometry( width, height );

const width = 9;  // ui: width
const height = 9;  // ui: height
const widthSegments = 2;  // ui: widthSegments
const heightSegments = 2;  // ui: heightSegments
const geometry = new THREE.PlaneGeometry(
	width, height,
	widthSegments, heightSegments );

PolyhedronGeometry

Takes a set of triangles centered around a point and projects them onto a sphere

RingGeometry

A 2D disc with a hole in the center

const innerRadius = 2;  // ui: innerRadius
const outerRadius = 7;  // ui: outerRadius
const thetaSegments = 18;  // ui: thetaSegments
const geometry = new THREE.RingGeometry(
	innerRadius, outerRadius, thetaSegments );

const innerRadius = 2;  // ui: innerRadius
const outerRadius = 7;  // ui: outerRadius
const thetaSegments = 18;  // ui: thetaSegments
const phiSegments = 2;  // ui: phiSegments
const thetaStart = Math.PI * 0.25;  // ui: thetaStart
const thetaLength = Math.PI * 1.5;  // ui: thetaLength
const geometry = new THREE.RingGeometry(
	innerRadius, outerRadius,
	thetaSegments, phiSegments,
	thetaStart, thetaLength );

ShapeGeometry

A 2D outline that gets triangulated

const shape = new THREE.Shape();
const x = - 2.5;
const y = - 5;
shape.moveTo( x + 2.5, y + 2.5 );
shape.bezierCurveTo( x + 2.5, y + 2.5, x + 2, y, x, y );
shape.bezierCurveTo( x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5 );
shape.bezierCurveTo( x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5 );
shape.bezierCurveTo( x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5 );
shape.bezierCurveTo( x + 8, y + 3.5, x + 8, y, x + 5, y );
shape.bezierCurveTo( x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5 );
const geometry = new THREE.ShapeGeometry( shape );

const shape = new THREE.Shape();
const x = - 2.5;
const y = - 5;
shape.moveTo( x + 2.5, y + 2.5 );
shape.bezierCurveTo( x + 2.5, y + 2.5, x + 2, y, x, y );
shape.bezierCurveTo( x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5 );
shape.bezierCurveTo( x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5 );
shape.bezierCurveTo( x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5 );
shape.bezierCurveTo( x + 8, y + 3.5, x + 8, y, x + 5, y );
shape.bezierCurveTo( x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5 );
const curveSegments = 5;  // ui: curveSegments
const geometry = new THREE.ShapeGeometry( shape, curveSegments );

SphereGeometry

A sphere

const radius = 7;  // ui: radius
const widthSegments = 12;  // ui: widthSegments
const heightSegments = 8;  // ui: heightSegments
const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );

const radius = 7;  // ui: radius
const widthSegments = 12;  // ui: widthSegments
const heightSegments = 8;  // ui: heightSegments
const phiStart = Math.PI * 0.25;  // ui: phiStart
const phiLength = Math.PI * 1.5;  // ui: phiLength
const thetaStart = Math.PI * 0.25;  // ui: thetaStart
const thetaLength = Math.PI * 0.5;  // ui: thetaLength
const geometry = new THREE.SphereGeometry(
	radius,
	widthSegments, heightSegments,
	phiStart, phiLength,
	thetaStart, thetaLength );

TetrahedronGeometry

A tetrahedron (4 sides)

const radius = 7;  // ui: radius
const geometry = new THREE.TetrahedronGeometry( radius );

const radius = 7;  // ui: radius
const detail = 2;  // ui: detail
const geometry = new THREE.TetrahedronGeometry( radius, detail );

TextGeometry

3D text generated from a 3D font and a string

const loader = new THREE.FontLoader();

loader.load('../resources/threejs/fonts/helvetiker_regular.typeface.json', (font) => {
  const text = 'three.js';  // ui: text
  const geometry = new THREE.TextGeometry(text, {
    font: font,
    size: 3,  // ui: size
    depth: 0.2,  // ui: depth
    curveSegments: 12,  // ui: curveSegments
    bevelEnabled: true,  // ui: bevelEnabled
    bevelThickness: 0.15,  // ui: bevelThickness
    bevelSize: 0.3,  // ui: bevelSize
    bevelSegments: 5,  // ui: bevelSegments
  });
  ...
});

TorusGeometry

A torus (donut)

const radius = 5;  // ui: radius
const tubeRadius = 2;  // ui: tubeRadius
const radialSegments = 8;  // ui: radialSegments
const tubularSegments = 24;  // ui: tubularSegments
const geometry = new THREE.TorusGeometry(
	radius, tubeRadius,
	radialSegments, tubularSegments );

TorusKnotGeometry

A torus knot

const radius = 3.5;  // ui: radius
const tubeRadius = 1.5;  // ui: tubeRadius
const radialSegments = 8;  // ui: radialSegments
const tubularSegments = 64;  // ui: tubularSegments
const p = 2;  // ui: p
const q = 3;  // ui: q
const geometry = new THREE.TorusKnotGeometry(
	radius, tubeRadius, tubularSegments, radialSegments, p, q );

TubeGeometry

A circle traced down a path

class CustomSinCurve extends THREE.Curve {

	constructor( scale ) {

		super();
		this.scale = scale;

	}
	getPoint( t ) {

		const tx = t * 3 - 1.5;
		const ty = Math.sin( 2 * Math.PI * t );
		const tz = 0;
		return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale );

	}

}

const path = new CustomSinCurve( 4 );
const tubularSegments = 20;  // ui: tubularSegments
const radius = 1;  // ui: radius
const radialSegments = 8;  // ui: radialSegments
const closed = false;  // ui: closed
const geometry = new THREE.TubeGeometry(
	path, tubularSegments, radius, radialSegments, closed );

EdgesGeometry

A helper object that takes another geometry as input and generates edges only if the angle between faces is greater than some threshold. For example if you look at the box at the top it shows a line going through each face showing every triangle that makes the box. Using an EdgesGeometry instead the middle lines are removed. Adjust the thresholdAngle below and you'll see the edges below that threshold disappear.

const size = 8;
const widthSegments = 2;
const heightSegments = 2;
const depthSegments = 2;
const boxGeometry = new THREE.BoxGeometry(
    size, size, size,
    widthSegments, heightSegments, depthSegments);
const geometry = new THREE.EdgesGeometry(boxGeometry);

const radius = 7;
const widthSegments = 6;
const heightSegments = 3;
const sphereGeometry = new THREE.SphereGeometry(
    radius, widthSegments, heightSegments);
const thresholdAngle = 1;  // ui: thresholdAngle
const geometry = new THREE.EdgesGeometry(sphereGeometry, thresholdAngle);

WireframeGeometry

Generates geometry that contains one line segment (2 points) per edge in the given geometry. Without this you'd often be missing edges or get extra edges since WebGL generally requires 2 points per line segment. For example if all you had was a single triangle there would only be 3 points. If you tried to draw it using a material with wireframe: true you would only get a single line. Passing that triangle geometry to a WireframeGeometry will generate a new geometry that has 3 lines segments using 6 points.

const size = 8;
const widthSegments = 2;  // ui: widthSegments
const heightSegments = 2;  // ui: heightSegments
const depthSegments = 2;  // ui: depthSegments
const geometry = new THREE.WireframeGeometry(
    new THREE.BoxGeometry(
      size, size, size,
      widthSegments, heightSegments, depthSegments));

Building an Example

We'll go over creating custom geometry in another article. For now let's make an example creating each type of primitive. We'll start with the examples from the previous article.

Near the top let's set a background color

This tells three.js to clear to lightish gray.

The camera needs to change position so that we can see all the objects.

Let's add a function, addObject, that takes an x, y position and an Object3D and adds the object to the scene.

Let's also make a function to create a random colored material. We'll use a feature of Color that lets you set a color based on hue, saturation, and luminance.

hue goes from 0 to 1 around the color wheel with red at 0, green at .33 and blue at .66. saturation goes from 0 to 1 with 0 having no color and 1 being most saturated. luminance goes from 0 to 1 with 0 being black, 1 being white and 0.5 being the maximum amount of color. In other words as luminance goes from 0.0 to 0.5 the color will go from black to hue. From 0.5 to 1.0 the color will go from hue to white.

We also passed side: THREE.DoubleSide to the material. This tells three to draw both sides of the triangles that make up a shape. For a solid shape like a sphere or a cube there's usually no reason to draw the back sides of triangles as they all face inside the shape. In our case though we are drawing a few things like the PlaneGeometry and the ShapeGeometry which are 2 dimensional and so have no inside. Without setting side: THREE.DoubleSide they would disappear when looking at their back sides.

I should note that it's faster to draw when not setting side: THREE.DoubleSide so ideally we'd set it only on the materials that really need it but in this case we are not drawing too much so there isn't much reason to worry about it.

Let's make a function, addSolidGeometry, that we pass a geometry and it creates a random colored material via createMaterial and adds it to the scene via addObject.

Now we can use this for the majority of the primitives we create. For example creating a box

If you look in the code below you'll see a similar section for each type of geometry.

const scene = new THREE.Scene();
+scene.background = new THREE.Color(0xAAAAAA);

-const fov = 75;
+const fov = 40;
const aspect = 2;  // the canvas default
const near = 0.1;
-const far = 5;
+const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-camera.position.z = 2;
+camera.position.z = 120;

const objects = [];
const spread = 15;

function addObject(x, y, obj) {
  obj.position.x = x * spread;
  obj.position.y = y * spread;

  scene.add(obj);
  objects.push(obj);
}

function createMaterial() {
  const material = new THREE.MeshPhongMaterial({
    side: THREE.DoubleSide,
  });

  const hue = Math.random();
  const saturation = 1;
  const luminance = .5;
  material.color.setHSL(hue, saturation, luminance);

  return material;
}

function addSolidGeometry(x, y, geometry) {
  const mesh = new THREE.Mesh(geometry, createMaterial());
  addObject(x, y, mesh);
}

{
  const width = 8;
  const height = 8;
  const depth = 8;
  addSolidGeometry(-2, -2, new THREE.BoxGeometry(width, height, depth));
}

TextGeometry and Font Loading

There are a couple of notable exceptions to the pattern above. The biggest is probably the TextGeometry. It needs to load 3D font data before it can generate a mesh for the text. That data loads asynchronously so we need to wait for it to load before trying to create the geometry. By promisifiying font loading we can make it mush easier. We create a FontLoader and then a function loadFont that returns a promise that on resolve will give us the font. We then create an async function called doit and load the font using await. And finally create the geometry and call addObject to add it the scene.

There's one other difference. We want to spin the text around its center but by default three.js creates the text such that its center of rotation is on the left edge. To work around this we can ask three.js to compute the bounding box of the geometry. We can then call the getCenter method of the bounding box and pass it our mesh's position object. getCenter copies the center of the box into the position. It also returns the position object so we can call multiplyScalar(-1) to position the entire object such that its center of rotation is at the center of the object.

If we then just called addSolidGeometry like with previous examples it would set the position again which is no good. So, in this case we create an Object3D which is the standard node for the three.js scene graph. Mesh is inherited from Object3D as well. We'll cover how the scene graph works in another article. For now it's enough to know that like DOM nodes, children are drawn relative to their parent. By making an Object3D and making our mesh a child of that we can position the Object3D wherever we want and still keep the center offset we set earlier.

If we didn't do this the text would spin off center.

{
  const loader = new FontLoader();
  // promisify font loading
  function loadFont(url) {
    return new Promise((resolve, reject) => {
      loader.load(url, resolve, undefined, reject);
    });
  }

  async function doit() {
    const font = await loadFont('resources/threejs/fonts/helvetiker_regular.typeface.json');  /* threejs.org: url */
    const geometry = new TextGeometry('three.js', {
      font: font,
      size: 3.0,
      depth: .2,
      curveSegments: 12,
      bevelEnabled: true,
      bevelThickness: 0.15,
      bevelSize: .3,
      bevelSegments: 5,
    });
    const mesh = new THREE.Mesh(geometry, createMaterial());
    geometry.computeBoundingBox();
    geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);

    const parent = new THREE.Object3D();
    parent.add(mesh);

    addObject(-1, -1, parent);
  }
  doit();
}

Line Geometry Helpers

The other exceptions are the 2 line based examples for EdgesGeometry and WireframeGeometry. Instead of calling addSolidGeometry they call addLineGeometry which looks like this

It creates a black LineBasicMaterial and then creates a LineSegments object which is a wrapper for Mesh that helps three know you're rendering line segments (2 points per segment).

Each of the primitives has several parameters you can pass on creation and it's best to look in the documentation for all of them rather than repeat them here. You can also click the links above next to each shape to take you directly to the docs for that shape.

function addLineGeometry(x, y, geometry) {
  const material = new THREE.LineBasicMaterial({color: 0x000000});
  const mesh = new THREE.LineSegments(geometry, material);
  addObject(x, y, mesh);
}

Points and PointsMaterial

There is one other pair of classes that doesn't really fit the patterns above. Those are the PointsMaterial and the Points class. Points is like LineSegments above in that it takes a a BufferGeometry but draws points at each vertex instead of lines. To use it you also need to pass it a PointsMaterial which take a size for how large to make the points.

You can turn off sizeAttenuation by setting it to false if you want the points to be the same size regardless of their distance from the camera.

const radius = 7;
const widthSegments = 12;
const heightSegments = 8;
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
const material = new THREE.PointsMaterial({
    color: 'red',
    size: 0.2,     // in world units
});
const points = new THREE.Points(geometry, material);
scene.add(points);

const material = new THREE.PointsMaterial({
    color: 'red',
+    sizeAttenuation: false,
+    size: 3,       // in pixels
-    size: 0.2,     // in world units
});
...

Subdivisions

One other thing that's important to cover is that almost all shapes have various settings for how much to subdivide them. A good example might be the sphere geometries. Spheres take parameters for how many divisions to make around and how many top to bottom. For example

The first sphere has 5 segments around and 3 high which is 15 segments or 30 triangles. The second sphere has 24 segments by 10. That's 240 segments or 480 triangles. The last one has 50 by 50 which is 2500 segments or 5000 triangles.

It's up to you to decide how many subdivisions you need. It might look like you need a high number of segments but remove the lines and the flat shading and we get this

It's now not so clear that the one on the right with 5000 triangles is entirely better than the one in the middle with only 480. If you're only drawing a few spheres, like say a single globe for a map of the earth, then a single 10000 triangle sphere is not a bad choice. If on the other hand you're trying to draw 1000 spheres then 1000 spheres times 10000 triangles each is 10 million triangles. To animate smoothly you need the browser to draw at 60 frames per second so you'd be asking the browser to draw 600 million triangles per second. That's a lot of computing.

Sometimes it's easy to choose. For example you can also choose to subdivide a plane.

The plane on the left is 2 triangles. The plane on the right is 200 triangles. Unlike the sphere there is really no trade off in quality for most use cases of a plane. You'd most likely only subdivide a plane if you expected to want to modify or warp it in some way. A box is similar.

So, choose whatever is appropriate for your situation. The less subdivisions you choose the more likely things will run smoothly and the less memory they'll take. You'll have to decide for yourself what the correct tradeoff is for your particular situation.

If none of the shapes above fit your use case you can load geometry for example from a .obj file or a .gltf file. You can also create your own custom BufferGeometry.


Scene Graph

Summary: Extraction fallback content. [stop]

Scene Graph

##### Scene Graph

This article is part of a series of articles about three.js. The
first article is [three.js fundamentals](fundamentals.html). If
you haven't read that yet you might want to consider starting there.

Three.js's core is arguably its scene graph. A scene graph in a 3D
engine is a hierarchy of nodes in a graph where each node represents
a local space.

That's kind of abstract so let's try to give some examples.

One example might be solar system, sun, earth, moon.

The Earth orbits the Sun. The Moon orbits the Earth. The Moon
moves in a circle around the Earth. From the Moon's point of
view it's rotating in the "local space" of the Earth. Even though
its motion relative to the Sun is some crazy spirograph like
curve from the Moon's point of view it just has to concern itself with rotating
around the Earth's local space.

To think of it another way, you living on the Earth do not have to think
about the Earth's rotation on its axis nor its rotation around the
Sun. You just walk or drive or swim or run as though the Earth is
not moving or rotating at all. You walk, drive, swim, run, and live
in the Earth's "local space" even though relative to the sun you are
spinning around the earth at around 1000 miles per hour and around
the sun at around 67,000 miles per hour. Your position in the solar
system is similar to that of the moon above but you don't have to concern
yourself. You just worry about your position relative to the earth in its
"local space".

Let's take it one step at a time. Imagine we want to make
a diagram of the sun, earth, and moon. We'll start with the sun by
just making a sphere and putting it at the origin. Note: We're using
sun, earth, moon as a demonstration of how to use a scene graph. Of course
the real sun, earth, and moon use physics but for our purposes we'll
fake it with a scene graph.

We're using a really low-polygon sphere. Only 6 subdivisions around its equator.
This is so it's easy to see the rotation.

We're going to reuse the same sphere for everything so we'll set a scale
for the sun mesh of 5x.

We also set the phong material's `emissive` property to yellow. A phong material's
emissive property is basically the color that will be drawn with no light hitting
the surface. Light is added to that color.

Let's also put a single point light in the center of the scene. We'll go into more
details about point lights later but for now the simple version is a point light
represents light that emanates from a single point.

To make it easy to see we're going to put the camera directly above the origin
looking down. The easiest way to do that is to use the `lookAt` function. The `lookAt`
function will orient the camera from its position to "look at" the position
we pass to `lookAt`. Before we do that though we need to tell the camera
which way the top of the camera is facing or rather which way is "up" for the
camera. For most situations positive Y being up is good enough but since
we are looking straight down we need to tell the camera that positive Z is up.

In the render loop, adapted from previous examples, we're rotating all
objects in our `objects` array with this code.

Since we added the `sunMesh` to the `objects` array it will rotate.

[click here to open in a separate window](/manual/examples/scenegraph-sun.html)

Now let's add in the earth.

We make a material that is blue but we gave it a small amount of  *emissive*  blue
so that it will show up against our black background.

We use the same `sphereGeometry` with our new blue `earthMaterial` to make
an `earthMesh`. We position that 10 units to the left of the sun
and add it to the scene.  Since we added it to our `objects` array it will
rotate too.

[click here to open in a separate window](/manual/examples/scenegraph-sun-earth.html)

You can see both the sun and the earth are rotating but the earth is not
going around the sun. Let's make the earth a child of the sun

and...

[click here to open in a separate window](/manual/examples/scenegraph-sun-earth-orbit.html)

What happened? Why is the earth the same size as the sun and why is it so far away?
I actually had to move the camera from 50 units above to 150 units above to see the earth.

We made the `earthMesh` a child of the `sunMesh`. The `sunMesh` has
its scale set to 5x with `sunMesh.scale.set(5, 5, 5)`. That means the
`sunMesh`s local space is 5 times as big. Anything put in that space
 will be multiplied by 5. That means the earth is now 5x larger and
 its distance from the sun (`earthMesh.position.x = 10`) is also
 5x as well.

 Our scene graph currently looks like this

To fix it let's add an empty scene graph node. We'll parent both the sun and the earth
to that node.

Here we made an [`Object3D`](/docs/#api/en/core/Object3D). Like a [`Mesh`](/docs/#api/en/objects/Mesh) it is also a node in the scene graph
but unlike a [`Mesh`](/docs/#api/en/objects/Mesh) it has no material or geometry. It just represents a local space.

Our new scene graph looks like this

Both the `sunMesh` and the `earthMesh` are children of the `solarSystem`. All 3
are being rotated and now because the `earthMesh` is not a child of the `sunMesh`
it is no longer scaled by 5x.

[click here to open in a separate window](/manual/examples/scenegraph-sun-earth-orbit-fixed.html)

Much better. The earth is smaller than the sun and it's rotating around the sun
and rotating itself.

Continuing that same pattern let's add a moon.

Again we added more invisible scene graph nodes. The first, an [`Object3D`](/docs/#api/en/core/Object3D) called `earthOrbit`
and added both the `earthMesh` and the `moonOrbit` to it, also new. We then added the `moonMesh`
to the `moonOrbit`. The new scene graph looks like this.

and here's that

[click here to open in a separate window](/manual/examples/scenegraph-sun-earth-moon.html)

You can see the moon follows the spirograph pattern shown at the top
of this article but we didn't have to manually compute it. We just
setup our scene graph to do it for us.

It is often useful to draw something to visualize the nodes in the scene graph.
Three.js has some helpful ummmm, helpers to ummm, ... help with this.

One is called an [`AxesHelper`](/docs/#api/en/helpers/AxesHelper). It draws 3 lines representing the local
X,
Y, and
Z axes. Let's add one to every node we
created.

On our case we want the axes to appear even though they are inside the spheres.
To do this we set their material's `depthTest` to false which means they will
not check to see if they are drawing behind something else. We also
set their `renderOrder` to 1 (the default is 0) so that they get drawn after
all the spheres. Otherwise a sphere might draw over them and cover them up.

[click here to open in a separate window](/manual/examples/scenegraph-sun-earth-moon-axes.html)

We can see the
x (red) and
z (blue) axes. Since we are looking
straight down and each of our objects is only rotating around its
y axis we don't see much of the y (green) axes.

It might be hard to see some of them as there are 2 pairs of overlapping axes. Both the `sunMesh`
and the `solarSystem` are at the same position. Similarly the `earthMesh` and
`earthOrbit` are at the same position. Let's add some simple controls to allow us
to turn them on/off for each node.
While we're at it let's also add another helper called the [`GridHelper`](/docs/#api/en/helpers/GridHelper). It
makes a 2D grid on the X,Z plane. By default the grid is 10x10 units.

We're also going to use [lil-gui](https://github.com/georgealways/lil-gui) which is
a UI library that is very popular with three.js projects. lil-gui takes an
object and a property name on that object and based on the type of the property
automatically makes a UI to manipulate that property.

We want to make both a [`GridHelper`](/docs/#api/en/helpers/GridHelper) and an [`AxesHelper`](/docs/#api/en/helpers/AxesHelper) for each node. We need
a label for each node so we'll get rid of the old loop and switch to calling
some function to add the helpers for each node

`makeAxisGrid` makes an `AxisGridHelper` which is a class we'll create
to make lil-gui happy. Like it says above lil-gui
will automagically make a UI that manipulates the named property
of some object. It will create a different UI depending on the type
of property. We want it to create a checkbox so we need to specify
a `bool` property. But, we want both the axes and the grid
to appear/disappear based on a single property so we'll make a class
that has a getter and setter for a property. That way we can let lil-gui
think it's manipulating a single property but internally we can set
the visible property of both the [`AxesHelper`](/docs/#api/en/helpers/AxesHelper) and [`GridHelper`](/docs/#api/en/helpers/GridHelper) for a node.

One thing to notice is we set the `renderOrder` of the [`AxesHelper`](/docs/#api/en/helpers/AxesHelper)
to 2 and for the [`GridHelper`](/docs/#api/en/helpers/GridHelper) to 1 so that the axes get drawn after the grid.
Otherwise the grid might overwrite the axes.

[click here to open in a separate window](/manual/examples/scenegraph-sun-earth-moon-axes-grids.html)

Turn on the `solarSystem` and you'll see how the earth is exactly 10
units out from the center just like we set above. You can see how the
earth is in the  *local space*  of the `solarSystem`. Similarly if you
turn on the `earthOrbit` you'll see how the moon is exactly 2 units
from the center of the  *local space*  of the `earthOrbit`.

A few more examples of scene graphs. An automobile in a simple game world might have a scene graph like this

If you move the car's body all the wheels will move with it. If you wanted the body
to bounce separate from the wheels you might parent the body and the wheels to a "frame" node
that represents the car's frame.

Another example is a human in a game world.

You can see the scene graph gets pretty complex for a human. In fact
that scene graph above is simplified. For example you might extend it
to cover every finger (at least another 28 nodes) and every toe
(yet another 28 nodes) plus ones for the face and jaw, the eyes and maybe more.

Let's make one semi-complex scene graph. We'll make a tank. The tank will have
6 wheels and a turret. The tank will follow a path. There will be a sphere that
moves around and the tank will target the sphere.

Here's the scene graph. The meshes are colored in green, the [`Object3D`](/docs/#api/en/core/Object3D)s in blue,
the lights in gold, and the cameras in purple. One camera has not been added
to the scene graph.

Look in the code to see the setup of all of these nodes.

For the target, the thing the tank is aiming at, there is a `targetOrbit`
([`Object3D`](/docs/#api/en/core/Object3D)) which just rotates similar to the `earthOrbit` above. A
`targetElevation` ([`Object3D`](/docs/#api/en/core/Object3D)) which is a child of the `targetOrbit` provides an
offset from the `targetOrbit` and a base elevation. Childed to that is another
[`Object3D`](/docs/#api/en/core/Object3D) called `targetBob` which just bobs up and down relative to the
`targetElevation`. Finally there's the `targetMesh` which is just a cube we
rotate and change its colors

For the tank there's an [`Object3D`](/docs/#api/en/core/Object3D) called `tank` which is used to move everything
below it around. The code uses a [`SplineCurve`](/docs/#api/en/extras/curves/SplineCurve) which it can ask for positions
along that curve. 0.0 is the start of the curve. 1.0 is the end of the curve. It
asks for the current position where it puts the tank. It then asks for a
position slightly further down the curve and uses that to point the tank in that
direction using [`Object3D.lookAt`](/docs/#api/en/core/Object3D.lookAt).

The turret on top of the tank is moved automatically by being a child
of the tank. To point it at the target we just ask for the target's world position
and then again use [`Object3D.lookAt`](/docs/#api/en/core/Object3D.lookAt)

There's a `turretCamera` which is a child of the `turretMesh` so
it will move up and down and rotate with the turret. We make that
aim at the target.

There is also a `targetCameraPivot` which is a child of `targetBob` so it floats
around with the target. We aim that back at the tank. Its purpose is to allow the
`targetCamera` to be offset from the target itself. If we instead made the camera
a child of `targetBob` and just aimed the camera itself it would be inside the
target.

Finally we rotate all the wheels

For the cameras we setup an array of all 4 cameras at init time with descriptions.

and cycle through our cameras at render time.

[click here to open in a separate window](/manual/examples/scenegraph-tank.html)

I hope this gives some idea of how scene graphs work and how you might use them.
Making [`Object3D`](/docs/#api/en/core/Object3D) nodes and parenting things to them is an important step to using
a 3D engine like three.js well. Often it might seem like some complex math is necessary
to make something move and rotate the way you want. For example without a scene graph
computing the motion of the moon or where to put the wheels of the car relative to its
body would be very complicated but using a scene graph it becomes much easier.

[Next up we'll go over materials](materials.html).

// an array of objects whose rotation to update
const objects = [];

// use just one sphere for everything
const radius = 1;
const widthSegments = 6;
const heightSegments = 6;
const sphereGeometry = new THREE.SphereGeometry(
    radius, widthSegments, heightSegments);

const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
sunMesh.scale.set(5, 5, 5);  // make the sun large
scene.add(sunMesh);
objects.push(sunMesh);

{
  const color = 0xFFFFFF;
  const intensity = 500;
  const light = new THREE.PointLight(color, intensity);
  scene.add(light);
}

const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 50, 0);
camera.up.set(0, 0, 1);
camera.lookAt(0, 0, 0);

objects.forEach((obj) => {
  obj.rotation.y = time;
});

const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
earthMesh.position.x = 10;
scene.add(earthMesh);
objects.push(earthMesh);

+const solarSystem = new THREE.Object3D();
+scene.add(solarSystem);
+objects.push(solarSystem);

const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
sunMesh.scale.set(5, 5, 5);
-scene.add(sunMesh);
+solarSystem.add(sunMesh);
objects.push(sunMesh);

const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
earthMesh.position.x = 10;
-sunMesh.add(earthMesh);
+solarSystem.add(earthMesh);
objects.push(earthMesh);

+const earthOrbit = new THREE.Object3D();
+earthOrbit.position.x = 10;
+solarSystem.add(earthOrbit);
+objects.push(earthOrbit);

const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
-earthMesh.position.x = 10; // note that this offset is already set in its parent's THREE.Object3D object "earthOrbit"
-solarSystem.add(earthMesh);
+earthOrbit.add(earthMesh);
objects.push(earthMesh);

+const moonOrbit = new THREE.Object3D();
+moonOrbit.position.x = 2;
+earthOrbit.add(moonOrbit);

+const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222});
+const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
+moonMesh.scale.set(.5, .5, .5);
+moonOrbit.add(moonMesh);
+objects.push(moonMesh);

// add an AxesHelper to each node
objects.forEach((node) => {
  const axes = new THREE.AxesHelper();
  axes.material.depthTest = false;
  axes.renderOrder = 1;
  node.add(axes);
});

-// add an AxesHelper to each node
-objects.forEach((node) => {
-  const axes = new THREE.AxesHelper();
-  axes.material.depthTest = false;
-  axes.renderOrder = 1;
-  node.add(axes);
-});

+function makeAxisGrid(node, label, units) {
+  const helper = new AxisGridHelper(node, units);
+  gui.add(helper, 'visible').name(label);
+}
+
+makeAxisGrid(solarSystem, 'solarSystem', 25);
+makeAxisGrid(sunMesh, 'sunMesh');
+makeAxisGrid(earthOrbit, 'earthOrbit');
+makeAxisGrid(earthMesh, 'earthMesh');
+makeAxisGrid(moonOrbit, 'moonOrbit');
+makeAxisGrid(moonMesh, 'moonMesh');

// move target
targetOrbit.rotation.y = time * .27;
targetBob.position.y = Math.sin(time * 2) * 4;
targetMesh.rotation.x = time * 7;
targetMesh.rotation.y = time * 13;
targetMaterial.emissive.setHSL(time * 10 % 1, 1, .25);
targetMaterial.color.setHSL(time * 10 % 1, 1, .25);

const tankPosition = new THREE.Vector2();
const tankTarget = new THREE.Vector2();

...

// move tank
const tankTime = time * .05;
curve.getPointAt(tankTime % 1, tankPosition);
curve.getPointAt((tankTime + 0.01) % 1, tankTarget);
tank.position.set(tankPosition.x, 0, tankPosition.y);
tank.lookAt(tankTarget.x, 0, tankTarget.y);

const targetPosition = new THREE.Vector3();

...

// face turret at target
targetMesh.getWorldPosition(targetPosition);
turretPivot.lookAt(targetPosition);

// make the turretCamera look at target
turretCamera.lookAt(targetPosition);

// make the targetCameraPivot look at the tank
tank.getWorldPosition(targetPosition);
targetCameraPivot.lookAt(targetPosition);

wheelMeshes.forEach((obj) => {
  obj.rotation.x = time * 3;
});

const cameras = [
  { cam: camera, desc: 'detached camera', },
  { cam: turretCamera, desc: 'on turret looking at target', },
  { cam: targetCamera, desc: 'near target looking at tank', },
  { cam: tankCamera, desc: 'above back of tank', },
];

const infoElem = document.querySelector('#info');

const camera = cameras[time * .25 % cameras.length | 0];
infoElem.textContent = camera.desc;


Materials

Summary: An overview of the materials available in three.js, covering how to set material properties, the different material types (Basic, Lambert, Phong, Toon, Standard, Physical, and special-use materials), and common properties shared across materials.

Introduction to Materials

Three.js provides several types of materials. They define how objects will appear in the scene. Which materials you use really depends on what you're trying to accomplish.

This article is part of a series of articles about three.js. The first article is three.js fundamentals. If you haven't read that yet and you're new to three.js you might want to consider starting there.

Setting Material Properties

There are 2 ways to set most material properties. One at creation time which we've seen before.

The other is after creation.

const material = new THREE.MeshPhongMaterial({
  color: 0xFF0000,    // red (can also use a CSS color string here)
  flatShading: true,
});

const material = new THREE.MeshPhongMaterial();
material.color.setHSL(0, 1, .5);  // red
material.flatShading = true;

THREE.Color Properties

Note that properties of type THREE.Color have multiple ways to be set.

material.color.set(0x00FFFF);    // same as CSS's #RRGGBB style
material.color.set(cssString);   // any CSS color, eg 'purple', '#F32',
                                 // 'rgb(255, 127, 64)',
                                 // 'hsl(180, 50%, 25%)'
material.color.set(someColor)    // some other THREE.Color
material.color.setHSL(h, s, l)   // where h, s, and l are 0 to 1
material.color.setRGB(r, g, b)   // where r, g, and b are 0 to 1

Color Strings at Creation Time

At creation time you can pass either a hex number or a CSS string.

const m1 = new THREE.MeshBasicMaterial({color: 0xFF0000});         // red
const m2 = new THREE.MeshBasicMaterial({color: 'red'});            // red
const m3 = new THREE.MeshBasicMaterial({color: '#F00'});           // red
const m4 = new THREE.MeshBasicMaterial({color: 'rgb(255,0,0)'});   // red
const m5 = new THREE.MeshBasicMaterial({color: 'hsl(0,100%,50%)'}); // red

Basic, Lambert, and Phong Materials

The MeshBasicMaterial is not affected by lights. The MeshLambertMaterial computes lighting only at the vertices vs the MeshPhongMaterial which computes lighting at every pixel. The MeshPhongMaterial also supports specular highlights.

Why have all 3 when MeshPhongMaterial can do the same things as MeshBasicMaterial and MeshLambertMaterial? The reason is the more sophisticated material takes more GPU power to draw. On a slower GPU like say a mobile phone you might want to reduce the GPU power needed to draw your scene by using one of the less complex materials. It also follows that if you don't need the extra features then use the simplest material. If you don't need the lighting and the specular highlight then use the MeshBasicMaterial.

Shininess

The shininess setting of the MeshPhongMaterial determines the shininess of the specular highlight. It defaults to 30.

Note that setting the emissive property to a color on either a MeshLambertMaterial or a MeshPhongMaterial and setting the color to black (and shininess to 0 for phong) ends up looking just like the MeshBasicMaterial.

MeshToonMaterial

The MeshToonMaterial is similar to the MeshPhongMaterial with one big difference. Rather than shading smoothly it uses a gradient map (an X by 1 texture) to decide how to shade. The default uses a gradient map that is 70% brightness for the first 70% and 100% after but you can supply your own gradient map. This ends up giving a 2 tone look that looks like a cartoon.

Physically Based Rendering (PBR) Materials

Next up there are 2 physically based rendering materials. Physically Based Rendering is often abbreviated PBR.

The materials above use simple math to make materials that look 3D but they aren't what actually happens in real world. The 2 PBR materials use much more complex math to come close to what actually happens in the real world.

MeshStandardMaterial

The first one is MeshStandardMaterial. The biggest difference between MeshPhongMaterial and MeshStandardMaterial is it uses different parameters. MeshPhongMaterial had a shininess setting. MeshStandardMaterial has 2 settings roughness and metalness.

At a basic level roughness is the opposite of shininess. Something that has a high roughness, like a baseball doesn't have hard reflections whereas something that's not rough, like a billiard ball, is very shiny. Roughness goes from 0 to 1.

The other setting, metalness, says how metal the material is. Metals behave differently than non-metals. 0 for non-metal and 1 for metal.

MeshPhysicalMaterial

The MeshPhysicalMaterial is same as the MeshStandardMaterial but it adds a clearcoat parameter that goes from 0 to 1 for how much to apply a clearcoat gloss layer and a clearCoatRoughness parameter that specifies how rough the gloss layer is.

Material Performance Progression

The various standard materials progress from fastest to slowest MeshBasicMaterial ➡ MeshLambertMaterial ➡ MeshPhongMaterial ➡ MeshStandardMaterial ➡ MeshPhysicalMaterial. The slower materials can make more realistic looking scenes but you might need to design your code to use the faster materials on low powered or mobile machines.

ShadowMaterial

There are 3 materials that have special uses. ShadowMaterial is used to get the data created from shadows. We haven't covered shadows yet. When we do we'll use this material to take a peek at what's happening behind the scenes.

MeshDepthMaterial

The MeshDepthMaterial renders the depth of each pixel where pixels at negative near of the camera are 0 and negative far are 1. Certain special effects can use this data which we'll get into at another time.

MeshNormalMaterial

The MeshNormalMaterial will show you the normals of geometry. Normals are the direction a particular triangle or pixel faces. MeshNormalMaterial draws the view space normals (the normals relative to the camera). x is red, y is green, and z is blue so things facing to the right will be pink, to the left will be aqua, up will be light green, down will be purple, and toward the screen will be lavender.

ShaderMaterial and RawShaderMaterial

ShaderMaterial is for making custom materials using the three.js shader system. RawShaderMaterial is for making entirely custom shaders with no help from three.js. Both of these topics are large and will be covered later.

Common Material Properties

Most materials share a bunch of settings all defined by Material. See the docs for all of them but let's go over two of the most commonly used properties.

flatShading: whether or not the object looks faceted or smooth. default = false.

side: which sides of triangles to show. The default is THREE.FrontSide. Other options are THREE.BackSide and THREE.DoubleSide (both sides). Most 3D objects drawn in three are probably opaque solids so the back sides (the sides facing inside the solid) do not need to be drawn. The most common reason to set side is for planes or other non-solid objects where it is common to see the back sides of triangles.

material.needsUpdate

This topic rarely affects most three.js apps but just as an FYI... Three.js applies material settings when a material is used where "used" means "something is rendered that uses the material". Some material settings are only applied once as changing them requires lots of work by three.js. In those cases you need to set material.needsUpdate = true to tell three.js to apply your material changes. The most common settings that require you to set needsUpdate if you change the settings after using the material are:

- flatShading
- adding or removing a texture

Changing a texture is ok, but if want to switch from using no texture to using a texture or from using a texture to using no texture then you need to set needsUpdate = true.

In the case of going from texture to no-texture it is often just better to use a 1x1 pixel white texture.

As mentioned above most apps never run into these issues. Most apps do not switch between flat shaded and non flat shaded. Most apps also either use textures or a solid color for a given material, they rarely switch from using one to using the other.