I'm trying to learn quaternions and decided to implement my own quaternion class.
To test it I made a couple vertex shaders, one that gets a model matrix (calculated from the quaternion) and another that gets the rotation quaternion directly and rotates vertices in-shader.
I then feed these a quaternion that rotates around the Y axis t / 10000
radians (t = time):
const rotation = Quat.fromAxisAngle(new Vec3([0, 1, 0]), t / 10000)
The result surprises me because my model is rotating counter-clockwise but I expected it to rotate clockwise, since the angle is increasing (checked it, quat's xyz increase with time). Rotation around the X or Z axes is also backwards.
All translations as well as matrix rotations work as expected in my coordinate system. I suspect my formulas are wrong, and probably assume +Z is forward. If I transpose my model matrix (or post-multiply with it) it rotates as expected, which suggests it's a handedness problem.
Where are my formulas wrong and how can I understand the math behind this?
Details
- My matrices are column-major in memory (to match GLSL's
mat4
). - My coordinate system is +X right, +Y up, -Z forward.
My code
Quaternion from axis + angle
function fromAxisAngle(axis: Vec3, angle: number, dest = new Quat()): Quat {
angle *= 0.5
const sin = Math.sin(angle)
dest.x = axis.x * sin
dest.y = axis.y * sin
dest.z = axis.z * sin
dest.w = Math.cos(angle)
return dest
}
Note: I noticed if I negate this quaternion's x, y and z, everthing works as expected. Is this the root cause? I thought quaternions didn't have handedness!
Since this is actually just inverting the quaternion (and thus its rotation) I'm afraid I'm just patching over the root cause. I don't want to patch the issue, I want to fix my math!
Matrix from quaternion
toMat4(dest = new Mat4()): Mat4 {
const { x, y, z, w } = this
const x2 = x + x
const y2 = y + y
const z2 = z + z
const xx = x * x2
const xy = x * y2
const xz = x * z2
const yy = y * y2
const yz = y * z2
const zz = z * z2
const wx = w * x2
const wy = w * y2
const wz = w * z2
// Notice matrix is column-major
// Source:
// https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix
dest.init([
1 - (yy + zz), xy - wz, xz + wy, 0,
xy + wz, 1 - (xx + zz), yz - wx, 0,
xz - wy, yz + wx, 1 - (xx + yy), 0,
0, 0, 0, 1,
])
return dest
}
Matrix vertex shader
precision mediump float;
attribute vec3 aVertexPosition;
attribute vec2 aVertexUV;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 vUV;
void main(void) {
vUV = aVertexUV;
gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertexPosition.xyz, 1.0);
}
Quaternion vertex shader
precision mediump float;
attribute vec3 aVertexPosition;
attribute vec2 aVertexUV;
struct Transform {
float scale;
vec3 translation;
vec4 rotation;
};
uniform Transform uModel;
uniform Transform uView;
uniform mat4 uProjection;
varying vec2 vUV;
// Source:
// https://twistedpairdevelopment.wordpress.com/2013/02/11/rotating-a-vector-by-a-quaternion-in-glsl/
vec3 rotateVector(vec4 quat, vec3 vec) {
return vec + 2.0 * cross(cross(vec, quat.xyz) + quat.w * vec, quat.xyz);
}
void main(void) {
vUV = aVertexUV;
vec3 world = rotateVector(uModel.rotation, aVertexPosition * uModel.scale) + uModel.translation;
vec3 view = rotateVector(uView.rotation, world * uView.scale) + uView.translation;
gl_Position = uProjection * vec4(view, 1.0);
}