Zoom to cursor with Trackball Controls

I’m an absolute dunce in 3D who’s trying to figure out how to zoom towards the cursor position using the Trackball controls. I’ve seen @WestLangley’s related work introducing scroll to cursor for the Orbit and Map controls, and want to achieve something similar for the Trackball controls.

Here’s my current WIP. (My changes to the default trackball controls are basically to store a new variable named _mouseWheelLocation and the code inside this.zoomCamera.) If you zoom gently toward one vertex of the cube in the distance, you’ll find you zoom toward that vertex, but the camera zooms way too strongly in that direction and flies right past the cube.

I’m wondering if someone here can help me understand what I’m missing. I’ve been studying the Trackball controls today, but still don’t understand them entirely, so any pointers others can offer would be hugely helpful!

Any chance you can show a live example via JSFiddle or Codepen?

Sure thing! Here’s a little Codepen: https://codepen.io/duhaime/pen/VwLrWZm

The Trackball controls are there in their entirety, but I essentially only changed the zoomCamera function…

Any pointers would be hugely helpful!

Well I found a solution to this task. First off, there are numerous examples online of how to disable the trackball control’s zoom listener and create a custom mousewheel listener that zooms towards the cursor, e.g.:

controls.noZoom = true;

window.addEventListener('mousewheel', function(e) {
  var x = ( event.clientX / window.innerWidth ) * 2 - 1,
      y = - ( event.clientY / window.innerHeight ) * 2 + 1,
      vector = new THREE.Vector3(x, y, 1),
      factor = 0.005,
      func = e.deltaY < 0 ? 'addVectors' : 'subVectors';

That approach disregards the momentum of the scroll though, which is a really nice effect I wanted to preserve. So I wanted to zoom toward the user’s cursor while preserving momentum. To figure out how to do so, I studied the trackball controls a bit and learned that the panCamera function creates a Vector2 that specifies the amount to move in the x and y planes within world coordinates:

pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );

So my goal was to essentially apply a similar force, except the x and y components to be applied would be determined by the user’s cursor position. To figure out those x and y components, I store the position of the user’s cursor when scrolling in a Vec2 _mouseWheelLocation. (That variable stores the position of the mousewheel event in clip coordinates scaled -1:1 in each dimension).

Then, inside the zoom event handler, I converted the clip coords to world space coords in a Vec3 vector. Next I determined the z distance of the camera to vector, and I figured out the percent of that distance that the camera would travel in the current frame (pz). Then I just applied a translation force to the camera in the x y planes towards vector that was proportional to the distance being travelled in the z plane (pz). So the zoomCamera function looks like:

this.zoomCamera = (function() {
  return function zoomCamera() {
    var dest = new THREE.Vector3(),
        pan = new THREE.Vector3(),
        objectUp = new THREE.Vector3(),
    if ( _state === STATE.TOUCH_ZOOM_PAN ) {
      factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
      _touchZoomDistanceStart = _touchZoomDistanceEnd;
      _eye.multiplyScalar( factor );
    } else {

      factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;
      if ( factor !== 1.0 && factor > 0.0 ) {

        // set the direction toward the location the user mousewheeled
        factor = factor || 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;

        // determine the coordinates to zoom towards
        dest.set(_mouseWheelLocation.x, _mouseWheelLocation.y, 0.0);

        // convert target from screen coords (0:1) to clip coords (-1:1)
        dest = dest.addScalar(-0.5).multiplyScalar(2.0);

        // find the world space coordinates of user mouse position during zoom
        var direction = dest.sub(_this.object.position).normalize(),
            distance = - _this.object.position.z / direction.z,
            scaled = direction.multiplyScalar(distance),
            dest = _this.object.position.clone().add(scaled);

        // find the distance we're scrolling in the z plane
        var zz = _eye.clone().multiplyScalar(factor).z - _eye.clone().z;

        // use the percent of zoom in the z dimension to scale changes in x y planes
        var pz = zz / _this.object.position.z;

        // apply the translation force in the x y planes
        var pan = new THREE.Vector3(),
            objectUp = new THREE.Vector3();

        // find the distance between camera and destination in the x & y planes
        var dx = dest.x - _this.object.position.x,
            dy = dest.y - _this.object.position.y;

        // apply the x pan component
        pan.copy( _eye ).cross( _this.object.up ).setLength( dx * pz );

        // apply the y pan component
        pan.add( objectUp.copy( _this.object.up ).setLength( dy * pz ) );

        // actually add those forces to the target

        // apply the z translation component
        _eye.multiplyScalar( factor );

      if ( _this.staticMoving ) {
        _zoomStart.copy( _zoomEnd );
      } else {
        _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;


Here’s a fiddle that inlines the updated controls to zoom towards the cursor.

Here’s the full diff of the controls before and after these changes were made.

If anyone with more experience in 3d can give me advice on how they would perform the task of zooming toward the user cursor, I’d certainly be grateful. I thought it could be helpful to clean up some of this code and potentially submit a PR to make this an optional, non-default feature for the trackball controls, as the question of how to zoom toward cursor with the trackball controls has come up several times in GitHub and StackOverflow. In any event, any input from others would be super helpful!