-
-
Notifications
You must be signed in to change notification settings - Fork 0
A.03 Transformations
In graphics applications, it is common to apply transformations such as scaling, rotation, and translation to objects before displaying them on the screen. For example, let's consider a scenario where you want to move an object to a different position in the scene. In order to achieve this, you need to move all the vertices of the object by applying a transformation on them. Matrices are mathematical entities that represent transformations. This means that if we treat vertex positions as vectors in a 3D space, we can multiply them by a matrix to change their position in the scene. In the example given, we would perform a matrix multiplication between each vertex position
Here,
Now, it’s interesting how we can visualize it in two different ways. Obviously, we can move (translate)
The same concept applies when rotating or scaling a vertex position. As we will explore in this tutorial, vector transformations and changes of coordinate systems are mathematically equivalent. In other words, transforming an object is equivalent to transforming its frame of reference, and vice versa.
A transformation
In appendix 01, it was demonstrated that a generic bound vector
Here,
Here,
Then, we can conclude that applying a linear transformation to a vector
where
That’s exactly what we have seen in appendix 02. Indeed, if we perform the multiplication between the matrix
We just found that to transform a vector
where
However, we can also interpret
Here, we have a vector
This representation corresponds to the diagonal of the parallelogram formed by scaling the vectors
As depicted on the right side of the above illustration, we have that
That is,
At this point, it should come as no surprise that the inverse matrix
In a 3D Cartesian coordinate system, vectors can be scaled independently in three directions by scaling their respective components. That is, given a scaling
Here,
The scaling is uniform if the scaling factors are equal
$S(\mathbf{u}+\mathbf{v})=\big( s_x(u_x+v_x),\ s_y(u_y+v_y),\ s_z(u_z+v_z) \big)=$
$(s_xu_x+s_xv_x,\ s_yu_y+s_yv_y,\ s_zu_z+s_zv_z)=$
$(s_xu_x,\ s_yu_y,\ s_zu_z)+(s_xv_x,\ s_yv_y,\ s_zv_z)=$
$S(\mathbf{u})+S(\mathbf{v})$
$S(k\mathbf{v})=(s_xkv_x,\ s_ykv_y,\ s_zkv_z)=$
$k(s_xv_x,\ s_yv_y,\ s_zv_z)=$
$kS(\mathbf{v})$
We know that a linear transformation can be associated with a matrix whose columns are the transformed standard basis vectors. In 3D Cartesian coordinate systems, we have that
Then, the matrix
This matrix is associated with the scaling operation. Consequently, we can apply scaling to any 3D vector by multiplying it with the scaling matrix
Example:
Given a minimum point
To scale the square, we need to multiply both
The following illustration shows the result of these transformations
A rotation is a linear transformation, but we won’t provide a formal proof here since it is simpler to observe, from the illustration below, that the rotation of the sum of two vectors (that is, the rotation of the diagonal of the parallelogram defined by the two vectors) is equivalent to the sum of rotations of the two vectors (that is, the diagonal of the rotated parallelogram). At the same time, rotating a uniformly scaled vector is equivalent to first rotating the vector and then applying the uniform scaling.
Finding the matrix associated with a rotation can be slightly more challenging compared to scaling. However, for the purpose of our discussion, we will consider
Now, let's consider the left side of the illustration below.
We know that
Now, let's consider the projection of the vector
where
Then,
That is,
In a similar way, the orthogonal projection of
Then, we have that
Now, we can compute
As you can see,
where
where
For example, if we want to rotate about the x-, y- and z-axes, we need to set
Example:
Given a minimum point
To rotate the square we need to multiply
You can easily verify that the columns of
Now that we know how to scale and rotate vectors, we'd also like to move points. While we can easily relocate points using a transformation called translation, we face two challenges when it comes to moving vectors:
-
we certainly can move points (vertex positions) to relocate 3D objects in the scene. However, for free vectors, this transformation doesn’t make any sense since direction and magnitude don’t change after a translation (the point of application for a free vector is irrelevant). Therefore, we need to distinguish between points and vectors.
-
moving a point can’t be expressed as a linear combination of the standard basis vectors. That is, translation is not a linear transformation, so we can’t associate a
$3\times 3$ matrix with it. Fortunately, we can still find a way to incorporate translations into our matrix equation$\mathbf{w}=\mathbf{Mv}$ .
Affine transformations extend linear ones by adding translations. In the following section, we will explore this type of transformation and aim to resolve the aforementioned issues.
To move a bound vector (point)
For example, let's consider a 2D point
At the same time, we can translate the frame by using the same displacement vector
So, it seems that translation of vectors and translation of coordinate systems are mathematically equivalent, just like with linear transformations (in the next section we will formally prove that this is true in general for affine transformations). This means that we can translate a frame to apply the same transformation to a vector. So far so good, but the question is: can we find a
$T(\mathbf{u}+\mathbf{v})=(u_x+v_x+t_x,\ u_y+v_y+t_y,\ u_z+v_z+t_z)$
while
$T(\mathbf{u})+T(\mathbf{v})=(u_x+t_x,\ u_y+t_y,\ u_z+t_z)+(v_x+t_x,\ v_y+t_y,\ v_z+t_z)=$
$(u_x+v_x+2t_x,\ u_y+v_y+2t_y,\ u_z+v_z+2t_z)$
Then,
However, we can still find a way to include translations in our matrix equation
As explained in appendix 01, homogeneous coordinates introduce an "extra" coordinate. This means that starting from a 3D Cartesian systems, we step into the 4-th dimension. Fortunately, we just pop in to pick up what we need, and leave immediately after.
A point in 3D space can be represented in homogeneous coordinates by the tuple
Points:
Vectors:
We need this distinction between points and vectors as we want to apply translations to points without affecting vectors. Now, since we are using four components, we can try to find a
Let’s see what happens if we multiply a generic point
That’s exactly the definition of
The vector
However, we'd like to include linear transformations as well. For this purpose, we can try to embed the
Now, if we multiply a generic vector
where
As you can see, the translation doesn’t affect the vector, and the result is the same we showed in equation
On the other hand, if we multiply a generic point
Here, we transform
Furthermore, the difference of two points is the vector from a point to the other one. Indeed, from the equation above, we have
which is a vector, as it’s not bound to the origin of the frame of reference (see the illustration below, on the left side). On the other hand, the sum of two points is a point as well, but it doesn’t make any sense (unlike the sum of two vectors, which is the resultant force, direction or speed).
Then, the matrix
Imagine you want to scale, rotate and translate a vector
The vector
In a matrix by vector multiplication, we need to perform
However, thanks to the associative property of matrix multiplication we can write
Now, we have two matrix by matrix multiplications and a matrix by vector multiplication.
Each matrix by matrix multiplication needs
It seems that we need more operations to perform if we first multiply the matrices. However, imagine you want to transform 10 thousand vectors by the same matrices
Then, it is strongly recommended to transform vectors with a matrix which is a composition of all the transformations you want to apply to the vectors.
GLM provides convenient helper functions for building
The glm::scale_slow function offers a naive implementation for scaling. As shown in the following code snippet, it follows the definition presented in this tutorial:
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> scale_slow(mat<4, 4, T, Q> const& m, vec<3, T, Q> const& v)
{
mat<4, 4, T, Q> Result(T(1));
Result[0][0] = v.x;
Result[1][1] = v.y;
Result[2][2] = v.z;
return m * Result;
}
However, considering that the first parameter is used as the left operand in a matrix multiplication with the scaling matrix, we can optimize this composition by recognizing that multiplying a
We can leverage this insight to implement a faster version of the scaling transformation. The code snippet below demonstrates this approach:
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> scale(mat<4, 4, T, Q> const& m, vec<3, T, Q> const& v)
{
mat<4, 4, T, Q> Result;
Result[0] = m[0] * v[0];
Result[1] = m[1] * v[1];
Result[2] = m[2] * v[2];
Result[3] = m[3];
return Result;
}
Recall that matrices in GLM are defined column by column, so both Result[0]
and m[0]
refer to the first column.
The same principle is applied to build a rotation matrix. A naive/slow version is provided, adhering to the definition presented in this tutorial:
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> rotate_slow(mat<4, 4, T, Q> const& m, T angle, vec<3, T, Q> const& v)
{
T const a = angle;
T const c = cos(a);
T const s = sin(a);
mat<4, 4, T, Q> Result;
vec<3, T, Q> axis = normalize(v);
Result[0][0] = c + (static_cast<T>(1) - c) * axis.x * axis.x;
Result[0][1] = (static_cast<T>(1) - c) * axis.x * axis.y + s * axis.z;
Result[0][2] = (static_cast<T>(1) - c) * axis.x * axis.z - s * axis.y;
Result[0][3] = static_cast<T>(0);
Result[1][0] = (static_cast<T>(1) - c) * axis.y * axis.x - s * axis.z;
Result[1][1] = c + (static_cast<T>(1) - c) * axis.y * axis.y;
Result[1][2] = (static_cast<T>(1) - c) * axis.y * axis.z + s * axis.x;
Result[1][3] = static_cast<T>(0);
Result[2][0] = (static_cast<T>(1) - c) * axis.z * axis.x + s * axis.y;
Result[2][1] = (static_cast<T>(1) - c) * axis.z * axis.y - s * axis.x;
Result[2][2] = c + (static_cast<T>(1) - c) * axis.z * axis.z;
Result[2][3] = static_cast<T>(0);
Result[3] = vec<4, T, Q>(0, 0, 0, 1);
return m * Result;
}
In the optimized version, we compute
In other words, the i-th row of the resultant matrix is the sum of the rows of the left operand, scaled by the elements in the i-th row of the right operand matrix. You can easily verify this by multiplying two simple
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> rotate(mat<4, 4, T, Q> const& m, T angle, vec<3, T, Q> const& v)
{
T const a = angle;
T const c = cos(a);
T const s = sin(a);
vec<3, T, Q> axis(normalize(v));
vec<3, T, Q> temp((T(1) - c) * axis);
mat<4, 4, T, Q> Rotate;
Rotate[0][0] = c + temp[0] * axis[0];
Rotate[0][1] = temp[0] * axis[1] + s * axis[2];
Rotate[0][2] = temp[0] * axis[2] - s * axis[1];
Rotate[1][0] = temp[1] * axis[0] - s * axis[2];
Rotate[1][1] = c + temp[1] * axis[1];
Rotate[1][2] = temp[1] * axis[2] + s * axis[0];
Rotate[2][0] = temp[2] * axis[0] + s * axis[1];
Rotate[2][1] = temp[2] * axis[1] - s * axis[0];
Rotate[2][2] = c + temp[2] * axis[2];
mat<4, 4, T, Q> Result;
Result[0] = m[0] * Rotate[0][0] + m[1] * Rotate[0][1] + m[2] * Rotate[0][2];
Result[1] = m[0] * Rotate[1][0] + m[1] * Rotate[1][1] + m[2] * Rotate[1][2];
Result[2] = m[0] * Rotate[2][0] + m[1] * Rotate[2][1] + m[2] * Rotate[2][2];
Result[3] = m[3];
return Result;
}
Translation matrices can be constructed using the glm::translate function, which is implemented as follows:
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> translate(mat<4, 4, T, Q> const& m, vec<3, T, Q> const& v)
{
mat<4, 4, T, Q> Result(m);
Result[3] = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3];
return Result;
}
This function scales the columns of the matrix passed as the first parameter using the components of the vector passed as the second parameter and assigns the result to the last column of the resultant matrix. Therefore, to make it work according to the definition provided in this tutorial, you should pass a
To conclude this section, when composing multiple transformations into a single matrix using the first parameter of the transformation helper functions (glm::scale, glm::rotate, and glm::translate), you should call them in reverse order. This is because the first parameter is used as the left operand in a matrix multiplication with the transformation matrix built by the helper function. For example, if you want to scale, rotate, and translate an object, you need to first call glm::translate and pass the returned matrix to glm::rotate. Then, the matrix returned by glm::rotate must be passed to glm::scale. In pseudocode:
S = glm::scale(IdentityMatrix, scalingVector);
R = glm::rotate(IdentityMatrix, axisOfRotationVector);
T = glm::translate(IdentityMatrix, displacementVector);
M = T * R * S;
// Equivalent to:
T = glm::translate(IdentityMatrix, displacementVector);
R = glm::rotate(T, axisOfRotationVector);
M = glm::scale(R, scalingVector); // M = TRS
This ensures that the transformations are applied in the correct order.
Source code: LearnVulkan
[1] Practical Linear Algebra: A Geometry Toolbox (Farin, Hansford)
If you found the content of this tutorial somewhat useful or interesting, please consider supporting this project by clicking on the Sponsor button. Whether a small tip, a one time donation, or a recurring payment, it's all welcome! Thank you!