This lesson covers the use of basic mathematical operations on matrices in computer graphics.
One of the basic tasks of computer graphics is performing simple image transformations. Imagine wanting to make a simple platform game, similar to Super Mario ®.
It consists of many small pictures called sprites. Animation in this game consists of two basic elements:
We need to quickly replace images to show consequtive frames of animation
We need to rotate, scale and translate each sprite in the 2D space
In this lesson we will discuss the latter element, i.e. how to efficiently move, scale and rotate objects in linear space. For simplicity, we will practice these tasks in 2D space, but this can easily be expanded into any number of dimensions.
Let us start with the following code:
<style> body { background-color:#ccc; } </style>
<script src="//cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.7/p5.js"></script>
<body>
<script type="text/javascript">
var imgA;
var imgB;
function setup() {
createCanvas(512,512);
background(255);
imgA = createImage(512,512);
imgB = createImage(512,512);
imgA.loadPixels();
imgB.loadPixels();
var d = pixelDensity();
for(var i=0; i<512*512*4*d; i+=4) {
imgA.pixels[i]=240;
imgA.pixels[i+1]=250;
imgA.pixels[i+2]=240;
imgA.pixels[i+3]=255;
imgB.pixels[i]=240;
imgB.pixels[i+1]=240;
imgB.pixels[i+2]=250;
imgB.pixels[i+3]=255;
}
imgA.updatePixels();
imgB.updatePixels();
}
function draw() {
if (!keyIsDown(32)) {
image(imgA,0,0);
text('Image A',10,20);
} else {
image(imgB,0,0);
text('Image B',10,20);
}
}
</script>
</body>
This program creates a canvas with two buffers used for drawing. First, a buffer named imgA filled with light green color is displayed, and after pressing space the second buffer named imgB (light blue) is displayed. These two images will help with observing the differences between transformations in the next steps.
The operations described in this exercise are used to transform objects in space (i.e. a Cartesian coordinate system). The object can be a single point, a group of points defining a shape (e.g., four vertices of a square) or even a set of pixels in a raster image (each pixel has 2 coordinates, x and y). For simplicity we will transform single points in 2D space.
Let us start with the definition of such a point in vector form. A vector is a set (list) of numbers. We usually save the vectors as either a row or a column of a 1-D matrix:
In 2D graphics, the vector representing the point in 2D space has 3 values. The first two are the x and y coordinates, and the third is always 1. This way we obtain a point in an affine space, which allows us to perform the operations of rotation, scaling, and translation. The third coordinate always equal to 1 is necessary to move from a linear space (i.e. vector space allowing only rotation and scaling) into the affine space. It is also easy to verify if mathematical operations were performed correctly, as a different value in this position indicates an error.
As the first task, let us create a makeVector function, with the arguments x and y. This function should return a one-dimensional array, as shown above. Arrays in JavaScript are defined as follows: var tab=[1,2,3,4];
Next, create a drawVector function that takes two arguments, img and vec. The first argument is the image we want to draw on (imgA or imgB), and the second argument is the point (in vector form) to be drawn. You can use the img.set(x, y, colour) function to draw. After using this function, run the img.updatePixels() function to update the buffer in the memory.
The last step in this task is to implement the mouseDragged() function. In this function we should first create a vector using the makeVector method, giving the coordinates of mouseX and mouseY, and then pass the obtained vector to the drawVector function, together with the imgA image (we will use this image only for now).
After successfully accomplishing this task, we should be able to draw simple shapes (curves defined by dragging the mouse with the left mouse key pressed) on the imgA image.
In the previous task, you might have a feeling that creating vectors is pointless, because you can set the pixels' position directly in the image. However, there is a good reason why we are introducing the concept of a 3-dimensional vector representation of a 2D point. This is because transformations mentioned before (translations, rotations, scaling) can be defined mathematically as multiplication operations, i.e. multiplying the vector by a matrix. The vector represents the coordinates of the object (point) to be transformed, and the matrix represents the transformation.
Before we proceed to the application of these transformations, let us first define basic matrices, starting from the simplest - identity matrix:
This matrix does not make any transformation - it is a unity in the matrix space. The identity matrix of size n is the n×n square matrix with ones on the main diagonal and zeros elsewhere.
Another, often used matrix is a translation matrix:
Replacing tx and ty with translation values (for the object to be moved), this matrix performs translation after performing a multiplication operation with the vector representing the object.
Similarly, we can define a scaling matrix:
The rotation matrix is a bit more complicated:
Letter Θ represents the angle of rotation. It is worth remembering, however, that in JavaScript (as well as most programming languages) trigonometric functions take values in radians and not in degrees. Therefore, in order to make a rotation through an angle of 30 degrees, it is necessary to enter ~0.5236 as the value of Θ. In order to convert the angle from degrees to radians, we should divide the degrees first by 180, and then multiply the obtained value by π (in JavaScript Math.PI).
Finally, less often used transform, i.e. shear transform, with the following transformation matrix:
To get credit for this task, implement the functions that create all the above matrices according to the specifications (similarly to makeVector, e. g. makeIdentity or makeScale etc.). We will not see them working for now, but you can use the console.log command to see them in the console. You can place this outside any function, at the end of the script (and then it will be executed only once).
Having created a vector and transformation matrices, we can apply these transformations.
In order to apply the transformation, a multiplication operation of the transformation matrix by the vector should be performed using the formula below:
The letter V in the above formula is the length of the vector v, and W is the length of the vector w. Thus, the size of the matrix must be VxW.
Create a function performing the above multiplication operation (a matrix by a vector), and then modify the mouseDragged function as follows. After drawing the pixel (in imgA), multiply its vector by one of the above matrices, and then draw the obtained vector on the imgB image.
After implementing this part of the task, you should be able to draw a picture, and after pressing the space bar you should see what it looks like after making transformations on it. Watch how the transformations look like. What is translation, what is rotation and what is scaling? Make a matrix for each transformation and comment all but one to show the teacher any of them.
Tip: in order to facilitate the debugging of this function, remember that the result of the operation must be a vector with last value always equal to 1. Besides, remember that the multiplication of any vector by a unit matrix should result in the same vector.
So far, we learned how to make any transformation, but this operation is still not more efficient than a direct modification of coordinate values. If we want to make a few transformations one after the other, we have to perform the first one, then the second one (taking the result of the first one as the input), and so on:
However, this it is mathematically identical to the operation of multiplying the first vector with the product of all matrices:
This corresponds to performing transformation represented by M1 first, then M2, and so on (matrices are in reverse order).
The above formula can accelerate the operation of drawing algorithms by an order of magnitude; for instance, the complexity of O(n²) can be decreased to O(n).
To complete this task, implement a function for matrix multiplication:
In the above formula the dimensions of the matrix O are AxB, the dimensions of the matrix M are AxC, and the dimensions of the matrix N are CxB.
To complete this task, modify the code in mouseDragged to perform a sequence of several transformations. Note that the order of transformations is important! In other words, the matrix multiplication operation is not commutative. Implement an example where one sequence of transformations (e. g. translation + rotation + scaling) is shown on the imgA, and another sequence using the same transformations, but in a different order (e. g. scaling + translation + rotation) is shown on the imgB.
Tip: implement some examples to debug the code. It is worth checking the multiplication of matrices with the unit matrix, which should result in obtaining the same matrix.
Prepare an application, including interactive elements of the user interface, for drawing on imgA and then applying selected transformations and drawing the result on imgB. The easiest way to do this is by adding HTML elements (buttons, fields to input numbers) to the application and controlling the application in JavaScript. The application should have the following elements:
show at all times the content of the transformation matrix used at any given time
enable the user to reset the transformation matrix to the unit matrix
allow the user to apply the selected transformation, i.e. rotation, scaling, translation, shear, and multiply the current transformation matrix by the appropriate transformation matrix, corresponding to selected transform
allow the user to change transformation parameters.