std/linear/quaternion

std/linear/src/quaternion.trb

Quaternion, a rotation of space that composes and interpolates without shearing and without the gimbal lock that three angles in a row have.

It is four numbers: the axis of the rotation, scaled by the sine of half the angle, and the cosine of half the angle. Nothing in the library asks a caller to know that; Quaternion.rotation builds one from an axis and an Angle, and Quaternion.applied turns a point with it.

type Quaternion

type Quaternion<Scalar: Real = Float> with Multiply, Negate

A rotation of space, as four numbers.

The sense is right-handed: a positive angle around unitZ takes unitX towards unitY, which is the same sense Angle and Matrix3.rotationAroundZ use.

Examples

const turn = Quaternion.rotation around: Vector3(0.0, 0.0, 1.0), by: Angle.degrees(90.0)
print turn.applied(to: Vector3(1.0, 0.0, 0.0)).isCloseTo(Vector3(0.0, 1.0, 0.0), tolerance: 0.0001)

Pitfalls

  • Only a quaternion of length one is a rotation. Every constructor here answers one, and composing two of them keeps the length - but arithmetic that drifts is put back with Quaternion.normalized.

  • A rotation and its negation turn space the same way, so two quaternions that are equal as rotations can compare unequal. Compare what they do (apply both to the axes) rather than the four numbers.

  • Quaternion.interpolated takes the straight path between the two and puts the length back, so a long rotation turns fastest in the middle. Quaternion.sphericallyInterpolated turns at an even rate and costs an arc cosine and three sines; the two agree at both ends.

Related

  • Matrix3 - the same rotation as a matrix, which is what a shader wants.
  • Angle - what a rotation is measured in.

field x

x: Scalar

The first component of the axis part.

field y

y: Scalar

The second component of the axis part.

field z

z: Scalar

The third component of the axis part.

field w

w: Scalar

The cosine of half the angle.

const identity

static identity: Self = Quaternion Scalar.zero, Scalar.zero, Scalar.zero, Scalar.one

No rotation, over whichever scalar is asked for: Quaternion<Fixed>.identity.

fn rotation

static fn rotation(around: Vector3<Scalar>, by: Angle<Scalar>): Self

The rotation by that angle around that axis. The axis is normalized first, so it may have any length; a zero axis answers no rotation at all.

print Quaternion.rotation(around: Vector3(0.0, 1.0, 0.0), by: Angle.degrees(180.0)).w

fn axisPart

fn axisPart(): Vector3<Scalar>

The axis part, without the fourth number.

fn conjugate

fn conjugate(): Self

The rotation that undoes this one, for a quaternion of length one.

fn negate

fn negate(): Self

Every number with its sign flipped, which turns space the same way and is therefore not the inverse.

fn lengthSquared

fn lengthSquared(): Scalar

The sum of the four squares, which is one for every rotation.

fn length

fn length(): Scalar

The length over all four numbers.

fn normalized

fn normalized(): Self

The same rotation with the length put back to one. A zero quaternion answers no rotation.

fn multiply

fn multiply(other: Self): Self

The composition: self after other, the same order Matrix3 composes in.

fn applied

fn applied(to: Vector3<Scalar>): Vector3<Scalar>

The point, turned by the rotation.

const turn = Quaternion.rotation around: Vector3(0.0, 0.0, 1.0), by: Angle.degrees(90.0)
print turn.applied(to: Vector3(2.0, 0.0, 0.0))

fn toMatrix3

fn toMatrix3(): Matrix3<Scalar>

The same rotation as a matrix, which is the form a shader and a scene graph want.

fn interpolated

fn interpolated(toward: Self, by: Scalar): Self

The rotation by of the way from here to there, along the straight line between the four numbers and then back onto the sphere. It takes the shorter of the two ways around, which is what an animation means by it. It is cheap, and it turns fastest in the middle of a long rotation; Quaternion.sphericallyInterpolated turns at an even rate.

fn sphericallyInterpolated

fn sphericallyInterpolated(toward: Self, by: Scalar): Self

The rotation by of the way from here to there at a constant speed: the angle from the start grows in step with by, where Quaternion.interpolated speeds up in the middle of a long rotation. It takes the shorter of the two ways around.

const start = Quaternion.rotation around: Vector3(0.0, 0.0, 1.0), by: Angle.degrees(10.0)
const turn = Quaternion.rotation around: Vector3(0.0, 0.0, 1.0), by: Angle.degrees(170.0)
print start.sphericallyInterpolated(toward: turn, by: 0.5)

Two quaternions that are the same rotation to within the resolution of the scalar (the dot product is one after rounding) have no angle between them to divide by, and the answer is then Quaternion.interpolated's.

fn dot

fn dot(other: Self): Scalar

The dot product of the four numbers: the cosine of half the rotation between the two.

fn isCloseTo

fn isCloseTo(other: Self, tolerance: Scalar): Bool

Whether every number is within tolerance of the other quaternion's.