Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Let’s Build a 3D Graphics Engine: Linear Transformations

$
0
0
This entry is part 2 of 2 in the series Let's Build a 3D Graphics Engine

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:

Point Class
{
  Variables:
    num tuple[3]; //(x,y,z)

  Operators:
    Point AddVectorToPoint(Vector);
    Point SubtractVectorFromPoint(Vector);
    SubtractPointFromPoint(Point);

  Functions:
    //draw a point at its position tuple with your favorite graphics API
    drawPoint;
}

Vector Class
{
  Variables:
    num tuple[3]; //(x,y,z)

  Operators:
    Vector AddVectorToVector(Vector);
    Vector SubtractVectorFromVector(Vector);
}

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.

Tip: If you do want to have a better grasp of the inner workings of these equations, then you should watch this video and read this PDF.

All linear transformations take this form:

\[B = F(A)\]

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 1×3 matrix, the vector \(A\) as another 1×3 matrix, and the linear transformation \(F\) as a 3×3 matrix (a transformation matrix).

This means that, when you expand the equation, it looks like this:

\[
\begin{bmatrix}
b_{0} \\
b_{1} \\
b_{2}
\end{bmatrix}
=
\begin{bmatrix}
f_{00} & f_{01} & f_{02}\\
f_{10} & f_{11} & f_{12}\\
f_{20} & f_{21} & f_{22}
\end{bmatrix}
\begin{bmatrix}
a_{0}\\
a_{1}\\
a_{2}
\end{bmatrix}
\]

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:

\[
\begin{bmatrix}
b_{0}\\
b_{1}\\
b_{2}
\end{bmatrix}
=
\begin{bmatrix}
f_{00}a_{0} + f_{01}a_{1} + f_{02}a_{2}\\
f_{10}a_{0} + f_{11}a_{1} + f_{12}a_{2}\\
f_{20}a_{0} + f_{21}a_{1} + f_{22}a_{2}\\
\end{bmatrix}
\]

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).

692px-Rotation_illustration2.svg

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

XY rotation matrix:
\[
\begin{bmatrix}
cos \theta & -sin \theta & 0\\
sin \theta & cos \theta & 0\\
0 & 0 & 1\\
\end{bmatrix}
\]

XZ rotation matrix:

\[
\begin{bmatrix}
cos \theta & 0 & sin \theta\\
0 & 1 & 0\\
-sin \theta & 0 & cos \theta
\end{bmatrix}
\]

YZ rotation matrix:

\[
\begin{bmatrix}
1 & 0 & 0\\
0 & cos \theta & -sin \theta\\
0 & sin \theta & cos \theta
\end{bmatrix}
\]

So to rotate a point \(A\) around the XY plane by 90 degrees (\(\pi/2\) radians – most math libraries have a function for converting degrees into radians), you would follow these steps:

\[
\begin{aligned}
\begin{bmatrix}
b_{0}\\
b_{1}\\
b_{2}
\end{bmatrix}
& =
\begin{bmatrix}
cos \frac{\pi}{2} & -sin \frac{\pi}{2} & 0\\
sin \frac{\pi}{2} & cos \frac{\pi}{2} & 0\\
0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
a_{0}\\
a_{1}\\
a_{2}
\end{bmatrix}\\
& =
\begin{bmatrix}
cos \frac{\pi}{2}a_{0} + -sin \frac{\pi}{2}a_{1} + 0a_{2}\\
sin \frac{\pi}{2}a_{0} + cos \frac{\pi}{2}a_{1} + 0a_{2}\\
0a_{0} + 0a_{1} + 1a_{2}
\end{bmatrix}\\
& =
\begin{bmatrix}
0a_{0} + -1a_{1} + 0a_{2}\\
1a_{0} + 0a_{1} + 0a_{2}\\
0a_{0} + 0a_{1} + 1a_{2}
\end{bmatrix}\\
& =
\begin{bmatrix}
-a_{1}\\
a_{0}\\
a_{2}
\end{bmatrix}
\end{aligned}
\]

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:

  1. Create output vector.
  2. Convert the degree input into radian form.
  3. Solve for each piece of the output vectors tuple by using the equations above.
  4. 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):

\[
\begin{bmatrix}
s0 & 0 & 0\\
0 & s1 & 0\\
0 & 0 & s2
\end{bmatrix}
\]

In order to make the input vector A \((a_{0}, a_{1}, a_{2})\) twice as large along the X axis (that is, using a scaling 3-tuple \(S = (2, 1, 1)\)), the math would look like this:

\[
\begin{aligned}
\begin{bmatrix}
b_{0}\\
b_{1}\\
b_{2}
\end{bmatrix}
& =
\begin{bmatrix}
s0 & 0 & 0\\
0 & s1 & 0\\
0 & 0 & s2
\end{bmatrix}
\begin{bmatrix}
a_{0}\\
a_{1}\\
a_{2}
\end{bmatrix}\\
& =
\begin{bmatrix}
2 & 0 & 0\\
0 & 1 & 0\\
0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
a_{0}\\
a_{1}\\
a_{2}
\end{bmatrix}\\
& =
\begin{bmatrix}
2a_{0} + 0a_{1} + 0a_{2}\\
0a_{0} + 1a_{1} + 0a_{2}\\
0a_{0} + 0a_{1} + 1a_{2}
\end{bmatrix}\\
& =
\begin{bmatrix}
2a_{0}\\
a_{1}\\
a_{2}
\end{bmatrix}
\end{aligned}
\]

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

scaling

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:

  1. Create output vector.
  2. 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).
  3. 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).

Our current classes:

Point Class
{
	Variables:
		num tuple[3]; //(x,y,z)

	Operators:
		Point AddVectorToPoint(Vector);
		Point SubtractVectorFromPoint(Vector);
		Vector SubtractPointFromPoint(Point);
		//sets the current point's position to that of the inputted point
		Null SetPointToPoint(Point);
	Functions:
		//draw a point at its position tuple with your favorite graphics API
		drawPoint;
}

Vector Class
{
	Variables:
		num tuple[3]; //(x,y,z)

	Operators:
		Vector AddVectorToVector(Vector);
		Vector SubtractVectorFromVector(Vector);
		Vector RotateXY(degrees);
		Vector RotateYZ(degrees);
		Vector RotateXZ(degrees);
		Vector Scale(s0,s1,s2);
}

With those specifications, let’s look at what our code could be:

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();
        }
    }
}

Now you should have a cool, little program to show off all of your new techniques! You can check out my simple demo here.


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.


Viewing all articles
Browse latest Browse all 728

Trending Articles