Camera rotation with ArcballControls

Hi.

I’m using ArcballControls but I also want to control the camera programmatically. The user should be able to control the viewpoint with the mouse and jump to specific views by clicking a button.

The documentation of ArcballControls says that after “manual changes to the camera’s transform” the update() method must be called. This works fine when changing the camera’s position and when changing the controls’ target. The only thing not working is the camera’s rotation. After calling update() the camera’s rotation resets to the values from before the manual change.

Am I doing something wrong?

Is this the desired behavior of the ArcballControls? If not, what is the reason? Is updating the rotation particularly complicated and/or expensive?

Thank you!

Edit: I’m only interested in rotation around the z-axis. Changing the x and y rotation would mean changing the target of the controls.

2 Likes

The entire point of controls is to do the rotations for you. After adding controls to the scene, the only way you should be controlling the rotation is by modifying the target (controls’ job is to override the rotation so that the camera looks at the target - if you modify the rotation manually, the camera won’t be looking at the target anymore :sweat_smile:)

Not really - because instead, it’s just impossible :relieved:

1 Like

I understand how the x and y rotation of the camera determines where the camera looks at. If I want the camera to look somewhere else I have to change the target.

But what about the z-rotation? I visualized the problem:

The camera looks at the same point in both images, i.e. the target does not change. It simply rotates around its own z-axis or changes its up vector if you will.

Am I missing something?

If the z-rotation somehow does change the target, how would I manipulate the target to achieve the z-rotation? As the target is just a Vector3 I don’t think this is possible.

Changing the up vector of the camera and calling update() worked:)

1 Like

@moritzhertler could you please share an example of how you achieved it? I was trying to do something similar with OrbitControls, but ended up rotating an object.

Sure. Here is a quick example:

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <style>
            body {
                margin: 0;
            }
        </style>
    </head>
    <body>
        <input
            type="range" id="rotation" value="0" min="-360" max="360" step="0.01"
            style="position: absolute; left: 50%; bottom: 2em; transform: translateX(-50%);"
        />
        <script type="module" src="/main.js"></script>
    </body>
</html>

main.js

import * as THREE from "three";
import { ArcballControls } from "three/addons/controls/ArcballControls.js";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight
);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

scene.add(new THREE.AxesHelper(3));
camera.position.z = 4;

const controls = new ArcballControls(camera, renderer.domElement, scene);

const DEGREES_TO_RADIANS = Math.PI / 180;

const input = document.getElementById("rotation");
input.addEventListener("input", (e) => {
    const radians = e.target.value * DEGREES_TO_RADIANS;
    camera.up = new THREE.Vector3(Math.cos(radians), Math.sin(radians), 0);
    controls.update();
});

function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}

animate();

This should work with OrbitControls as well.

1 Like

Hello, I realise this is solved however I thought I would supply another method as I was told that the up direction is not the “appropriate” way to modify when using ArcBallController. However each to their own I have no motive apart from a code share.

		function rollCamera(axis, radians) {
		//check the axis and set the vector
		let vector = new Vector3(0, 0, 1);
		if (axis === "Z") {
			vector = new Vector3(0, 0, 1);
		} else if (axis === "Y") {
			vector = new Vector3(0, 1, 0);
		} else if (axis === "X") {
			vector = new Vector3(1, 0, 0);
		} else {
			vector = new Vector3(0, 0, 1);
		}
		if (controls instanceof ArcballControls) {
			// Get the vector from the camera to the target (controls.target)
			const direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize();

			// Create a quaternion representing the rotation around the Z axis
			const quaternion = new THREE.Quaternion();
			quaternion.setFromAxisAngle(vector, radians);

			// Rotate the direction vector
			direction.applyQuaternion(quaternion);

			// Calculate the new position of the camera
			const distance = camera.position.distanceTo(controls.target);
			const newPosition = new THREE.Vector3().addVectors(controls.target, direction.multiplyScalar(distance));

			// Apply the new position and up vector to the camera
			camera.position.copy(newPosition);
			camera.up.applyQuaternion(quaternion);

			// Look at the target
			camera.lookAt(controls.target);

			// Update the camera's matrix and the controls
			camera.updateMatrixWorld();
			controls.update();
		} else if (controls instanceof TrackballControls) {
			//store the current camera position
			const position = controls.object.position.clone();
			// Store the current look at target
			const target = controls.target.clone();

			// Get the camera's local Z-axis (up direction)
			const cameraMatrix = new THREE.Matrix4();
			cameraMatrix.lookAt(position, target, controls.object.up);
			const cameraLocalZAxis = vector.applyMatrix4(cameraMatrix);

			// Rotate the camera around its local Z-axis (up direction)
			controls.object.up.applyAxisAngle(cameraLocalZAxis, radians);

			// Update the controls
			controls.update();
		}
	}

I’m calling this function from a lil-gui slider at the moment.

	const upOptions = ["X", "Y", "Z"];
	gui.add(params, "upDirection", upOptions).name("Up Direction Axis").onChange(function() {
		switch (params.upDirection) {
			case "Y":
				camera.up.set(0, 1, 0);
				break;
			case "X":
				camera.up.set(1, 0, 0);
				break;
			case "Z":
				camera.up.set(0, 0, 1);
				break;
		}
		camera.updateProjectionMatrix();
	});
	// Store the previous rotation angle
	let prevRotationAngle = params.rotationAngle;

	// Add a slider for rotation angle in degrees
	gui.add(params, "rotationAngle", 0, 360).name("View Angle (°)").onChange(function() {
		// Calculate the delta rotation angle
		const deltaAngle = params.rotationAngle - prevRotationAngle;

		// Convert delta angle to radians
		const deltaAngleRad = THREE.MathUtils.degToRad(deltaAngle);

		// Update the previous rotation angle
		prevRotationAngle = params.rotationAngle;

		// Call the rollCamera function with the delta angle in radians
		const axis = params.upDirection;
		rollCamera(axis, 0);
		rollCamera(axis, deltaAngleRad);
	});

Hoping this helps others search to do this.
Here is a little GIF that shows it in action.
RollCamera

2 Likes

Thanks for sharing! I ended up writing my own version of ArcballControls with easing, autorotate and other features.

1 Like

Nice! Would you mind publishing this somewhere?

Unfortunately I can’t do it yet. Maybe will be able to share it in future. But you can see it in action here: https://wearitar.com. The feedback is much appreciated!