' '

We consider three kinds of transformations:

  • Translation

  • Scaling

  • Rotation

In this extension, you will implement the mathematical part of transformations. On top of these, you will be able to build

Transformations are represented as matrices. For example, 2D scaling by a factor \(s\) is represented by the following 3×3 matrix:

\[ \begin{bmatrix} s & 0 & 0 \\ 0 & s & 0 \\ 0 & 0 & 1 \\ \end{bmatrix}\]

Transformations can be combined using matrix multiplication. For example, translating \((2, 3)\) followed by scaling by \(5\) gives ()

\[ \begin{bmatrix} 5 & 0 & 0 \\ 0 & 5 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 & 2 \\ 0 & 1 & 3 \\ 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} 5 & 0 & 10 \\ 0 & 5 & 15 \\ 0 & 0 & 1 \\ \end{bmatrix}\]
Important

Matrices and functions that create transformation-representing matrices have already been written for you. You can find them in math/transformation-matrices.h. You will need to rely on them for this extension.

During the implementation of the ray tracer you will need transformations and their inverse. For example, the inverse operation of "rotating something 90° around the X-axis" is "rotating -90° around the X-axis."

While it is possible the compute the inverse of a matrix, doing so can be quite computationally expensive. For instance, say you are given

\[ \begin{bmatrix} 5 & 0 & 10 \\ 0 & 5 & 15 \\ 0 & 0 & 1 \\ \end{bmatrix}\]

Inverting this requires some tricky calculations: However, because we know from above that this is actually translating and scaling, we can compute the inverse in a much simpler way:

  • The inverse of translating by \((2, 3)\) is translating by \((-2, -3)\).

  • The inverse of scaling by \(5\) is scaling by \(\frac15\).

  • Inverting a combined transformation involves changing the order of the transformations.

Knowing this, we can easily compute the inverse matrix:

\[ \left( \begin{bmatrix} 5 & 0 & 10 \\ 0 & 5 & 15 \\ 0 & 0 & 1 \\ \end{bmatrix} \right)^{-1} = \begin{bmatrix} 1 & 0 & -2 \\ 0 & 1 & -3 \\ 0 & 0 & 1 \\ \end{bmatrix} \cdot \begin{bmatrix} \frac15 & 0 & 0 \\ 0 & \frac15 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} \frac15 & 0 & -2 \\ 0 & \frac15 & -3 \\ 0 & 0 & 1 \\ \end{bmatrix}\]

The moral of the story is that inverting a matrix is much easier if you know which transformations it represents. So, whenever we create a matrix, we will immediately create its inverse along with it. We will bundle the matrix and its inverse in a separate object of type Transformation2D (or Transformation3D).

Important

Both Transformation2D and Transformation3D have already been defined for you. You can find them in math/transformation2d.h and math/transformation3d.h, respectively.

1. 2D

1.1. 2D Translation

This transformation has already been defined for you. The code can act as a guide for you to implement the two other transformations.

1.2. 2D Rotation

In the files math/transformation2d.h and math/transformation2d.cpp, declare/define a function with signature

Transformation2D rotate(Angle angle);

1.3. 2D Scaling

In the files math/transformation2d.h and math/transformation2d.cpp, declare/define a function with signature

Transformation2D scale(double sx, double sy);

2. 3D

2.1. 3D Translation

In the files math/transformation3d.h and math/transformation3d.cpp, declare/define a function with signature

Transformation3D translate(const Vector3D& displacement);

2.2. 3D Rotation

In the files math/transformation3d.h and math/transformation3d.cpp, declare/define functions with signature

Transformation3D rotate_x(Angle angle);
Transformation3D rotate_y(Angle angle);
Transformation3D rotate_z(Angle angle);
Note

The reason we define three separate functions for rotations is because rotations are not commutative.

2.3. 3D Scaling

In the files math/transformation3d.h and math/transformation3d.cpp, declare/define a function with signature

Transformation3D scale(double sx, double sy, double sz);

3. Bindings

No bindings are necessary: we won’t be needing this functionality in our scripting language. It will be used in other C++ code, though.

4. Evaluation

Write tests for each of the transformations, both in 2D and 3D. Write them concisely: define helper macros so that you can write your tests as

#define V(...)     (__VA_ARGS__)
#define P(...)     (__VA_ARGS__)

#define TEST_TRANSLATION2D(v, p, q)                                      \
    TEST_CASE("Translation by " #v " of " #p " should yield " #q)        \
    {                                                                    \
        auto transformation = transformations::translation(Vector2D v);  \
        auto original = Point2D p;                                       \
        auto transformed = Point2D q;                                    \
        auto& m = transformation.transformation_matrix;                  \
        auto& im = transformation.inverse_transformation_matrix;         \
                                                                         \
        CHECK(m * original == approx(transformed));                      \
        CHECK(im * transformed == approx(original));                     \
    }

TEST_TRANSLATION2D(V(1, 0), P(2, 3), P(3, 3))
TEST_TRANSLATION2D(V(4, 1), P(2, 3), P(6, 4))

Note how that a single macro tests both the transformation_matrix and inverse_transformation_matrix using the same data.