Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to set OrbitControls different camera.lookAt vs controls.target #18476

Closed
4 of 12 tasks
kmturley opened this issue Jan 24, 2020 · 9 comments
Closed
4 of 12 tasks

Comments

@kmturley
Copy link

kmturley commented Jan 24, 2020

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
  • Dev
  • r112
Browser
  • All of them
  • Chrome
  • Firefox
  • Internet Explorer
OS
  • All of them
  • Windows
  • macOS
  • Linux
  • Android
  • iOS
Hardware Requirements (graphics card, VR Device, ...)

None

@Mugen87
Copy link
Collaborator

Mugen87 commented Jan 26, 2020

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

@sciecode
Copy link
Contributor

sciecode commented Jan 26, 2020

@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

@kmturley
Copy link
Author

@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?

@sciecode
Copy link
Contributor

sciecode commented Jan 27, 2020

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.

@kmturley
Copy link
Author

kmturley commented Jan 27, 2020

@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

@kmturley
Copy link
Author

kmturley commented Jan 28, 2020

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/

@kmturley
Copy link
Author

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

@liangyuqi
Copy link

我设法通过修改 TransformControls 库来复制该功能(另一种方式)以包括:

  • 旋转阻尼
  • 自由旋转模式(不锁定轴)

TransformControls.js 第 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 );

第 128 行:

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

第 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;

  }

};

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

第 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();
  }
};

第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 );
}

第 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";
}

演示在这里:https :
//jsfiddle.net/kmturley/2p1v6bcw/18/

hello,it doesn't work ,
console: Uncaught TypeError: Class constructor Object3D cannot be invoked without 'new'

@kmturley
Copy link
Author

@liangyuqi These examples are one year old. Three.js source code has since changed.
I found this open-source solution which does most of what I needed: https://github.com/paulhax/spin-controls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants