Animating Quaternion Rotation

I’m working on a flight simulator and I’ve gone and made all the logic based on trigonometry and Euler Angles, I believe.

Well this has created a lot of unwanted issues with maintaining correct rotations. I’ve learned a lot from looking into the issue.

I’m now intending to use Quaternion Rotations for all the logic, however since I have not used them before I am having a hard time understanding how to Tween/Animate these rotations.

Below is my basic Tweening function with Tween.js. Hypothetically here I am looking to tween the X axis 90 degrees. My camera rotation is set as “YXZ”.

cameraTween(deg, that, camera, time, callback){
	const sceneCamera = window.scene.getObjectByName(camera),
		  cameraTween = new TWEEN.Tween({ rotation: new THREE.Quaternion() })
							.to({ rotation: new THREE.Quaternion().setFromAxisAngle( new THREE.Vector3( 0,1,0 ), that.getRadians(deg) ) }, time )
							.easing( TWEEN.Easing.Quadratic.Out )
							.onUpdate( (tween) => {
								console.log(tween.rotation);
								const newCamRotation = new THREE.Quaternion().setFromAxisAngle( new THREE.Vector3( 0,1,0 ), tween.rotation );

								sceneCamera.quaternion.multiplyQuaternions( newCamRotation, sceneCamera.quaternion )
							 } )
							.onComplete( () => {
								callback();
							}).start();
}

Any ideas on how to tween these Quaternion rotations?

Thank you!

3 Likes

Hi!
There’s an example with quaternions: https://threejs.org/examples/#webgl_math_orientation_transform

1 Like

Hello, thanks for the link!

In the source code it seems that the rotation is being set like so…

// compute target rotation
rotationMatrix.lookAt( target.position, mesh.position, mesh.up );
targetRotation.setFromRotationMatrix( rotationMatrix );

This is troublesome because I need to implement the rotation from degrees and not a position.

Essentially passing a degree to my function and having the camera rotate that many degrees via a stepped animation.

Is there a way to approach this without Vector3 position x,y,z?

Hi! Quaternions have a slerp method that allows you to interpolate from a starting quaternion to an end quaternion.

I use this in my current project like so:

let time = {t: 0};

let r = new TWEEN.Tween(time, this.tweens)
    .to({t: 1}, CAMERA_TWEEN_DURATION)
    .onUpdate((tween) => {
         this.camera.quaternion.slerp(this.player.quaternion, tween.t);
    })
    .easing(TWEEN.Easing.Quartic.InOut)
    .start();
2 Likes

Thank you very much!!!

I have achieved this now like so:

quaternionTween(deg, that, camera, time, callback){
    const sceneCamera = window.scene.getObjectByName(camera),
	      cameraTween = new TWEEN.Tween({ t: 0 })
						.to({ t: 1 }, time*20 )
						.easing( TWEEN.Easing.Quadratic.Out )
						.onUpdate( (tween) => {
							sceneCamera.quaternion.slerp( 
								new THREE.Quaternion().setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), deg ), 
							tween.t );
						 } )
						.onComplete( () => {
							callback();
						}).start();
}

Curiously, the Tween time was much faster than before. I had to multiply it by 20 to roughly get the normal speed.

I noticed the quick tweening too, not sure why that happens. I just ignored it, didnt really bother me :smiley:

Also quick suggestion, don’t create new Quaternions and Vector3’s in your update, ideally make them once outside and then tween to that. If your target constantly changes then update your quaternion using .copy() or .set() :slight_smile: Much better for memory

Totally.
I intend to go back and optimize after implementing a working version!

These quaternions don’t seem to work in the same way as my previous rotation tweens.

I am using Z and X keys to pan the helicopter cockpit camera left and right respectively. So if you are looking left you press X and then you are looking center. Press one more time and you are looking right. Simple right?

Well it seems that going from 90 degrees to 0 degrees takes me in a full 180. My code appears to be updating so I am wondering the reason behind this?

case "z": // Look Left and Down
				if (this.lookRight == true) {
					// Look Center
					this.quaternionTween(0, new THREE.Vector3( 0, 1, 0 ), "camera", 1000, () => { this.lookRight = false });
					this.cockpitRotationTween(0, 1000);
				} else if (this.lookRight == false && this.lookLeft == false) {
					// Look Left
					this.quaternionTween(90, new THREE.Vector3( 0, 1, 0 ), "camera", 1000, () => { this.lookLeft = true });
					this.cockpitRotationTween(100, 1000);
				} 
				break;

case "x": // Look Right and Down
				if (this.lookLeft == true) {
					// Look Center
					this.quaternionTween(0, new THREE.Vector3( 0, 1, 0 ), "camera", 1000, () => { this.lookLeft = false });
					this.cockpitRotationTween(0, 1000);
				} else if (this.lookLeft == false && this.lookRight == false) {
					// Look Right
					this.quaternionTween(-90, new THREE.Vector3( 0, 1, 0 ), "camera", 1000, () => { this.lookRight = true });
					this.cockpitRotationTween(-100, 1000);
				}
				break;

Don’t mind the cockpitRotationTween as it simply offsets HTML elements ( cockpit images ) with left CSS property.

The quaternionTween looks like this…

quaternionTween(deg, vector, camera, time, callback){
    const sceneCamera = window.scene.getObjectByName(camera),
	      slerpTarget = new THREE.Quaternion().setFromAxisAngle( vector, deg ),
	      cameraTween = new TWEEN.Tween({ t: 0 })
						.to({ t: 1 }, time*20 )
						.easing( TWEEN.Easing.Quadratic.Out )
						.onUpdate( (tween) => {
							sceneCamera.quaternion.slerp( slerpTarget, tween.t );
						 } )
						.onComplete( () => {
							callback();
						}).start();
}

setFromAxisAngle takes an axis and an angle - in radians! You’re giving it an angle in degrees. Convert that to radians with deg *= THREE.Math.DEG2RAD;

Ah yes.

A bit of an obvious mistake there. I have a function in this class for this.getRadians(deg);

It looks like I was having a scope issue with my callback where the booleans weren’t being set so the wrong degrees were being passed. 90/-90 instead of 90/0 or -90/0… and of course needing to use radians instead.

This has been most helpful. I believe my issue has been solved.

Thank you very much :slight_smile:

1 Like

The ‘much faster time’ is because you are using slerp directly on sceneCamera, so you are changing the starting quaternion of the interpolation as you interpolate. Better to use the static version, i.e.

THREE.Quaternion.slerp( startQuaternion, endQuaternion, mesh.quaternion, t );

1 Like

Thanks Rob,

I will be coming back to working on that project later this year and will most definitely look into using your method. The speed difference is actually a big problem in the cockpit animations.

@robertoranon

I’ve come back to fix this here and I had this code for slerping…

quaternionTween(deg, vector, that, camera, time){
	const sceneCamera = window.scene.getObjectByName(camera),
		  slerpStart  = sceneCamera.quaternion,
		  slerpTarget = new THREE.Quaternion().setFromAxisAngle( vector, that.getRadians(deg) ),
		  cameraTween = new TWEEN.Tween({ t: 0 })
							.to({ t: 1 }, time )
							.easing( TWEEN.Easing.Quadratic.Out )
							.onUpdate( (tween) => {
								sceneCamera.quaternion.slerp( slerpTarget, tween.t );
							 } ).start();
}

Then I went to do the static method. I don’t think I understand it correctly though because I receive an error.

quaternionTween(deg, vector, that, camera, time){
   const sceneCamera = window.scene.getObjectByName('heliCam').children[0],
		 slerpStart  = new THREE.Quaternion().set( 0, 0, 0, 1).normalize(),
	     slerpTarget = new THREE.Quaternion().setFromAxisAngle( vector, that.getRadians(deg) );

 		THREE.Quaternion.slerp( slerpStart, slerpTarget, sceneCamera, time )
} 

The error says this…

Uncaught TypeError: Cannot read property 'x' of undefined
at Vector3.copy (bundle.js:2958)
at PerspectiveCamera.copy (bundle.js:10308)
at PerspectiveCamera.copy (bundle.js:22891)
at PerspectiveCamera.copy (bundle.js:22973)
at Function.slerp (bundle.js:2248)
at Helicopter.quaternionTween (bundle.js:66345)
at bundle.js:66267

Also I was wondering if I could just change the t value or what not with the Tween Library instead of putting math inside the frame update function.

You are not using the function correctly, have you looked at the documentation?

https://threejs.org/docs/#api/en/math/Quaternion

… the third parameter is the resulting quaternion, so it cannot be the camera.

Okay so I have attempted with the newQuaternion in the third parameter for the function. Then applying the quaternion to the sceneCamera but it doesn’t animate.

let newQuaternion = new THREE.Quaternion(), 
     sceneCamera  = window.scene.getObjectByName('heliCam').children[0],
     slerpTarget  = new THREE.Quaternion().setFromAxisAngle( vector, that.getRadians(deg) );

THREE.Quaternion.slerp( sceneCamera.quaternion, slerpTarget, newQuaternion, time );
sceneCamera.applyQuaternion(newQuaternion);