MeshNormalMaterial

Useful for: getting up and running quickly

We’ll start with the MeshNormalMaterial , the multi-coloured material we’ve used in the examples so far. It maps the normal vectors to RGB colours: in other words, it uses colour to distinguish the position of the vector in 3D space.

const material = new THREE.MeshNormalMaterial();

Note that, if you want to change the colours of MeshNormalMaterial you could simply use a CSS filter, and alter the hue: e.g. filter: hue-rotate(90deg) .

In my experience, the MeshNormalMaterial is most useful for getting up-and-running quickly. For more control over the look of your objects, it’s best to use something else.

MeshBasicMaterial

Useful for: wireframes

If you want your object to have a uniform colour, you can use MeshBasicMaterial , as it is not affected by lights. I find this useful for wireframes. To use wireframe mode, simply pass { wireframe: true } as an argument.

const material = new THREE.MeshBasicMaterial({

wireframe: true,

color: 0xdaa520

});

The disadvantage of the basic material is that it offers no clues about the depth of the material. Every material has options for wireframing, but a performant solution that includes depth is the MeshDepthMaterial .

MeshLambertMaterial

Useful for: high performance (but lower accuracy)

This is the first material which is affected by lights, so to see what we’re doing we’ll need to add some light to our scene. In the code below, we’ll add to spotlights, with a hint of yellow to create a warmer effect:

const scene = new THREE.Scene(); const frontSpot = new THREE.SpotLight(0xeeeece);

frontSpot.position.set(1000, 1000, 1000);

scene.add(frontSpot); const frontSpot2 = new THREE.SpotLight(0xddddce);

frontSpot2.position.set(-500, -500, -500);

scene.add(frontSpot2);

Now let’s add the material for our shape. As it looks a bit like a piece of jewellery, I thought I’d go for a gold-ish colour. The other property, emissive , is the colour the object emits from itself (without any light source). Often, it works best as a dark colour — such as a dark shade of grey, like below:

const material = new THREE.MeshLambertMaterial({

color: 0xdaa520,

emissive: 0x111111,

});

As you can see in the example below, the colour is more-or-less correct, but the way it’s interacting with the light doesn’t make for the most realistic appearance. For that, we’ll need to use the MeshPhongMaterial or the MeshStandardMaterial .

MeshPhongMaterial

Useful for: medium performance and accuracy

This material offers a compromise between performance and appearance, and therefore it’s a good middle-ground for applications that need to be performant while also achieving a higher level of quality than the MeshLambertMaterial .

We can now alter a new property, specular , which defines the brightness and colour of the surface’s reflectivity. While the emissive property is usually dark, the specular one often works best as a light colour. Below, we’re using a light grey:

const material = new THREE.MeshPhongMaterial({

color: 0xdaa520,

emissive: 0x000000,

specular: 0xbcbcbc,

});

Visually, the above image reflects the light in a much more convincing way, but it’s still not perfect. The white light is a bit too bright, and the material looks more rubbery than metallic (our desired effect). We can get a better result using the MeshStandardMaterial .

MeshStandardMaterial

Useful for: high accuracy (but lower performance)

This is the highest-accuracy of Three.js’s materials, although it comes at the expense of greater processing power. MeshStandardMaterial comes with a couple of additional properties — metalness and roughness , both of which take values between 0 and 1 .

The metalness property alters the way the object reflects so that it’s closer in nature to a metal. This is because conductive materials, like metals, have different reflective properties to dielectric materials, like ceramics.

roughness adds a further layer of customisation. You can think of it as the opposite of glossiness: a value of 0 is extremely glossy, while a value of 1 is extremely rough (meaning very little light is reflected).

const material = new THREE.MeshStandardMaterial({

color: 0xfcc742,

emissive: 0x111111,

specular: 0xffffff,

metalness: 1,

roughness: 0.55,

});

This is definitely the most realistic result but bear in mind that it is more resource-intensive.

The materials discussed above are the ones I’ve encountered most commonly, but to see what other options are available, head over to the docs.

Loaders

As we’ve discussed, it’s possible to manually define the geometry of your meshes. However, in practice, many people will often prefer to import their geometries from 3D files. Luckily, Three.js has plenty of supported loaders, covering most of the major 3D file formats.

The basic ObjectLoader loads a JSON resource, using the JSON Object/Scene format. But most loaders need to be imported manually. You can find a list of supported loaders in this GitHub directory, and import them like so, using the GitHub file path. Here are imports for several common formats:

// GLTF

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // OBJ

import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; // STL

import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; // FBX

import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'; // 3MF

import { 3MFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';

The recommended file format for online viewing is GLTF , on the grounds that it’s ‘focused on runtime asset delivery, compact to transmit and fast to load’.

Of course, there may be reasons to prefer a specific file (for example, if the quality is your priority, or if you need to accurately represent files for 3D printing). But if you can, you’ll likely get the best performance online by importing your files in the GLTF format.

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

import model from '../models/sample.gltf'; let loader = new GLTFLoader(); loader.load(model, function (geometry) {

// if the model is loaded successfully, add it to your scene here

}, undefined, function (err) {

console.error(err);

});

Bringing it all together

One of the reasons that Three.js can seem intimidating is that, if you’re building something from scratch, a fair amount of code is required just to see something on your screen. In every example so far, we’ve needed to create a scene and a camera. To keep things simpler, I’ve kept this code out of the way at the bottom, but we’ll now look at bringing everything together.

The way you order and organise your code is up to you. In simpler examples, like the ones in these articles, it makes sense to include all the Three.js code in one place. But, in practice, it can be useful to divide the separate elements to make the codebase easier to manage and scale.

For simplicity, we’ll look at the elements you’ll need to render a single object and we’ll do so in a single JavaScript file. Here’s an outline to get you started, which will render the :

Should I use a framework?

Finally, it’s worth discussing whether or not to use a framework-specific incarnation of Three.js. Currently, the most popular of these is the fantastic react-three-fiber, for React. For users of React, there are some big advantages to using a package like this — namely, you get to keep a component-based structure, which allows you to easily manage and re-use code.

But, for beginners, I recommend starting with regular, vanilla Javascript. This is because most online material written about Three.js refers to Three.js in vanilla JavaScript. Based on my experience as a learner, it could be confusing to learn Three.js via a package if — for example — you constantly have to translate Three.js objects and methods to components and props. (But once you’re more comfortable with Three.js, use whatever packages you like!)

How to add vanilla Three.js to a framework

Three.js will give you an HTML object (often called renderer.domElement ) which you can append to any HTML element in your app. So, for example, if you have a div with id="threejs" , you can simply include the following in your Three.js code:

document.getElementById('threejs').appendChild(renderer.domElement);

Some frameworks have a preferred way for you to access DOM nodes — such as a ref in React, an $ref in Vue or a ngRef in Angular — which come with certain advantages over querying elements directly from the DOM. As an example, we’ll now look at a quick implementation strategy for React.

A strategy for React

If you use React, here’s one way of incorporating a regular Three.js file into one of your components. In one file, ThreeEntryPoint.js , we’ll hold our vanilla Three.js code:

export default function ThreeEntryPoint(sceneRef) {

let renderer = new THREE.WebGLRenderer();

// ...

sceneRef.appendChild(renderer.domElement);

}

We should export this as a function, which takes one argument: a React ref to an element in our React component. We can now make our component, ThreeContainer.js :

import React, { Component } from 'react';

import ThreeEntryPoint from './threejs/ThreeEntryPoint'; export default class ThreeContainer extends Component { componentDidMount() {

ThreeEntryPoint(this.scene);

} render() {

return (

<>

<div ref={element => this.scene = element} />

</>

);

}

}

The imported ThreeEntryPoint function should go inside our componentDidMount method, and pass our new div as its argument, using refs.

For an example of this approach in action, feel free to clone the following repository and try it for yourself: https://github.com/BretCameron/three-js-sample.

Conclusion

There’s a lot more that could be said about Three.js, but I hope this article has given you enough information to get started with this powerful technology. When I began learning Three.js, I couldn’t find any resource quite like this article, so I hope that I have helped make the technology more accessible to first-timers.

If you have any questions or you think there’s something useful that could be added, let me know in the comments!