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