Physics

Summary: An overview of how to integrate physics engines into three.js projects, covering three main approaches: using the official physics addons, third-party JS/TS libraries, and WASM-based engines.

Physics

Physics engines allow you to simulate physical phenomena like gravity, collisions, and forces within your 3D capabilities. In a typical three.js scene, objects are moved by directly modifying their position or rotation. When using a physics engine, however, you create a parallel physics world where bodies react to forces and collisions. You then synchronize the three.js meshes with these physics bodies on every frame, creating the illusion of a physically simulated environment.

It should be noted that the physics engine does not necessarily have to be updated every frame. Usually, to keep experiences consistent, physics are updated at fixed time steps. For instance, it could be that we are running the game loop at 60fps but the physics engine at 30fps (that is 1/30=3.33ms) while updating the three.js meshes' transforms (e.g., positions and rotations) with the most recent state from the physics engine.

Physics simulations are particularly useful for games, interactive visualizations, and any application requiring realistic object behavior, such as objects falling, bouncing, or sliding.

Integration Approaches

There are three main ways to integrate a physics engine into a three.js project:

1. Using Three.js Physics Addons

Three.js provides wrapper classes for several popular physics engines in the *examples/jsm/physics* directory. These addons simplify the setup process by handling the initialization of the physics world and the synchronization of meshes.

Available addons include:

- **AmmoPhysics:** A wrapper for Ammo.js (Bullet Physics).
- **JoltPhysics:** A wrapper for Jolt Physics.
- **RapierPhysics:** A wrapper for Rapier.

These addons effectively hide much of the complexity of the underlying engines. For standard use cases, they offer a very quick way to get started.

###### Examples

- [physics / ammo / instancing](https://threejs.org/examples/physics_ammo_instancing.html)
- [physics / jolt / instancing](https://threejs.org/examples/physics_jolt_instancing.html)
- [physics / rapier / instancing](https://threejs.org/examples/physics_rapier_instancing.html)

2. Using 3rd-Party Physics JS/TS Libraries

Many physics engines are written directly in JavaScript or TypeScript and are designed to work easily with the web ecosystem. Libraries like **cannon-es** are popular choices because they are lightweight and easy to integrate specifically with three.js.

When using these libraries, you instantiate the physics world and bodies yourself, then manually copy the position and quaternion from the physics body to the three.js mesh in your animation loop.

###### Projects

- **[cannon-es](https://github.com/pmndrs/cannon-es)**: A lightweight 3D physics engine purely written in JS/TS. Under an MIT license. Apparently no longer maintained (latest commit more than a couple years ago).
- **[cannon.js](https://github.com/schteppe/cannon.js)**: A lightweight 3D physics engine purely written in JavaScript. Under an MIT license. No longer maintained (latest commit more than a couple years ago). Consider using its more recent fork cannon-es.
- **[phy](https://github.com/lo-th/phy)**: Physics engine for three.js purely written in JavaScript. Under an MIT license. Latest commit. Currently maintained.
- **[Oimo.js](https://github.com/lo-th/Oimo.js)**: A no-longer maintained lightweight 3D physics engine written purely in JavaScript. Under an MIT license. Consider using phy instead (as per the author's advise).

It should also be noted that there are a couple of 3D physics engines that are seemingly written in JS/TS but are in reality calling other standalone 3D physics engines. For example:

- **[Physijs](https://github.com/chandlerprall/Physijs)**: Calls ammo.js under the hood to do physics work in a separate thread (using web workers). Under MIT license. Apparently no longer maintained (latest commit more than a couple of years ago).
- **[enable3d](https://github.com/enable3d/enable3d)**: 3D physics framework for three.js built on top of ammo.js. Under LGPL-3.0 license. Apparently maintained.

3. Importing WASM-based Engines

For maximum performance, stability, and precision, especially with complex simulations, you can use physics engines written in C++ or Rust (or any other language that supports WASM) that have been compiled to WebAssembly (WASM). Engines like **Ammo.js** (a port of Bullet Physics) and **Rapier** fall into this category.

While this approach offers the most features and best performance, it often requires more setup code to handle the WASM memory management and interaction with the physics API directly.

###### Examples

- [physics / ammo / break](https://threejs.org/examples/physics_ammo_break.html)
- [physics / ammo / cloth](https://threejs.org/examples/physics_ammo_cloth.html)
- [physics / ammo / rope](https://threejs.org/examples/physics_ammo_rope.html)
- [physics / ammo / terrain](https://threejs.org/examples/physics_ammo_terrain.html)
- [physics / ammo / volume](https://threejs.org/examples/physics_ammo_volume.html)

###### Projects

- **[JoltPhysics](https://github.com/jrouwe/JoltPhysics)**: A multi core friendly rigid body physics and collision detection library. Written in C++. Under MIT license. Actively maintained. Proven with its usage in world-renowned titles and game engines including: Horizon Forbidden West, Death Stranding 2, and official supported by the Godot game engine.
- **[PhysX](https://github.com/NVIDIA-Omniverse/PhysX)**: Industry-standard realtime 3D physics engine provided by NVIDIA. Under BSD-3-Clause license. Actively maintained and very stable.
- **[Rapier](https://github.com/dimforge/rapier)**: 2D and 3D physics engines focused on performance. Written in Rust. Under an MIT license. Actively maintained.
- **[Bullet](https://github.com/bulletphysics/bullet3)**: Real-time collision detection and multi-physics simulation for VR, games, visual effects, robotics, machine learning etc. Written in C++. Under a ZLIB license. Potentially no longer maintained.

Some of these multi-platform 3D physics engines have a ready-to-use WASM port, including:

- **[JoltPhysics.js](https://github.com/jrouwe/JoltPhysics.js)**: Port of JoltPhysics to JavaScript using Emscripten. Under MIT license. Currently maintained.
- **[physx-js-webidl](https://github.com/fabmax/physx-js-webidl)**: Javascript WASM bindings for Nvidia PhysX. Under MIT license. Currently maintained.
- **[Rapier.js](https://github.com/dimforge/rapier.js)**: Official JavaScript bindings for the Rapier physics engine. Under Apache-2.0 license. Actively maintained.
- **[Ammo.js](https://github.com/kripken/ammo.js)**: Direct port of the Bullet physics engine to JavaScript using Emscripten. No longer maintained (latest commit a couple of years ago). Under an MIT-like custom permissive license.


Rendering on Demand

Summary: A guide explaining how to render Three.js scenes on demand rather than continuously using requestAnimationFrame, to save power and battery on devices where continuous animation is unnecessary.

Rendering on Demand

The topic might be obvious to many people but just in case ... most Three.js examples render continuously. In other words they setup a `requestAnimationFrame` loop or " *rAF loop* " something like this

For something that animates this makes sense but what about for something that does not animate? In that case rendering continuously is a waste of the devices power and if the user is on portable device it wastes the user's battery.

The most obvious way to solve this is to render once at the start and then render only when something changes. Changes include textures or models finally loading, data arriving from some external source, the user adjusting a setting or the camera or giving other relevant input.

Let's take an example from [the article on responsiveness](responsive.html) and modify it to render on demand.

First we'll add in the [`OrbitControls`](/docs/#examples/controls/OrbitControls) so there is something that could change that we can render in response to.

and set them up

Since we won't be animating the cubes anymore we no longer need to keep track of them

We can remove the code to animate the cubes and the calls to `requestAnimationFrame`

then we need to render once

We need to render anytime the [`OrbitControls`](/docs/#examples/controls/OrbitControls) change the camera settings. Fortunately the [`OrbitControls`](/docs/#examples/controls/OrbitControls) dispatch a `change` event anytime something changes.

We also need to handle the case where the user resizes the window. That was handled automatically before since we were rendering continuously but now what we are not we need to render when the window changes size.

And with that we get something that renders on demand.

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

The [`OrbitControls`](/docs/#examples/controls/OrbitControls) have options to add a kind of inertia to make them feel less stiff. We can enable this by setting the `enableDamping` property to true.

With `enableDamping` on we need to call `controls.update` in our render function so that the [`OrbitControls`](/docs/#examples/controls/OrbitControls) can continue to give us new camera settings as they smooth out the movement. But, that means we can't call `render` directly from the `change` event because we'll end up in an infinite loop. The controls will send us a `change` event and call `render`, `render` will call `controls.update`. `controls.update` will send another `change` event.

We can fix that by using `requestAnimationFrame` to call `render` but we need to make sure we only ask for a new frame if one has not already been requested which we can do by keeping a variable that tracks if we've already requested a frame.

We should probably also use `requestRenderIfNotRequested` for resizing as well

It might be hard to see the difference. Try clicking on the example below and use the arrow keys to move around or dragging to spin. Then try clicking on the example above and do the same thing and you should be able to tell the difference. The one above snaps when you press an arrow key or drag, the one below slides.

[click here to open in a separate window](/manual/examples/render-on-demand-w-damping.html)

Let's also add a simple lil-gui GUI and make its changes render on demand.

Let's allow setting the color and x scale of each cube. To be able to set the color we'll use the `ColorGUIHelper` we created in the [article on lights](lights.html).

First we need to create a GUI

and then for each cube we'll create a folder and add 2 controls, one for `material.color` and another for `cube.scale.x`.

You can see above lil-gui controls have an `onChange` method that you can pass a callback to be called when the GUI changes a value. In our case we just need it to call `requestRenderIfNotRequested`. The call to `folder.open` makes the folder start expanded.

[click here to open in a separate window](/manual/examples/render-on-demand-w-gui.html)

I hope this gives some idea of how to make three.js render on demand instead of continuously. Apps/pages that render three.js on demand are not as common as most pages using three.js are either games or 3d animated art but examples of pages that might be better rendering on demand would be say a map viewer, a 3d editor, a 3d graph generator, a product catalog, etc...

function render() {
  ...
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

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

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

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

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

-function render(time) {
-  time *= 0.001;
+function render() {

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

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

  renderer.render(scene, camera);

-  requestAnimationFrame(render);
}

-requestAnimationFrame(render);

+let renderRequested = false;

function render() {
+  renderRequested = false;

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

  renderer.render(scene, camera);
}
render();

+function requestRenderIfNotRequested() {
+  if (!renderRequested) {
+    renderRequested = true;
+    requestAnimationFrame(render);
+  }
+}

-controls.addEventListener('change', render);
+controls.addEventListener('change', requestRenderIfNotRequested);

-window.addEventListener('resize', render);
+window.addEventListener('resize', requestRenderIfNotRequested);

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import {GUI} from 'three/addons/libs/lil-gui.module.min.js';

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

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

  cube.position.x = x;

+  const folder = gui.addFolder(Cube${x});
+  folder.addColor(new ColorGUIHelper(material, 'color'), 'value')
+      .name('color')
+      .onChange(requestRenderIfNotRequested);
+  folder.add(cube.scale, 'x', .1, 1.5)
+      .name('scale x')
+      .onChange(requestRenderIfNotRequested);
+  folder.open();

  return cube;
}


Debugging - GLSL

Summary: A guide on debugging GLSL shaders in Three.js, offering practical tips for troubleshooting fragment and vertex shader issues through visualization techniques, simplified shaders, and developer tools.

Debugging - GLSL

This site so far does not teach GLSL just like it does not teach JavaScript. Those are really large topics. If you want to learn GLSL consider checking out these articles as a starting place.

If you already know GLSL then here are a few tips for debugging.

When I'm making a new GLSL shader and nothing appears generally the first thing I do is change the fragment shader to return a solid color. For example at the very bottom of the shader I might put

```
void main() {

  ...

  gl_FragColor = vec4(1, 0, 0, 1);  // red
}
```

If I see the object I was trying to draw then I know the issue is related to my fragment shader. It could be anything like bad textures, uninitialized uniforms, uniforms with the wrong values but at least I have a direction to look.

To test some of those I might start trying to draw some of the inputs. For example if I'm using normals in the fragment shader then I might add

```
gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1);
```

Normals go from -1 to +1 so by multiplying by 0.5 and adding 0.5 we get values that go from 0.0 to 1.0 which makes them useful for colors.

Try this with some things you know work and you'll start getting an idea of what normals *normally* look like. If your normals don't look normal then you have some clue where to look. If you're manipulating normals in the fragments shader you can use the same technique to draw the result of that manipulation.

Similarly if we're using textures there will be texture coordinates and we can draw them with something like

```
gl_FragColor = vec4(fract(vUv), 0, 1);
```

The `fract` is there in case we're using texture coordinates that go outside the 0 to 1 range. This is common if `texture.repeat` is set to something greater than 1.

You can do similar things for all values in your fragment shader. Figure out what their range is likely to be, add some code to set `gl_FragColor` with that range scaled to 0.0 to 1.0

To check textures try a [`CanvasTexture`](/docs/#api/en/textures/CanvasTexture) or a [`DataTexture`](/docs/#api/en/textures/DataTexture) that you know works.

Conversely, if after setting `gl_FragColor` to red I still see nothing then I have a hint my issue might be in the direction of the things related to the vertex shader. Some matrices might be wrong or my attributes might have bad data or be setup incorrectly.

I'd first look at the matrices. I might put a breakpoint right after my call to `renderer.render(scene, camera)` and then start expanding things in the inspector. Is the camera's world matrix and projection matrix at least not full of `NaN`s? Expanding the scene and looking at its `children` I'd check that the world matrices look reasonable (no `NaN`s) and last 4 values of each matrix look reasonable for my scene. If I expect my scene to be 50x50x50 units and some matrix shows 552352623.123 clearly something is wrong there.

Just like we did for the fragment shader we can also draw values from the vertex shader by passing them to the fragment shader. Declare a varying in both and pass the value you're not sure is correct. In fact if my shader use using normals I'll change the fragment shader to display them like is mentioned above and then just set `vNormal` to the value I want to display but scaled so the values go from 0.0 to 1.0. I then look at the results and see if they fit my expectations.

Another good thing to do is use a simpler shader. Can you draw your data with [`MeshBasicMaterial`](/docs/#api/en/materials/MeshBasicMaterial)? If you can then try it and make sure it shows up as expected.

If not what's the simplest vertex shader that will let you visualize your geometry? Usually it's as simple as

```
gl_Position = projection * modelView * vec4(position.xyz, 1);
```

If that works start adding in your changes a little at a time.

Yet another thing you can do is use the Shader Editor extension for Chrome or similar for other browsers. It's a great way to look at how other shaders are working. It's also good as you can make some of the changes suggested above live while the code is running.


Tips

Summary: A collection of small tips and solutions for common issues encountered when using three.js, including taking screenshots, keyboard input, transparency, and background animations.

Taking A Screenshot of the Canvas

In the browser there are effectively 2 functions that will take a screenshot. The old one
[`canvas.toDataURL`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)
and the new better one
[`canvas.toBlob`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob)

So you'd think it would be easy to take a screenshot by just adding some code like

[click here to open in a separate window](/manual/examples/tips-screenshot-bad.html)

When I tried it I got this screenshot

Yes, it's just a black image.

It's possible it worked for you depending on your browser/OS but in general
it's not likely to work.

The issue is that for performance and compatibility reasons, by default the browser
will clear a WebGL canvas's drawing buffer after you've drawn to it.

The solution is to call your rendering code just before capturing.

In our code we need to adjust a few things. First let's separate
out the rendering code.

Now that `render` is only concerned with actually rendering
we can call it just before capturing the canvas.

And now it should work.

[click here to open in a separate window](/manual/examples/tips-screenshot-good.html)

For a different solution see the next item.

<canvas id="c"></canvas>
+<button id="screenshot" type="button">Save...</button>

const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

const saveBlob = (function() {
  const a = document.createElement('a');
  document.body.appendChild(a);
  a.style.display = 'none';
  return function saveData(blob, fileName) {
     const url = window.URL.createObjectURL(blob);
     a.href = url;
     a.download = fileName;
     a.click();
  };
}());

+const state = {
+  time: 0,
+};

-function render(time) {
-  time *= 0.001;
+function render() {
  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }

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

  renderer.render(scene, camera);

-  requestAnimationFrame(render);
}

+function animate(time) {
+  state.time = time * 0.001;
+
+  render();
+
+  requestAnimationFrame(animate);
+}
+requestAnimationFrame(animate);

const elem = document.querySelector('#screenshot');
elem.addEventListener('click', () => {
+  render();
  canvas.toBlob((blob) => {
    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
  });
});

Preventing the canvas being cleared

Let's say you wanted to let the user paint with an animated
object. You need to pass in `preserveDrawingBuffer: true` when
you create the [`WebGLRenderer`](/docs/#api/en/renderers/WebGLRenderer). This prevents the browser from
clearing the canvas. You also need to tell three.js not to clear
the canvas as well.

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

Note that if you were serious about making a drawing program this would not be a
solution as the browser will still clear the canvas anytime we change its
resolution. We're changing is resolution based on its display size. Its display
size changes when the window changes size. That includes when the user downloads
a file, even in another tab, and the browser adds a status bar. It also includes when
the user turns their phone and the browser switches from portrait to landscape.

If you really wanted to make a drawing program you'd
[render to a texture using a render target](rendertargets.html).

const canvas = document.querySelector('#c');
-const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
+const renderer = new THREE.WebGLRenderer({
+  canvas,
+  preserveDrawingBuffer: true,
+  alpha: true,
+});
+renderer.autoClearColor = false;

Getting Keyboard Input

Throughout these tutorials we've often attached event listeners to the `canvas`.
While many events work, one that does not work by default is keyboard
events.

To get keyboard events, set the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/tabIndex)
of the canvas to 0 or more. Eg.

This ends up causing a new issue though. Anything that has a `tabindex` set
will get highlighted when it has the focus. To fix that set its focus CSS outline
to none

To demonstrate here are 3 canvases

and some css just for the last canvas

Let's attach the same event listeners to all of them

Notice you can't get the first canvas to accept keyboard input.
The second canvas you can but it gets highlighted. The 3rd
canvas has both solutions applied.

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

<canvas id="c1"></canvas>
<canvas id="c2" tabindex="0"></canvas>
<canvas id="c3" tabindex="1"></canvas>

document.querySelectorAll('canvas').forEach((canvas) => {
  const ctx = canvas.getContext('2d');

  function draw(str) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(str, canvas.width / 2, canvas.height / 2);
  }
  draw(canvas.id);

  canvas.addEventListener('focus', () => {
    draw('has focus press a key');
  });

  canvas.addEventListener('blur', () => {
    draw('lost focus');
  });

  canvas.addEventListener('keydown', (e) => {
    draw(`keyCode: ${e.keyCode}`);
  });
});

Making the Canvas Transparent

By default THREE.js makes the canvas opaque. If you want the
canvas to be transparent pass in [`alpha:true`](/docs/#api/en/renderers/WebGLRenderer#alpha) when you create
the [`WebGLRenderer`](/docs/#api/en/renderers/WebGLRenderer)

You probably also want to tell it that your results are **not** using premultiplied alpha

Three.js defaults to the canvas using
[`premultipliedAlpha: true`](/docs/#api/en/renderers/WebGLRenderer#premultipliedAlpha) but defaults
to materials outputting [`premultipliedAlpha: false`](/docs/#api/en/materials/Material#premultipliedAlpha).

If you'd like a better understanding of when and when not to use premultiplied alpha
here's [a good article on it](https://developer.nvidia.com/content/alpha-blending-pre-or-not-pre).

In any case let's setup a simple example with a transparent canvas.

We applied the settings above to the example from [the article on responsiveness](responsive.html).
Let's also make the materials more transparent.

And let's add some HTML content

as well as some CSS to put the canvas in front

note that `pointer-events: none` makes the canvas invisible to the mouse
and touch events so you can select the text beneath.

[click here to open in a separate window](/manual/examples/tips-transparent-canvas.html)

const canvas = document.querySelector('#c');
-const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
+const renderer = new THREE.WebGLRenderer({
+  canvas,
+  alpha: true,
+});

const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({
  canvas,
  alpha: true,
+  premultipliedAlpha: false,
});

function makeInstance(geometry, color, x) {
-  const material = new THREE.MeshPhongMaterial({color});
+  const material = new THREE.MeshPhongMaterial({
+    color,
+    opacity: 0.5,
+  });

...

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

+

+
Cubes-R-Us!

+
We make the best cubes!

+

+

body {
    margin: 0;
}
#c {
    width: 100%;
    height: 100%;
    display: block;
+    position: fixed;
+    left: 0;
+    top: 0;
+    z-index: 2;
+    pointer-events: none;
}
+#content {
+  font-size: 7vw;
+  font-family: sans-serif;
+  text-align: center;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}

Making your background a three.js animation

A common question is how to make a three.js animation be the background of
a webpage.

There are 2 obvious ways.

- Set the canvas CSS `position` to `fixed` as in

You can basically see this exact solution on the previous example. Just set `z-index` to -1
and the cubes will appear behind the text.

A small disadvantage to this solution is your JavaScript must integrate with the page
and if you have a complex page then you need to make sure none of the JavaScript in your
three.js visualization conflict with the JavaScript doing other things in the page.

- Use an `iframe`

This is the solution used on [the front page of this site](/).

In your webpage just insert an iframe, for example

Then style the iframe to fill the window and be in the background
which is basically the same code as we used above for the canvas
except we also need to set `border` to `none` since iframes have
a border by default.

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

#c {
 position: fixed;
 left: 0;
 top: 0;
 ...
}

<iframe id="background" src="responsive.html">
<div>
  Your content goes here.
</div>

#background {
    position: fixed;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    z-index: -1;
    border: none;
    pointer-events: none;
}


Optimize Lots of Objects

Summary: A guide on optimizing three.js performance by merging many separate geometries into a single mesh, using the recreation of a WebGL Globe with demographic data as an example. Covers data loading, initial slow implementation, geometry merging with BufferGeometryUtils, and restoring per-object colors using vertex colors.

Optimize Lots of Objects

There are many ways to optimize things for three.js. One way is often referred to as *merging geometry*. Every [`Mesh`](/docs/#api/en/objects/Mesh) you create and three.js represents 1 or more requests by the system to draw something. Drawing 2 things has more overhead than drawing 1 even if the results are the same so one way to optimize is to merge meshes.

Let's show an example of when this is a good solution for an issue. Let's re-create the [WebGL Globe](https://globe.chromeexperiments.com/).

The first thing we need to do is get some data. The WebGL Globe said the data they use comes from [SEDAC](http://sedac.ciesin.columbia.edu/gpw/). Checking out the site I saw there was [demographic data in a grid format](https://beta.sedac.ciesin.columbia.edu/data/set/gpw-v4-basic-demographic-characteristics-rev10). I downloaded the data at 60 minute resolution. Then I took a look at the data

It looks like this

```
 ncols         360
 nrows         145
 xllcorner     -180
 yllcorner     -60
 cellsize      0.99999999999994
 NODATA_value  -9999
 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 -9999 ...
 ...
```

There's a few lines that are like key/value pairs followed by lines with a value per grid point, one line for each row of data points.

To make sure we understand the data let's try to plot it in 2D.

First some code to load the text file

```js
async function loadFile(url) {
  const res = await fetch(url);
  return res.text();
}
```

The code above returns a `Promise` with the contents of the file at `url`;

Then we need some code to parse the file

```js
function parseData(text) {
  const data = [];
  const settings = {data};
  let max;
  let min;
  // split into lines
  text.split('\n').forEach((line) => {
    // split the line by whitespace
    const parts = line.trim().split(/\s+/);
    if (parts.length === 2) {
      // only 2 parts, must be a key/value pair
      settings[parts[0]] = parseFloat(parts[1]);
    } else if (parts.length > 2) {
      // more than 2 parts, must be data
      const values = parts.map((v) => {
        const value = parseFloat(v);
        if (value === settings.NODATA_value) {
          return undefined;
        }
        max = Math.max(max === undefined ? value : max, value);
        min = Math.min(min === undefined ? value : min, value);
        return value;
      });
      data.push(values);
    }
  });
  return Object.assign(settings, {min, max});
}
```

The code above returns an object with all the key/value pairs from the file as well as a `data` property with all the data in one large array and the `min` and `max` values found in the data.

Then we need some code to draw that data

```js
function drawData(file) {
  const {min, max, data} = file;
  const range = max - min;
  const ctx = document.querySelector('canvas').getContext('2d');
  // make the canvas the same size as the data
  ctx.canvas.width = ncols;
  ctx.canvas.height = nrows;
  // but display it double size so it's not too small
  ctx.canvas.style.width = px(ncols * 2);
  ctx.canvas.style.height = px(nrows * 2);
  // fill the canvas to dark gray
  ctx.fillStyle = '#444';
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  // draw each data point
  data.forEach((row, latNdx) => {
    row.forEach((value, lonNdx) => {
      if (value === undefined) {
        return;
      }
      const amount = (value - min) / range;
      const hue = 1;
      const saturation = 1;
      const lightness = amount;
      ctx.fillStyle = hsl(hue, saturation, lightness);
      ctx.fillRect(lonNdx, latNdx, 1, 1);
    });
  });
}

function px(v) {
  return ${v | 0}px;
}

function hsl(h, s, l) {
  return hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%);
}
```

And finally gluing it all together

```js
loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
  .then(parseData)
  .then(drawData);
```

Gives us this result

[click here to open in a separate window](/manual/examples/gpw-data-viewer.html)

So that seems to work.

Let's try it in 3D. Starting with the code from [rendering on demand](rendering-on-demand.html) We'll make one box per data in the file.

First let's make a simple sphere with a texture of the world. Here's the texture

<img src="../examples/resources/images/world.jpg">

And the code to set it up.

```js
{
  const loader = new THREE.TextureLoader();
  const texture = loader.load('resources/images/world.jpg', render);
  const geometry = new THREE.SphereGeometry(1, 64, 32);
  const material = new THREE.MeshBasicMaterial({map: texture});
  scene.add(new THREE.Mesh(geometry, material));
}
```

Notice the call to `render` when the texture has finished loading. We need this because we're [rendering on demand](rendering-on-demand.html) instead of continuously so we need to render once when the texture is loaded.

Then we need to change the code that drew a dot per data point above to instead make a box per data point.

```js
function addBoxes(file) {
  const {min, max, data} = file;
  const range = max - min;

  // make one box geometry
  const boxWidth = 1;
  const boxHeight = 1;
  const boxDepth = 1;
  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
  // make it so it scales away from the positive Z axis
  geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));

  // these helpers will make it easy to position the boxes
  // We can rotate the lon helper on its Y axis to the longitude
  const lonHelper = new THREE.Object3D();
  scene.add(lonHelper);
  // We rotate the latHelper on its X axis to the latitude
  const latHelper = new THREE.Object3D();
  lonHelper.add(latHelper);
  // The position helper moves the object to the edge of the sphere
  const positionHelper = new THREE.Object3D();
  positionHelper.position.z = 1;
  latHelper.add(positionHelper);

  const lonFudge = Math.PI * .5;
  const latFudge = Math.PI * -0.135;
  data.forEach((row, latNdx) => {
    row.forEach((value, lonNdx) => {
      if (value === undefined) {
        return;
      }
      const amount = (value - min) / range;
      const material = new THREE.MeshBasicMaterial();
      const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
      const saturation = 1;
      const lightness = THREE.MathUtils.lerp(0.1, 1.0, amount);
      material.color.setHSL(hue, saturation, lightness);
      const mesh = new THREE.Mesh(geometry, material);
      scene.add(mesh);

      // adjust the helpers to point to the latitude and longitude
      lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
      latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;

      // use the world matrix of the position helper to
      // position this mesh.
      positionHelper.updateWorldMatrix(true, false);
      mesh.applyMatrix4(positionHelper.matrixWorld);

      mesh.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
    });
  });
}
```

The code is mostly straight forward from our test drawing code.

We make one box and adjust its center so it scales away from positive Z. If we didn't do this it would scale from the center but we want them to grow away from the origin.

Of course we could also solve that by parenting the box to more [`THREE.Object3D`](/docs/#api/en/core/Object3D) objects like we covered in [scene graphs](scenegraph.html) but the more nodes we add to a scene graph the slower it gets.

We also setup this small hierarchy of nodes of `lonHelper`, `latHelper`, and `positionHelper`. We use these objects to compute a position around the sphere were to place the box.

Above the green bar represents `lonHelper` and is used to rotate toward longitude on the equator. The blue bar represents `latHelper` which is used to rotate to a latitude above or below the equator. The red sphere represents the offset that that `positionHelper` provides.

We could do all of the math manually to figure out positions on the globe but doing it this way leaves most of the math to the library itself so we don't need to deal with.

For each data point we create a [`MeshBasicMaterial`](/docs/#api/en/materials/MeshBasicMaterial) and a [`Mesh`](/docs/#api/en/objects/Mesh) and then we ask for the world matrix of the `positionHelper` and apply that to the new [`Mesh`](/docs/#api/en/objects/Mesh). Finally we scale the mesh at its new position.

Like above, we could also have created a `latHelper`, `lonHelper`, and `positionHelper` for every new box but that would be even slower.

There are up to 360x145 boxes we're going to create. That's up to 52000 boxes. Because some data points are marked as "NO_DATA" the actual number of boxes we're going to create is around 19000. If we added 3 extra helper objects per box that would be nearly 80000 scene graph nodes that THREE.js would have to compute positions for. By instead using one set of helpers to just position the meshes we save around 60000 operations.

A note about `lonFudge` and `latFudge`. `lonFudge` is π/2 which is a quarter of a turn. That makes sense. It just means the texture or texture coordinates start at a different offset around the globe. `latFudge` on the other hand I have no idea why it needs to be π * -0.135, that's just an amount that made the boxes line up with the texture.

The last thing we need to do is call our loader

```js
loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
  .then(parseData)
-  .then(drawData)
+  .then(addBoxes)
+  .then(render);
```

Once the data has finished loading and parsing then we need to render at least once since we're [rendering on demand](rendering-on-demand.html).

[click here to open in a separate window](/manual/examples/lots-of-objects-slow.html)

If you try to rotate the example above by dragging on the sample you'll likely notice it's slow.

We can check the framerate by [opening the devtools](debugging-javascript.html) and turning on the browser's frame rate meter.

On my machine I see a framerate under 20fps.

That doesn't feel very good to me and I suspect many people have slower machines which would make it even worse. We'd better look into optimizing.

For this particular problem we can merge all the boxes into a single geometry. We're currently drawing around 19000 boxes. By merging them into a single geometry we'd remove 18999 operations.

Here's the new code to merge the boxes into a single geometry.

```js
function addBoxes(file) {
  const {min, max, data} = file;
  const range = max - min;

  // these helpers will make it easy to position the boxes
  // We can rotate the lon helper on its Y axis to the longitude
  const lonHelper = new THREE.Object3D();
  scene.add(lonHelper);
  // We rotate the latHelper on its X axis to the latitude
  const latHelper = new THREE.Object3D();
  lonHelper.add(latHelper);
  // The position helper moves the object to the edge of the sphere
  const positionHelper = new THREE.Object3D();
  positionHelper.position.z = 1;
  latHelper.add(positionHelper);
+  // Used to move the center of the box so it scales from the position Z axis
+  const originHelper = new THREE.Object3D();
+  originHelper.position.z = 0.5;
+  positionHelper.add(originHelper);

  const lonFudge = Math.PI * .5;
  const latFudge = Math.PI * -0.135;
+  const geometries = [];
  data.forEach((row, latNdx) => {
    row.forEach((value, lonNdx) => {
      if (value === undefined) {
        return;
      }
      const amount = (value - min) / range;

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

      // adjust the helpers to point to the latitude and longitude
      lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
      latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;

+      // use the world matrix of the origin helper to
+      // position this geometry
+      positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
+      originHelper.updateWorldMatrix(true, false);
+      geometry.applyMatrix4(originHelper.matrixWorld);
+
+      geometries.push(geometry);
    });
  });

+  const mergedGeometry = BufferGeometryUtils.mergeGeometries(
+      geometries, false);
+  const material = new THREE.MeshBasicMaterial({color:'red'});
+  const mesh = new THREE.Mesh(mergedGeometry, material);
+  scene.add(mesh);

}
```

Above we removed the code that was changing the box geometry's center point and are instead doing it by adding an `originHelper`. Before we were using the same geometry 19000 times. This time we are creating new geometry for every single box and since we are going to use `applyMatrix` to move the vertices of each box geometry we might as well do it once instead of twice.

At the end we pass an array of all the geometries to `BufferGeometryUtils.mergeGeometries` which will combined all of them into a single mesh.

We also need to include the `BufferGeometryUtils`

```js
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
```

And now, at least on my machine, I get 60 frames per second

[click here to open in a separate window](/manual/examples/lots-of-objects-merged.html)

So that worked but because it's one mesh we only get one material which means we only get one color where as before we had a different color on each box. We can fix that by using vertex colors.

Vertex colors add a color per vertex. By setting all the colors of each vertex of each box to specific colors every box will have a different color.

```js
+const color = new THREE.Color();

const lonFudge = Math.PI * .5;
const latFudge = Math.PI * -0.135;
const geometries = [];
data.forEach((row, latNdx) => {
  row.forEach((value, lonNdx) => {
    if (value === undefined) {
      return;
    }
    const amount = (value - min) / range;

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

    // adjust the helpers to point to the latitude and longitude
    lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
    latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;

    // use the world matrix of the origin helper to
    // position this geometry
    positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
    originHelper.updateWorldMatrix(true, false);
    geometry.applyMatrix4(originHelper.matrixWorld);

+    // compute a color
+    const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
+    const saturation = 1;
+    const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
+    color.setHSL(hue, saturation, lightness);
+    // get the colors as an array of values from 0 to 255
+    const rgb = color.toArray().map(v => v * 255);
+
+    // make an array to store colors for each vertex
+    const numVerts = geometry.getAttribute('position').count;
+    const itemSize = 3;  // r, g, b
+    const colors = new Uint8Array(itemSize * numVerts);
+
+    // copy the color into the colors array for each vertex
+    colors.forEach((v, ndx) => {
+      colors[ndx] = rgb[ndx % 3];
+    });
+
+    const normalized = true;
+    const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
+    geometry.setAttribute('color', colorAttrib);

    geometries.push(geometry);
  });
});
```

The code above looks up the number or vertices needed by getting the `position` attribute from the geometry. We then create a `Uint8Array` to put the colors in. It then adds that as an attribute by calling `geometry.setAttribute`.

Lastly we need to tell three.js to use the vertex colors.

```js
const mergedGeometry = BufferGeometryUtils.mergeGeometries(
    geometries, false);
-const material = new THREE.MeshBasicMaterial({color:'red'});
+const material = new THREE.MeshBasicMaterial({
+  vertexColors: true,
+});
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);
```

And with that we get our colors back

[click here to open in a separate window](/manual/examples/lots-of-objects-merged-vertexcolors.html)

Merging geometry is a common optimization technique. For example rather than 100 trees you might merge the trees into 1 geometry, a pile of individual rocks into a single geometry of rocks, a picket fence from individual pickets into one fence mesh. Another example in Minecraft it doesn't likely draw each cube individually but rather creates groups of merged cubes and also selectively removing faces that are never visible.

The problem with making everything one mesh though is it's no longer easy to move any part that was previously separate. Depending on our use case though there are creative solutions. We'll explore one in [another article](optimize-lots-of-objects-animated.html).


Optimize Lots of Objects Animated

Summary: A continuation of the article about optimizing lots of objects, this tutorial covers how to graph and animate between multiple sets of data using morphtargets in Three.js, including loading multiple data files, generating comparison datasets, building a UI to switch between them, and creating smooth transitions with a TweenManager.

Optimize Lots of Objects Animated

This article is a continuation of an article about optimizing lots of objects. If you haven't read that yet please read it before proceeding.

In the previous article we merged around 19000 cubes into a single geometry. This had the advantage that it optimized our drawing of 19000 cubes but it had the disadvantage of make it harder to move any individual cube.

Depending on what we are trying to accomplish there are different solutions. In this case let's graph multiple sets of data and animate between the sets.

The first thing we need to do is get multiple sets of data. Ideally we'd probably pre-process data offline but in this case let's load 2 sets of data and generate 2 more

Here's our old loading code

The code above will load all the files in `fileInfos` and when done each object in `fileInfos` will have a `file` property with the loaded file. `name` and `hueRange` we'll use later. `name` will be for a UI field. `hueRange` will be used to choose a range of hues to map over.

The two files above are apparently the number of men per area and the number of women per area as of 2010. Note, I have no idea if this data is correct but it's not important really. The important part is showing different sets of data.

Let's generate 2 more sets of data. One being the places where the number men are greater than the number of women and vice versa, the places where the number of women are greater than the number of men.

The first thing let's write a function that given a 2 dimensional array of arrays like we had before will map over it to generate a new 2 dimensional array of arrays

Like the normal `Array.map` function the `mapValues` function calls a function `fn` for each value in the array of arrays. It passes it the value and both the row and column indices.

Now let's make some code to generate a new file that is a comparison between 2 files

The code above uses `mapValues` to generate a new set of data that is a comparison based on the `compareFn` function passed in. It also tracks the `min` and `max` comparison results. Finally it makes a new file with all the same properties as `baseFile` except with a new `min`, `max` and `data`.

Then let's use that to make 2 new sets of data

Now let's generate a UI to select between these sets of data. First we need some UI html

and some CSS to make it appear in the top left area

Then we can go over each file and generate a set of merged boxes per set of data and an element which when hovered over will show that set and hide all others.

The one more change we need from the previous example is we need to make `addBoxes` take a `hueRange`

and with that we should be able to show 4 sets of data. Hover the mouse over the labels or touch them to switch sets

Note, there are a few strange data points that really stick out. I wonder what's up with those!??! In any case how do we animate between these 4 sets of data.

Lots of ideas.

- Just fade between them using `Material.opacity`

The problem with this solution is the cubes perfectly overlap which means there will be z-fighting issues. It's possible we could fix that by changing the depth function and using blending. We should probably look into it.

- Scale up the set we want to see and scale down the other sets

Because all the boxes have their origin at the center of the planet if we scale them below 1.0 they will sink into the planet. At first that sounds like a good idea but the issue is all the low height boxes will disappear almost immediately and not be replaced until the new data set scales up to 1.0. This makes the transition not very pleasant. We could maybe fix that with a fancy custom shader.

- Use Morphtargets

Morphtargets are a way were we supply multiple values for each vertex in the geometry and morph or lerp (linear interpolate) between them. Morphtargets are most commonly used for facial animation of 3D characters but that's not their only use.

Let's try morphtargets.

We'll still make a geometry for each set of data but we'll then extract the `position` attribute from each one and use them as morphtargets.

First let's change `addBoxes` to just make and return the merged geometry.

There's one more thing we need to do here though. Morphtargets are required to all have exactly the same number of vertices. Vertex #123 in one target needs have a corresponding Vertex #123 in all other targets. But, as it is now different data sets might have some data points with no data so no box will be generated for that point which would mean no corresponding vertices for another set. So, we need to check across all data sets and either always generate something if there is data in any set or, generate nothing if there is data missing in any set. Let's do the latter.

Now we'll change the code that was calling `addBoxes` to use `makeBoxes` and setup morphtargets

Above we make geometry for each data set, use the first one as the base, then get a `position` attribute from each geometry and add it as a morphtarget to the base geometry for `position`.

Now we need to change how we're showing and hiding the various data sets. Instead of showing or hiding a mesh we need to change the influence of the morphtargets. For the data set we want to see we need to have an influence of 1 and for all the ones we don't want to see to we need to have an influence of 0.

We could just set them to 0 or 1 directly but if we did that we wouldn't see any animation, it would just snap which would be no different than what we already have. We could also write some custom animation code which would be easy but because the original webgl globe uses an animation library let's use the same one here.

We need to include the library

And then create a `Tween` to animate the influences.

We're also suppose to call `TWEEN.update` every frame inside our render loop but that points out a problem. "tween.js" is designed for continuous rendering but we are rendering on demand. We could switch to continuous rendering but it's sometimes nice to only render on demand as it well stop using the user's power when nothing is happening so let's see if we can make it animate on demand.

We'll make a `TweenManager` to help. We'll use it to create the `Tween`s and track them. It will have an `update` method that will return `true` if we need to call it again and `false` if all the animations are finished.

To use it we'll create one

We'll use it to create our `Tween`s.

Then we'll update our render loop to update the tweens and keep rendering if there are still animations running.

And with that we should be animating between data sets.

I hope going through this was helpful. Using morphtargets is a common technique to move lots of objects. As an example we could give every cube a random place in another target and morph from that to their first positions on the globe. That might be a cool way to introduce the globe.

Next you might be interested in adding labels to a globe which is covered in Aligning HTML Elements to 3D.

Note: We could try to just graph percent of men or percent of women or the raw difference but based on how we are displaying the info, cubes that grow from the surface of the earth, we'd prefer most cubes to be low. If we used one of these other comparisons most cubes would be about 1/2 their maximum height which would not make a good visualization. Feel free to change the `amountGreaterThan` from `Math.max(a - b, 0)` to something like `(a - b)` "raw difference" or `a / (a + b)` "percent" and you'll see what I mean.

loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
  .then(parseData)
  .then(addBoxes)
  .then(render);

async function loadData(info) {
  const text = await loadFile(info.url);
  info.file = parseData(text);
}

async function loadAll() {
  const fileInfos = [
    {name: 'men',   hueRange: [0.7, 0.3], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
    {name: 'women', hueRange: [0.9, 1.1], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
  ];

  await Promise.all(fileInfos.map(loadData));

  ...
}
loadAll();

function mapValues(data, fn) {
  return data.map((row, rowNdx) => {
    return row.map((value, colNdx) => {
      return fn(value, rowNdx, colNdx);
    });
  });
}

function makeDiffFile(baseFile, otherFile, compareFn) {
  let min;
  let max;
  const baseData = baseFile.data;
  const otherData = otherFile.data;
  const data = mapValues(baseData, (base, rowNdx, colNdx) => {
    const other = otherData[rowNdx][colNdx];
      if (base === undefined || other === undefined) {
        return undefined;
      }
      const value = compareFn(base, other);
      min = Math.min(min === undefined ? value : min, value);
      max = Math.max(max === undefined ? value : max, value);
      return value;
  });
  // make a copy of baseFile and replace min, max, and data
  // with the new data
  return {...baseFile, min, max, data};
}

{
  const menInfo = fileInfos[0];
  const womenInfo = fileInfos[1];
  const menFile = menInfo.file;
  const womenFile = womenInfo.file;

  function amountGreaterThan(a, b) {
    return Math.max(a - b, 0);
  }
  fileInfos.push({
    name: '>50%men',
    hueRange: [0.6, 1.1],
    file: makeDiffFile(menFile, womenFile, (men, women) => {
      return amountGreaterThan(men, women);
    }),
  });
  fileInfos.push({
    name: '>50% women',
    hueRange: [0.0, 0.4],
    file: makeDiffFile(womenFile, menFile, (women, men) => {
      return amountGreaterThan(women, men);
    }),
  });
}

#ui {
  position: absolute;
  left: 1em;
  top: 1em;
}
#ui>div {
  font-size: 20pt;
  padding: 1em;
  display: inline-block;
}
#ui>div.selected {
  color: red;
}

// show the selected data, hide the rest
function showFileInfo(fileInfos, fileInfo) {
  fileInfos.forEach((info) => {
    const visible = fileInfo === info;
    info.root.visible = visible;
    info.elem.className = visible ? 'selected' : '';
  });
  requestRenderIfNotRequested();
}

const uiElem = document.querySelector('#ui');
fileInfos.forEach((info) => {
  const boxes = addBoxes(info.file, info.hueRange);
  info.root = boxes;
  const div = document.createElement('div');
  info.elem = div;
  div.textContent = info.name;
  uiElem.appendChild(div);
  div.addEventListener('mouseover', () => {
    showFileInfo(fileInfos, info);
  });
});
// show the first set of data
showFileInfo(fileInfos, fileInfos[0]);

-function addBoxes(file) {
+function addBoxes(file, hueRange) {

  ...

    // compute a color
-    const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
+    const hue = THREE.MathUtils.lerp(...hueRange, amount);

  ...

-function addBoxes(file, hueRange) {
+function makeBoxes(file, hueRange) {
  const {min, max, data} = file;
  const range = max - min;

  ...

-  const mergedGeometry = BufferGeometryUtils.mergeGeometries(
-      geometries, false);
-  const material = new THREE.MeshBasicMaterial({
-    vertexColors: true,
-  });
-  const mesh = new THREE.Mesh(mergedGeometry, material);
-  scene.add(mesh);
-  return mesh;
+  return BufferGeometryUtils.mergeGeometries(
+     geometries, false);
}

+function dataMissingInAnySet(fileInfos, latNdx, lonNdx) {
+  for (const fileInfo of fileInfos) {
+    if (fileInfo.file.data[latNdx][lonNdx] === undefined) {
+      return true;
+    }
+  }
+  return false;
+}

-function makeBoxes(file, hueRange) {
+function makeBoxes(file, hueRange, fileInfos) {
  const {min, max, data} = file;
  const range = max - min;

  ...

  const geometries = [];
  data.forEach((row, latNdx) => {
    row.forEach((value, lonNdx) => {
+      if (dataMissingInAnySet(fileInfos, latNdx, lonNdx)) {
+        return;
+      }
      const amount = (value - min) / range;

  ...

+// make geometry for each data set
+const geometries = fileInfos.map((info) => {
+  return makeBoxes(info.file, info.hueRange, fileInfos);
+});
+
+// use the first geometry as the base
+// and add all the geometries as morphtargets
+const baseGeometry = geometries[0];
+baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) => {
+  const attribute = geometry.getAttribute('position');
+  const name = target${ndx};
+  attribute.name = name;
+  return attribute;
+});
+baseGeometry.morphAttributes.color = geometries.map((geometry, ndx) => {
+  const attribute = geometry.getAttribute('color');
+  const name = target${ndx};
+  attribute.name = name;
+  return attribute;
+});
+const material = new THREE.MeshBasicMaterial({
+  vertexColors: true,
+});
+const mesh = new THREE.Mesh(baseGeometry, material);
+scene.add(mesh);

const uiElem = document.querySelector('#ui');
fileInfos.forEach((info) => {
-  const boxes = addBoxes(info.file, info.hueRange);
-  info.root = boxes;
  const div = document.createElement('div');
  info.elem = div;
  div.textContent = info.name;
  uiElem.appendChild(div);
  function show() {
    showFileInfo(fileInfos, info);
  }
  div.addEventListener('mouseover', show);
  div.addEventListener('touchstart', show);
});
// show the first set of data
showFileInfo(fileInfos, fileInfos[0]);

import * as THREE from 'three';
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import TWEEN from 'three/addons/libs/tween.module.js';

// show the selected data, hide the rest
function showFileInfo(fileInfos, fileInfo) {
+  const targets = {};
-  fileInfos.forEach((info) => {
+  fileInfos.forEach((info, i) => {
    const visible = fileInfo === info;
-    info.root.visible = visible;
    info.elem.className = visible ? 'selected' : '';
+    targets[i] = visible ? 1 : 0;
  });
+  const durationInMs = 1000;
+  new TWEEN.Tween(mesh.morphTargetInfluences)
+    .to(targets, durationInMs)
+    .start();
  requestRenderIfNotRequested();
}

class TweenManger {
  constructor() {
    this.numTweensRunning = 0;
  }
  _handleComplete() {
    --this.numTweensRunning;
    console.assert(this.numTweensRunning >= 0);
  }
  createTween(targetObject) {
    const self = this;
    ++this.numTweensRunning;
    let userCompleteFn = () => {};
    // create a new tween and install our own onComplete callback
    const tween = new TWEEN.Tween(targetObject).onComplete(function(...args) {
      self._handleComplete();
      userCompleteFn.call(this, ...args);
    });
    // replace the tween's onComplete function with our own
    // so we can call the user's callback if they supply one.
    tween.onComplete = (fn) => {
      userCompleteFn = fn;
      return tween;
    };
    return tween;
  }
  update() {
    TWEEN.update();
    return this.numTweensRunning > 0;
  }
}

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

  ...

// show the selected data, hide the rest
function showFileInfo(fileInfos, fileInfo) {
  const targets = {};
  fileInfos.forEach((info, i) => {
    const visible = fileInfo === info;
    info.elem.className = visible ? 'selected' : '';
    targets[i] = visible ? 1 : 0;
  });
  const durationInMs = 1000;
-  new TWEEN.Tween(mesh.morphTargetInfluences)
+  tweenManager.createTween(mesh.morphTargetInfluences)
    .to(targets, durationInMs)
    .start();
  requestRenderIfNotRequested();
}

function render() {
  renderRequested = false;

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

+  if (tweenManager.update()) {
+    requestRenderIfNotRequested();
+  }

  controls.update();
  renderer.render(scene, camera);
}
render();


OffscreenCanvas

Summary: A tutorial on using OffscreenCanvas with Three.js to offload rendering work to web workers, including basic setup, picking, OrbitControls integration, and fallback for browsers without OffscreenCanvas support.

OffscreenCanvas

[`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) is a relatively new browser feature currently only available in Chrome but apparently coming to other browsers. `OffscreenCanvas` allows a web worker to render to a canvas. This is a way to offload heavy work, like rendering a complex 3D scene, to a web worker so as not to slow down the responsiveness of the browser. It also means data is loaded and parsed in the worker so possibly less jank while the page loads.

Getting *started* using it is pretty straight forward. Let's port the 3 spinning cube example from [the article on responsiveness](responsive.html).

Workers generally have their code separated into another script file whereas most of the examples on this site have had their scripts embedded into the HTML file of the page they are on.

In our case we'll make a file called `offscreencanvas-cubes.js` and copy all the JavaScript from [the responsive example](responsive.html) into it. We'll then make the changes needed for it to run in a worker.

We still need some JavaScript in our HTML file. The first thing we need to do there is look up the canvas and then transfer control of that canvas to be offscreen by calling `canvas.transferControlToOffscreen`.

function main() {
  const canvas = document.querySelector('#c');
  const offscreen = canvas.transferControlToOffscreen();

  ...

Starting the Worker

We can then start our worker with `new Worker(pathToScript, {type: 'module'})`. and pass the `offscreen` object to it.

function main() {
  const canvas = document.querySelector('#c');
  const offscreen = canvas.transferControlToOffscreen();
  const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
}
main();

Worker Message Communication

It's important to note that workers can't access the `DOM`. They can't look at HTML elements nor can they receive mouse events or keyboard events. The only thing they can generally do is respond to messages sent to them and send messages back to the page.

To send a message to a worker we call [`worker.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) and pass it 1 or 2 arguments. The first argument is a JavaScript object that will be [cloned](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) and sent to the worker. The second argument is an optional array of objects that are part of the first object that we want *transferred* to the worker. These objects will not be cloned. Instead they will be *transferred* and will cease to exist in the main page. Cease to exist is the probably the wrong description, rather they are neutered. Only certain types of objects can be transferred instead of cloned. They include `OffscreenCanvas` so once transferred the `offscreen` object back in the main page is useless.

Workers receive messages from their `onmessage` handler. The object we passed to `postMessage` arrives on `event.data` passed to the `onmessage` handler on the worker. The code above declares a `type: 'main'` in the object it passes to the worker. This object has no meaning to the browser. It's entirely for our own usage. We'll make a handler that based on `type` calls a different function in the worker. Then we can add functions as needed and easily call them from the main page.

const handlers = {
  main,
};

self.onmessage = function(e) {
  const fn = handlers[e.data.type];
  if (typeof fn !== 'function') {
    throw new Error('no handler for type: ' + e.data.type);
  }
  fn(e.data);
};

Adapting the Main Function for Worker

You can see above we just look up the handler based on the `type` pass it the `data` that was sent from the main page.

So now we just need to start changing the `main` we pasted into `offscreencanvas-cubes.js` from [the responsive article](responsive.html).

Instead of looking up the canvas from the DOM we'll receive it from the event data.

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

  ...

Handling Size Changes

Remembering that workers can't see the DOM at all the first problem we run into is `resizeRendererToDisplaySize` can't look at `canvas.clientWidth` and `canvas.clientHeight` as those are DOM values. Here's the original code

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

Instead we'll need to send sizes as they change to the worker. So, let's add some global state and keep the width and height there.

const state = {
  width: 300,  // canvas default
  height: 150,  // canvas default
};

Then let's add a `'size'` handler to update those values.

+function size(data) {
+  state.width = data.width;
+  state.height = data.height;
+}

const handlers = {
  main,
+  size,
};

Now we can change `resizeRendererToDisplaySize` to use `state.width` and `state.height`

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

and where we compute the aspect we need similar changes

function render(time) {
  time *= 0.001;

  if (resizeRendererToDisplaySize(renderer)) {
-    camera.aspect = canvas.clientWidth / canvas.clientHeight;
+    camera.aspect = state.width / state.height;
    camera.updateProjectionMatrix();
  }

  ...

Back in the main page we'll send a `size` event anytime the page changes size.

const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);

+function sendSize() {
+  worker.postMessage({
+    type: 'size',
+    width: canvas.clientWidth,
+    height: canvas.clientHeight,
+  });
+}
+
+window.addEventListener('resize', sendSize);
+sendSize();

We also call it once to send the initial size.

And with just those few changes, assuming your browser fully supports `OffscreenCanvas` it should work. Before we run it though let's check if the browser actually supports `OffscreenCanvas` and if not display an error. First let's add some HTML to display the error.

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

+
no OffscreenCanvas support

+

and some CSS for that

#noOffscreenCanvas {
    display: flex;
    width: 100%;
    height: 100%;
    align-items: center;
    justify-content: center;
    background: red;
    color: white;
}

and then we can check for the existence of `transferControlToOffscreen` to see if the browser supports `OffscreenCanvas`

function main() {
  const canvas = document.querySelector('#c');
+  if (!canvas.transferControlToOffscreen) {
+    canvas.style.display = 'none';
+    document.querySelector('#noOffscreenCanvas').style.display = '';
+    return;
+  }
  const offscreen = canvas.transferControlToOffscreen();
  const worker = new Worker('offscreencanvas-picking.js', {type: 'module});
  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);

  ...

and with that, if your browser supports `OffscreenCanvas` this example should work

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

So that's great but since not every browser supports `OffscreenCanvas` at the moment let's change the code to work with both `OffscreenCanvas` and if not then fallback to using the canvas in the main page like normal.

> As an aside, if you need OffscreenCanvas to make your page responsive then it's not clear what the point of having a fallback is. Maybe based on if you end up running on the main page or in a worker you might adjust the amount of work done so that when running in a worker you can do more than when running in the main page. What you do is really up to you.

Adding Fallback Support

The first thing we should probably do is separate out the three.js code from the code that is specific to the worker. That way we can use the same code on both the main page and the worker. In other words we will now have 3 files

- our html file.

`threejs-offscreencanvas-w-fallback.html`

- a JavaScript that contains our three.js code.

`shared-cubes.js`

- our worker support code

`offscreencanvas-worker-cubes.js`

`shared-cubes.js` and `offscreencanvas-worker-cubes.js` are basically the split of our previous `offscreencanvas-cubes.js` file. First we copy all of `offscreencanvas-cubes.js` to `shared-cube.js`. Then we rename `main` to `init` since we already have a `main` in our HTML file and we need to export `init` and `state`

import * as THREE from 'three';

-const state = {
+export const state = {
  width: 300,   // canvas default
  height: 150,  // canvas default
};

-function main(data) {
+export function init(data) {
  const {canvas} = data;
  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});

and cut out the just the non three.js relates parts

-function size(data) {
-  state.width = data.width;
-  state.height = data.height;
-}
-
-const handlers = {
-  main,
-  size,
-};
-
-self.onmessage = function(e) {
-  const fn = handlers[e.data.type];
-  if (typeof fn !== 'function') {
-    throw new Error('no handler for type: ' + e.data.type);
-  }
-  fn(e.data);
-};

Then we copy those parts we just deleted to `offscreencanvas-worker-cubes.js` and import `shared-cubes.js` as well as call `init` instead of `main`.

import {init, state} from './shared-cubes.js';

function size(data) {
  state.width = data.width;
  state.height = data.height;
}

const handlers = {
-  main,
+  init,
  size,
};

self.onmessage = function(e) {
  const fn = handlers[e.data.type];
  if (typeof fn !== 'function') {
    throw new Error('no handler for type: ' + e.data.type);
  }
  fn(e.data);
};

Similarly we need to include `shared-cubes.js` in the main page

<script type="module">
+import {init, state} from './shared-cubes.js';

We can remove the HTML and CSS we added previously

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

-
no OffscreenCanvas support

-

and some CSS for that

-#noOffscreenCanvas {
-    display: flex;
-    width: 100%;
-    height: 100%;
-    align-items: center;
-    justify-content: center;
-    background: red;
-    color: white;
-}

Then let's change the code in the main page to call one start function or another depending on if the browser supports `OffscreenCanvas`.

function main() {
  const canvas = document.querySelector('#c');
-  if (!canvas.transferControlToOffscreen) {
-    canvas.style.display = 'none';
-    document.querySelector('#noOffscreenCanvas').style.display = '';
-    return;
-  }
-  const offscreen = canvas.transferControlToOffscreen();
-  const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
-  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
+  if (canvas.transferControlToOffscreen) {
+    startWorker(canvas);
+  } else {
+    startMainPage(canvas);
+  }
  ...

We'll move all the code we had to setup the worker inside `startWorker`

function startWorker(canvas) {
  const offscreen = canvas.transferControlToOffscreen();
  const worker = new Worker('offscreencanvas-worker-cubes.js', {type: 'module'});
  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);

  function sendSize() {
    worker.postMessage({
      type: 'size',
      width: canvas.clientWidth,
      height: canvas.clientHeight,
    });
  }

  window.addEventListener('resize', sendSize);
  sendSize();

  console.log('using OffscreenCanvas');
}

and send `init` instead of `main`

-  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
+  worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);

for starting in the main page we can do this

function startMainPage(canvas) {
  init({canvas});

  function sendSize() {
    state.width = canvas.clientWidth;
    state.height = canvas.clientHeight;
  }
  window.addEventListener('resize', sendSize);
  sendSize();

  console.log('using regular canvas');
}

and with that our example will run either in an OffscreenCanvas or fallback to running in the main page.

[click here to open in a separate window](/manual/examples/offscreencanvas-w-fallback.html)

So that was relatively easy. Let's try picking. We'll take some code from the `RayCaster` example from [the article on picking](picking.html) and make it work offscreen.

Let's copy the `shared-cube.js` to `shared-picking.js` and add the picking parts. We copy in the `PickHelper`

class PickHelper {
  constructor() {
    this.raycaster = new THREE.Raycaster();
    this.pickedObject = null;
    this.pickedObjectSavedColor = 0;
  }
  pick(normalizedPosition, scene, camera, time) {
    // restore the color if there is a picked object
    if (this.pickedObject) {
      this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
      this.pickedObject = undefined;
    }

    // cast a ray through the frustum
    this.raycaster.setFromCamera(normalizedPosition, camera);
    // get the list of objects the ray intersected
    const intersectedObjects = this.raycaster.intersectObjects(scene.children);
    if (intersectedObjects.length) {
      // pick the first object. It's the closest one
      this.pickedObject = intersectedObjects[0].object;
      // save its color
      this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
      // set its emissive color to flashing red/yellow
      this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
    }
  }
}

const pickPosition = {x: 0, y: 0};
const pickHelper = new PickHelper();

Picking with OffscreenCanvas

We updated `pickPosition` from the mouse like this

function getCanvasRelativePosition(event) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: (event.clientX - rect.left) * canvas.width  / rect.width,
    y: (event.clientY - rect.top ) * canvas.height / rect.height,
  };
}

function setPickPosition(event) {
  const pos = getCanvasRelativePosition(event);
  pickPosition.x = (pos.x / canvas.width ) *  2 - 1;
  pickPosition.y = (pos.y / canvas.height) * -2 + 1;  // note we flip Y
}
window.addEventListener('mousemove', setPickPosition);

A worker can't read the mouse position directly so just like the size code let's send a message with the mouse position. Like the size code we'll send the mouse position and update `pickPosition`

function size(data) {
  state.width = data.width;
  state.height = data.height;
}

+function mouse(data) {
+  pickPosition.x = data.x;
+  pickPosition.y = data.y;
+}

const handlers = {
  init,
+  mouse,
  size,
};

self.onmessage = function(e) {
  const fn = handlers[e.data.type];
  if (typeof fn !== 'function') {
    throw new Error('no handler for type: ' + e.data.type);
  }
  fn(e.data);
};

Back in our main page we need to add code to pass the mouse to the worker or the main page.

+let sendMouse;

function startWorker(canvas) {
  const offscreen = canvas.transferControlToOffscreen();
  const worker = new Worker('offscreencanvas-worker-picking.js', {type: 'module'});
  worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);

+  sendMouse = (x, y) => {
+    worker.postMessage({
+      type: 'mouse',
+      x,
+      y,
+    });
+  };

  function sendSize() {
    worker.postMessage({
      type: 'size',
      width: canvas.clientWidth,
      height: canvas.clientHeight,
    });
  }

  window.addEventListener('resize', sendSize);
  sendSize();

  console.log('using OffscreenCanvas');  /* eslint-disable-line no-console */
}

function startMainPage(canvas) {
  init({canvas});

+  sendMouse = (x, y) => {
+    pickPosition.x = x;
+    pickPosition.y = y;
+  };

  function sendSize() {
    state.width = canvas.clientWidth;
    state.height = canvas.clientHeight;
  }
  window.addEventListener('resize', sendSize);
  sendSize();

  console.log('using regular canvas');  /* eslint-disable-line no-console */
}

Then we can copy in all the mouse handling code to the main page and make just minor changes to use `sendMouse`

function setPickPosition(event) {
  const pos = getCanvasRelativePosition(event);
-  pickPosition.x = (pos.x / canvas.clientWidth ) *  2 - 1;
-  pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1;  // note we flip Y
+  sendMouse(
+      (pos.x / canvas.clientWidth ) *  2 - 1,
+      (pos.y / canvas.clientHeight) * -2 + 1);  // note we flip Y
}

function clearPickPosition() {
  // unlike the mouse which always has a position
  // if the user stops touching the screen we want
  // to stop picking. For now we just pick a value
  // unlikely to pick something
-  pickPosition.x = -100000;
-  pickPosition.y = -100000;
+  sendMouse(-100000, -100000);
}
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);

window.addEventListener('touchstart', (event) => {
  // prevent the window from scrolling
  event.preventDefault();
  setPickPosition(event.touches[0]);
}, {passive: false});

window.addEventListener('touchmove', (event) => {
  setPickPosition(event.touches[0]);
});

window.addEventListener('touchend', clearPickPosition);

and with that picking should be working with `OffscreenCanvas`.

[click here to open in a separate window](/manual/examples/offscreencanvas-w-picking.html)

Let's take it one more step and add in the [`OrbitControls`](/docs/#examples/controls/OrbitControls). This will be little more involved. The [`OrbitControls`](/docs/#examples/controls/OrbitControls) use the DOM pretty extensively checking the mouse, touch events, and the keyboard.

Unlike our code so far we can't really use a global `state` object without re-writing all the OrbitControls code to work with it. The OrbitControls take an `HTMLElement` to which they attach most of the DOM events they use. Maybe we could pass in our own object that has the same API surface as a DOM element. We only need to support the features the OrbitControls need.

Digging through the [OrbitControls source code](https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js) it looks like we need to handle the following events.

- contextmenu
- pointerdown
- pointermove
- pointerup
- touchstart
- touchmove
- touchend
- wheel
- keydown

For the pointer events we need the `ctrlKey`, `metaKey`, `shiftKey`, `button`, `pointerType`, `clientX`, `clientY`, `pageX`, and `pageY`, properties.

For the keydown events we need the `ctrlKey`, `metaKey`, `shiftKey`, and `keyCode` properties.

For the wheel event we only need the `deltaY` property.

And for the touch events we only need `pageX` and `pageY` from the `touches` property.

So, let's make a proxy object pair. One part will run in the main page, get all those events, and pass on the relevant property values to the worker. The other part will run in the worker, receive those events and pass them on using events that have the same structure as the original DOM events so the OrbitControls won't be able to tell the difference.

Here's the code for the worker part.

OrbitControls with OffscreenCanvas

import {EventDispatcher} from 'three';

class ElementProxyReceiver extends EventDispatcher {
  constructor() {
    super();
  }
  handleEvent(data) {
    this.dispatchEvent(data);
  }
}

All it does is if it receives a message it dispatches it. It inherits from [`EventDispatcher`](/docs/#api/en/core/EventDispatcher) which provides methods like `addEventListener` and `removeEventListener` just like a DOM element so if we pass it to the OrbitControls it should work.

`ElementProxyReceiver` handles 1 element. In our case we only need one but it's best to think head so lets make a manager to manage more than one of them.

class ProxyManager {
  constructor() {
    this.targets = {};
    this.handleEvent = this.handleEvent.bind(this);
  }
  makeProxy(data) {
    const {id} = data;
    const proxy = new ElementProxyReceiver();
    this.targets[id] = proxy;
  }
  getProxy(id) {
    return this.targets[id];
  }
  handleEvent(data) {
    this.targets[data.id].handleEvent(data.data);
  }
}

We can make a instance of `ProxyManager` and call its `makeProxy` method with an id which will make an `ElementProxyReceiver` that responds to messages with that id.

Let's hook it up to our worker's message handler.

const proxyManager = new ProxyManager();

function start(data) {
  const proxy = proxyManager.getProxy(data.canvasId);
  init({
    canvas: data.canvas,
    inputElement: proxy,
  });
}

function makeProxy(data) {
  proxyManager.makeProxy(data);
}

...

const handlers = {
-  init,
-  mouse,
+  start,
+  makeProxy,
+  event: proxyManager.handleEvent,
   size,
};

self.onmessage = function(e) {
  const fn = handlers[e.data.type];
  if (typeof fn !== 'function') {
    throw new Error('no handler for type: ' + e.data.type);
  }
  fn(e.data);
};

In our shared three.js code we need to import the [`OrbitControls`](/docs/#examples/controls/OrbitControls) and set them up.

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

export function init(data) {
-  const {canvas} = data;
+  const {canvas, inputElement} = data;
  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});

+  const controls = new OrbitControls(camera, inputElement);
+  controls.target.set(0, 0, 0);
+  controls.update();

Notice we're passing the OrbitControls our proxy via `inputElement` instead of passing in the canvas like we do in other non-OffscreenCanvas examples.

Next we can move all the picking event code from the HTML file to the shared three.js code as well while changing `canvas` to `inputElement`.

function getCanvasRelativePosition(event) {
-  const rect = canvas.getBoundingClientRect();
+  const rect = inputElement.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  };
}

function setPickPosition(event) {
  const pos = getCanvasRelativePosition(event);
-  sendMouse(
-      (pos.x / canvas.clientWidth ) *  2 - 1,
-      (pos.y / canvas.clientHeight) * -2 + 1);  // note we flip Y
+  pickPosition.x = (pos.x / inputElement.clientWidth ) *  2 - 1;
+  pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1;  // note we flip Y
}

function clearPickPosition() {
  // unlike the mouse which always has a position
  // if the user stops touching the screen we want
  // to stop picking. For now we just pick a value
  // unlikely to pick something
-  sendMouse(-100000, -100000);
+  pickPosition.x = -100000;
+  pickPosition.y = -100000;
}

*inputElement.addEventListener('mousemove', setPickPosition);
*inputElement.addEventListener('mouseout', clearPickPosition);
*inputElement.addEventListener('mouseleave', clearPickPosition);

*inputElement.addEventListener('touchstart', (event) => {
  // prevent the window from scrolling
  event.preventDefault();
  setPickPosition(event.touches[0]);
}, {passive: false});

*inputElement.addEventListener('touchmove', (event) => {
  setPickPosition(event.touches[0]);
});

*inputElement.addEventListener('touchend', clearPickPosition);

Back in the main page we need code to send messages for all the events we enumerated above.

let nextProxyId = 0;
class ElementProxy {
  constructor(element, worker, eventHandlers) {
    this.id = nextProxyId++;
    this.worker = worker;
    const sendEvent = (data) => {
      this.worker.postMessage({
        type: 'event',
        id: this.id,
        data,
      });
    };

    // register an id
    worker.postMessage({
      type: 'makeProxy',
      id: this.id,
    });
    for (const [eventName, handler] of Object.entries(eventHandlers)) {
      element.addEventListener(eventName, function(event) {
        handler(event, sendEvent);
      });
    }
  }
}

`ElementProxy` takes the element who's events we want to proxy. It then registers an id with the worker by picking one and sending it via the `makeProxy` message we setup earlier. The worker will make an `ElementProxyReceiver` and register it to that id.

We then have an object of event handlers to register. This way we can pass handlers only for these events we want to forward to the worker.

When we start the worker we first make a proxy and pass in our event handlers.

function startWorker(canvas) {
  const offscreen = canvas.transferControlToOffscreen();
  const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', {type: 'module'});

+  const eventHandlers = {
+    contextmenu: preventDefaultHandler,
+    mousedown: mouseEventHandler,
+    mousemove: mouseEventHandler,
+    mouseup: mouseEventHandler,
+    pointerdown: mouseEventHandler,
+    pointermove: mouseEventHandler,
+    pointerup: mouseEventHandler,
+    touchstart: touchEventHandler,
+    touchmove: touchEventHandler,
+    touchend: touchEventHandler,
+    wheel: wheelEventHandler,
+    keydown: filteredKeydownEventHandler,
+  };
+  const proxy = new ElementProxy(canvas, worker, eventHandlers);
  worker.postMessage({
    type: 'start',
    canvas: offscreen,
+    canvasId: proxy.id,
  }, [offscreen]);
  console.log('using OffscreenCanvas');  /* eslint-disable-line no-console */
}

And here are the event handlers. All they do is copy a list of properties from the event they receive. They are passed a `sendEvent` function to which they pass the data they make. That function will add the correct id and send it to the worker.

const mouseEventHandler = makeSendPropertiesHandler([
  'ctrlKey',
  'metaKey',
  'shiftKey',
  'button',
  'pointerType',
  'clientX',
  'clientY',
  'pointerId',
  'pageX',
  'pageY',
]);
const wheelEventHandlerImpl = makeSendPropertiesHandler([
  'deltaX',
  'deltaY',
]);
const keydownEventHandler = makeSendPropertiesHandler([
  'ctrlKey',
  'metaKey',
  'shiftKey',
  'keyCode',
]);

function wheelEventHandler(event, sendFn) {
  event.preventDefault();
  wheelEventHandlerImpl(event, sendFn);
}

function preventDefaultHandler(event) {
  event.preventDefault();
}

function copyProperties(src, properties, dst) {
  for (const name of properties) {
      dst[name] = src[name];
  }
}

function makeSendPropertiesHandler(properties) {
  return function sendProperties(event, sendFn) {
    const data = {type: event.type};
    copyProperties(event, properties, data);
    sendFn(data);
  };
}

function touchEventHandler(event, sendFn) {
  // preventDefault() fixes mousemove, mouseup and mousedown
  // firing when doing a simple touchup touchdown
  // Happens only at offscreen canvas
  event.preventDefault();
  const touches = [];
  const data = {type: event.type, touches};
  for (let i = 0; i < event.touches.length; ++i) {
    const touch = event.touches[i];
    touches.push({
      pageX: touch.pageX,
      pageY: touch.pageY,
      clientX: touch.clientX,
      clientY: touch.clientY,
    });
  }
  sendFn(data);
}

// The four arrow keys
const orbitKeys = {
  '37': true,  // left
  '38': true,  // up
  '39': true,  // right
  '40': true,  // down
};
function filteredKeydownEventHandler(event, sendFn) {
  const {keyCode} = event;
  if (orbitKeys[keyCode]) {
    event.preventDefault();
    keydownEventHandler(event, sendFn);
  }
}

This seems close to running but if we actually try it we'll see that the [`OrbitControls`](/docs/#examples/controls/OrbitControls) need a few more things.

One is they call `element.focus`. We don't need that to happen in the worker so let's just add a stub.

class ElementProxyReceiver extends THREE.EventDispatcher {
  constructor() {
    super();
  }
  handleEvent(data) {
    this.dispatchEvent(data);
  }
+  focus() {
+    // no-op
+  }
}

Another is they call `event.preventDefault` and `event.stopPropagation`. We're already handling that in the main page so those can also be a noop.

+function noop() {
+}

class ElementProxyReceiver extends THREE.EventDispatcher {
  constructor() {
    super();
  }
  handleEvent(data) {
+    data.preventDefault = noop;
+    data.stopPropagation = noop;
    this.dispatchEvent(data);
  }
  focus() {
    // no-op
  }
}

Another is they look at `clientWidth` and `clientHeight`. We were passing the size before but we can update the proxy pair to pass that as well.

In the worker...

class ElementProxyReceiver extends THREE.EventDispatcher {
  constructor() {
    super();
  }
+  get clientWidth() {
+    return this.width;
+  }
+  get clientHeight() {
+    return this.height;
+  }
+  getBoundingClientRect() {
+    return {
+      left: this.left,
+      top: this.top,
+      width: this.width,
+      height: this.height,
+      right: this.left + this.width,
+      bottom: this.top + this.height,
+    };
+  }
  handleEvent(data) {
+    if (data.type === 'size') {
+      this.left = data.left;
+      this.top = data.top;
+      this.width = data.width;
+      this.height = data.height;
+      return;
+    }
    data.preventDefault = noop;
    data.stopPropagation = noop;
    this.dispatchEvent(data);
  }
  focus() {
    // no-op
  }
}

back in the main page we need to send the size and the left and top positions as well. Note that as is we don't handle if the canvas moves, only if it resizes. If you wanted to handle moving you'd need to call `sendSize` anytime something moved the canvas.

class ElementProxy {
  constructor(element, worker, eventHandlers) {
    this.id = nextProxyId++;
    this.worker = worker;
    const sendEvent = (data) => {
      this.worker.postMessage({
        type: 'event',
        id: this.id,
        data,
      });
    };

    // register an id
    worker.postMessage({
      type: 'makeProxy',
      id: this.id,
    });
+    sendSize();
    for (const [eventName, handler] of Object.entries(eventHandlers)) {
      element.addEventListener(eventName, function(event) {
        handler(event, sendEvent);
      });
    }

+    function sendSize() {
+      const rect = element.getBoundingClientRect();
+      sendEvent({
+        type: 'size',
+        left: rect.left,
+        top: rect.top,
+        width: element.clientWidth,
+        height: element.clientHeight,
+      });
+    }
+
+    window.addEventListener('resize', sendSize);
  }
}

and in our shared three.js code we no longer need `state`

-export const state = {
-  width: 300,   // canvas default
-  height: 150,  // canvas default
-};

...

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

function render(time) {
  time *= 0.001;

  if (resizeRendererToDisplaySize(renderer)) {
-    camera.aspect = state.width / state.height;
+    camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
    camera.updateProjectionMatrix();
  }

  ...

A few more hacks. The OrbitControls add `pointermove` and `pointerup` events to the `ownerDocument` of the element to handle mouse capture (when the mouse goes outside the window).

Further the code references the global `document` but there is no global document in a worker.

We can solve all of these with a 2 quick hacks. In our worker code we'll re-use our proxy for both problems.

function start(data) {
  const proxy = proxyManager.getProxy(data.canvasId);
+  proxy.ownerDocument = proxy; // HACK!
+  self.document = {} // HACK!
  init({
    canvas: data.canvas,
    inputElement: proxy,
  });
}

This will give the [`OrbitControls`](/docs/#examples/controls/OrbitControls) something to inspect which matches their expectations.

I know that was kind of hard to follow. The short version is: `ElementProxy` runs on the main page and forwards DOM events to `ElementProxyReceiver` in the worker which masquerades as an `HTMLElement` that we can use both with the [`OrbitControls`](/docs/#examples/controls/OrbitControls) and with our own code.

The final thing is our fallback when we are not using OffscreenCanvas. All we have to do is pass the canvas itself as our `inputElement`.

function startMainPage(canvas) {
-  init({canvas});
+  init({canvas, inputElement: canvas});
  console.log('using regular canvas');
}

and now we should have OrbitControls working with OffscreenCanvas

[click here to open in a separate window](/manual/examples/offscreencanvas-w-orbitcontrols.html)

This is probably the most complicated example on this site. It's a little hard to follow because there are 3 files involved for each sample. The HTML file, the worker file, the shared three.js code.

I hope it wasn't too difficult to understand and that it provided some useful examples of working with three.js, OffscreenCanvas and web workers.