Textures

Summary: A comprehensive guide to textures in Three.js covering how to create, load, and apply textures to materials, including memory usage considerations, filtering and mipmaps, and texture wrapping/repeating/offsetting/rotating settings.

Hello Texture

Textures are *generally* images that are most often created in some 3rd party program like Photoshop or GIMP. For example let's put this image on cube.

We'll modify one of our first samples. All we need to do is create a TextureLoader. Call its load method with the URL of an image and set the material's `map` property to the result instead of setting its `color`.

Note that we're using MeshBasicMaterial so no need for any lights.

const loader = new THREE.TextureLoader();
const texture = loader.load( 'resources/images/wall.jpg' );
texture.colorSpace = THREE.SRGBColorSpace;

const material = new THREE.MeshBasicMaterial({
  map: texture,
});

6 Textures, a different one on each face of a cube

How about 6 textures, one on each face of a cube?

We just make 6 materials and pass them as an array when we create the Mesh

It works!

It should be noted though that not all geometry types supports multiple materials. BoxGeometry can use 6 materials one for each face. ConeGeometry can use 2 materials, one for the bottom and one for the cone. CylinderGeometry can use 3 materials, bottom, top, and side. For other cases you will need to build or load custom geometry and/or modify texture coordinates.

It's far more common in other 3D engines and far more performant to use a Texture Atlas if you want to allow multiple images on a single geometry. A Texture atlas is where you put multiple images in a single texture and then use texture coordinates on the vertices of your geometry to select which parts of a texture are used on each triangle in your geometry.

What are texture coordinates? They are data added to each vertex of a piece of geometry that specify what part of the texture corresponds to that specific vertex. We'll go over them when we start building custom geometry.

const loader = new THREE.TextureLoader();
const materials = [
  new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-1.jpg')}),
  new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-2.jpg')}),
  new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-3.jpg')}),
  new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-4.jpg')}),
  new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-5.jpg')}),
  new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-6.jpg')}),
];
const cube = new THREE.Mesh(geometry, materials);

function loadColorTexture( path ) {
  const texture = loader.load( path );
  texture.colorSpace = THREE.SRGBColorSpace;
  return texture;
}

Loading Textures

The Easy Way

Most of the code on this site uses the easiest method of loading textures. We create a TextureLoader and then call its load method. This returns a Texture object.

It's important to note that using this method our texture will be transparent until the image is loaded asynchronously by three.js at which point it will update the texture with the downloaded image.

This has the big advantage that we don't have to wait for the texture to load and our page will start rendering immediately. That's probably okay for a great many use cases but if we want we can ask three.js to tell us when the texture has finished downloading.

const texture = loader.load('resources/images/flower-1.jpg');

Waiting for a texture to load

To wait for a texture to load the `load` method of the texture loader takes a callback that will be called when the texture has finished loading. Going back to our top example we can wait for the texture to load before creating our Mesh and adding it to scene like this

Unless you clear your browser's cache and have a slow connection you're unlikely to see the any difference but rest assured it is waiting for the texture to load.

const loader = new THREE.TextureLoader();
loader.load('resources/images/wall.jpg', (texture) => {
  texture.colorSpace = THREE.SRGBColorSpace;
  const material = new THREE.MeshBasicMaterial({
    map: texture,
  });
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);
  cubes.push(cube);  // add to our list of cubes to rotate
});

Waiting for multiple textures to load

To wait until all textures have loaded you can use a LoadingManager. Create one and pass it to the TextureLoader then set its onLoad property to a callback.

The LoadingManager also has an onProgress property we can set to another callback to show a progress indicator.

First we'll add a progress bar in HTML

and the CSS for it

Then in the code we'll update the scale of the `progressbar` in our `onProgress` callback. It gets called with the URL of the last item loaded, the number of items loaded so far, and the total number of items loaded.

Unless you clear your cache and have a slow connection you might not see the loading bar.

const loadManager = new THREE.LoadingManager();
const loader = new THREE.TextureLoader(loadManager);

const materials = [
  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
];

loadManager.onLoad = () => {
  const cube = new THREE.Mesh(geometry, materials);
  scene.add(cube);
  cubes.push(cube);  // add to our list of cubes to rotate
};

#loading {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}
#loading .progress {
    margin: 1.5em;
    border: 1px solid white;
    width: 50vw;
}
#loading .progressbar {
    margin: 2px;
    background: white;
    height: 1em;
    transform-origin: top left;
    transform: scaleX(0);
}

const loadingElem = document.querySelector('#loading');
const progressBarElem = loadingElem.querySelector('.progressbar');

loadManager.onLoad = () => {
  loadingElem.style.display = 'none';
  const cube = new THREE.Mesh(geometry, materials);
  scene.add(cube);
  cubes.push(cube);  // add to our list of cubes to rotate
};

loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => {
  const progress = itemsLoaded / itemsTotal;
  progressBarElem.style.transform = scaleX(${progress});
};

Loading textures from other origins

To use images from other servers those servers need to send the correct headers. If they don't you cannot use the images in three.js and will get an error. If you run the server providing the images make sure it sends the correct headers. If you don't control the server hosting the images and it does not send the permission headers then you can't use the images from that server.

For example imgur, flickr, and github all send headers allowing you to use images hosted on their servers in three.js. Most other websites do not.

Memory Usage

Textures are often the part of a three.js app that use the most memory. It's important to understand that *in general*, textures take `width * height * 4 * 1.33` bytes of memory.

Notice that says nothing about compression. I can make a .jpg image and set its compression super high. For example let's say I was making a scene of a house. Inside the house there is a table and I decide to put this wood texture on the top surface of the table

That image is only 157k so it will download relatively quickly but it is actually 3024 x 3761 pixels in size. Following the equation above that's

3024 * 3761 * 4 * 1.33 = 60505764.5

That image will take 60 MEG OF MEMORY! in three.js. A few textures like that and you'll be out of memory.

I bring this up because it's important to know that using textures has a hidden cost. In order for three.js to use the texture it has to hand it off to the GPU and the GPU *in general* requires the texture data to be uncompressed.

The moral of the story is make your textures small in dimensions not just small in file size. Small in file size = fast to download. Small in dimensions = takes less memory. How small should you make them? As small as you can and still look as good as you need them to look.

JPG vs PNG

This is pretty much the same as regular HTML in that JPGs have lossy compression, PNGs have lossless compression so PNGs are generally slower to download. But, PNGs support transparency. PNGs are also probably the appropriate format for non-image data like normal maps, and other kinds of non-image maps which we'll go over later.

It's important to remember that a JPG doesn't use less memory than a PNG in WebGL. See above.

Filtering and Mips

Let's apply this 16x16 texture

To a cube

Let's draw that cube really small

Hmmm, I guess that's hard to see. Let's magnify that tiny cube

How does the GPU know which colors to make each pixel it's drawing for the tiny cube? What if the cube was so small that it's just 1 or 2 pixels?

This is what filtering is about.

If it was Photoshop, Photoshop would average nearly all the pixels together to figure out what color to make those 1 or 2 pixels. That would be a very slow operation. GPUs solve this issue using mipmaps.

Mips are copies of the texture, each one half as wide and half as tall as the previous mip where the pixels have been blended to make the next smaller mip. Mips are created until we get all the way to a 1x1 pixel mip.

Now, when the cube is drawn so small that it's only 1 or 2 pixels large the GPU can choose to use just the smallest or next to smallest mip level to decide what color to make the tiny cube.

In three.js you can choose what happens both when the texture is drawn larger than its original size and what happens when it's drawn smaller than its original size.

For setting the filter when the texture is drawn larger than its original size you set `texture.magFilter` property to either `THREE.NearestFilter` or `THREE.LinearFilter`. `NearestFilter` means just pick the closet single pixel from the original texture. With a low resolution texture this gives you a very pixelated look like Minecraft.

`LinearFilter` means choose the 4 pixels from the texture that are closest to the where we should be choosing a color from and blend them in the appropriate proportions relative to how far away the actual point is from each of the 4 pixels.

Nearest vs Linear comparison shown.

For setting the filter when the texture is drawn smaller than its original size you set the `texture.minFilter` property to one of 6 values:

- `THREE.NearestFilter` — same as above, choose the closest pixel in the texture
- `THREE.LinearFilter` — same as above, choose 4 pixels from the texture and blend them
- `THREE.NearestMipmapNearestFilter` — choose the appropriate mip then choose one pixel
- `THREE.NearestMipmapLinearFilter` — choose 2 mips, choose one pixel from each, blend the 2 pixels
- `THREE.LinearMipmapNearestFilter` — chose the appropriate mip then choose 4 pixels and blend them
- `THREE.LinearMipmapLinearFilter` — choose 2 mips, choose 4 pixels from each and blend all 8 into 1 pixel

Here's an example showing all 6 settings

One thing to notice is the top left and top middle using `NearestFilter` and `LinearFilter` don't use the mips. Because of that they flicker in the distance because the GPU is picking pixels from the original texture. On the left just one pixel is chosen and in the middle 4 are chosen and blended but it's not enough come up with a good representative color. The other 4 strips do better with the bottom right, `LinearMipmapLinearFilter` being best.

If you click the picture above it will toggle between the texture we've been using above and a texture where every mip level is a different color.

This makes it more clear what is happening. You can see in the top left and top middle the first mip is used all the way into the distance. The top right and bottom middle you can clearly see where a different mip is used.

Switching back to the original texture you can see the bottom right is the smoothest, highest quality. You might ask why not always use that mode. The most obvious reason is sometimes you want things to be pixelated for a retro look or some other reason. The next most common reason is that reading 8 pixels and blending them is slower than reading 1 pixel and blending. While it's unlikely that a single texture is going to be the difference between fast and slow as we progress further into these articles we'll eventually have materials that use 4 or 5 textures all at once. 4 textures * 8 pixels per texture is looking up 32 pixels for ever pixel rendered. This can be especially important to consider on mobile devices.

Repeating, offseting, rotating, wrapping a texture

Textures have settings for repeating, offseting, and rotating a texture.

By default textures in three.js do not repeat. To set whether or not a texture repeats there are 2 properties, `wrapS` for horizontal wrapping and `wrapT` for vertical wrapping.

They can be set to one of:

- `THREE.ClampToEdgeWrapping` — the last pixel on each edge is repeated forever
- `THREE.RepeatWrapping` — the texture is repeated
- `THREE.MirroredRepeatWrapping` — the texture is mirrored and repeated

For example to turn on wrapping in both directions:

Repeating is set with the `repeat` property.

Offseting the texture can be done by setting the `offset` property. Textures are offset with units where 1 unit = 1 texture size. On other words 0 = no offset and 1 = offset one full texture amount.

Rotating the texture can be set by setting the `rotation` property in radians as well as the `center` property for choosing the center of rotation. It defaults to 0,0 which rotates from the bottom left corner. Like offset these units are in texture size so setting them to `.5, .5` would rotate around the center of the texture.

Let's modify the top sample above to play with these values

First we'll keep a reference to the texture so we can manipulate it

Then we'll use lil-gui again to provide a simple interface.

As we did in previous lil-gui examples we'll use a simple class to give lil-gui an object that it can manipulate in degrees but that will set a property in radians.

We also need a class that will convert from a string like `"123"` into a number like `123` since three.js requires numbers for enum settings like `wrapS` and `wrapT` but lil-gui only uses strings for enums.

Using those classes we can setup a simple GUI for the settings above

The last thing to note about the example is that if you change `wrapS` or `wrapT` on the texture you must also set `texture.needsUpdate` so three.js knows to apply those settings. The other settings are automatically applied.

This is only one step into the topic of textures. At some point we'll go over texture coordinates as well as 9 other types of textures that can be applied to materials.

For now let's move on to lights.

someTexture.wrapS = THREE.RepeatWrapping;
someTexture.wrapT = THREE.RepeatWrapping;

const timesToRepeatHorizontally = 4;
const timesToRepeatVertically = 2;
someTexture.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically);

const xOffset = .5;   // offset by half the texture
const yOffset = .25;  // offset by 1/4 the texture
someTexture.offset.set(xOffset, yOffset);

someTexture.center.set(.5, .5);
someTexture.rotation = THREE.MathUtils.degToRad(45);

const texture = loader.load('resources/images/wall.jpg');
const material = new THREE.MeshBasicMaterial({
  map: texture,
});

import {GUI} from 'three/addons/libs/lil-gui.module.min.js';

class DegRadHelper {
  constructor(obj, prop) {
    this.obj = obj;
    this.prop = prop;
  }
  get value() {
    return THREE.MathUtils.radToDeg(this.obj[this.prop]);
  }
  set value(v) {
    this.obj[this.prop] = THREE.MathUtils.degToDeg(v);
  }
}

class StringToNumberHelper {
  constructor(obj, prop) {
    this.obj = obj;
    this.prop = prop;
  }
  get value() {
    return this.obj[this.prop];
  }
  set value(v) {
    this.obj[this.prop] = parseFloat(v);
  }
}

const wrapModes = {
  'ClampToEdgeWrapping': THREE.ClampToEdgeWrapping,
  'RepeatWrapping': THREE.RepeatWrapping,
  'MirroredRepeatWrapping': THREE.MirroredRepeatWrapping,
};

function updateTexture() {
  texture.needsUpdate = true;
}

const gui = new GUI();
gui.add(new StringToNumberHelper(texture, 'wrapS'), 'value', wrapModes)
  .name('texture.wrapS')
  .onChange(updateTexture);
gui.add(new StringToNumberHelper(texture, 'wrapT'), 'value', wrapModes)
  .name('texture.wrapT')
  .onChange(updateTexture);
gui.add(texture.repeat, 'x', 0, 5, .01).name('texture.repeat.x');
gui.add(texture.repeat, 'y', 0, 5, .01).name('texture.repeat.y');
gui.add(texture.offset, 'x', -2, 2, .01).name('texture.offset.x');
gui.add(texture.offset, 'y', -2, 2, .01).name('texture.offset.y');
gui.add(texture.center, 'x', -.5, 1.5, .01).name('texture.center.x');
gui.add(texture.center, 'y', -.5, 1.5, .01).name('texture.center.y');
gui.add(new DegRadHelper(texture, 'rotation'), 'value', -360, 360)
  .name('texture.rotation');


Lights

Summary: A tutorial on how to use the various kinds of lights in three.js, covering AmbientLight, HemisphereLight, DirectionalLight, PointLight, SpotLight, and RectAreaLight, with progressively built code examples and interactive demos.

Introduction

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 and also the article on setting up your environment. The previous article was about textures.

Let's go over how to use the various kinds of lights in three.

Starting with one of our previous samples let's update the camera. We'll set the field of view to 45 degrees, the far plane to 100 units, and we'll move the camera 10 units up and 20 units back from the origin

Next let's add OrbitControls. OrbitControls let the user spin or orbit the camera around some point. The OrbitControls are an optional feature of three.js so first we need to include them in our page

Then we can use them. We pass the OrbitControls a camera to control and the DOM element to use to get input events

We also set the target to orbit around to 5 units above the origin and then call controls.update so the controls will use the new target.

Next up let's make some things to light up. First we'll make ground plane. We'll apply a tiny 2x2 pixel checkerboard texture that looks like this

First we load the texture, set it to repeating, set the filtering to nearest, and set how many times we want it to repeat. Since the texture is a 2x2 pixel checkerboard, by repeating and setting the repeat to half the size of the plane each check on the checkerboard will be exactly 1 unit large;

We then make a plane geometry, a material for the plane, and a mesh to insert it in the scene. Planes default to being in the XY plane but the ground is in the XZ plane so we rotate it.

Let's add a cube and a sphere so we have 3 things to light including the plane

Now that we have a scene to light up let's add lights!

*const fov = 45;
const aspect = 2;  // the canvas default
const near = 0.1;
*const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+camera.position.set(0, 10, 20);

import * as THREE from 'three';
+import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 5, 0);
controls.update();

const planeSize = 40;

const loader = new THREE.TextureLoader();
const texture = loader.load('resources/images/checker.png');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
texture.colorSpace = THREE.SRGBColorSpace;
const repeats = planeSize / 2;
texture.repeat.set(repeats, repeats);

const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
  map: texture,
  side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -.5;
scene.add(mesh);

{
  const cubeSize = 4;
  const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
  const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
  const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
  scene.add(mesh);
}
{
  const sphereRadius = 3;
  const sphereWidthDivisions = 32;
  const sphereHeightDivisions = 16;
  const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
  const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
  scene.add(mesh);
}

AmbientLight

First let's make an AmbientLight

Let's also make it so we can adjust the light's parameters. We'll use lil-gui again. To be able to adjust the color via lil-gui we need a small helper that presents a property to lil-gui that looks like a CSS hex color string (eg: #FF8844). Our helper will get the color from a named property, convert it to a hex string to offer to lil-gui. When lil-gui tries to set the helper's property we'll assign the result back to the light's color.

Here's the helper:

And here's our code setting up lil-gui

And here's the result (click here to open in a separate window)

Click and drag in the scene to orbit the camera.

Notice there is no definition. The shapes are flat. The AmbientLight effectively just multiplies the material's color by the light's color times the intensity.

That's it. It has no direction. This style of ambient lighting is actually not all that useful as lighting as it's 100% even so other than changing the color of everything in the scene it doesn't look much like lighting. What it does help with is making the darks not too dark.

const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.AmbientLight(color, intensity);
scene.add(light);

class ColorGUIHelper {
  constructor(object, prop) {
    this.object = object;
    this.prop = prop;
  }
  get value() {
    return '#' + this.object[this.prop].getHexString();
  }
  set value(hexString) {
    this.object[this.prop].set(hexString);
  }
}

color = materialColor * light.color * light.intensity;

HemisphereLight

Let's switch the code to a HemisphereLight. A HemisphereLight takes a sky color and a ground color and just multiplies the material's color between those 2 colors—the sky color if the surface of the object is pointing up and the ground color if the surface of the object is pointing down.

Here's the new code

Let's also update the lil-gui code to edit both colors

The result (click here to open in a separate window)

Notice again there is almost no definition, everything looks kind of flat. The HemisphereLight used in combination with another light can help give a nice kind of influence of the color of the sky and ground. In that way it's best used in combination with some other light or a substitute for an AmbientLight.

-const color = 0xFFFFFF;
+const skyColor = 0xB1E1FF;  // light blue
+const groundColor = 0xB97A20;  // brownish orange
const intensity = 1;
-const light = new THREE.AmbientLight(color, intensity);
+const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
scene.add(light);

const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
+gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('skyColor');
+gui.addColor(new ColorGUIHelper(light, 'groundColor'), 'value').name('groundColor');
gui.add(light, 'intensity', 0, 5, 0.01);

DirectionalLight

Let's switch the code to a DirectionalLight. A DirectionalLight is often used to represent the sun.

Notice that we had to add the light and the light.target to the scene. A three.js DirectionalLight will shine in the direction of its target.

Let's make it so we can move the target by adding it to our GUI.

It's kind of hard to see what's going on. Three.js has a bunch of helper objects we can add to our scene to help visualize invisible parts of a scene. In this case we'll use the DirectionalLightHelper which will draw a plane, to represent the light, and a line from the light to the target. We just pass it the light and add it to the scene.

While we're at it let's make it so we can set both the position of the light and the target. To do this we'll make a function that given a Vector3 will adjust its x, y, and z properties using lil-gui.

Note that we need to call the helper's update function anytime we change something so the helper knows to update itself. As such we pass in an onChangeFn function to get called anytime lil-gui updates a value.

Then we can use that for both the light's position and the target's position like this

Now we can move the light, and its target (click here to open in a separate window)

Orbit the camera and it gets easier to see. The plane represents a DirectionalLight because a directional light computes light coming in one direction. There is no point the light comes from, it's an infinite plane of light shooting out parallel rays of light.

const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
gui.add(light, 'intensity', 0, 5, 0.01);
gui.add(light.target.position, 'x', -10, 10);
gui.add(light.target.position, 'z', -10, 10);
gui.add(light.target.position, 'y', 0, 10);

const helper = new THREE.DirectionalLightHelper(light);
scene.add(helper);

function makeXYZGUI(gui, vector3, name, onChangeFn) {
  const folder = gui.addFolder(name);
  folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
  folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
  folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
  folder.open();
}

+function updateLight() {
+  light.target.updateMatrixWorld();
+  helper.update();
+}
+updateLight();

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
gui.add(light, 'intensity', 0, 5, 0.01);

+makeXYZGUI(gui, light.position, 'position', updateLight);
+makeXYZGUI(gui, light.target.position, 'target', updateLight);

PointLight

A PointLight is a light that sits at a point and shoots light in all directions from that point. Let's change the code.

Let's also switch to a PointLightHelper

and as there is no target the onChange function can be simpler.

Note that at some level a PointLightHelper has no um, point. It just draws a small wireframe diamond. It could just as easily be any shape you want, just add a mesh to the light itself.

A PointLight has the added property of distance. If the distance is 0 then the PointLight shines to infinity. If the distance is greater than 0 then the light shines its full intensity at the light and fades to no influence at distance units away from the light.

Let's setup the GUI so we can adjust the distance.

And now try it out (click here to open in a separate window)

Notice when distance is > 0 how the light fades out.

const color = 0xFFFFFF;
-const intensity = 1;
+const intensity = 150;
-const light = new THREE.DirectionalLight(color, intensity);
+const light = new THREE.PointLight(color, intensity);
light.position.set(0, 10, 0);
-light.target.position.set(-5, 0, 0);
scene.add(light);
-scene.add(light.target);

-const helper = new THREE.DirectionalLightHelper(light);
+const helper = new THREE.PointLightHelper(light);
scene.add(helper);

function updateLight() {
-  light.target.updateMatrixWorld();
  helper.update();
}
-updateLight();

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
gui.add(light, 'intensity', 0, 250, 1);
+gui.add(light, 'distance', 0, 40).onChange(updateLight);

makeXYZGUI(gui, light.position, 'position', updateLight);
-makeXYZGUI(gui, light.target.position, 'target', updateLight);

SpotLight

Spotlights are effectively a point light with a cone attached where the light only shines inside the cone. There's actually 2 cones. An outer cone and an inner cone. Between the inner cone and the outer cone the light fades from full intensity to zero.

To use a SpotLight we need a target just like the directional light. The light's cone will open toward the target.

Modifying our DirectionalLight with helper from above

The spotlight's cone's angle is set with the angle property in radians. We'll use our DegRadHelper from the texture article to present a UI in degrees.

The inner cone is defined by setting the penumbra property as a percentage from the outer cone. In other words when penumbra is 0 then the inner cone is the same size (0 = no difference) from the outer cone. When the penumbra is 1 then the light fades starting in the center of the cone to the outer cone. When penumbra is .5 then the light fades starting from 50% between the center of the outer cone.

Notice with the default penumbra of 0 the spotlight has a very sharp edge whereas as you adjust the penumbra toward 1 the edge blurs.

It might be hard to see the cone of the spotlight. The reason is it's below the ground. Shorten the distance to around 5 and you'll see the open end of the cone.

const color = 0xFFFFFF;
-const intensity = 1;
+const intensity = 150;
-const light = new THREE.DirectionalLight(color, intensity);
+const light = new THREE.SpotLight(color, intensity);
scene.add(light);
scene.add(light.target);

-const helper = new THREE.DirectionalLightHelper(light);
+const helper = new THREE.SpotLightHelper(light);
scene.add(helper);

gui.add(new DegRadHelper(light, 'angle'), 'value', 0, 90).name('angle').onChange(updateLight);

RectAreaLight

There's one more type of light, the RectAreaLight, which represents exactly what it sounds like, a rectangular area of light like a long fluorescent light or maybe a frosted sky light in a ceiling.

The RectAreaLight only works with the MeshStandardMaterial and the MeshPhysicalMaterial so let's change all our materials to MeshStandardMaterial

To use the RectAreaLight we need to include some extra three.js optional data and we'll include the RectAreaLightHelper to help us visualize the light

and we need to call RectAreaLightUniformsLib.init

If you forget the data the light will still work but it will look funny so be sure to remember to include the extra data.

Now we can create the light

One thing to notice is that unlike the DirectionalLight and the SpotLight, the RectAreaLight does not use a target. It just uses its rotation. Another thing to notice is the helper needs to be a child of the light. It is not a child of the scene like other helpers.

Let's also adjust the GUI. We'll make it so we can rotate the light and adjust its width and height

And here is that (click here to open in a separate window)

It's important to note each light you add to the scene slows down how fast three.js renders the scene so you should always try to use as few as possible to achieve your goals.

Next up let's go over dealing with cameras.

...

  const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-  const planeMat = new THREE.MeshPhongMaterial({
+  const planeMat = new THREE.MeshStandardMaterial({
    map: texture,
    side: THREE.DoubleSide,
  });
  const mesh = new THREE.Mesh(planeGeo, planeMat);
  mesh.rotation.x = Math.PI * -.5;
  scene.add(mesh);
}
{
  const cubeSize = 4;
  const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
- const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
+ const cubeMat = new THREE.MeshStandardMaterial({color: '#8AC'});
  const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
  scene.add(mesh);
}
{
  const sphereRadius = 3;
  const sphereWidthDivisions = 32;
  const sphereHeightDivisions = 16;
  const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-  const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
+ const sphereMat = new THREE.MeshStandardMaterial({color: '#CA8'});
  const mesh = new THREE.Mesh(sphereGeo, sphereMat);
  mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
  scene.add(mesh);
}

import * as THREE from 'three';
+import {RectAreaLightUniformsLib} from 'three/addons/lights/RectAreaLightUniformsLib.js';
+import {RectAreaLightHelper} from 'three/addons/helpers/RectAreaLightHelper.js';

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
+  RectAreaLightUniformsLib.init();

const color = 0xFFFFFF;
*const intensity = 5;
+const width = 12;
+const height = 4;
*const light = new THREE.RectAreaLight(color, intensity, width, height);
light.position.set(0, 10, 0);
+light.rotation.x = THREE.MathUtils.degToRad(-90);
scene.add(light);

*const helper = new RectAreaLightHelper(light);
*light.add(helper);

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
gui.add(light, 'intensity', 0, 10, 0.01);
gui.add(light, 'width', 0, 20);
gui.add(light, 'height', 0, 20);
gui.add(new DegRadHelper(light.rotation, 'x'), 'value', -180, 180).name('x rotation');
gui.add(new DegRadHelper(light.rotation, 'y'), 'value', -180, 180).name('y rotation');
gui.add(new DegRadHelper(light.rotation, 'z'), 'value', -180, 180).name('z rotation');

makeXYZGUI(gui, light.position, 'position');


Cameras

Summary: An in-depth guide to cameras in three.js, covering PerspectiveCamera and OrthographicCamera, frustum concepts, depth buffer precision issues (z-fighting), and practical examples including split-screen views and 2D rendering.

Cameras

This article is one 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.

Let's talk about cameras in three.js. We covered some of this in the first article but we'll cover it in more detail here.

The most common camera in three.js and the one we've been using up to this point is the PerspectiveCamera. It gives a 3d view where things in the distance appear smaller than things up close.

The PerspectiveCamera defines a frustum. A frustum is a solid pyramid shape with the tip cut off. By name of a solid I mean for example a cube, a cone, a sphere, a cylinder, and a frustum are all names of different kinds of solids.

cube
cone
sphere
cylinder
frustum

I only point that out because I didn't know it for years. Some book or page would mention frustum and my eyes would glaze over. Understanding it's the name of a type of solid shape made those descriptions suddenly make more sense 😅

A PerspectiveCamera defines its frustum based on 4 properties. near defines where the front of the frustum starts. far defines where it ends. fov, the field of view, defines how tall the front and back of the frustum are by computing the correct height to get the specified field of view at near units from the camera. The aspect defines how wide the front and back of the frustum are. The width of the frustum is just the height multiplied by the aspect.

Adjusting Camera Settings with GUI

Let's use the scene from the previous article that has a ground plane, a sphere, and a cube and make it so we can adjust the camera's settings.

To do that we'll make a MinMaxGUIHelper for the near and far settings so far is always greater than near. It will have min and max properties that lil-gui will adjust. When adjusted they'll set the 2 properties we specify.

Now we can setup our GUI like this.

Anytime the camera's settings change we need to call the camera's updateProjectionMatrix function so we made a function called updateCamera add passed it to lil-gui to call it when things change.

You can adjust the values and see how they work. Note we didn't make aspect settable since it's taken from the size of the window so if you want to adjust the aspect open the example in a new window and then size the window.

class MinMaxGUIHelper {
  constructor(obj, minProp, maxProp, minDif) {
    this.obj = obj;
    this.minProp = minProp;
    this.maxProp = maxProp;
    this.minDif = minDif;
  }
  get min() {
    return this.obj[this.minProp];
  }
  set min(v) {
    this.obj[this.minProp] = v;
    this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
  }
  get max() {
    return this.obj[this.maxProp];
  }
  set max(v) {
    this.obj[this.maxProp] = v;
    this.min = this.min;  // this will call the min setter
  }
}

function updateCamera() {
  camera.updateProjectionMatrix();
}

const gui = new GUI();
gui.add(camera, 'fov', 1, 180).onChange(updateCamera);
const minMaxGUIHelper = new MinMaxGUIHelper(camera, 'near', 'far', 0.1);
gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);

Two Cameras with Scissor and CameraHelper

Still, I think it's a little hard to see so let's change the example so it has 2 cameras. One will show our scene as we see it above, the other will show another camera looking at the scene the first camera is drawing and showing that camera's frustum.

To do this we can use the scissor function of three.js. Let's change it to draw 2 scenes with 2 cameras side by side using the scissor function.

First off let's use some HTML and CSS to define 2 side by side elements. This will also help us with events so both cameras can easily have their own OrbitControls.

And the CSS that will make those 2 views show up side by side overlaid on top of the canvas.

Then in our code we'll add a CameraHelper. A CameraHelper draws the frustum for a Camera.

Now let's look up the 2 view elements.

And we'll set our existing OrbitControls to respond to the first view element only.

Let's make a second PerspectiveCamera and a second OrbitControls. The second OrbitControls is tied to the second camera and gets input from the second view element.

Finally we need to render the scene from the point of view of each camera using the scissor function to only render to part of the canvas.

Here is a function that given an element will compute the rectangle of that element that overlaps the canvas. It will then set the scissor and viewport to that rectangle and return the aspect for that size.

And now we can use that function to draw the scene twice in our render function.

The code above sets the background color of the scene when rendering the second view to dark blue just to make it easier to distinguish the two views.

We can also remove our updateCamera code since we're updating everything in the render function.

And now you can use one view to see the frustum of the other.

On the left you can see the original view and on the right you can see a view showing the frustum of the camera on the left. As you adjust near, far, fov and move the camera with mouse you can see that only what's inside the frustum shown on the right appears in the scene on the left.

Adjust near up to around 20 and you'll easily see the front of objects disappear as they are no longer in the frustum. Adjust far below about 35 and you'll start to see the ground plane disappear as it's no longer in the frustum.

This brings up the question, why not just set near to 0.0000000001 and far to 10000000000000 or something like that so you can just see everything? The reason is your GPU only has so much precision to decide if something is in front or behind something else. That precision is spread out between near and far. Worse, by default the precision close the camera is detailed and the precision far from the camera is coarse. The units start with near and slowly expand as they approach far.

.split {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;
}
.split>div {
  width: 100%;
  height: 100%;
}

const cameraHelper = new THREE.CameraHelper(camera);

...

scene.add(cameraHelper);

const view1Elem = document.querySelector('#view1');
const view2Elem = document.querySelector('#view2');

-const controls = new OrbitControls(camera, canvas);
+const controls = new OrbitControls(camera, view1Elem);

const camera2 = new THREE.PerspectiveCamera(
  60,  // fov
  2,   // aspect
  0.1, // near
  500, // far
);
camera2.position.set(40, 10, 30);
camera2.lookAt(0, 5, 0);

const controls2 = new OrbitControls(camera2, view2Elem);
controls2.target.set(0, 5, 0);
controls2.update();

function setScissorForElement(elem) {
  const canvasRect = canvas.getBoundingClientRect();
  const elemRect = elem.getBoundingClientRect();

  // compute a canvas relative rectangle
  const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
  const left = Math.max(0, elemRect.left - canvasRect.left);
  const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
  const top = Math.max(0, elemRect.top - canvasRect.top);

  const width = Math.min(canvasRect.width, right - left);
  const height = Math.min(canvasRect.height, bottom - top);

  // setup the scissor to only render to that part of the canvas
  const positiveYUpBottom = canvasRect.height - bottom;
  renderer.setScissor(left, positiveYUpBottom, width, height);
  renderer.setViewport(left, positiveYUpBottom, width, height);

  // return the aspect
  return width / height;
}

function render() {

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

+    resizeRendererToDisplaySize(renderer);
+
+    // turn on the scissor
+    renderer.setScissorTest(true);
+
+    // render the original view
+    {
+      const aspect = setScissorForElement(view1Elem);
+
+      // adjust the camera for this aspect
+      camera.aspect = aspect;
+      camera.updateProjectionMatrix();
+      cameraHelper.update();
+
+      // don't draw the camera helper in the original view
+      cameraHelper.visible = false;
+
+      scene.background.set(0x000000);
+
+      // render
+      renderer.render(scene, camera);
+    }
+
+    // render from the 2nd camera
+    {
+      const aspect = setScissorForElement(view2Elem);
+
+      // adjust the camera for this aspect
+      camera2.aspect = aspect;
+      camera2.updateProjectionMatrix();
+
+      // draw the camera helper in the 2nd view
+      cameraHelper.visible = true;
+
+      scene.background.set(0x000040);
+
+      renderer.render(scene, camera2);
+    }

-    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }

  requestAnimationFrame(render);
}

-function updateCamera() {
-  camera.updateProjectionMatrix();
-}

const gui = new GUI();
-gui.add(camera, 'fov', 1, 180).onChange(updateCamera);
+gui.add(camera, 'fov', 1, 180);
const minMaxGUIHelper = new MinMaxGUIHelper(camera, 'near', 'far', 0.1);
-gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
-gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
+gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near');
+gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far');

Z-Fighting and Depth Precision

Starting with the top example, let's change the code to insert 20 spheres in a row.

and let's set near to 0.00001

We also need to tweak the GUI code a little to allow 0.00001 if the value is edited

What do you think will happen?

This is an example of z fighting where the GPU on your computer does not have enough precision to decide which pixels are in front and which pixels are behind.

One solution is to tell three.js use to a different method to compute which pixels are in front and which are behind. We can do that by enabling logarithmicDepthBuffer when we create the WebGLRenderer.

If this didn't fix the issue for you then you've run into one reason why you can't always use this solution. That reason is because only certain GPUs support it. As of September 2018 almost no mobile devices support this solution whereas most desktops do.

Another reason not to choose this solution is it can be significantly slower than the standard solution.

Even with this solution there is still limited resolution. Make near even smaller or far even bigger and you'll eventually run into the same issues.

What that means is that you should always make an effort to choose a near and far setting that fits your use case. Set near as far away from the camera as you can and not have things disappear. Set far as close to the camera as you can and not have things disappear. If you're trying to draw a giant scene and show a close up of someone's face so you can see their eyelashes while in the background you can see all the way to mountains 50 kilometers in the distance well then you'll need to find other creative solutions that maybe we'll go over later. For now, just be aware you should take care to choose appropriate near and far values for your needs.

{
  const sphereRadius = 3;
  const sphereWidthDivisions = 32;
  const sphereHeightDivisions = 16;
  const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
  const numSpheres = 20;
  for (let i = 0; i < numSpheres; ++i) {
    const sphereMat = new THREE.MeshPhongMaterial();
    sphereMat.color.setHSL(i * .73, 1, 0.5);
    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, i * sphereRadius * -2.2);
    scene.add(mesh);
  }
}

const fov = 45;
const aspect = 2;  // the canvas default
-const near = 0.1;
+const near = 0.00001;
const far = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

-gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
+gui.add(minMaxGUIHelper, 'min', 0.00001, 50, 0.00001).name('near').onChange(updateCamera);

-const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
+const renderer = new THREE.WebGLRenderer({
+  antialias: true,
+  canvas,
+  logarithmicDepthBuffer: true,
+});

OrthographicCamera

The 2nd most common camera is the OrthographicCamera. Rather than specify a frustum it specifies a box with the settings left, right top, bottom, near, and far. Because it's projecting a box there is no perspective.

Let's change the 2 view example above to use an OrthographicCamera in the first view.

First let's setup an OrthographicCamera.

We set left and bottom to -1 and right and top to 1. This would make a box 2 units wide and 2 units tall but we're going to adjust the left and top by the aspect of the rectangle we're drawing to. We'll use the zoom property to make it easy to adjust how many units are actually shown by the camera.

Let's add a GUI setting for zoom

The call to listen tells lil-gui to watch for changes. This is here because the OrbitControls can also control zoom. For example the scroll wheel on a mouse will zoom via the OrbitControls.

Last we just need to change the part that renders the left side to update the OrthographicCamera.

and now you can see an OrthographicCamera at work.

An OrthographicCamera is most often used if using three.js to draw 2D things. You'd decide how many units you want the camera to show. For example if you want one pixel of canvas to match one unit in the camera you could do something like

To put the origin at the center and have 1 pixel = 1 three.js unit something like

Or if we wanted the origin to be in the top left just like a 2D canvas we could use this

In which case the top left corner would be 0,0 just like a 2D canvas

Let's try it! First let's set the camera up

Then let's load 6 textures and make 6 planes, one for each texture. We'll parent each plane to a THREE.Object3D to make it easy to offset the plane so its center appears to be at its top left corner.

If you're running locally you'll also need to have setup. You might also want to read about using textures.

and we need to update the camera if the size of the canvas changes.

planes is an array of THREE.Mesh, one for each plane. Let's move them around based on the time.

And you can see the images bounce pixel perfect off the edges of the canvas using pixel math just like a 2D canvas

Another common use for an OrthographicCamera is to draw the up, down, left, right, front, back views of a 3D modeling program or a game engine's editor.

In the screenshot above you can see 1 view is a perspective view and 3 views are orthographic views.

That's the fundamentals of cameras. We'll cover a few common ways to move cameras in other articles. For now let's move on to shadows.

const left = -1;
const right = 1;
const top = 1;
const bottom = -1;
const near = 5;
const far = 50;
const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
camera.zoom = 0.2;

const gui = new GUI();
+gui.add(camera, 'zoom', 0.01, 1, 0.01).listen();

{
  const aspect = setScissorForElement(view1Elem);

  // update the camera for this aspect
-  camera.aspect = aspect;
+  camera.left   = -aspect;
+  camera.right  =  aspect;
  camera.updateProjectionMatrix();
  cameraHelper.update();

  // don't draw the camera helper in the original view
  cameraHelper.visible = false;

  scene.background.set(0x000000);
  renderer.render(scene, camera);
}

camera.left = -canvas.width / 2;
camera.right = canvas.width / 2;
camera.top = canvas.height / 2;
camera.bottom = -canvas.height / 2;
camera.near = -1;
camera.far = 1;
camera.zoom = 1;

camera.left = 0;
camera.right = canvas.width;
camera.top = 0;
camera.bottom = canvas.height;
camera.near = -1;
camera.far = 1;
camera.zoom = 1;

const left = 0;
const right = 300;  // default canvas size
const top = 0;
const bottom = 150;  // default canvas size
const near = -1;
const far = 1;
const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
camera.zoom = 1;

const loader = new THREE.TextureLoader();
const textures = [
  loader.load('resources/images/flower-1.jpg'),
  loader.load('resources/images/flower-2.jpg'),
  loader.load('resources/images/flower-3.jpg'),
  loader.load('resources/images/flower-4.jpg'),
  loader.load('resources/images/flower-5.jpg'),
  loader.load('resources/images/flower-6.jpg'),
];
const planeSize = 256;
const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
const planes = textures.map((texture) => {
  const planePivot = new THREE.Object3D();
  scene.add(planePivot);
  texture.magFilter = THREE.NearestFilter;
  const planeMat = new THREE.MeshBasicMaterial({
    map: texture,
    side: THREE.DoubleSide,
  });
  const mesh = new THREE.Mesh(planeGeo, planeMat);
  planePivot.add(mesh);
  // move plane so top left corner is origin
  mesh.position.set(planeSize / 2, planeSize / 2, 0);
  return planePivot;
});

function render() {

  if (resizeRendererToDisplaySize(renderer)) {
    camera.right = canvas.width;
    camera.bottom = canvas.height;
    camera.updateProjectionMatrix();
  }

  ...

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

  ...

  const distAcross = Math.max(20, canvas.width - planeSize);
  const distDown = Math.max(20, canvas.height - planeSize);

  // total distance to move across and back
  const xRange = distAcross * 2;
  const yRange = distDown * 2;
  const speed = 180;

  planes.forEach((plane, ndx) => {
    // compute a unique time for each plane
    const t = time * speed + ndx * 300;

    // get a value between 0 and range
    const xt = t % xRange;
    const yt = t % yRange;

    // set our position going forward if 0 to half of range
    // and backward if half of range to range
    const x = xt < distAcross ? xt : xRange - xt;
    const y = yt < distDown   ? yt : yRange - yt;

    plane.position.set(x, y, 0);
  });

  renderer.render(scene, camera);


Shadows

Summary: A comprehensive guide to shadows in three.js, covering fake shadow techniques using textures, shadow maps for DirectionalLight, SpotLight, and PointLight, including configuring shadow cameras, shadow map resolution, and troubleshooting common issues like missing shadows and shadow acne.

Shadows

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. The previous article was about cameras which is important to have read before you read this article as well as the article before that one about lights.

Shadows on computers can be a complicated topic. There are various solutions and all of them have tradeoffs including the solutions available in three.js.

Three.js by default uses shadow maps. The way a shadow map works is, for every light that casts shadows all objects marked to cast shadows are rendered from the point of view of the light. READ THAT AGAIN! and let it sink in.

In other words, if you have 20 objects, and 5 lights, and all 20 objects are casting shadows and all 5 lights are casting shadows then your entire scene will be drawn 6 times. All 20 objects will be drawn for light #1, then all 20 objects will be drawn for light #2, then #3, etc and finally the actual scene will be drawn using data from the first 5 renders.

It gets worse, if you have a point light casting shadows the scene has to be drawn 6 times just for that light!

For these reasons it's common to find other solutions than to have a bunch of lights all generating shadows. One common solution is to have multiple lights but only one directional light generating shadows.

Yet another solution is to use lightmaps and or ambient occlusion maps to pre-compute the effects of lighting offline. This results in static lighting or static lighting hints but at least it's fast. We'll cover both of those in another article.

Another solution is to use fake shadows. Make a plane, put a grayscale texture in the plane that approximates a shadow, draw it above the ground below your object.

For example let's use this texture as a fake shadow

const scene = new THREE.Scene();
scene.background = new THREE.Color('white');

Setting up the scene with checkerboard ground

We'll use some of the code from the previous article.

Let's set the background color to white.

Then we'll setup the same checkerboard ground but this time it's using a MeshBasicMaterial as we don't need lighting for the ground.

Note we're setting the color to 1.5, 1.5, 1.5. This will multiply the checkerboard texture's colors by 1.5, 1.5, 1.5. Since the texture's colors are 0x808080 and 0xC0C0C0 which is medium gray and light gray, multiplying them by 1.5 will give us a white and light grey checkerboard.

const loader = new THREE.TextureLoader();

{
  const planeSize = 40;

  const loader = new THREE.TextureLoader();
  const texture = loader.load('resources/images/checker.png');
  texture.wrapS = THREE.RepeatWrapping;
  texture.wrapT = THREE.RepeatWrapping;
  texture.magFilter = THREE.NearestFilter;
  const repeats = planeSize / 2;
  texture.repeat.set(repeats, repeats);

  const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
  const planeMat = new THREE.MeshBasicMaterial({
    map: texture,
    side: THREE.DoubleSide,
  });
  planeMat.color.setRGB(1.5, 1.5, 1.5);
  const mesh = new THREE.Mesh(planeGeo, planeMat);
  mesh.rotation.x = Math.PI * -.5;
  scene.add(mesh);
}

Loading the shadow texture

Let's load the shadow texture and make an array to remember each sphere and associated objects.

Then we'll make a sphere geometry and a plane geometry for the fake shadow.

const shadowTexture = loader.load('resources/images/roundshadow.png');

const sphereRadius = 1;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);

const planeSize = 1;
const shadowGeo = new THREE.PlaneGeometry(planeSize, planeSize);

Creating spheres with fake shadows

Now we'll make a bunch of spheres. For each sphere we'll create a base THREE.Object3D and we'll make both the shadow plane mesh and the sphere mesh children of the base. That way if we move the base both the sphere and the shadow will move. We need to put the shadow slightly above the ground to prevent z-fighting. We also set depthWrite to false so that the shadows don't mess each other up. We'll go over both of these issues in another article. The shadow is a MeshBasicMaterial because it doesn't need lighting.

We make each sphere a different hue and then save off the base, the sphere mesh, the shadow mesh and the initial y position of each sphere.

const numSpheres = 15;
for (let i = 0; i < numSpheres; ++i) {
  // make a base for the shadow and the sphere
  // so they move together.
  const base = new THREE.Object3D();
  scene.add(base);

  // add the shadow to the base
  // note: we make a new material for each sphere
  // so we can set that sphere's material transparency
  // separately.
  const shadowMat = new THREE.MeshBasicMaterial({
    map: shadowTexture,
    transparent: true,    // so we can see the ground
    depthWrite: false,    // so we don't have to sort
  });
  const shadowMesh = new THREE.Mesh(shadowGeo, shadowMat);
  shadowMesh.position.y = 0.001;  // so we're above the ground slightly
  shadowMesh.rotation.x = Math.PI * -.5;
  const shadowSize = sphereRadius * 4;
  shadowMesh.scale.set(shadowSize, shadowSize, shadowSize);
  base.add(shadowMesh);

  // add the sphere to the base
  const u = i / numSpheres;   // goes from 0 to 1 as we iterate the spheres.
  const sphereMat = new THREE.MeshPhongMaterial();
  sphereMat.color.setHSL(u, 1, .75);
  const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
  sphereMesh.position.set(0, sphereRadius + 2, 0);
  base.add(sphereMesh);

  // remember all 3 plus the y position
  sphereShadowBases.push({base, sphereMesh, shadowMesh, y: sphereMesh.position.y});
}

Setting up lights

We setup 2 lights. One is a HemisphereLight with the intensity set to 2 to really brighten things up. The other is a DirectionalLight so the spheres get some definition.

{
  const skyColor = 0xB1E1FF;  // light blue
  const groundColor = 0xB97A20;  // brownish orange
  const intensity = 2;
  const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
  scene.add(light);
}

{
  const color = 0xFFFFFF;
  const intensity = 1;
  const light = new THREE.DirectionalLight(color, intensity);
  light.position.set(0, 10, 5);
  light.target.position.set(-5, 0, 0);
  scene.add(light);
  scene.add(light.target);
}

Animating the spheres and shadows

It would render as is but let's animate there spheres. For each sphere, shadow, base set we move the base in the xz plane, we move the sphere up and down using Math.abs(Math.sin(time)) which gives us a bouncy animation. And, we also set the shadow material's opacity so that as each sphere goes higher its shadow fades out.

And here's 15 kind of bouncing balls.

In some apps it's common to use a round or oval shadow for everything but of course you could also use different shaped shadow textures. You might also give the shadow a harder edge. A good example of using this type of shadow is Animal Crossing Pocket Camp where you can see each character has a simple round shadow. It's effective and cheap. Monument Valley appears to also use this kind of shadow for the main character.

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

  ...

  sphereShadowBases.forEach((sphereShadowBase, ndx) => {
    const {base, sphereMesh, shadowMesh, y} = sphereShadowBase;

    // u is a value that goes from 0 to 1 as we iterate the spheres
    const u = ndx / sphereShadowBases.length;

    // compute a position for the base. This will move
    // both the sphere and its shadow
    const speed = time * .2;
    const angle = speed + u * Math.PI * 2 * (ndx % 1 ? 1 : -1);
    const radius = Math.sin(speed - ndx) * 10;
    base.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);

    // yOff is a value that goes from 0 to 1
    const yOff = Math.abs(Math.sin(time * 2 + ndx));
    // move the sphere up and down
    sphereMesh.position.y = y + THREE.MathUtils.lerp(-2, 2, yOff);
    // fade the shadow as the sphere goes up
    shadowMesh.material.opacity = THREE.MathUtils.lerp(1, .25, yOff);
  });

  ...

Shadow Maps Overview

So, moving on to shadow maps, there are 3 lights which can cast shadows. The DirectionalLight, the PointLight, and the SpotLight.

Let's start with the DirectionalLight with the helper example from the lights article.

Enabling shadows in the renderer

The first thing we need to do is turn on shadows in the renderer. Then we also need to tell the light to cast a shadow.

We also need to go to each mesh in the scene and decide if it should both cast shadows and/or receive shadows.

Let's make the plane (the ground) only receive shadows since we don't really care what happens underneath. For the cube and the sphere let's have them both receive and cast shadows.

And then we run it.

const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
renderer.shadowMap.enabled = true;

const light = new THREE.DirectionalLight(color, intensity);
light.castShadow = true;

const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.receiveShadow = true;

const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.castShadow = true;
mesh.receiveShadow = true;

...

const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.castShadow = true;
mesh.receiveShadow = true;

Debugging missing shadows

What happened? Why are parts of the shadows missing?

The reason is shadow maps are created by rendering the scene from the point of view of the light. In this case there is a camera at the DirectionalLight that is looking at its target. Just like the camera's we previously covered the light's shadow camera defines an area inside of which the shadows get rendered. In the example above that area is too small.

In order to visualize that area we can get the light's shadow camera and add a CameraHelper to the scene.

And now you can see the area for which shadows are cast and received.

Adjust the target x value back and forth and it should be pretty clear that only what's inside the light's shadow camera box is where shadows are drawn.

const cameraHelper = new THREE.CameraHelper(light.shadow.camera);
scene.add(cameraHelper);

Adjusting the shadow camera

We can adjust the size of that box by adjusting the light's shadow camera.

Let's add some GUI setting to adjust the light's shadow camera box. Since a DirectionalLight represents light all going in a parallel direction, the DirectionalLight uses an OrthographicCamera for its shadow camera. We went over how an OrthographicCamera works in the previous article about cameras.

Recall an OrthographicCamera defines its box or view frustum by its left, right, top, bottom, near, far, and zoom properties.

Again let's make a helper class for the lil-gui. We'll make a DimensionGUIHelper that we'll pass an object and 2 properties. It will present one property that lil-gui can adjust and in response will set the two properties one positive and one negative. We can use this to set left and right as width and up and down as height.

We'll also use the MinMaxGUIHelper we created in the camera article to adjust near and far.

We tell the GUI to call our updateCamera function anytime anything changes. Let's write that function to update the light, the helper for the light, the light's shadow camera, and the helper showing the light's shadow camera.

And now that we've given the light's shadow camera a GUI we can play with the values.

Set the width and height to about 30 and you can see the shadows are correct and the areas that need to be in shadow for this scene are entirely covered.

class DimensionGUIHelper {
  constructor(obj, minProp, maxProp) {
    this.obj = obj;
    this.minProp = minProp;
    this.maxProp = maxProp;
  }
  get value() {
    return this.obj[this.maxProp] * 2;
  }
  set value(v) {
    this.obj[this.maxProp] = v /  2;
    this.obj[this.minProp] = v / -2;
  }
}

const gui = new GUI();
gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
gui.add(light, 'intensity', 0, 2, 0.01);
{
  const folder = gui.addFolder('Shadow Camera');
  folder.open();
  folder.add(new DimensionGUIHelper(light.shadow.camera, 'left', 'right'), 'value', 1, 100)
    .name('width')
    .onChange(updateCamera);
  folder.add(new DimensionGUIHelper(light.shadow.camera, 'bottom', 'top'), 'value', 1, 100)
    .name('height')
    .onChange(updateCamera);
  const minMaxGUIHelper = new MinMaxGUIHelper(light.shadow.camera, 'near', 'far', 0.1);
  folder.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near').onChange(updateCamera);
  folder.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far').onChange(updateCamera);
  folder.add(light.shadow.camera, 'zoom', 0.01, 1.5, 0.01).onChange(updateCamera);
}

function updateCamera() {
  // update the light target's matrixWorld because it's needed by the helper
  light.target.updateMatrixWorld();
  helper.update();
  // update the light's shadow camera's projection matrix
  light.shadow.camera.updateProjectionMatrix();
  // and now update the camera helper we're using to show the light's shadow camera
  cameraHelper.update();
}
updateCamera();

Shadow map resolution

But this brings up the question, why not just set width and height to some giant numbers to just cover everything? Set the width and height to 100 and you might see something like this

What's going on with these low-res shadows?!

This issue is yet another shadow related setting to be aware of. Shadow maps are textures the shadows get drawn into. Those textures have a size. The shadow camera's area we set above is stretched across that size. That means the larger area you set, the more blocky your shadows will be.

You can set the resolution of the shadow map's texture by setting light.shadow.mapSize.width and light.shadow.mapSize.height. They default to 512x512. The larger you make them the more memory they take and the slower they are to compute so you want to set them as small as you can and still make your scene work. The same is true with the light's shadow camera area. Smaller means better looking shadows so make the area as small as you can and still cover your scene. Be aware that each user's machine has a maximum texture size allowed which is available on the renderer as renderer.capabilities.maxTextureSize.

Ok but what about near and far I hear you thinking. Can we set near to 0.00001 and far to 100000000

SpotLight shadows

Switching to the SpotLight the light's shadow camera becomes a PerspectiveCamera. Unlike the DirectionalLight's shadow camera where we could manually set most its settings, SpotLight's shadow camera is controlled by the SpotLight itself. The fov for the shadow camera is directly connected to the SpotLight's angle setting. The aspect is set automatically based on the size of the shadow map.

and we added back in the penumbra and angle settings from our article about lights.

You can notice, just like the last example if we set the angle high then the shadow map, the texture is spread over a very large area and the resolution of our shadows gets really low.

You can increase the size of the shadow map as mentioned above. You can also blur the result

const light = new THREE.DirectionalLight(color, intensity);
const light = new THREE.SpotLight(color, intensity);

PointLight shadows

And finally there's shadows with a PointLight. Since a PointLight shines in all directions the only relevant settings are near and far. Otherwise the PointLight shadow is effectively 6 SpotLight shadows each one pointing to the face of a cube around the light. This means PointLight shadows are much slower since the entire scene must be drawn 6 times, one for each direction.

Let's put a box around our scene so we can see shadows on the walls and ceiling. We'll set the material's side property to THREE.BackSide so we render the inside of the box instead of the outside. Like the floor we'll set it only to receive shadows. Also we'll set the position of the box so its bottom is slightly below the floor so the floor and the bottom of the box don't z-fight.

And of course we need to switch the light to a PointLight.

Use the position GUI settings to move the light around and you'll see the shadows fall on all the walls. You can also adjust near and far settings and see just like the other shadows when things are closer than near they no longer receive a shadow and they are further than far they are always in shadow.

{
  const cubeSize = 30;
  const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
  const cubeMat = new THREE.MeshPhongMaterial({
    color: '#CCC',
    side: THREE.BackSide,
  });
  const mesh = new THREE.Mesh(cubeGeo, cubeMat);
  mesh.receiveShadow = true;
  mesh.position.set(0, cubeSize / 2 - 0.1, 0);
  scene.add(mesh);
}

const light = new THREE.SpotLight(color, intensity);
const light = new THREE.PointLight(color, intensity);

....

// so we can easily see where the point light is
const helper = new THREE.PointLightHelper(light);
scene.add(helper);

Self shadow, shadow acne


Fog

Summary: This article explains how to add fog to a three.js scene using Fog and FogExp2 classes, how to synchronize fog with the background color, how to create GUI controls for adjusting fog, and how to use the material-level fog property.

Fog

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. If you haven't read about cameras you might want to start with this article.

Fog in a 3D engine is generally a way of fading to a specific color based on the distance from the camera. In three.js you add fog by creating Fog or FogExp2 object and setting it on the scene's fog property.

Fog lets you choose near and far settings which are distances from the camera. Anything closer than near is unaffected by fog. Anything further than far is completely the fog color. Parts between near and far fade from their material color to the fog color.

There's also FogExp2 which grows exponentially with distance from the camera.

To use either type of fog you create one and and assign it to the scene as in

const scene = new THREE.Scene();
{
  const color = 0xFFFFFF;  // white
  const near = 10;
  const far = 100;
  scene.fog = new THREE.Fog(color, near, far);
}

FogExp2

or for FogExp2 it would be

const scene = new THREE.Scene();
{
  const color = 0xFFFFFF;
  const density = 0.1;
  scene.fog = new THREE.FogExp2(color, density);
}

Choosing Between Fog and FogExp2

FogExp2 is closer to reality but Fog is used more commonly since it lets you choose a place to apply the fog so you can decide to show a clear scene up to a certain distance and then fade out to some color past that distance.

It's important to note that the fog is applied to *things that are rendered*. It is part of the calculation of each pixel of the color of the object. What that means is if you want your scene to fade to a certain color you need to set the fog **and** the background color to the same color. The background color is set using the scene.background property. To pick a background color you attach a THREE.Color to it. For example

scene.background = new THREE.Color('#F00');  // red

Example with Fog Added

Here is one of our previous examples with fog added. The only addition is right after setting up the scene we add the fog and set the scene's background color

const scene = new THREE.Scene();

{
  const near = 1;
  const far = 2;
  const color = 'lightblue';
  scene.fog = new THREE.Fog(color, near, far);
  scene.background = new THREE.Color(color);
}

Camera and Cube Setup

In the example below the camera's near is 0.1 and its far is 5. The camera is at z = 2. The cubes are 1 unit large and at Z = 0. This means with a fog setting of near = 1 and far = 2 the cubes will fade out right around their center.

Adding GUI Controls

Let's add an interface so we can adjust the fog. Again we'll use lil-gui. lil-gui takes an object and a property and automagically makes an interface for that type of property. We could just simply let it manipulate the fog's near and far properties but it's invalid to have near be greater than far so let's make a helper so lil-gui can manipulate a near and far property but we'll make sure near is less than or equal to far and far is greater than or equal near.

// We use this class to pass to lil-gui
// so when it manipulates near or far
// near is never > far and far is never < near
class FogGUIHelper {
  constructor(fog) {
    this.fog = fog;
  }
  get near() {
    return this.fog.near;
  }
  set near(v) {
    this.fog.near = v;
    this.fog.far = Math.max(this.fog.far, v);
  }
  get far() {
    return this.fog.far;
  }
  set far(v) {
    this.fog.far = v;
    this.fog.near = Math.min(this.fog.near, v);
  }
}

Wiring Up the GUI Helper

We can then add it like this

{
  const near = 1;
  const far = 2;
  const color = 'lightblue';
  scene.fog = new THREE.Fog(color, near, far);
  scene.background = new THREE.Color(color);

  const fogGUIHelper = new FogGUIHelper(scene.fog);
  gui.add(fogGUIHelper, 'near', near, far).listen();
  gui.add(fogGUIHelper, 'far', near, far).listen();
}

GUI Parameter Ranges and Listen

The near and far parameters set the minimum and maximum values for adjusting the fog. They are set when we setup the camera.

The .listen() at the end of the last 2 lines tells lil-gui to *listen* for changes. That way when we change near because of an edit to far or we change far in response to an edit to near lil-gui will update the other property's UI for us.

Adding Color Control

It might also be nice to be able to change the fog color but like was mentioned above we need to keep both the fog color and the background color in sync. So, let's add another *virtual* property to our helper that will set both colors when lil-gui manipulates it.

lil-gui can manipulate colors in 4 ways, as a CSS 6 digit hex string (eg: #112233). As an hue, saturation, value, object (eg: {h: 60, s: 1, v: }). As an RGB array (eg: [255, 128, 64]). Or, as an RGBA array (eg: [127, 200, 75, 0.3]).

It's easiest for our purpose to use the hex string version since that way lil-gui is only manipulating a single value. Fortunately THREE.Color as a getHexString method we get use to easily get such a string, we just have to prepend a '#' to the front.

// We use this class to pass to lil-gui
// so when it manipulates near or far
// near is never > far and far is never < near
// Also when lil-gui manipulates color we'll
// update both the fog and background colors.
class FogGUIHelper {
  constructor(fog, backgroundColor) {
    this.fog = fog;
    this.backgroundColor = backgroundColor;
  }
  get near() {
    return this.fog.near;
  }
  set near(v) {
    this.fog.near = v;
    this.fog.far = Math.max(this.fog.far, v);
  }
  get far() {
    return this.fog.far;
  }
  set far(v) {
    this.fog.far = v;
    this.fog.near = Math.min(this.fog.near, v);
  }
  get color() {
    return `#${this.fog.color.getHexString()}`;
  }
  set color(hexString) {
    this.fog.color.set(hexString);
    this.backgroundColor.set(hexString);
  }
}

Final GUI Setup with Color

We then call gui.addColor to add a color UI for our helper's virtual property.

{
  const near = 1;
  const far = 2;
  const color = 'lightblue';
  scene.fog = new THREE.Fog(color, near, far);
  scene.background = new THREE.Color(color);

  const fogGUIHelper = new FogGUIHelper(scene.fog, scene.background);
  gui.add(fogGUIHelper, 'near', near, far).listen();
  gui.add(fogGUIHelper, 'far', near, far).listen();
  gui.addColor(fogGUIHelper, 'color');
}

Sharp vs Smooth Transitions

You can see setting near to like 1.9 and far to 2.0 gives a very sharp transition between un-fogged and completely fogged. where as near = 1.1 and far = 2.9 should just about be the smoothest given our cubes are spinning 2 units away from the camera.

Material Fog Property

One last thing, there is a boolean fog property on a material for whether or not objects rendered with that material are affected by fog. It defaults to true for most materials. As an example of why you might want to turn the fog off, imagine you're making a 3D vehicle simulator with a view from the driver's seat or cockpit. You probably want the fog off for everything inside the vehicle when viewing from inside the vehicle.

A better example might be a house and thick fog outside house. Let's say the fog is set to start 2 meters away (near = 2) and completely fogged out at 4 meters (far = 4). Rooms are longer than 2 meters and the house is probably longer than 4 meters so you need to set the materials for the inside of the house to not apply fog otherwise when standing inside the house looking outside the wall at the far end of the room will look like it's in the fog.

Notice the walls and ceiling at the far end of the room are getting fog applied. By turning fog off on the materials for the house we can fix that issue.


Render Targets

Summary: A tutorial on using WebGLRenderTarget in three.js to render a scene into a texture, then apply that texture to other objects. Covers creation, rendering setup, use cases, depth/stencil buffer options, and dynamic resizing.

Introduction

A render target in three.js is basically a texture you can render to. After you render to it you can use that texture like any other texture.

Let's make a simple example. We'll start with an example from the article on responsiveness.

Rendering to a render target is almost exactly the same as normal rendering. First we create a WebGLRenderTarget.

const rtWidth = 512;
const rtHeight = 512;
const renderTarget = new THREE.WebGLRenderTarget(rtWidth, rtHeight);

Setting up a Camera and Scene for the Render Target

Then we need a Camera and a Scene.

Notice we set the aspect to the aspect for the render target, not the canvas. The correct aspect to use depends on what we are rendering for. In this case we'll use the render target's texture on the side of a cube. Since faces of the cube are square we want an aspect of 1.0.

const rtFov = 75;
const rtAspect = rtWidth / rtHeight;
const rtNear = 0.1;
const rtFar = 5;
const rtCamera = new THREE.PerspectiveCamera(rtFov, rtAspect, rtNear, rtFar);
rtCamera.position.z = 2;

const rtScene = new THREE.Scene();
rtScene.background = new THREE.Color('red');

Adding Objects to the Render Target Scene

We fill the scene with stuff. In this case we're using the light and the 3 cubes from the previous article.

{
  const color = 0xFFFFFF;
  const intensity = 1;
  const light = new THREE.DirectionalLight(color, intensity);
  light.position.set(-1, 2, 4);
*  rtScene.add(light);
}

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

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

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

  cube.position.x = x;

  return cube;
}

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

Using the Render Target as a Texture

The Scene and Camera from the previous article are still there. We'll use them to render to the canvas. We just need to add stuff to render.

Let's add a cube that uses the render target's texture.

const material = new THREE.MeshPhongMaterial({
  map: renderTarget.texture,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

Rendering in the Animation Loop

Now at render time first we render the render target scene to the render target.

Then we render the scene with the single cube that is using the render target's texture to the canvas.

And voilà.

function render(time) {
  time *= 0.001;

  ...

  // rotate all the cubes in the render target scene
  rtCubes.forEach((cube, ndx) => {
    const speed = 1 + ndx * .1;
    const rot = time * speed;
    cube.rotation.x = rot;
    cube.rotation.y = rot;
  });

  // draw render target scene to render target
  renderer.setRenderTarget(renderTarget);
  renderer.render(rtScene, rtCamera);
  renderer.setRenderTarget(null);

// rotate the cube in the scene
  cube.rotation.x = time;
  cube.rotation.y = time * 1.1;

  // render the scene to the canvas
  renderer.render(scene, camera);

Result

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

The cube is red because we set the `background` of the `rtScene` to red so the render target's texture is being cleared to red.

Common Uses for Render Targets

Render targets are used for all kinds of things. Shadows use render targets. Picking can use a render target. Various kinds of post processing effects require render targets. Rendering a rear view mirror in a car or a live view on a monitor inside a 3D scene might use a render target.

Notes on WebGLRenderTarget

A few notes about using WebGLRenderTarget.

Disabling Depth and Stencil Buffers

By default WebGLRenderTarget creates 2 textures. A color texture and a depth/stencil texture. If you don't need the depth or stencil textures you can request to not create them by passing in options. Example:

const rt = new THREE.WebGLRenderTarget(width, height, {
    depthBuffer: false,
    stencilBuffer: false,
  });

Resizing a Render Target

You might need to change the size of a render target

In the example above we make a render target of a fixed size, 512x512. For things like post processing you generally need to make a render target the same size as your canvas. In our code that would mean when we change the canvas size we would also update both the render target size and the camera we're using when rendering to the render target. Example:

function render(time) {
  time *= 0.001;

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

+    renderTarget.setSize(canvas.width, canvas.height);
+    rtCamera.aspect = camera.aspect;
+    rtCamera.updateProjectionMatrix();
}


Custom BufferGeometry

Summary: A guide on how to use BufferGeometry directly in three.js to create custom geometry, including building a cube from parallel arrays, using indices to reduce duplication, working with TypedArrays, and dynamically updating vertex data.

Custom BufferGeometry

BufferGeometry is three.js's way of representing all geometry. A BufferGeometry is essentially a collection of named BufferAttributes. Each BufferAttribute represents an array of one type of data: positions, normals, colors, uv, etc... Together, the named BufferAttributes represent parallel arrays of all the data for each vertex.

Above you can see we have 4 attributes: position, normal, color, uv. They represent parallel arrays which means that the Nth set of data in each attribute belongs to the same vertex. The vertex at index = 4 is highlighted to show that the parallel data across all attributes defines one vertex.

This brings up a point, here's a diagram of a cube with one corner highlighted.

Thinking about it that single corner needs a different normal for each face of the cube. A normal is info about which direction something faces. In the diagram the normals are presented by the arrows around the corner vertex showing that each face that shares that vertex position needs a normal that points in a different direction.

That corner needs different UVs for each face as well. UVs are texture coordinates that specify which part of a texture being drawn on a triangle corresponds to that vertex position. You can see the green face needs that vertex to have a UV that corresponds to the top right corner of the F texture, the blue face needs a UV that corresponds to the top left corner of the F texture, and the red face needs a UV that corresponds to the bottom left corner of the F texture.

A single vertex is the combination of all of its parts. If a vertex needs any part to be different then it must be a different vertex.

As a simple example let's make a cube using BufferGeometry. A cube is interesting because it appears to share vertices at the corners but really does not. For our example we'll list out all the vertices with all their data and then convert that data into parallel arrays and finally use those to make BufferAttributes and add them to a BufferGeometry.

Listing Cube Vertices

We start with a list of all the data needed for the cube. Remember again that if a vertex has any unique parts it has to be a separate vertex. As such to make a cube requires 36 vertices. 2 triangles per face, 3 vertices per triangle, 6 faces = 36 vertices.

Converting to Parallel Arrays

We can then translate all of that into 3 parallel arrays

const positions = [];
const normals = [];
const uvs = [];
for (const vertex of vertices) {
  positions.push(...vertex.pos);
  normals.push(...vertex.norm);
  uvs.push(...vertex.uv);
}

Creating BufferAttributes

Finally we can create a BufferGeometry and then a BufferAttribute for each array and add it to the BufferGeometry.

Note that the names are significant. You must name your attributes the names that match what three.js expects (unless you are creating a custom shader). In this case position, normal, and uv. If you want vertex colors then name your attribute color.

Above we created 3 JavaScript native arrays, positions, normals and uvs. We then convert those into TypedArrays of type Float32Array. A BufferAttribute requires a TypedArray not a native array. A BufferAttribute also requires you to tell it how many components there are per vertex. For the positions and normals we have 3 components per vertex, x, y, and z. For the UVs we have 2, u and v.

const geometry = new THREE.BufferGeometry();
  const positionNumComponents = 3;
  const normalNumComponents = 3;
  const uvNumComponents = 2;
  geometry.setAttribute(
      'position',
      new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
  geometry.setAttribute(
      'normal',
      new THREE.BufferAttribute(new Float32Array(normals), normalNumComponents));
  geometry.setAttribute(
      'uv',
      new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));

Using Indices

That's a lot of data. A small thing we can do is use indices to reference the vertices. Looking back at our cube data, each face is made from 2 triangles with 3 vertices each, 6 vertices total, but 2 of those vertices are exactly the same; The same position, the same normal, and the same uv. So, we can remove the matching vertices and then reference them by index. First we remove the matching vertices.

So now we have 24 unique vertices. Then we specify 36 indices for the 36 vertices we need drawn to make 12 triangles by calling BufferGeometry.setIndex with an array of indices.

Computing Normals Automatically

BufferGeometry has a computeVertexNormals method for computing normals if you are not supplying them. Unfortunately, since positions can not be shared if any other part of a vertex is different, the results of calling computeVertexNormals will generate seams if your geometry is supposed to connect to itself like a sphere or a cylinder.

For the cylinder above the normals were created using computeVertexNormals. If you look closely there is a seam on the cylinder. This is because there is no way to share the vertices at the start and end of the cylinder since they require different UVs so the function to compute them has no idea those are the same vertices to smooth over them. Just a small thing to be aware of. The solution is to supply your own normals.

Using TypedArrays

We can also use TypedArrays from the start instead of native JavaScript arrays. The disadvantage to TypedArrays is you must specify their size up front. Of course that's not that large of a burden but with native arrays we can just push values onto them and look at what size they end up by checking their length at the end. With TypedArrays there is no push function so we need to do our own bookkeeping when adding values to them.

In this example knowing the length up front is pretty easy since we're using a big block of static data to start.

const numVertices = vertices.length;
const positionNumComponents = 3;
const normalNumComponents = 3;
const uvNumComponents = 2;
const positions = new Float32Array(numVertices * positionNumComponents);
const normals = new Float32Array(numVertices * normalNumComponents);
const uvs = new Float32Array(numVertices * uvNumComponents);
let posNdx = 0;
let nrmNdx = 0;
let uvNdx = 0;
for (const vertex of vertices) {
  positions.set(vertex.pos, posNdx);
  normals.set(vertex.norm, nrmNdx);
  uvs.set(vertex.uv, uvNdx);
  posNdx += positionNumComponents;
  nrmNdx += normalNumComponents;
  uvNdx += uvNumComponents;
}

geometry.setAttribute(
    'position',
    new THREE.BufferAttribute(positions, positionNumComponents));
geometry.setAttribute(
    'normal',
    new THREE.BufferAttribute(normals, normalNumComponents));
geometry.setAttribute(
    'uv',
    new THREE.BufferAttribute(uvs, uvNumComponents));

geometry.setIndex([
   0,  1,  2,   2,  1,  3,  // front
   4,  5,  6,   6,  5,  7,  // right
   8,  9, 10,  10,  9, 11,  // back
  12, 13, 14,  14, 13, 15,  // left
  16, 17, 18,  18, 17, 19,  // top
  20, 21, 22,  22, 21, 23,  // bottom
]);

Dynamic Updates

A good reason to use typedarrays is if you want to dynamically update any part of the vertices.

I couldn't think of a really good example of dynamically updating the vertices so I decided to make a sphere and move each quad in and out from the center. Hopefully it's a useful example.

Here's the code to generate positions and indices for a sphere. The code is sharing vertices within a quad but it's not sharing vertices between quads because we want to be able to move each quad separately.

Because I'm lazy I used a small hierarchy of 3 Object3D objects to compute sphere points. How this works is explained in the article on optimizing lots of objects.

function makeSpherePositions(segmentsAround, segmentsDown) {
  const numVertices = segmentsAround * segmentsDown * 6;
  const numComponents = 3;
  const positions = new Float32Array(numVertices * numComponents);
  const indices = [];

  const longHelper = new THREE.Object3D();
  const latHelper = new THREE.Object3D();
  const pointHelper = new THREE.Object3D();
  longHelper.add(latHelper);
  latHelper.add(pointHelper);
  pointHelper.position.z = 1;
  const temp = new THREE.Vector3();

  function getPoint(lat, long) {
    latHelper.rotation.x = lat;
    longHelper.rotation.y = long;
    longHelper.updateMatrixWorld(true);
    return pointHelper.getWorldPosition(temp).toArray();
  }

  let posNdx = 0;
  let ndx = 0;
  for (let down = 0; down < segmentsDown; ++down) {
    const v0 = down / segmentsDown;
    const v1 = (down + 1) / segmentsDown;
    const lat0 = (v0 - 0.5) * Math.PI;
    const lat1 = (v1 - 0.5) * Math.PI;

    for (let across = 0; across < segmentsAround; ++across) {
      const u0 = across / segmentsAround;
      const u1 = (across + 1) / segmentsAround;
      const long0 = u0 * Math.PI * 2;
      const long1 = u1 * Math.PI * 2;

      positions.set(getPoint(lat0, long0), posNdx);  posNdx += numComponents;
      positions.set(getPoint(lat1, long0), posNdx);  posNdx += numComponents;
      positions.set(getPoint(lat0, long1), posNdx);  posNdx += numComponents;
      positions.set(getPoint(lat1, long1), posNdx);  posNdx += numComponents;

      indices.push(
        ndx, ndx + 1, ndx + 2,
        ndx + 2, ndx + 1, ndx + 3,
      );
      ndx += 4;
    }
  }
  return {positions, indices};
}

const segmentsAround = 24;
const segmentsDown = 16;
const {positions, indices} = makeSpherePositions(segmentsAround, segmentsDown);

const geometry = new THREE.BufferGeometry();
const positionNumComponents = 3;
const normalNumComponents = 3;

const positionAttribute = new THREE.BufferAttribute(positions, positionNumComponents);
positionAttribute.setUsage(THREE.DynamicDrawUsage);
geometry.setAttribute(
    'position',
    positionAttribute);
geometry.setAttribute(
    'normal',
    new THREE.BufferAttribute(normals, normalNumComponents));
geometry.setIndex(indices);

const temp = new THREE.Vector3();

...

for (let i = 0; i < positions.length; i += 3) {
  const quad = (i / 12 | 0);
  const ringId = quad / segmentsAround | 0;
  const ringQuadId = quad % segmentsAround;
  const ringU = ringQuadId / segmentsAround;
  const angle = ringU * Math.PI * 2;
  temp.fromArray(normals, i);
  temp.multiplyScalar(THREE.MathUtils.lerp(1, 1.4, Math.sin(time + ringId + angle) * .5 + .5));
  temp.toArray(positions, i);
}
positionAttribute.needsUpdate = true;

Dynamic Attribute Setup Notes

I've highlighted a few differences. We save a reference to the position attribute. We also mark it as dynamic. This is a hint to THREE.js that we're going to be changing the contents of the attribute often.

In our render loop we update the positions based off their normals every frame.

And we set positionAttribute.needsUpdate to tell THREE.js to use our changes.

Conclusion

I hope these were useful examples of how to use BufferGeometry directly to make your own geometry and how to dynamically update the contents of a BufferAttribute.