How does three.js handle rotations by non-unit quaternions?

With three.js, I use a quaternion to rotate an arrow (located on the unit sphere).

I have a complete script in this link: https://jsfiddle.net/PhilipZhu/937wov02/1/

Most of the codes are for setups. The relavant parts are typed below:

// define a quaternion

let u = -0.3; // scaling factor of non-unit quaternion
let t = 0.25*Math.PI; // rotate by angle 2*t

let w = Math.exp(u)*Math.cos(t);
let x = Math.exp(u)*Math.sin(t);
let y = 0;
let z = 0;

let q = { w: w, x: x, y: y, z: z };

The above code sets up a quaternion that rotates around x-axis by 2*t, and the quaternion is scaled by exp(u). That means when u=0, q is a unit quaternion.

// define an arrow shape
const points = [];
points.push( new THREE.Vector3(  0    , 1 ,  0     ) );
...
points.push( new THREE.Vector3(  0    , 1 ,  0     ) );

These points traces an arrow shape located on the unit sphere.

// Method 1: apply quaternion to Vector3 (blue)
let transformedPoints = points.map((point) => {
  return point.clone().applyQuaternion(quaternion);
});
const arrowMaterial1 = new THREE.LineBasicMaterial({color: 0x0000ff});
const geometry1 = new THREE.BufferGeometry().setFromPoints( transformedPoints );
const arrow1 = new THREE.Line( geometry1, arrowMaterial1 );
arrow1.position.set(0, 0, 0);
scene.add(arrow1);

// Method 2: apply quaternion to Object3 (green)
const arrowMaterial2 = new THREE.LineBasicMaterial({color: 0x00ff00});
const geometry2 = new THREE.BufferGeometry().setFromPoints( points );
const arrow2 = new THREE.Line( geometry2, arrowMaterial2 );
arrow2.position.set(0, 0, 0);
arrow2.setRotationFromQuaternion(quaternion);
scene.add(arrow2);

My main question is, the above method 1 and method 2 gives to different shapes when q is non-unitary.

Method 1 copies geometry data to (three.js) Vector3 objects and then apply the quaternion to Vector3, and this method gives intuitive outcome: rotated by expected angle and scaled with respect to the origin.

Method 2 sets a (three.js) Object3 object and then apply the quaternion to Ojbect3. This gives an unintuitive outcome that is different than what method 1 gives.

What is the reason (if there is any) behind it? or is it a bug that no one cares since non-unit quaternion transformation is not common at all?

As far as I know, only quaternions in a form { w: cos(θ), x: exsin(θ), y: eysin(θ), z: ezsin(θ) }, where e is the quaternion direction unit vector, are used to represent rotation in 3D space. By that definition, they are unit quaternions. So if you non-uniformly scale quaternion components, you need to .normalize() it before use, otherwise the formulas will not work correctly.

Maybe this will help you?

1 Like

Yes, I know. But I’m more interested in the implementation of the function than how to use, and what might be the advantage of implementing Object3’s quaternion rotation in a special way. For example, it might be interesting to know if it is related to numerical stability when you have to transform it repeatedly (a random guess). And if someday in my use case, I want to take advantage of the “incorrect” non-unit quaternion rotation, I might want to know what I am doing.

From Object3D.js:

applyQuaternion( q ) {
	this.quaternion.premultiply( q );
	return this;
}

From Quaternion.js:

premultiply( q ) {
	return this.multiplyQuaternions( q, this );
}

multiplyQuaternions( a, b ) {
	// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm

	const qax = a._x, qay = a._y, qaz = a._z, qaw = a._w;
	const qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w;

	this._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
	this._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
	this._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
	this._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;

	this._onChangeCallback();

	return this;
}
1 Like

See also

What you showed is the only correct definintion of quaternion multiplication, and both Vector3 and Object3 use the correct implementation for quaternion multiplication defined in Quaternion.js. I don’t think it explains my question. Please see the demo:

The green and blue arrow is not overlapping. I think it has to do with how threejs performs rotation on each vertices’/faces’ 3d coordinates using its quaternion property.

My message was about “I’m more interested in the implementation of the function”.

Right. It’s also helpful to have what you posted here for sure.

I’m not good with quaternions. I played a bit with your code. If the arrow originates from (0,0,0), it is easier to show larger arrows … and the green/blue arrows are different again, but it is easier to see the differences. Here are a few observations that may give a clue (although it might be just a coincidence):

  • point (0,0,0) is stable – a kind of expected
  • the blue is twice shorter (snapshot A)
  • the green is twice less rotated (snapshot B)

I’m curious whether the discrepancy is because of this: when the whole object is rotated, it acts as a group and this group has position (0,0,0); when individual points are rotated, each is rotated independently on the others and their positions are not (0,0,0).

1 Like

I followed the math process inside Three.js with a simple test. Assume we have point (1,0,0) and quaternion (x,y,z,w). Quaternioning the point as if it is THREE.Vector3 uses directly applyQuaternion and the transformed vector is: (ww+xx-zz-yy, 2xy+2zw, 2xz-2yw ).

When the point is quaternioned as if it is THREE.Object3D, the process converts the quaternion to a matrix and multiplies the vector and the matrix. In this case, the resulting vector is:
(1-2yy-2zz, 2xy+2zw, 2xz-2yw ).

Now, here is the interesting part:

The second and third components of both resulting vectors are the same. This is beautiful. Unfortunately, their first components are different: ww+xx-zz-yy and 1-2yy-2zz

By definition, for unit quaternions xx+yy+zz+ww =1, so 1-2yy-2zz = ww+xx-zz-yy and both resulting vectors are the same, but for non-unit quaternions the equation is not valid and the vectors are different.

So, my conclusion is that Three.js uses two ways of managing rotation from quaternion (direct and via matrix), which produce the same results for unit quaternions, but different results for non-unit quaternions. The actual reason for this is that for faster calculation it is assumed that quaternion conjugate q* and inverse q-1 are the same – this is true only for unit quaternions.

A simple analogy

Here is a simple analogy, in case the text above is unclear. Imagine a program that works with right triangles, for which c2 = a2 + b2. Sometimes the program uses c2, but sometimes it uses a2+b2. This is safe as long as all triangles are right triangles. But if acute or obtuse triangle is used, then the calculations are not safe, as c2 will no longer be equal to a2+b2.

2 Likes

This is very interesting! Thanks for sharing this. Then I’m assuming that the reason for threejs to have this discrepency is not due to some numerical stability that I would have imagined. Just that it’s convenient to manage rotation by matrix.

1 Like

Most likely it is the same reason as using normalized vectors (i.e. unit vectors) – it makes easier and faster calculations. The use of matrix for rotation is enforced by the GPU, as matrices are natively supported. If you write quaternion support in a shader, it will be fine, but you will loose the possibility to pack different types of transformations in a single entity (matrices can pack any number and any combination of translation, rotation, scaling, skewing, mirroring, projection – all in a single matrix).

1 Like