Recently, I was playing with the Zygote body Google Experiments, and noticed that the supported camera controls were quite constrained. The zoom-in, zoom-out and rotation were all centered around the Y axis, rather than the object underneath the mouse pointer. This made it difficult to focus on parts relatively far from the Y axis (shoulders for example). The Y axis is effectively locked as the "object of interest".

We have already seen 'better' orbit controls in CAD software like AutoCAD, etc... so its clearly not a new idea - on the contrary, its has existed for as long as 3D CAD software - probably longer than the internet itself. Looking around at different free 3D graphics related programs online, this pattern seems to repeat itself (looking at other 3D chrome experiments, BlocksCAD3d, etc).

I decided to implement such a set of orbit controls, which allow the object under the mouse pointer to be the 'object of interest' with respect to which the camera then rotates / zooms.

The demo below shows such a set of controls at work. The crucial parts of the source code for the controls follows below the demo (built on top of three.js).

Controls:

• Right click to orbit
• Mouse wheel to zoom in/out.

Figuring out the exact rotations turned out to be a nice little challenge - however, the final solution was fairly straight forward (as it often happens in mathematical maniputations). The three.js matrix operations make it fairly easy to implement the new orbiting manipulations (once the required rotations have been figured out). The code is well commented and should be fairly self explanatory.

``````  function orbit(xRot, yRot, camPos, camLookAt, target) {
const newXAxis = camLookAt.clone();
const lx = camLookAt.x;
const lz = camLookAt.z;
// horizontal axis perpendicular to the camera lookat
newXAxis.x = -lz;
newXAxis.z = lx;
newXAxis.y = 0;

// the .sub() and .add() wrt target are to make sure we do rotations
// wrt to the target position, and not to the origin
const newCamPos = camPos
.sub(target)
.applyAxisAngle( newXAxis, xRot ) // up / down rotation
.applyAxisAngle( yaxis, yRot ) // left / right rotation
camera.position.set(...newCamPos.toArray());

// rotate the lookat direction exactly equal to the rotation
const relLookAt = camLookAt
.applyAxisAngle( newXAxis, xRot )
.applyAxisAngle( yaxis, yRot )
camera.lookAt(...relLookAt.toArray());

camera.updateProjectionMatrix();
}

// retrieve the target under the mouse pointer
function getTarget(camera, event, highlight=false) {
const [x, y] = [event.offsetX, event.offsetY];
const [cw, ch] = [canvas.width, canvas.height];
const mouse3D = new THREE.Vector3( ( x / cw ) * 2 - 1,
-( y / ch ) * 2 + 1,
0.5 );
const raycaster =  new THREE.Raycaster();
raycaster.setFromCamera( mouse3D, camera );
const intersects = raycaster.intersectObjects( objects );
const nv = new THREE.Vector3();
camera.getWorldDirection(nv);
console.log({ raycaster, intersects, nv });
if ( intersects.length > 0 ) {
if (highlight) {
}
return intersects[0].object.position.clone();
}