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) {
e.preventDefault();
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';
vector.unproject(camera);
vector.sub(camera.position);
camera.position[func](camera.position,vector.setLength(factor));
controls.target[func](controls.target,vector.setLength(factor));
camera.updateProjectionMatrix();
})
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(),
factor;
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
dest.unproject(_this.object);
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
_this.target.add(pan)
// 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!