10 Three.JS Tips

In this article I’ll show you 10 useful tips when using the Three.JS library. All the tips are available on CodePen. Here is a link to the collection https://codepen.io/collection/RzgPww

YouTube video: 10 Three.JS Tips

No 1. Using scene.overrideMaterial

Did you know that the Scene class has a property overrideMaterial? If this is set then all meshes will be rendered using this material. Click here to open the ThreeJS Tips: 1&2 pen. You can fork this to play with it yourself or export it as a zip file. Let’s take a look at the JS code.

import * as THREE from 'three';
import GUI from 'https://unpkg.com/[email protected]/examples/jsm/libs/lil-gui.module.min.js';

Notice I’m using modules loaded in from unpkg.com. The latest Three.JS examples use import maps, allowing the developer to map a string to a url. My tips use the same method.

<script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/[email protected]/build/three.module.js"
    }
  }
</script>

The relevant code for this tip starts at line 51.

const mat = new THREE.MeshBasicMaterial( { color: 0xFFFFFF, wireframe: true });
  const gui = new GUI();
  const options = { 
    overrideMaterial: false, 
    background: 0x000000,
    lights: true
  }
  gui.add(options, 'overrideMaterial').onChange( value => {
    if (value){
      scene.overrideMaterial = mat;
    }else{
      scene.overrideMaterial = null;
    }
  });

I create a MeshBasicMaterial, that’s one that does not use lighting. The color is set to white and wireframe is true. I add a GUI using the lil-gui library a direct replacement to dat.gui. I add a gui control for overrideMaterial with a change event. I use this to toggle between assigning the MeshBasicMaterial or null to scene.overrideMaterial. This can be handy when debugging your scenes.

See the Pen ThreeJS Tips: 1 & 2 by Nik Lever (@nik-lever) on CodePen.

No 2. Set a scene.background color

By default the scene background color is black. If you use materials that rely on lights but forget to add a light, you will be faced with black. By setting a background color you’ll see a silhouette of your meshes. In the example, same CodePen link as tip 1. I use the addColor method of the GUI class to add a control that allows us to update the scene.background color.

gui.addColor(options, 'background').onChange( col => {
  scene.background = new THREE.Color(col);
});

No 3. Set scene.environment

If using a MeshStandardMaterial with roughness not 1 then you need to set a scene environment to reflect. This applies to any objects loaded using the GLTFLoader class, which uses MeshStandardMaterial. This setEnvironment function shows one method. CodePen link.

function setEnvironment(){
  const loader = new RGBELoader();
  const pmremGenerator = new THREE.PMREMGenerator( renderer );
  pmremGenerator.compileEquirectangularShader();
        
  loader.load( 'https://niksgames.com/threejs-games-course/assets/hdr/venice_sunset_1k.hdr', ( texture ) => {
    envmap = pmremGenerator.fromEquirectangular( texture ).texture;
    pmremGenerator.dispose();

    scene.environment = envmap;

  }, undefined, (err)=>{
    console.error( 'An error occurred setting the environment');
    console.error(err);
  } );
}

I use a RGBELoader for an hdr file and a PMREMGenerator. This class generates a Prefiltered, Mipmapped Radiance Environment Map (PMREM) from a cubeMap environment texture. This allows different levels of blur to be quickly accessed based on material roughness. The loaders onload event uses the fromEquirectangular method. The hdr is a spherical image map and this converts the rectangular image back into a sphere. Then we tidy up memory and set the scene.environment.

No 4. Renderer.outputEncoding

If you use the GLTFLoader class then make sure to set the renderer ouputEncoding property to THREE.sRGBEncoding.

renderer.outputEncoding = THREE.sRGBEncoding;

The default is THREE.linearEncoding. The GUI in this example let’s you see the effect

No 5. Renderer.physicallyCorrectLights

Again if you load models using the GLTFLoader class then set renderer.physicallyCorrectLights to true. The GUI in the example lets you see the effect.

No. 6 Setting matrixAutoUpdate

If you have lots of static objects in your scene then you’ll get a small performance boast by setting each meshes matrixAutoUpdate property to false. CodePen link.

function setAutoUpdate(value){
  factory.traverse( child => {
    if (child.isMesh && !child.name.includes('fan')){
      child.matrixAutoUpdate = value;
    }
  })
}

Now the render method does not have to calculate the world matrix for these meshes.

No.7 Getting the bounds of an object
The Box3 class has a great method for getting the axis-aligned bounds of an object. The minimum and maximum values of vertices in the x, y and z axes. The gui getBounds function shows how. CodePen link.

const gui = new GUI();
const options = {
  getBounds: () => {
    const bounds = new THREE.Box3();
    bounds.setFromObject(object);
    alert(`min:${bounds.min.x.toFixed(2)}, ${bounds.min.y.toFixed(2)}, ${bounds.min.z.toFixed(2)} max:${bounds.max.x.toFixed(2)}, ${bounds.max.y.toFixed(2)}, ${bounds.max.z.toFixed(2)}`);
  }
}
gui.add(options, 'getBounds');

Create a Box3 then use the setFromObject method passing the object you’re querying. The Box3 will then have its min and max set. In this function I use these to pass a string to an alert box.

No 8. Layers

If you have objects that need to be hidden and shown then layers are the way to go. You need to enable the layers for the camera and any lights. CodePen link

camera.layers.enable(0);
camera.layers.enable(1);
camera.layers.enable(2);
  	
light.layers.enable( 0 );
light.layers.enable( 1 );
light.layers.enable( 2 );

Then you use the layers property of an Object3D. This has a set method when you choose which layer this object applies to.

object.layers.set( layer );//layer is 0, 1 or 2

Having done the setup you can use the camera layers property. The toggle method takes an index to the layer and toggles visibility on and off.

const options = {
  'toggle red': ()=>{ camera.layers.toggle(0); },
  'enable all': ()=>{ camera.layers.enableAll(); },
}
gui.add(options, 'toggle red');
gui.add(options, 'enable all');

Three.JS supports 32 layers using the integer indices 0-31.

No. 9 SimplifyModifier

Three.JS includes a SimplifyModifier class that will reduce the polygon count of a mesh. Unfortunately, currently this does not support textured objects, uv values are lost. In the example in the onload event I create a clone of the loaded object. The original is stored as object.original and the clone as object.simple. CodePen link.

loader.load(
  'Nefertiti.glb',
  gltf => {      
    object = { original: gltf.scene.children[0] };
    object.simple = object.original.clone();
    scene.add( object.simple );
  });

Only the simple version is added to the scene. The gui has a slider to choose from 0 to 0.9 in 0.1 steps. A change event calls the simplify function.

function simplify(value){
  object.simple.geometry.dispose();
  
  const count = Math.floor(  
    object.original.geometry.attributes.position.count * 
    value ); // number of vertices to remove
  object.simple.geometry = modifier.modify( 
    object.original.geometry, count );
}

Here we dispose of the simple object geometry. Calculate the vertex count to reduce by. Then set the new geometry using the SimplifyModifier’s modify method. This takes two parameters the source geometry and an integer value of the number of vertices to reduce the vertex count by.

No. 10 OrbitControls change event

Last up is a battery saver on mobile devices. If your scene is static but uses OrbitControls then don’t use a render loop, where the scene will be rendered up to 60 times a second, which is very demanding on your processor unnecessarily. Instead add an event listener to your OrbitControls instance, detecting a change event. Use this to render your scene. CodePen link.

controls.addEventListener( 'change', () => {
  renderer.render( scene, camera );
});

Here’s another 10.