There is a lot of ground to cover when it comes to quaternions in threejs, but one has to start somewhere with them so here we are. Quaternions and prove to be very confusing at first compared to what you might be used to for setting rotations, but with a little effort some of that confusion can be addressed to get to at least a basic, functional , level of understanding. They are far more complex than Euler objects, but that complexly is justified for some situations that can come up when working on projects.
When it comes to setting the rotation of an object such as a mesh object, camera, or any kind of object3d based object one might just use the look at method of the object3d class and move on with ones life. No judgment with that, it is a very useful method, I use it all the time myself. However I do so with an understanding that the look at method does have some limitations when it comes to setting the rotation of an object. The same can be said of directly working with the rotation property that stores the current object rotation in the form of a Euler object. Euler objects might be easy to understand in terms of what the deal is with the public properties of such objects, but I pay a price for that simplicity and can end up dealing with problems like Gimbel lock.
Quaternion objects and what to know first
In this post I am writing about Quaternion in the form of Quaternion class objects in the javaScript library known as threejs. This is not in any way a getting started post on threejs, the javaScript programming language, and other basic skills that are required before hand. In the basic section of this post I do try to keep the source code examples as simple as possible, but it should go without saying that I am making a lot of assumptions here. It is okay if you find this subject a little overwhelming at first because this is very much a more advanced subject compared to a lot of other features of threejs. In any case, regardless of skill level or experience, you might want to learn more, or refresh on a few things first.
Start with Euler angles, buffer geometry rotation methods, and Object3d.lookAt first if you are new to rotations
If you are still fairly new to threejs and you have not looked into things like the Euler class, and the Object3d.lookAt method I would suggest that would be a good starting point first. Working with those features are a whole world easier, it is only when you start to run into problems with them that you might want to start looking into alternatives to those features. Also when it comes to geometry there are a number of rotation methods in the buffer geometry class. Using those methods might be expensive in terms of system resources, but they are often used just once to adjust the state of a geometry, and in any case they are another way to go about rotating things.
The source code examples here are also on github
I have the source code examples that I am writing about here up on my test threejs repository on Github. This is also where I park all the source code examples for my many other blog posts on threejs as well. In addition cloning down the repo, installing the packages, and starting the server might be the fastest way to get these examples as well as the many others working on your end.
Version Numbers matter
When I first wrote this post I was using r146 of threejs, and as such the examples follow the style rules that I have set for that revision. With that said I am still using old script tags over that of JSM with these examples. There are a lot of other little details with this revision, why I am not moving forward with newer revisions at this time at least with editing older content, and the direction that threejs is going in general. I will not be getting into those details in depth here of course however in any case I have found that I just need to always have a little section such as this to make it clear what the deal is with this sort of thing.
If you used threejs as long as I have then you know what the deal is, if not threejs is a fast moving project and code breaking changes are made to if often. Always be aware of what revision you are using and of possible what revision an author of content on threejs was suing when they wrote or updated the post last.
1 - Some basic getting started examples of Quaternion objects
In this section I will be writing about some basic examples of quaternions. However I think that I have to say that even when it comes to basic examples of quaternions things might prove to be not so basic. They are a little complex and that is just simply the nature of them compared to Euler objects. However they are still only so hard and with a little effort you can at least understand what the deal is with the public properties of these kinds of objects. SO I think that will be the main thing that I will focus on in this basic section.
1.1 - Directly setting the quaternion of a mesh object using the set from axis method
When it comes to mesh objects, and any object3d class based object for that matter, there is directly working with the quaternion property of the object3d class. This is an alternative to the object3d rotation property which is an instance of Euler rather than that of quaternion. As such any change to the quaternion object3d property should also update the state of the rotation property and vice versa as they are both ways to getting and setting the local rotation of an object.
Maybe the best way to get started with quaternion objects would be to start working with the set from axis angle method of the quaternion class. There is also directly working with the various properties, but doing so is not as easy as what you might be used to with the Euler class, more on that later in this section. For now there is just calling the set from axis angle method off of the quaternion property and passing a vector3 object that will be used to define the direction of the axis, and then an angle in radians.
Two points about these argument values to keep in mind, the vector3 normalize method and unit conversion of angles. The Vector3 object that is passed to the set from axis angle method should be normalized to a vector unit length of 1. That was the case to begin with but I am doing it anyway as a way to help be clear about this. If you are fuzzy on what what this is about then it might be a good idea to read up more on the Vector3 normalize method, and about vectors in general. When it comes the the angle value the method expects a radian value, if you would like to work with degrees then there is working out the simple expressions of making use of functions that there are to work with in the math utils object to help with this kind of conversion.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
Thus far when it comes to setting the rotation of an object I often end up just using the look at method or directly work with the Euler class instance stored in the rotation property. When working with a Euler object is is fairly easy to just directly mutate the public properties of the object. Each axis value of a Euler object is just simply a radian value so I just need to set a value in that range for x, y, and z and that is all there is to it. However doing the same with a quaternion is not so easy, and this is one thing that one should be aware of right away when starting to work with this kinds of objects.
If you are like me and you want to know how to directly work with these properties then maybe a good idea for a starting point would be to look at the source code for the set from axis angle method. At least this is what I did in order to start to get a better idea of what is going on here. For this demo I have a set rotation by axis helper function that works in a very similar way to that of the set from axis angle method. The reason why is because it is based on the actual threejs source code that is used for the method. I just made a few very simple changes that have to do with things like normalizing the given axis vector3 object and tagging a degree rather than radian value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
const mesh1 = new THREE.Mesh( new THREE.CylinderGeometry(0, 0.25, 1), new THREE.MeshNormalMaterial());
mesh1.geometry.rotateX(Math.PI * 0.5);
//mesh1.lookAt(0, 0, 1);
scene.add(mesh1);
// ---------- ----------
// SETTING ROTATION WITH QUATERNION
// ---------- ----------
const q = new THREE.Quaternion();
// vector does not need to be normalized, and
// I can use degree values for the angle with this custom
// set rotation by axis method
const v_axis = new THREE.Vector3( 0, 10, 0);
const degree = 45;
setRotationByAxis(q, v_axis, degree);
mesh1.rotation.setFromQuaternion(q);
// ---------- ----------
// RENDER
// ---------- ----------
camera.position.set(2, 2, 2);
camera.lookAt(0,0,0);
renderer.render(scene, camera);
The main point here is to look at what is going on when it comes to setting the x,y,z, and w values of Quaternion object. It is very different from what you might be used to when it comes to working with Euler objects. Just directly setting the values for the properties is not as straight forward. However there is a certain methodology here, it is a little hard to follow maybe, but still only so hard.
2 - Methods of the Quaternion class
Just like with any other class in threejs there are a number of prototype methods to work with. I am not going to be getting around to all of them here but I think I should have a section in this post where I wrote a thing or two about maybe some of the most impotent ones to be aware of for starters. In the basic section I wrote a thing or two about the set from axis angle method and it would seem that of you are only going to bother with one method that seems like a very impotent one. However I am sure that many others will prove to be useful as well.
2.1 - The set from axis angle method
From what I have gathered this far it seems like often quaternions are described as having a vector part and a scalar part. This is what the set from axis angle method comes up a lot as this just seems like a fast easy way to set the vector and scalar part of these kinds of objects. When using the set from axis method the first argument shroud be a normalized vector3 object, and then the next argument should be an angle in radians.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
There should be at least one or more methods that can be used to transition from one quaternion object to another quaternions object as this is often the case with many other objects in threejs. For example when it comes to the Vector3 class there is of course the lerp method that allows for me to quickly transition from one vector3 object to another vector3 object by passing the new target vector and then an alpha value that is the magnitude between the current vector and target vector to move. It would look like this is no lerp method, but there is a slerp method which is more or less the quaternion equivalent of that.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
q.setFromAxisAngle( new THREE.Vector3( x, y, z ).normalize(), THREE.MathUtils.degToRad(degree) );
};
// ---------- ----------
// OBJECTS
// ---------- ----------
scene.add( new THREE.GridHelper( 10,10 ) );
const mesh1 = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial());
scene.add(mesh1);
// ---------- ----------
// QUATERNION OBJECTS
// ---------- ----------
const q1 = new THREE.Quaternion();
setQ(q1,0,1,0,0);
const q2 = new THREE.Quaternion();
setQ(q2,0,1,0, -45);
// ---------- ----------
// ANIMATION LOOP
// ---------- ----------
camera.position.set(2, 2, 2);
camera.lookAt(0,0,0);
const FPS_UPDATE = 20, // fps rate to update ( low fps for low CPU use, but choppy video )
FPS_MOVEMENT = 30; // fps rate to move object by that is independent of frame update rate
FRAME_MAX = 800;
let secs = 0,
frame = 0,
lt = newDate();
// update
const update = function(frame, frameMax){
const a1 = frame / frameMax;
const a2 = 1 - Math.abs(0.5 - a1) / 0.5;
mesh1.quaternion.copy(q1).slerp(q2, a2);
};
// loop
const loop = () => {
const now = newDate(),
secs = (now - lt) / 1000;
requestAnimationFrame(loop);
if(secs > 1 / FPS_UPDATE){
// update, render
update( Math.floor(frame), FRAME_MAX);
renderer.render(scene, camera);
// step frame
frame += FPS_MOVEMENT * secs;
frame %= FRAME_MAX;
lt = now;
}
};
loop();
2.3 - The premultiply method
Often it would seem that I am in a situation in which I need to preform not one, but two or more rotations. With that said it would seem that the premultiplication method is a decent tool for preforming this kind of task with quaternions. In this demo I am creating not one, but two quaternion objects. I am then have three mesh objects, one of which I set to the state of the first quaternion, the next I set to the other quaternion, and then I use the copy method along with the premultiply to update the third mesh object to a Premultiplication of the first and second quatrenion object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
// update mesh object local rotations with quaternion objects
// where mesh1 and mesh 2 are just the current state of q1 and q2
// and the rotation of mesh3 is q1 premultiplyed by q2
mesh1.quaternion.copy(q1);
mesh2.quaternion.copy(q2);
mesh3.quaternion.copy(q1).premultiply(q2);
};
// loop
const loop = () => {
const now = newDate(),
secs = (now - lt) / 1000;
requestAnimationFrame(loop);
if(secs > 1 / FPS_UPDATE){
// update, render
update( Math.floor(frame), FRAME_MAX);
renderer.render(scene, camera);
// step frame
frame += FPS_MOVEMENT * secs;
frame %= FRAME_MAX;
lt = now;
}
};
loop();
2.4 - The set from unit vectors method
For the most part I like to use the set from axis angle method as a way to define the state of a quaternion object. However another great method for this is the set from unit vectors method which allows me to define the state in the form of a from and to vector3 object. This way I can think in terms of having a vector3 object that is a direction that I want to set, and other vector3 that is the direction that I am coming from to this new direction. Because I am using the vector3 class here I can make use of methods like the lerp method of the vector3 class to update the state of the to vector3 object starting at the from vector3 to the desired end vector3 object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
// update, render to 3d canvas, and then render to 2d canvas
update(sm);
renderer.render(scene, camera);
render2d(sm);
// step frame
sm.frame_frac += sm.FPS_MOVEMENT * sm.secs;
sm.frame_frac %= sm.FRAME_MAX;
sm.frame = Math.floor(sm.frame_frac);
sm.tick = (sm.tick += 1) % sm.FRAME_MAX;
sm.lt = sm.now;
}
};
loop();
3 - The Euler class and Quaternion class
The Euler class is still often used to set an orientation of an object. Also there are a lot of reasons why I might want to use a Euler object over a Quaternion, for one thing they are easier to work with, and if I can use one without running into any major problems with it for the most part i would say they work fine. There are still limitations of Euler objects, so in this section I will be writing about what those limitations are and how Quaternions help to address them. Also I will want to touch base on how to convert from Euler to quaternion and back again as well, as well as anything else that might come up when it comes to Euler objects and how they related to quaternion.
3.1 - Converting Euler to and from Quaternion
If I am dealing with the rotation and quaternion properties of any object3d class based object then conversion to and from Euler is done automatically. If I change the state of the Euler object stored at the rotation property of a mesh, camera, or any other object3d based object that in turn will also update the state of the quaternion property of such objects as well. It is only when dealing with stand alone objects where I might need to use the set from Euler method of the Quaternion class or the set from Quaternion method of the Euler class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
scene.background = new THREE.Color('#0f0f0f');
scene.add(new THREE.GridHelper(10, 10));
const camera = new THREE.PerspectiveCamera(50, 320 / 240, 0.1, 1000);
3.2 - Gimbal Lock demo Of Euler compared to doing the same with Quaternion
One major draw back with the Euler class is that I can end up running into problems that have to do with Gimbal lock. This is an issue where two axis of rotation will become aligned with each other and as such I and up losing an axis of control, or one kind of rotation will turn into another. For example in this demo I have two objects that are comped of a collection of mesh objects that look like airplanes kind of. I also have two rotation update methods for them, one of which makes use of Euler objects and the other makes use of Quaternion objects to do so. Both objects update just fine when pitch is at 90, but when I pitch both objects up 90 to 0 so they are both pointing up, yaw turns into roll for the object that is updated by way of Euler angles. However with the quaternion object yaw is still yaw and things are working as expected.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
scene.background = new THREE.Color('#0f0f0f');
scene.add(new THREE.GridHelper(10, 10));
const camera = new THREE.PerspectiveCamera(50, 320 / 240, 0.1, 1000);
const FPS_UPDATE = 20, // fps rate to update ( low fps for low CPU use, but choppy video )
FPS_MOVEMENT = 30; // fps rate to move object by that is independent of frame update rate
FRAME_MAX = 400;
let secs = 0,
frame = 0,
lt = newDate();
// update
const update = function(frame, frameMax){
const a1 = frame / frameMax;
updateByEuler(obj1, a1);
updateByQuaternion(obj2, a1);
};
// loop
const loop = () => {
const now = newDate(),
secs = (now - lt) / 1000;
requestAnimationFrame(loop);
if(secs > 1 / FPS_UPDATE){
// update, render
update( Math.floor(frame), FRAME_MAX);
renderer.render(scene, camera);
// step frame
frame += FPS_MOVEMENT * secs;
frame %= FRAME_MAX;
lt = now;
}
};
loop();
4 - User space methods for quaternions
There are a whole lot of great prototype methods to work with in the Quaternion class, however there is not going to be everything of course. Some times I might just need to have some kind of user space methods because some kind of function is just not baked into the prototype at all. Other times there might be something to work with, but there might be some reason to have something that does the same thing in a slightly different way.
4.1 - A get axis angle method
As I have covered in the methods section there is the set from axis angle method that can be used to set the state of a quaternion with a normalized vector3 object and an angle in radians for the scalar. However what if I need to get those values from a quaternion in the event that they are not known? For this demo I have a get axis radian from quaternion method that will get the axis angle from a quaternion. The w value of the quaternion is very much what I want when it comes to this. However I will need to make use of an expression that involves the use of Math.acos in other to get a workable radian value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
On top of getting a workable axis angle from a quantization in the event that it is not known I might also end up in situations in which I would want to get a vector3 object of the axis from a quaternion as well.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
5 - Sphere rotation animation loop project using the Quaternion Class
Thus far I have one decent animation loop example that I have made for this post that makes use of several features of the Quaternion Class. The goal here is to rotate a sphere, but do so in a way in which I am always rotating the sphere on the axis. This means that I am always going to want to have the very top and bottom of this sphere lined up with the axis. I am then going to want to move the axis around while always rotating the sphere on this axis. So then in a way I am going to need to always preform two rotations, one to make the sphere lined up with the axis, and then another to rotate it on the axis.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// ---------- ----------
// SCENE, CAMERA, RENDERER
// ---------- ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 32 / 24, 0.1, 1000);
// setFromUnitVectors and setFromAxisAngle methods
const q1 = new THREE.Quaternion();
q1.setFromUnitVectors(v_up, v_axis);
mesh1.quaternion.copy(q1);
const q2 = new THREE.Quaternion();
q2.setFromAxisAngle(v_axis, Math.PI * 2 * a4);
// premultiply with q2
mesh1.quaternion.premultiply(q2);
arrowHelper.setDirection(v_axis);
};
// loop
const loop = () => {
const now = newDate(),
secs = (now - lt) / 1000;
requestAnimationFrame(loop);
if(secs > 1 / FPS_UPDATE){
// update, render
update( Math.floor(frame), FRAME_MAX);
renderer.render(scene, camera);
// step frame
frame += FPS_MOVEMENT * secs;
frame %= FRAME_MAX;
lt = now;
}
};
loop();
Conclusion
There is a whole lot more to wrote about when it comes to these kinds of objects of course. I am sure that I will come around to edit and expand this post a bit now and then sure. However there are many things where I think it would be best to write a whole other post maybe rather than going off the deep end when it comes to future edits of this.