Written by Deven Joshi

One of the most powerful widgets in the Flutter catalog (and for me, one of the most underrated) is the Transform widget. Transform widgets allow us to fundamentally alter how widgets look and behave, allowing us to create new, complex types of animations. Underneath the Transform widget, a 4D matrix powers the actual transformation — defined by the Matrix4 class. While Flutter provides easy ways to do transformations such as translation, scaling, and rotation, we can use the Matrix4 to create even more awesome things such as 3D perspective transformation.

In this article, we are going to explore the 4D matrix itself and what the individual values in it do. In the previous Transform Deep Dive I wrote, we discussed how to use Transforms without directly interacting with a 4D matrix. Check it out if you need a refresher on the easier uses of the widget.

What is a 4D matrix?

Even though anything with “4D” attached to it sounds cool by default, in reality, a 4D matrix is simply a matrix with 4 rows and 4 columns. We need to use a 4D matrix to transform an object in 3 dimensions (Here, the dimensions are what we're used to: length, breadth and height).

Identity Matrix

This formation of the matrix is called an identity matrix. The best way to think of an identity matrix is that this is the equivalent of the number ‘1’ in the matrix form — It leaves things untouched when used to transform widgets.

Using different combinations of numbers in this matrix, we can manipulate the shape, size, orientation, etc of a given object.

Let's look at how we do this.

Basic Setup

Let's take a look at the code that we are going to use for experimentation:

class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { double x = 0; double y = 0; double z = 0; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Transform( transform: Matrix4( 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1, )..rotateX(x)..rotateY(y)..rotateZ(z), alignment: FractionalOffset.center, child: GestureDetector( onPanUpdate: (details) { setState(() { y = y - details.delta.dx / 100; x = x + details.delta.dy / 100; }); }, child: Container( color: Colors.red, height: 200.0, width: 200.0, ), ), ), ), ); } }

The code simply uses a Transform widget and a colored Container to transform. We define an identity Matrix4 for the starting point which does… well.. nothing at all. The rest of the code with the GestureDectector allows us to rotate the square in X and Y directions. The code for rotating the square is final and we do not intend to make any changes to it. If you need more information on what we're doing, I suggest looking at Wm Leler‘s article on Perspective in Flutter where a similar method is used. The x, y, and z variables simply track the amount of rotation that has already been done.

We will come back to why we're allowing the user to rotate the square later, for now, we focus on the matrix and basic 2D results.

Note 1: The line alignment: FractionalOffset.center, sets the centre of the transformation to the centre of the square. Note 2 (for nerds): Matrix4 is in column-major order by default. In the way the code is written, we effectively write it in row-major format. Hence, all the row and column values written will be inverted along the diagonal.

Here's how the screen looks right now:

Very interesting square we have there. Let's see if we can do something with it.

Scaling using the matrix

Let's try to scale (expand/contract) the square in X, Y, Z directions and then all together.

Scaling in X

To scale an object in the X direction (horizontal), change the (0,0) value of the matrix with the scaling factor required.

Let's try doing this. With a scaling factor of 1.5, our square becomes:

Scaling in Y

To scale an object in the Y direction (vertical), change the (0,0) value of the matrix with the scaling factor required.

Let's try doing this. With a scaling factor of 1.5, our square becomes:

Scaling in Z?

Theoretically, the value (2,2) will scale in the Z direction, with a similar matrix.

Since this doesn't apply to our 2D square, we'll let it go.

Scaling in all directions

One way to scale in all directions is to just combine the ways above and use it to uniformly scale our square. However, we can also use another method:

!

We can substitute the position (3,3) with 1 divided by the scale factor we want to use. So if we want to make the square twice as big, we would use 1/2, which is 0.5.

Here's the square using 0.5 at (3,3):

Translation using the matrix

To translate in the X, Y or Z, directions, we can easily change the bottom three values for X, Y and Z values respectively.

Important: The matrix given above represents the matrix in the code. Since the actual Matrix4 goes by column-major format, X, Y and Z would be at (0,3), (1,3) and (2,3) respectively.

Let's take an X translation of 75 as an example:

Rotation using the matrix

All things can't be easy in life- hence, as simple as it was to scale and translate objects with the matrix, rotation is no easy thing to remember and something you would mess up on your math finals.

For rotation on around X axis:

For rotation around the Y axis:

For rotation around the Z axis:

This may just explain why in the starter code we had

..rotateX***(*x*)***..rotateY***(*y*)***..rotateZ***(*z*)***

and not a manual implementation of angles and further matrix multiplication.

Perspective Transformations

Perspective transformations modify lengths over positions along an axis. The most common use of this is to give the user a sense of depth — the way railway tracks look wide where you're standing but get shorter and shorter as you look farther. In this context, the side of the square closer to us should look bigger and the one away from us should look smaller.

Let's take a look to better understand this. Currently, when we play around with the square with the starter code, it looks like this:

There is no perception of depth when the square rotates. We can fix this using perspective transformation. To enable depth perception in the Z direction, we need to set this value in the matrix in the code-

You may have seen other tutorials using this line:

..setEntry***(***3, 2, 0.001)

This reduces lengths when the object goes farther away and increases them when they come nearer.

Let's set the z value to 0.002 and see what happens to our square.

We can now see the difference and the nearer side becomes bigger and the farther side becomes smaller. A higher value will give a higher gradient of length by distance.

But… that's not it

If you noticed a symmetrical theme for all axes in this article, you may be thinking “what about the two values above the depth perception value for the Z axis”?

Similar to the Z axis, the other two values also work the same way.

Setting this value allows us to perceive distance along the X axis like we did with the Z axis. While this isn't necessarily the same effect, this is still theoretically identical to the perspective transformation done above.

Here, as position in the X axis increases, length of sides decrease, something like this:

Similarly, for the Y axis:

Origin in computer graphics is at the top-left corner, hence, the Y position increases as we go down. Therefore, length shrinks going down since the Y value going down is “farther”.

Deven Joshi is a mobile developer (Flutter + Android) | Technical Writer | Speaker | #Hack19 Winner | AAD graduate. You can follow him on twitter @DevenJoshi7 and on GitHub