as you can see in Video I move camera from its current position to (0, 100, 0) for top view.
I used tween for animation.
as you can see animation is not perfect.
camera moves from current position to (0, 100, 0). but camera movement is not same that I want.
camera moves perfect. but in last camera rotate at its own axis.
What you are seeing is called gimbal lock.
You’ll want to use Quaternions for the rotation to ameliorate this issue. Check out this example: https://threejs.org/examples/#webgl_math_orientation_transform
I believe this post can also shed some light on how to solve the issue with code for you to test: Animating Quaternion Rotation
What if I use OrbitControls, where the camera is always looking at the target, but I want to use a button that also starts a movement above the target (similar to what @RAUMIK_RANA tried, but with orbit controls in addition) which does also trigger a gimble lock at the moment. Would I need to disable the controls, tween the quaternion manually, tween the position manually, update the matrix and then turn it back on?
Sadly when setting orbitControls.enable = false
I still can control it, unlike what the docs say, which seems to be a bug.
EDIT:
Since a simple disabling does not work I used a workaround. I simply set a threshold before reaching the point of the gimbal lock.
Where I handle the button click event and state (state management via Pinia Store and action subscription):
if (name === 'toggle2DView') {
if (this.canvasStore.is2DView) {
// Set camera position to look from top down to target by changing the max polar angle
if (this.controls) {
this.lastCameraPosition = camera.position.clone()
const destination: Vector3 = new Vector3(
this.controls.target.x,
ControlsHandler.cameraIntroductionVector.y,
this.controls.target.z
)
this.moveCamera(destination, ControlsHandler.cameraTweenSpeed / 2, false)
.then((): void => {
// Fix the angle to avoid tilting
console.log(this.controls!.getPolarAngle())
this.controls!.maxPolarAngle = this.controls!.getPolarAngle()
this.controls!.minPolarAngle = this.controls!.getPolarAngle()
})
.catch((): void => {
this.canvasStore.is2DView = false
this.setPolarAngles()
})
}
} else {
this.setPolarAngles()
if (this.lastCameraPosition) {
// Calculate the reset position 100 units further away from p0 in the direction of p1
// to get a distanced point from the target position in the same plane and the old height.
const distanceFromTarget: number = 150
const p0: Vector3 = new Vector3(
this.controls!.target.x,
this.lastCameraPosition.y,
this.controls!.target.z
)
const p1: Vector3 = new Vector3(
camera.position.x,
this.lastCameraPosition.y,
camera.position.z
)
const directionP0ToP1: Vector3 = p1.sub(p0).normalize()
const resetPosition: Vector3 = p0.add(
directionP0ToP1.multiplyScalar(distanceFromTarget)
)
this.moveCamera(resetPosition, ControlsHandler.cameraTweenSpeed, true).catch(
(): void => {
this.canvasStore.is2DView = true
}
)
}
}
}
/**
* Moves the camera to the specified 3D position and returns a promise.
* @param destination The {@link Vector3} position to move the camera to.
* @param time The execution time for the movement animation.
* @param isInterruptible If the created tween should be saved and be cancelable by other interactions.
* @returns A promise that resolves when the animation is complete.
*/
private moveCamera(destination: Vector3, time: number, isInterruptible: boolean): Promise<void> {
if (!this.controls) {
return Promise.reject('Controls not initialized')
}
// Check if there's an ongoing tween, don't create a new one and reject interaction
if (this.uninterruptibleTween && this.uninterruptibleTween.isPlaying()) {
return Promise.reject('Uninterruptible tween playing')
}
const camera: PerspectiveCamera = this.renderManager.camera
// Check if the camera position is already at the target position
if (camera.position.equals(destination)) {
return Promise.resolve()
} else if (
this.controls.target.x === destination.x &&
this.controls.target.z === destination.z
) {
// Avoid gimbal lock issue
const destinationDirection: Vector3 = new Vector3()
.copy(destination)
.sub(camera.position)
.normalize()
// Calculate a point a certain distance away from the destination along the direction vector
const distanceThreshold: number = 10
destination = new Vector3()
.copy(destination)
.sub(destinationDirection.multiplyScalar(distanceThreshold))
}
return new Promise<void>((resolve): void => {
const tween: Tween<Vector3> = new Tween(camera.position)
.to(destination, time)
.easing(TWEEN.Easing.Quadratic.Out)
.onStop((): void => {
resolve()
})
.onComplete((): void => {
camera.clearViewOffset()
camera.updateProjectionMatrix()
this.controls!.update()
this.uninterruptibleTween = null
resolve()
})
.start()
if (isInterruptible) {
// Makes the tween cancelable on controls interruption
this.interruptibleTween = tween
} else {
this.uninterruptibleTween = tween
}
})
}