Quaternions Quaternions are an interesting number system, and are especially useful for representing rotations and orientations in 3D. Much like their cousins, the complex numbers, quaternions are an extension of the reals. Quaternions are composed of one real number, and three non-real parts: . The real part is often referred to as the "scalar" part and the complex part is often referred to as the "vector" part. As such, they are often represented as the sum of a vector and a scalar. This convention is used from here on. Algebraically, quaternions are defined by the relationship Addition is simple, and is done component-wise. Multiplication can be done out component-wise, but treating the vector and scalar parts separately has a much neater representation using the dot and cross products for the vector components:
But quaternions live in , so how can we use them to represent rotations and orientations in ? Easy! Simply construct a quaternion that has a scalar part of zero. Given a vector that we want to operate on with unit quaternion , we can define a quaternion operator as , where is the conjugate. So now we have a way to apply quaternions to points, let's build a quaternion for a particular angle and axis of rotation, respectively. The quaternion is incredibly simple:
tex.block` q=\cos \frac{\theta}{2} + \mathbf{u}\sin \frac{\theta}{2} `
And so, as before we apply the quaternion as . This rotates our vector about by the angle , which is exactly what we wanted.
So Why Use Quaternions? Why bother using this fancy 4D math for rotations and orientations, when you could just use good old 3D Euler angles? Quaternions offer two major benefits, namely performance improvements and a lack of gimbal lock issues. Euler angle representations suffer from gimbal lock, where the alignment of successive angles causes a loss of a degree of freedom. This is especially apparent when interpolating between orientations, as shown below. An Euler angle system will not always take the shortest path between two angles, whereas interpolating between quaternion orientations will always be smooth and follow the shortest path. The rings represent each of the axes of motion for the Euler rotation.
visualization_1 = { const renderer = new THREE.WebGLRenderer({antialias: true}); const controls = new THREE.OrbitControls(camera, renderer.domElement); renderer.setSize(width, height); renderer.setPixelRatio(devicePixelRatio); invalidation.then(() => (controls.dispose(), renderer.dispose())); controls.addEventListener("change", () => renderer.render(scene, camera)); while (true){ update(); renderer.render(scene, camera); yield renderer.domElement; } }
As you can see, one cube rotates smoothly, as you'd expect. This is the cube using quaternion interpolation. The other cube is forced to interpolate all three rotational axes at once to move between the rotations, causing undesirable motion.
The other major advantage of using quaternions to represent rotation is computational efficiency. When compared to rotation matrices, quaternions offer performance and stability benefits. Computing quaternion rotations requires fewer multiplications, and is more numerically stable than rotation matrices. In many cases, however, Euler rotations are a better representation of underlying physical systems, especially in robotics applications where the joints in question are effectively a series of 1DOF joints. The choice is up to the designer of the system, and there are cases where either rotation representation may be best.
scene = { const scene = new THREE.Scene(); sceneObjects.eulerCube = new THREE.Mesh(cube.geometry,cube.material); sceneObjects.quatCube = new THREE.Mesh(cube.geometry,cube.material); sceneObjects.eulerCube.position.z = 1; sceneObjects.quatCube.position.z = -1; scene.add(sceneObjects.eulerCube); scene.add(sceneObjects.quatCube); //Toruses var xTorusGeo = new THREE.TorusGeometry(1,0.1,8,32); var xTorusMat = new THREE.MeshBasicMaterial({color:0xff0000,opacity:0.5,transparent:true}); var xTorus = new THREE.Mesh( xTorusGeo, xTorusMat); xTorus.rotation.y = Math.PI *0.5; //scene.add( xTorus ); var yTorusGeo = new THREE.TorusGeometry(0.8,0.1,8,32); var yTorusMat = new THREE.MeshBasicMaterial({color:0x00ff00,opacity:0.5,transparent:true}); var yTorus = new THREE.Mesh( yTorusGeo, yTorusMat); yTorus.rotation.y = Math.PI * 0.5; //scene.add( yTorus ); var zTorusGeo = new THREE.TorusGeometry(0.6,0.1,8,32); var zTorusMat = new THREE.MeshBasicMaterial({color:0x0000ff,opacity:0.5,transparent:true}); var zTorus = new THREE.Mesh( zTorusGeo, zTorusMat); zTorus.rotation.x = Math.PI * 0.5; //scene.add( zTorus ); var ah = new THREE.ArrowHelper(new THREE.Vector3(0,1,0), new THREE.Vector3(0,-0.5,0), 1, 0x000000); var zRotGRP = new THREE.Group(); zRotGRP.add(zTorus); zRotGRP.add(ah); var yRotGRP = new THREE.Group(); yRotGRP.add(yTorus); var xRotGRP = new THREE.Group(); xRotGRP.add(xTorus); var gimbalGRP = new THREE.Group(); gimbalGRP.add(xRotGRP); gimbalGRP.add(yRotGRP); gimbalGRP.add(zRotGRP); gimbalGRP.position.z = 1; scene.add(gimbalGRP); //Animation var quatKF = new THREE.QuaternionKeyframeTrack('.quaternion',[0,1,2,3],[ 0.5,0.5,-0.5,0.5,0,0.7071067811865475,0, 0.7071067811865476,0,0.7071067811865475,0, 0.7071067811865476,0.5,0.5,-0.5,0.5]); var qClip = new THREE.AnimationClip( 'qAction', 4, [quatKF]); sceneObjects.quatMixer = new THREE.AnimationMixer( sceneObjects.quatCube); var qClipAction = sceneObjects.quatMixer.clipAction(qClip); qClipAction.play(); var a = Math.PI * 0.5; var vecKF = new THREE.VectorKeyframeTrack('.rotation', [0,1,2,3],[a,0,-a,a,0,-a,0,a,0,0,a,0]); var vecKFX = new THREE.NumberKeyframeTrack('.rotation[x]',[0,1,2,3],[a,0,0,a]); var vecKFY = new THREE.NumberKeyframeTrack('.rotation[y]',[0,1,2,3],[0,a,a,0]); var vecKFZ = new THREE.NumberKeyframeTrack('.rotation[z]',[0,1,2,3],[-a,0,0,-a]); var eClip = new THREE.AnimationClip ( 'eAction', 4, [vecKFX,vecKFY,vecKFZ]); var xyClip = new THREE.AnimationClip( 'yzAction', 4, [vecKFZ,vecKFY]); var xClip = new THREE.AnimationClip( 'zAction', 4, [vecKFZ]); sceneObjects.mixers = []; sceneObjects.eMixer = new THREE.AnimationMixer( sceneObjects.eulerCube); sceneObjects.xMixer = new THREE.AnimationMixer( xRotGRP); sceneObjects.yMixer = new THREE.AnimationMixer( yRotGRP); sceneObjects.zMixer = new THREE.AnimationMixer( zRotGRP); sceneObjects.mixers.push(sceneObjects.quatMixer); sceneObjects.mixers.push(sceneObjects.eMixer); sceneObjects.mixers.push(sceneObjects.xMixer); sceneObjects.mixers.push(sceneObjects.yMixer); sceneObjects.mixers.push(sceneObjects.zMixer); var eClipAction = sceneObjects.eMixer.clipAction(eClip); eClipAction.play(); sceneObjects.xMixer.clipAction(xClip).play(); sceneObjects.yMixer.clipAction(xyClip).play(); sceneObjects.zMixer.clipAction(eClip).play(); scene.background = new THREE.Color(0xFFFFFF); // var helper = new THREE.GridHelper( 6, 6); helper.material.opacity = 0.25; helper.material.transparent = true; scene.add(helper); var axesHelper = new THREE.AxesHelper( 1 ); scene.add( axesHelper ); return scene; }
sceneObjects = ({})
cube = { const material = new THREE.MeshBasicMaterial(); material.vertexColors = THREE.FaceColors const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); var colorsArray = [new THREE.Color(255,0,0), new THREE.Color(0,255,0),new THREE.Color(0,0,255), new THREE.Color(0,150,150), new THREE.Color(150,0,150), new THREE.Color(150,150,0)]; for(var i = 0; i<6; i++){ for(var j = 0; j<2; j++){ geometry.faces[i*2 + j].color = colorsArray[i]; } } var m = new THREE.Mesh(geometry, material); m.rotation.x = Math.PI * 0.5; m.rotation.z = Math.PI * -0.5; return m; }
update = { var clock = new THREE.Clock(); return function update() { var delta = clock.getDelta(); sceneObjects.mixers.forEach(function(item, index, _) { item.update(delta); }); //cube.rotation.x += speed*delta; } }
camera = { const fov = 45; const aspect = width / height; const near = 1; const far = 1000; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.set(2, 2, -2) camera.lookAt(new THREE.Vector3(0, 0, 0)); return camera; }
height = 600
THREE = { const THREE = window.THREE = await require("three@0.96/build/three.min.js"); await require("three@0.96/examples/js/controls/OrbitControls.js").catch(() => {}); return THREE; }
import {slider} from "@jashkenas/inputs"