Three.js: Ability to set OrbitControls different camera.lookAt vs controls.target

Created on 25 Jan 2020  路  7Comments  路  Source: mrdoob/three.js

Feature request

OrbitControls allows us to quickly implement a rotating camera around a scene. But many people actually use it for rotating an object. There are use cases where you don't always want your camera viewport to look directly at the controls rotation target.

Consider the SketchFab object viewer for example:
https://sketchfab.com/3d-models/the-whole-world-712c2de7426f4eb59aa101d21cd72492

When you rotate an object with the camera zoomed out, all functions well:
Screen Shot 2020-01-24 at 2 43 25 PM

You can zoom and pan to focus the camera on the poles using Shift-Mouse-drag:
Screen Shot 2020-01-24 at 2 44 06 PM

But now when you continue to rotate around the globe, you suddenly start seeing inside the object and strange angles (because the rotation target is not the center of the globe anymore):
Screen Shot 2020-01-24 at 2 59 03 PM

1) Allow camera.lookAt to be offset from controls.target
Now I understand it's intended functionality for OrbitControls to keep the camera looking at the target as it's primarily for orbiting a scene not an object, but wondering if it's possible to support more customization of the controls target vs camera lookAt functionality? currently when using OrbitControls the camera.lookAt function is effectively disabled.

2) Create ObjectControls for exploring/panning individual objects
Or if you would consider a new ObjectControls template which rotates the object smoothly instead of the camera? allowing for movement of the camera independently from the object rotation? since many people are using OrbitControls to imitate rotating an object rather than a scene anyway? There is an example someone has created here:
http://benchung.com/smooth-mouse-rotation-three-js/

Your default jsfiddle actually shows this problem very well (Try panning using Shift-Mouse-drag):
https://jsfiddle.net/hyok6tvj/

Other people have asked for a solution to this use case here:
https://stackoverflow.com/questions/34526500/threejs-orbitcontrol-with-different-rotate-center-and-lookat-point

Three.js version
  • [x] Dev
  • [x] r112
Browser
  • [x] All of them
  • [ ] Chrome
  • [ ] Firefox
  • [ ] Internet Explorer
OS
  • [x] All of them
  • [ ] Windows
  • [ ] macOS
  • [ ] Linux
  • [ ] Android
  • [ ] iOS
Hardware Requirements (graphics card, VR Device, ...)

None

Enhancement

Most helpful comment

@kmturley Do you mean something like this target/pivot OrbitControls Example ?

The main difference in this version is that I've decoupled pivot, which is the point the camera rotates around, and target, which is the point the camera looks at.
In this specific case, I've modified the pan command to move the target position, but the pivot stays centered at the object.

I agree with @Mugen87, that perhaps this doesn't fit in the default OrbitControls, but feel free to use this version if it meets your expectations. I just don't guarantee it is entirely functional, as I haven't fully tested it.

/js/controls/OrbitControls - non-module
/jsm/controls/OrbitControls.js - module

All 7 comments

Allow camera.lookAt to be offset from controls.target

To keep OrbitControls simple, if would let the user implement this enhancement on app level. Right now, the implementation of OrbitControls is clear and comprehensible. TBH, performing this decoupling makes the controls only confusing.

Create ObjectControls for exploring/panning individual objects

Have you considered to use TransformControls for this?

https://threejs.org/examples/misc_controls_transform

@kmturley Do you mean something like this target/pivot OrbitControls Example ?

The main difference in this version is that I've decoupled pivot, which is the point the camera rotates around, and target, which is the point the camera looks at.
In this specific case, I've modified the pan command to move the target position, but the pivot stays centered at the object.

I agree with @Mugen87, that perhaps this doesn't fit in the default OrbitControls, but feel free to use this version if it meets your expectations. I just don't guarantee it is entirely functional, as I haven't fully tested it.

/js/controls/OrbitControls - non-module
/jsm/controls/OrbitControls.js - module

@Mugen87 I tried using Transform controls, but couldn't get a good working version. Although I found out I could fake the effect need by moving the sphere to the scene 0,0,0, it doesn't stop the Y axis rotation issue where the camera goes inside an object, I could lock the Y axis to prevent this.
https://jsfiddle.net/kmturley/ht1n3o4z/16/

@sciecode That's exactly the functionality needed. I created a demo here showing the camera pointing at the north pole of a globe (but the camera rotates around the globe normally):
https://jsfiddle.net/kmturley/6qxh3zc9/24/

I exposed the pan() function so pan could also be set programmatically:

this.pan = function (deltaX, deltaY) {
    pan(deltaX, deltaY);
}

Then setting the pan after the controls init using:

camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set(0, radius, radius * 2);

// controls
orbit = new OrbitControls( camera, renderer.domElement );
orbit.enableDamping = true;
orbit.pan(0, 200);

Ideally it would inherit the camera.lookAt setting like this:

camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set(0, radius, radius * 2);
camera.lookAt(0, radius, 0);

// controls
orbit = new OrbitControls( camera, renderer.domElement );
orbit.enableDamping = true;

Is that possible?

I exposed the pan() function so pan could also be set programmatically

No need for it. You may set controls.target directly after initialization.

orbit = new OrbitControls( camera, renderer.domElement );
orbit.target.set( 0, 200, 0 );

Is that possible?

As we said previously, it's unlikely this feature will get implemented in the default OrbitControls, let's see how others feel about it.

But if we choose to do it, my implementation definitely won't make to the final PR, as I modified some core mechanics to account for your use-case. However, given that enough support is given for adding the feature, I'll gladly adapt it to work correctly with our current implementation.

@sciecode after testing side-by-side your version doesn't exactly function how I expected. The horizontal orbit is correct, but the vertical rotation is off:

If you rotate your version vertically the globe still pivots around the control target:
Screen Shot 2020-01-27 at 2 32 21 PM
https://jsfiddle.net/kmturley/6qxh3zc9/29/

In my manual object spinning implementation the globe pivots around it's center:
Screen Shot 2020-01-27 at 2 32 35 PM
https://jsfiddle.net/kmturley/6317jd95/11/

I created a third version using TransformControls:
Which functions correctly, but requires the controls to be visible and does not support damping
Screen Shot 2020-01-27 at 3 42 31 PM
https://jsfiddle.net/kmturley/ht1n3o4z/27/

This is the previous functionality:
rotation

When you moved the pan/target you get this:
rotation3

Your version does this (which is better):
rotation2

But when rotating the vertical rotation it looks like this:
rotation4

What i'm looking for is this:
rotation5

I guess this could be accomplished by moving the target as the object is rotated

I managed to replicate the functionality (another way) by modifying the TransformControls library to include:

  • Damping on rotation
  • Free spin mode (without locking to axis)

TransformControls.js line 69

// Set to true to enable damping (inertia)
// If damping is enabled, you must call controls.update() in your animation loop
defineProperty( "enableDamping", false );
defineProperty( "dampingFactor", 0.05 );
defineProperty( "freeMode", false );
defineProperty( "rotateSpeed", 1.0 );

line 128:

var rotateStart = new Vector2();
var rotateEnd = new Vector2();
var rotateDelta = new Vector2();

line 255:

this.pointerHover = function ( pointer ) {

  if ( this.object === undefined || this.dragging === true || ( pointer.button !== undefined && pointer.button !== 0 ) ) return;

  if (this.freeMode === true) {
    this.axis = 'XYZE';
    return;
  }
  ray.setFromCamera( pointer, this.camera );

  var intersect = ray.intersectObjects( _gizmo.picker[ this.mode ].children, true )[ 0 ] || false;

  if ( intersect ) {

    this.axis = intersect.object.name;

  } else {

    this.axis = null;

  }

};

line 523:
var ROTATION_SPEED = 2 / worldPosition.distanceTo( _tempVector.setFromMatrixPosition( this.camera.matrixWorld ) );

line 724:

this.update = function () {
  if (this.dragging === false && scope.enableDamping ) {
    quaternionStart.copy( this.object.quaternion );
    offset = new Vector3(
      rotateDelta.x * 5,
      - rotateDelta.y * 5,
      0
    );
    rotateDelta.x *= 1 - scope.dampingFactor;
    rotateDelta.y *= 1 - scope.dampingFactor;
    var ROTATION_SPEED = .5 / worldPosition.distanceTo( _tempVector.setFromMatrixPosition( this.camera.matrixWorld ) );
    rotationAxis.copy( offset ).cross( eye ).normalize();
    rotationAngle = offset.dot( _tempVector.copy( rotationAxis ).cross( this.eye ) ) * ROTATION_SPEED;
    rotationAxis.applyQuaternion( parentQuaternionInv );
    this.object.quaternion.copy( _tempQuaternion.setFromAxisAngle( rotationAxis, rotationAngle ) );
    this.object.quaternion.multiply( quaternionStart ).normalize();
  }
};

line 644:

function onPointerDown( event ) {
  if ( ! scope.enabled ) return;
  rotateStart.set( event.clientX, event.clientY );
  document.addEventListener( "mousemove", onPointerMove, false );
  scope.pointerHover( getPointer( event ) );
  scope.pointerDown( getPointer( event ) );
}
function onPointerMove( event ) {
  if ( ! scope.enabled ) return;
  rotateEnd.set( event.clientX, event.clientY );
  rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
  scope.pointerMove( getPointer( event ) );
  rotateStart.copy( rotateEnd );
}

line 1178:

if (this.freeMode === true) {
  this.gizmo[ "translate" ].visible = false;
  this.gizmo[ "rotate" ].visible = false;
  this.gizmo[ "scale" ].visible = false;

  this.helper[ "translate" ].visible = false;
  this.helper[ "rotate" ].visible = false;
  this.helper[ "scale" ].visible = false;
} else {
  this.gizmo[ "translate" ].visible = this.mode === "translate";
  this.gizmo[ "rotate" ].visible = this.mode === "rotate";
  this.gizmo[ "scale" ].visible = this.mode === "scale";

  this.helper[ "translate" ].visible = this.mode === "translate";
  this.helper[ "rotate" ].visible = this.mode === "rotate";
  this.helper[ "scale" ].visible = this.mode === "scale";
}

Demo here:
https://jsfiddle.net/kmturley/2p1v6bcw/18/

Your suggested workaround using TransformControls works, Thankyou!
I will close this feature request and open a new feature request for TransformControls containing:

  • Damping on rotation
  • Free spin mode (without locking to axis)
  • Ability to hide the guides/gizmos
Was this page helpful?
0 / 5 - 0 ratings

Related issues

donmccurdy picture donmccurdy  路  3Comments

alexprut picture alexprut  路  3Comments

filharvey picture filharvey  路  3Comments

jack-jun picture jack-jun  路  3Comments

fuzihaofzh picture fuzihaofzh  路  3Comments