Search code examples
javascriptthree.jsgsaporbitcontrols

Adding point-and-click navigation to a three.js project where OrbitControls is enabled


I am making a navigation test in three.js with a point-and-click navigation functionality.

The point-and-click mechanic is really simple : a raycaster determines the point in the plane where the user clicked and I move the camera there with gsap:

const move_cam = () => {
    const new_pos = { ...marker.position };
    gsap.to(camera.position, {
      duration: 2,
      x: new_pos.x,
      z: new_pos.z,

      onComplete: () => {
      }
    });
  };

The problem with this is that, when used along with OrbitControls, the camera moves while always pointing to the same place : camera is moving but stays focused on one point

The reason is that OrbitControls gives a target to the camera to keep looking at, allowing it to "orbit".

My first attempt was just to disable OrbitControls during the camera movement with controls.enabled= false and reenabling it after the fact with the callback function onComplete() of gsap but it doesn't work since controls.enabled= false only disables the interaction with the user but it doesn't keep the camera from looking at its target.

I also thought about preventing OrbitControls to affect the camera during the movement by adding a condition to the animation loop :

if (!camera_is_moving) {
controls.update();
}

But the camera goes back to looking at the target as soon as the animation is finished.

As a last ressort, I decided to store the distance of the camera to its target in an variable called offset and then using that offset to define a new target at the end of the animation. And the move_cam() function ended up like this :

const move_cam = () => {
    camera_is_moving = true;
    const offset = {
      x: controls.target.x - camera.position.x,
      y: controls.target.y - camera.position.y,
      z: controls.target.z - camera.position.z,
    };

    const new_pos = { ...marker.position };
    new_pos.y = CAMERA_HEIGHT;
    
    gsap.to(camera.position, {
      duration: 2,
      x: new_pos.x,
      y: new_pos.y,
      z: new_pos.z,

      onComplete: () => {
        controls.target.x = offset.x + camera.position.x;
        // controls.target.y= offset.x + camera.position.y;
        controls.target.z = offset.x + camera.position.z;

        offset.x = controls.target.x - camera.position.x;
        offset.y = controls.target.y - camera.position.y;
        offset.z = controls.target.z - camera.position.z;
        camera_is_moving = false;
      }

    });
  };

I was sure it would work and I kind of did but the camera kind of twitches at the end of the animation as if the new target I assigned was not quite the correct one :

Camera twitches at the end of animation

If you look closely at the gif, right at the end of the animation, the camera stutters a bit. Sometimes it's very significant and sometimes it's just a small movement.

I don't know what's causing this, my objective is that the camera's rotation stays the same as before the animation so I thought that if I offset the camera's target by the same amount as the camera itself, it would work but apparently, it didn't.

Can anyone help me with this?

I uploaded the project in this Github repo if you want to try and see what I mean. The full js file is here.

Thank you in advance.


Solution

  • I managed to solve it by animating the camera target separately with gsap instead of setting its value in the onComplete() callback so the move_cam() function ended up like this :

    const move_cam = () => {
        camera_is_moving = true;
        controls.enabled = false;
        const offset = {
          x: controls.target.x - camera.position.x,
          y: controls.target.y - camera.position.y,
          z: controls.target.z - camera.position.z,
        };
        const new_pos = { ...marker.position };
        new_pos.y = camera.position.y;
    
        gsap.to(this.camera.position, {
          duration: this.camera_travel_duration,
          x: new_pos.x,
          y: new_pos.y,
          z: new_pos.z,
    
          onComplete: () => {
            offset.x = controls.target.x - camera.position.x;
            offset.y = controls.target.y - camera.position.y;
            offset.z = controls.target.z - camera.position.z;
    
            camera_is_moving = false;
            controls.enabled = true;
          },
        });
        gsap.to(controls.target, {
          duration: camera_travel_duration,
          x: offset.x + new_pos.x,
          z: offset.z + new_pos.z,
        });
      };

    I solves my problem completely but I still don't know why the previous code didn't work.