Welcome to the second part of our 3D Graphics Engine series! This time we are going to be talking about linear transformations, which will let us alter properties like the rotation and scaling of our vectors, and look at how to apply them to the classes we've already built.

If you haven't already read the first part of this series, I suggest you do so now. Just in case you don't remember, here is a quick recap of what we created last time:

Those two classes will be the basis of our entire graphics engine, where the first represents a point (a physical location within your space) and the second represents a vector (the space/movement between two points).

For our discussion on linear transformations, you should make a small change to the Point class: instead of outputting data to a console line like before, use your favorite Graphics API and have the function draw the current point to the screen.

Foundations of Linear Transformations

Just a warning: Linear Transformation equations look a lot worse than they actually are. There will be some trigonometry involved, but you don't have to actually know how to do that trigonometry: I will explain what you have to give each function and what you will get out, and for the in-between stuff you can just use any calculator or math library that you may have.

This states that if you have a linear transformation function \(F()\), and your input is the vector \(A\), then your output will be the vector \(B\).

Each of these pieces - the two vectors and the function - can be represented as a matrix: the vector \(B\) as a 1x3 matrix, the vector \(A\) as another 1x3 matrix, and the linear transformation \(F\) as a 3x3 matrix (a transformation matrix).

If you have ever taken a class in trigonometry or linear algebra, you are probably starting to remember the nightmare that was matrix math. Luckily, there is a simpler way to write out this equation to take most of the trouble out of it. It looks like this:

However, these equations may be altered by having a second input, such as in the case of rotations, where a vector and its rotation amount must both be given. Let's take a look at how rotations work.

Rotations

A rotation is, by definition, a circular movement of an object around a point of rotation. The point of rotation for our space can be one of three possibilities: either the XY plane, the XZ plane, or the YZ plane (where each plane is made up of two of our basis vectors that we discussed in the first part of the series).

Our three points of rotation mean that we have three separate rotation matrices, as follows:

So if your initial point \(A\) was \((3,4,5)\), then your output point \(B\) would be \((-4,3,5)\).

Exercise: Rotation Functions

As an exercise, try creating three new functions for the Vector class. One should rotate the vector around the XY plane, one around the YZ plane, and one around the XZ plane. Your functions should receive the desired amount of degrees for rotation as an input, and return a vector as an output.

The basic flow of your functions should be as follows:

Create output vector.

Convert the degree input into radian form.

Solve for each piece of the output vectors tuple by using the equations above.

Return the output vector.

Scaling

Scaling is a transformation that either enlarges or diminishes an object based on a set scale.

Performing this transformation is fairly simple (at least compared to rotations). A scaling transformation requires two inputs: an input vector and a scaling 3-tuple, which defines how the input vector should be scaled in regards to each of the space's basis axes.

For example, in the scaling tuple \((s_{0},s_{1},s_{2})\), \(s_{0}\) represents the scaling along the X axis, \(s_{1}\) along the Y axis, and \(s_{2}\) along the Z axis.

The scaling transformation matrix is as follows (where \(s_{0}\), \(s_{1}\), and \(s_{2}\) are the elements of the scaling 3-tuple):

So if given the input vector \(A = (3,4,0)\), then your output vector \(B\) would be \((6,4,0)\).

Exercise: Scaling Functions

As another exercise, add a new function to your vector class for scaling. This new function should take in a scaling 3-tuple and return an output vector.

The basic flow of your functions should be as follows:

Create output vector.

Solve for each piece of the output vectors tuple by using the equation above (which can be simplified to y0 = x0 * s0; y1 = x1*s1; y2 = x2*s2).

Return the output vector.

Let's Build Something!

Now that you've got linear transformations under your belt, let's build a quick little program to show off your new skills. We're going to make a program that draws a group of points to the screen, and then allows us to modify them as a whole by performing linear transformations on them.

Before starting, we will also want to add another function to our Point class. This will be called setPointToPoint(), and will simply set the current point's position to that of the point that is passed to it. It will receive a point as an input, and will return nothing.

Here are some quick specifications for our program:

The program will hold 100 points in an array.

When the D key is pressed, the program will wipe the current screen and redraw the points.

When the A key is pressed, the program will scale all of the points' locations by 0.5.

When the S key is pressed, the program will scale all of the points' locations by 2.0.

When the R key is pressed, the program will rotate all of the points' location by 15 degrees on the XY plane.

When the Escape key is pressed, the program will exit (unless you're making it with JavaScript or another web-oriented language).

main{
//setup for your favorite Graphics API here
//setup for keyboard input (may not be required) here
//create an array of 100 points
Point Array pointArray[100];
for (int x = 0; x < pointArray.length; x++)
{
//Set its location to a random point on the screen
pointArray[x].tuple = [random(0,screenWidth), random(0,screenHeight), random(0,desiredDepth));
}
//this function clears the screen and then draws all of the points
function redrawScreen()
{
//use your Graphics API's clear screen function
ClearTheScreen();
for (int x = 0; x < pointArray.length; x++)
{
//draw the current point to the screen
pointArray[x].drawPoint();
}
}
// while the escape is not being pressed, carry out the main loop
while (esc != pressed)
{
// perform various actions based on which key is pressed
if (key('d') == pressed)
{
redrawScreen();
}
if (key('a') == pressed)
{
//create the space's origin as a point
Point origin = new Point(0,0,0);
Vector tempVector;
for (int x = 0; x < pointArray.length; x++)
{
//store the current vector address for the point, and set the point
tempVector = pointArray[x].subtractPointFromPoint(origin);
//reset the point so that the scaled vector can be added
pointArray[x].setPointToPoint(origin);
//scale the vector and set the point to its new, scaled location
pointArray[x].addVectorToPoint(tempVector.scale(0.5,0.5,0.5));
}
redrawScreen();
}
if(key('s') == pressed)
{
//create the space's origin as a point
Point origin = new Point(0,0,0);
Vector tempVector;
for (int x = 0; x < pointArray.length; x++)
{
//store the current vector address for the point, and set the point
tempVector = pointArray[x].subtractPointFromPoint(origin);
//reset the point so that the scaled vector can be added
pointArray[x].setPointToPoint(origin);
//scale the vector and set the point to its new, scaled location
pointArray[x].addVectorToPoint(tempVector.scale(2.0,2.0,2.0));
}
redrawScreen();
}
if(key('r') == pressed)
{
//create the space's origin as a point
Point origin = new Point(0,0,0);
Vector tempVector;
for (int x = 0; x < pointArray.length; x++)
{
//store the current vector address for the point, and set the point
tempVector = pointArray[x].subtractPointFromPoint(origin);
//reset the point so that the scaled vector can be added
pointArray[x].setPointToPoint(origin);
//scale the vector and set the point to its new, scaled location
pointArray[x].addVectorToPoint(tempVector.rotateXY(15));
}
redrawScreen();
}
}
}

Conclusion

While we certainly didn't cover every possible linear transformation available, our micro-engine is starting to take shape.

As always, there are some things that got left out of our engine for simplicity (namely shearing and reflections in this part). If you want to find out more about those two types of linear transformations, you can find out more about them on Wikipedia and its related links.

In the next part of this series, we will be covering different view spaces and how to cull objects that are outside of our view.

If you need extra help, head over to Envato Studio, where you can find plenty of fantastic 3D Design & Modeling services. These experienced providers can help you with a wide range of different projects, so just browse the providers, read the reviews and ratings, and choose the right person to help you.