2D and 3D transformations
Introduction to essential operations on vectors and points.
Contents
Transformations are essential operations involved in scene management — object relations, hierarchies, animations etc. They extend basic vectors and matrices in Math namespace, see its documentation for more information about usage with CMake.
Magnum provides classes for transformations in both 2D and 3D. Each class is suited for different purposes, but their usage is nearly the same to make your life simpler. This page will explain the basic operation and differences between various representations.
Representing transformations
The first and most straightforward way to represent transformations is to use homogeneous transformation matrix, i.e. Matrix3 for 2D and Matrix4 for 3D. The matrices are able to represent all possible types of transformations — rotation, translation, scaling, reflection etc. and also projective transformation, thus they are used at the very core of graphics pipeline and are supported natively in OpenGL.
On the other hand, matrices need 9 or 16 floats to represent the transformation, which has implications on both memory usage and performance (relatively slow matrix multiplication). It is also relatively hard to extract transformation properties (such as rotation angle/axis) from them, interpolate between them or compute inverse transformation. They suffer badly from so-called floating-point drift — e.g. after a few combined rotations the transformation won't be pure rotation anymore, but will involve also a bit of scaling, shearing and whatnot.
However, you can trade some transformation features for improved performance and better behavior — for just a rotation you can use Complex in 2D and Quaternion in 3D, or DualComplex and DualQuaternion if you want also translation. It is not possible to represent scaling, reflection or other transformations with them, but they occupy only 2 or 4 floats (4 or 8 floats in dual versions), can be easily inverted and interpolated and have many other awesome properties. However, they are not magic so they also suffer slightly from floating-point drift, but not too much and the drift can be accounted for more easily than with matrices.
Transformation types
Transformation matrices and (dual) complex numbers or quaternions have completely different internals, but they share the same API to achieve the same things, greatly simplifying their usage. In many cases it is even possible to hot-swap the transformation class type without changing any function calls.
Default (identity) transformation
Default-constructed Matrix3, Matrix4, Complex, Quaternion, DualComplex and DualQuaternion represent identity transformation, so you don't need to worry about them in initialization.
Rotation
2D rotation is represented solely by its angle in counterclockwise direction and rotation transformation can be created by calling Matrix3::
auto a = Matrix3::rotation(23.0_degf); auto b = Complex::rotation(Rad(Constants::piHalf())); auto c = DualComplex::rotation(-1.57_radf);
3D rotation is represented by angle and (three-dimensional) axis. The rotation can be created by calling Matrix4::
auto a = Quaternion::rotation(60.0_degf, Vector3::xAxis()); auto b = DualQuaternion::rotation(-1.0_degf, Vector3(1.0f, 0.5f, 3.0f).normalized()); auto c = Matrix4::rotationZ(angle);
Rotations are always around origin. Rotation about arbitrary point can be done by applying translation to have the point at origin, performing the rotation and then translating back. Read below for more information.
Translation
2D translation is defined by two-dimensional vector and can be created with Matrix3::
auto a = Matrix3::translation(Vector2::xAxis(-5.0f)); auto b = DualComplex::translation({-1.0f, 0.5f});
3D translation is defined by three-dimensional vector and can be created with Matrix4::
auto a = Matrix4::translation(vector); auto b = DualQuaternion::translation(Vector3::zAxis(1.3f));
Scaling and reflection
Scaling is defined by two- or three-dimensional vector and is represented by matrices. You can create it with Matrix3::
auto a = Matrix3::scaling(Vector2::xScale(2.0f)); auto b = Matrix4::scaling({2.0f, -2.0f, 1.5f}); auto c = Matrix4::scaling(Vector3(10.0f));
Reflections are defined by normal along which to reflect (i.e., two- or three-dimensional vector of unit length) and they are also represented by matrices. Reflection is created with Matrix3::
auto a = Matrix3::reflection(Vector2::yAxis()); auto b = Matrix4::reflection(axis.normalized());
Scaling and reflection is also done relative to origin, you can use method mentioned above to scale or reflect around arbitrary point.
Scaling and reflection can be (to some extent) also represented by complex numbers and quaternions, but it has some bad properties and would make some operations more expensive, so it's not implemented.
Projective transformations
Projective transformations eploit the full potential of transformation matrices. In 2D there is only one projection type, which can be created with Matrix3::
auto a = Matrix3::projection({4.0f, 3.0f}); auto b = Matrix4::orthographicProjection({4.0f, 3.0f}, 0.001f, 100.0f); auto c = Matrix4::perspectiveProjection(35.0_degf, 1.333f, 0.001f, 100.0f);
Composing and inverting transformations
Transformations (of the same representation) can be composed simply by multiplying them, it works the same for matrices, complex numbers, quaternions and their dual counterparts. Order of multiplication matters — the transformation on the right-hand side of multiplication is applied first, the transformation on the left-hand side is applied second. For example, rotation followed by translation is done like this:
auto a = DualComplex::translation(Vector2::yAxis(2.0f))* DualComplex::rotation(25.0_degf); auto b = Matrix4::translation(Vector3::yAxis(5.0f))* Matrix4::rotationY(25.0_degf);
Inverse transformation can be computed using Matrix3::
Transforming vectors and points
Transformations can be used directly for transforming vectors and points. Vector transformation does not involve translation, in 2D can be done using Matrix3::
auto transformation = Matrix3::rotation(-30.0_degf)*Matrix3::scaling(Vector2(3.0f)); Vector2 transformed = transformation.transformVector({1.5f, -7.9f});
Point transformation involves also translation, in 2D is done with Matrix3::
auto transformation = DualQuaternion::rotation(-30.0_degf, Vector3::xAxis())* DualQuaternion::translation(Vector3::yAxis(3.0f)); Vector3 transformed = transformation.transformPointNormalized({1.5f, 3.0f, -7.9f});
Transformation properties and conversion
It is possible to extract some transformation properties from transformation matrices, particularly translation vector, rotation/scaling part of the matrix (or pure rotation if the matrix has uniform scaling) and also base vectors:
Matrix4 transformation; Matrix3x3 rotationScaling = transformation.rotationScaling(); Vector3 up = transformation.up(); Vector3 right = transformation.right(); Matrix3 b; Matrix2x2 rotation = b.rotation(); Float xTranslation = b.translation().x();
Extracting scaling and rotation from arbitrary transformation matrices is harder and can be done using Math::
You can also recreate transformation matrix from rotation and translation parts:
Matrix3 c = Matrix3::from(rotation, {1.0f, 3.0f});
Complex numbers and quaternions are far better in this regard and they allow you to extract rotation angle using Complex::
DualComplex a; Rad rotationAngle = a.rotation().angle(); Vector2 translation = a.translation(); Quaternion b; Vector3 rotationAxis = b.axis();
You can convert Complex and Quaternion to rotation matrix using Complex::
Quaternion a; auto rotation = Matrix4::from(a.toMatrix(), {}); DualComplex b; Matrix3 transformation = b.toMatrix();
Conversion the other way around is possible only from rotation matrices using Complex::
Matrix3 rotation; auto a = Complex::fromMatrix(rotation.rotationScaling()); Matrix4 transformation; auto b = DualQuaternion::fromMatrix(transformation);
Transformation interpolation
Normalizing transformations
When doing multiplicative transformations, e.g. adding rotating to a transformation many times during an animation, the resulting transformation will accumulate rounding errors and behave strangely. For transformation matrices this can't always be fixed, because they can represent any transformation (and thus no algorithm can't tell if the transformation is in expected form or not). If you restrict yourselves (e.g. only uniform scaling and no skew), the matrix can be reorthogonalized using Math::
Matrix4 transformation; Math::Algorithms::gramSchmidtOrthonormalizeInPlace(transformation);
For quaternions and complex number this problem can be solved far more easily using Complex::
DualQuaternion transformation; transformation = transformation.normalized();