Plugins
This note is based on DISCOVER three.js, mostly excerpts with some personal understanding
Introduction
The three.js core is a powerful, lightweight, and focused rendering framework with deliberately limited functionality. It has everything needed to create and render physically correct scenes, but it doesn't have everything needed to create a game or product configurator. Even when building relatively simple applications, you'll often find you need functionality not in the core library. When this happens, before writing any code yourself, check if there's a plugin available. The three.js repository contains hundreds of extensions located in the examples/jsm folder. For those using package managers, these are also included in the NPM package.
- One of many post-processing effects
- A loader for the Autodesk FBX format
- An exporter for the glTF format
- Physically accurate ocean and sky
You can find the module containing OrbitControls in the three.js repository's examples/jsm/controls/ folder in a file called OrbitControls.js. There's also an official example demonstrating OrbitControls. For a quick reference of all control settings and features, go to the OrbitControls documentation page.
Creating controls.js
Create systems/controls.js:
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
function createControls(camera, canvas) {
const controls = new OrbitControls(camera, canvas);
// damping and auto rotation require
// the controls to be updated each frame
// this.controls.autoRotate = true;
controls.enableDamping = true;
controls.tick = () => controls.update();
return controls;
}
export { createControls };enableDampingenables damping for added realism- By default, the controls rotate around the scene center, the point (0,0,0). This is stored in the
controls.targetproperty, which is aVector3. We can move this target to a new position:controls.target.set(1,2,3); - Whenever the user triggers this plugin, it produces some animation, so we add
controls.tick = () => controls.update();
Using in World.js
import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
import { createControls } from './systems/controls.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
import { Loop } from './systems/Loop.js';
let camera;
let renderer;
let scene;
let loop;
class World {
constructor(container) {
camera = createCamera();
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
const controls = createControls(camera, renderer.domElement);
const cube = createCube();
const light = createLights();
loop.updatables.push(controls);
// stop the cube's animation
// loop.updatables.push(cube);
scene.add(cube, light);
const resizer = new Resizer(container, camera, renderer);
}
render() {
// draw a single frame
renderer.render(scene, camera);
}
start() {
loop.start();
}
stop() {
loop.stop();
}
}
export { World };Manual Camera Control
Cutting to a New Camera Position
// move the camera
camera.position.set(1, 2, 3);
// and/or rotate the camera
camera.rotation.set(0.5, 0, 0);
// then tell the controls to update
controls.update();If you're calling .update in a loop, you don't need to do it manually - just move the camera. If you move the camera without calling .update, strange things will happen, so be careful!
When you move the camera, controls.target doesn't move. If you haven't moved it, it will stay at the scene center. When you move the camera to a new position but keep the target the same, the camera not only moves but also rotates to continue pointing at the target. This means camera movement may not work as expected when using controls. Usually, you need to move both the camera and target simultaneously to get the desired result.
Smooth Transition to a New Camera Position
If you want to smoothly animate the camera to a new position, you'll likely need to transition both the camera and target simultaneously, and the best place to do this is in the controls.tick method. However, you need to disable the controls during the animation, otherwise if the user tries to move the camera before the animation completes, you'll end up with controls conflicting with the animation, usually with catastrophic results.
controls.enabled = false;Saving and Restoring View State
controls.saveState();
// sometime later:
controls.reset();Destroying Controls
controls.dispose();On-Demand Rendering
To use orbit controls with on-demand rendering, you must render a frame when this event fires:
Using OrbitControls with on-demand rendering:
controls.addEventListener('change', () => {
renderer.render(scene, camera);
});To set this up in World.js, you would use this.render:
World.js: Using OrbitControls with on-demand rendering
controls.addEventListener('change', () => {
this.render();
});Next, in main.js, ensure we no longer start the loop. Instead, render the initial frame:
main.js: Render a single frame instead of starting the loop
// render the initial frame
world.render();If you make these changes in your application, you'll find this causes a small problem. When we render the initial frame in main.js, the texture hasn't loaded yet, so the cube appears black. If we run the loop, this frame is almost immediately replaced by a new frame after the texture loads, so the cube is black for only a few milliseconds and might not even be noticed. However, with on-demand rendering, we now only generate new frames when the user interacts with the scene and moves the camera. Once you move the controls, sure enough, a new frame is created and the texture appears.
So you also need to generate a new frame after the texture loads. We won't cover how to do that here, but hopefully it highlights why on-demand rendering is trickier than using a loop. You must consider all cases that need a new frame (for example, don't forget you also need to render a frame on resize).