Skip to content

On standards

Yoan edited this page Jan 16, 2018 · 7 revisions

Matrix storage layout

Matrices can be row-major or column-major at your option, because there are use cases for both layouts, even though column-major happens to perform better in computer graphics, because the matrix * column_vector product is more efficient with this layout.

Put simply, a matrix structure is said to be "row-major" when it is stored as an array of rows. Likewise, it is said to be "column-major" when it is stored as an array of columns.

Row-major and column-major only describe a matrix's in-memory layout - it has no impact on their effect or meaning as transforms in this library!

In the docs and the code, matrix elements are written as Mij, where i is the row index and j is the column index, independently of storage layout. This convention has been chosen because it is the mathematical standard.

With this convention, a 4x4 translation matrix is written as follows, regardless of storage layout:

1 0 0 x
0 1 0 y
0 0 1 z
0 0 0 1

The new() function and Display implementation of matrices follow this convention, regardless of storage layout.
The same goes for indexing by (i, j) tuples. No matter the layout, m[(0, 3)] always means "1st row, 4th column".

Also, the effect of multiplication is not affected by in-memory representation. That is, we have :

use vek::Vec2;
use vek::mat::row_major::Mat2 as Rows2;
use vek::mat::column_major::Mat2 as Cols2;
let a = Rows2::new(
    1, 2,
    3, 4
);
let b = Cols2::new(
    1, 2,
    3, 4
);
let v = Vec2::new(2, 3);
assert_eq!(format!("{}", a), format!("{}", b));
assert_eq!(v * a, v * b);
assert_eq!(a * v, b * v);

Cross-layout matrix multiplication is supported. We have

  • rm * rm -> rm
  • cm * cm -> cm
  • rm * cm -> cm
  • cm * rm -> rm
  • rv * rm -> rv
  • rm * cv -> cv
  • rv * cm -> rv
  • cm * cv -> cv

Where rm is row-major-matrix, cm column-major-matrix, rv row-vector and cv column-vector.

Angular units

This might seem obvious, but the preferred angular unit is the radian, because it's the unit that's actually used by trigonometric functions.

Degrees might be more convenient for displaying in GUIs, but this is none of my concern. Even so, said GUI could benefit from a "rotation widget" defined for this very purpose.
The Unity engine treats every angle as expressed in degrees, and I find this to be a mistake, even if the intent is to "make it easier for beginners".

Handedness

You might have heard about "left-handed" and "right-handed" spaces. Put simply, the only difference is how we, as humans, visualize the Z-axis in our mind.
In both spaces, X points to what we perceive as "the right" and Y points to what we perceive as "upwards".
In a left-handed space, Z points to what we perceive as "forwards".
In a right-handed space, Z points to what we perceive as "backwards".

To help me remember this, I like to cusp fingers of both my hands such that both thumbs point to the right, and both index fingers point upwards. Then, see where the middle fingers naturally points.

It is important to note that this doesn't change the underlying maths, only the way we visualize our 3D space.
No matter the convention, the cross product of "positive X" with "positive Y" always yields "positive Z".

Want an opinion ? Left-handed feels more natural to me. It's also the convention adopted by the Unity engine (not that I'm a huge fan of Unity though).

The general consensus appears to be that it's best to pick one convention and make sure to stick to it throughout the codebase. It's important to know the caveats, otherwise you're in for a lot of confusion.

  • Reversing any one of the X, Y or Z axii is enough to go from right-handed to left-handed, and vice versa.
  • Reversing a rotation axis is the same as reversing the rotation angle, and vice versa.

vek has no convention except the mathematical convention, which means :

  • Rotating a vector with a quaternion is done as q * v * qi and NOT qi * v * q (where qi is the inverse of q), which would reverse the rotation angle.
  • Because of the mathematical definition of the cross product, rotations via quaternions have a well-established "handedness".
    If you picture space as right-handed, you may use the "right-hand rule", which says that, if your thumb points in the direction of the rotation axis, then positive rotations along that axis follow the direction pointed by your other curved fingers.
    The same rule applies for left-handed spaces with your left hand!
  • Rotation matrices have the same effect as rotation quaternions, given the same parameters.

Matrix multiplication order

In vek you'll sometimes see convenience methods such as translated_3d(self, v).
Say Mat4 provides a way to create a 3D translation matrix via translation_3d(v), then translated_3d(self, v) is simply defined as Mat4::translation_3d(v) * self.

But wait. How can I be so confident about the multiplication order ? The answer is, it's an implicit convention adopted by vek. It's also the one promoted by countless tutorials and libraries, but nobody ever said it was universal.

This convention says that, given some point P and some transform matrix T, then "P transformed by T" is written T * P.
This, in turn, assumes that P is a column vector. As far as vek is concerned, there's no difference between a row vector and a column vector. In any case, while it's roughly the same thing in the realm of code, it's definitely not in the mathematical realm.

Some (rarer) people out there prefer the reverse convention, in which "P transformed by T" is written P * T. They treat P as a row vector and ensure that elements of T are properly laid out in memory so that the multiplication gives the expected result.
Well, if you feel like following this convention with vek (and any matrix library for that matter), you have to transpose everything!
In short, here are correct ways to write "P transformed by T" with vek:

extern crate vek;
#[macro_use]
extern crate approx;

use vek::{Vec4, Mat4, Vec3};

// Some point
let p = Vec4::new(1_f32, 5., 3., 1.);
// Some advanced transform matrix
let m = Mat4::scaling_3d(5_f32).rotated_x(3.).translated_3d(Vec3::unit_x());
// Ta-dah!
assert_relative_eq!(m * p, p * m.transposed());
Clone this wiki locally