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?


  • 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
    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);

Turns out my quaternion rotations are completely fine. Positive rotations are CCW in right-handed coordinate systems[1].

My rotation matrix functions were the ones rotating backwards! And in fact they were transposed by mistake.

[1] https://www.evl.uic.edu/ralph/508S98/coordinates.html


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.