A First-Person Turner Using Trigonometry

A First-Person Turner Using Trigonometry

The very first 3D program I wrote and placed on this website illustrated how to move a single point around along the three axes of movement. This mirrors the movement that you'd see in a first-person game if you were walking forward or backward, stepping sideways, or moving straight up and down (as if in an elevator, perhaps). While this effectively covers any kind of moving around you could do, you might have noticed that in the program, you, the viewer, are always facing the same direction. Obviously, in a 3D engine it's important to be able to not only move around, but also TURN around while still remaining in the same place. To this end, this page endeavors to explain how to make a simple "first-person turner", a program that lets you turn around in 3D, by using trigonometric functions.

This program will allow you to turn along just one axis; you'll be able to turn left and right. Aircraft pilots might think of this turning motion as yaw. For most people, it's the most fundamental axis of rotation in a first-person game (and in first-person real life as well); the pioneering first-person shooters like Wolfenstein 3D and Catacomb 3D only supported rotating along this axis. It wasn't until much later games that you were able to look up and down as well (what's called the "pitch" axis on an airplane), and even today's first-person shooters make little use of the "roll" axis that is invoked by a person tilting or leaning to one side, as if trying to read an emoticon. System Shock was one of the first games that actually allowed you to lean in this way (for the purpose of leaning around corners to minimize exposure to enemy fire); most first-person shooters today allow you to lean for the same purpose, but it's definitely a little-used axis of rotation that most people on their feet don't think about too much. Once you understand the principle of how to rotate in 3D, it's not too difficult to reapply this process to the other axes of rotation, so to minimize confusion and fuss while learning how to rotate, I'll focus on the yaw axis since it's by far the most fundamental.

That said, what happens when you turn left or right, whether in a game or in real life? Think about it: What happens to the objects around you, relative to your field of view? Well, if you turn right, objects move to the left across your field of vision, and vice-versa, right? But that's not all that happens: Objects also distort slightly. If you take a square piece of paper and tilt it so that one side is farther from you than the other, the far side of the piece of paper appears shorter than the near side. Of course, this isn't the case: The sides of the paper are the same length, but three-dimensional perspective makes farther lines seem shorter.

To create the simplest possible illustration of how perspective changes in a 3D rotation and how you'd implement those perspective changes in a computer program, I opted to use 4 points in a square formation. You won't see an actual filled-out square in this program, but you will see four dots on the screen which, if you connected them, would form a square outline. This pseudo-square starts off with you looking directly at it; the sides of the square are all drawn with the same length, because all four points are exactly the same distance from you. The program accepts only two control keys: The left arrow key and the right arrow key. By pressing these two keys, you can rotate your in-program persona and watch how the square shape changes as you turn around. As you turn to the left, you'll notice that the right side of the square appears to be getting a bit longer than the left, and vice-versa. As a side note, it's worth mentioning if you haven't already realized that what's actually happening here is the *points* are moving around, not you. The locations of the points change on the screen to simulate the act of you turning around, but your character (him/her)self is actually stationary. This is how all computer 3D engines work: In reality, you don't rotate or move around. Instead, the whole game world rotates and moves around you! You can analyze this in some kind of interesting philosophical context if you think about it for a while, and even extend it to the real world (who's to say that we move in real life? What if the world is actually just moving around us in reality too?), but basically, it's necessary to do things this way because of course you don't actually turn around in your seat when you turn in a game, and your computer monitor is just a static piece of glass that can't move around either, so the only way to make it feel like you're moving is to make everything move around the screen. The net visual effect is the same.

That said, the program keeps track of a single integer variable that represents your heading in space. A full circle is usually divided into 360 degrees of rotation, so the variable loops around from 0 to 359. Pressing the left or right arrow keys will change this variable by a single degree, and then adjust the position of the points on the screen accordingly. Just as with the first program in which we moved a single point in 3D, each of these 4 points has three 3D variables that define their x, y, and z position in 3D space; these coordinates are then converted into 2D coordinates that can be plotted on a computer screen.

Okay, that's all well and good; hopefully now we understand what the program is supposed to do. The really meaty question, of course, is: How does it do it? How does a program actually take a one-degree change in heading and calculate the changes this makes in the 3D coordinates of a point? That's where the trigonometry comes in.

If you're not already familiar with trigonometry, you'll probably want to take a quick crash course in how it works using a resource such as my math section. The actual level of knowledge you need isn't terribly deep; as long as you understand the sine and cosine, that should cover most of what you need for now.

Now it's time to visualize the act of turning around from a bird's-eye view. Imagine floating directly over someone as an object rotates around them. (It probably makes more sense to visualize this in terms of the object moving around the person rather than the person just spinning around, since after all, we really are making the objects spin around the viewer in a 3D engine anyway.) Since this is a pure rotation with no translation involved, the distance between the object and the viewer never changes. The object can go all the way around the person, and the two entities will have been the same distance apart the whole time. If you know your geometric shapes, you'll instantly realize that what we're describing here--a shape formed by a line traced around a center that's equidistant at all points along the line--is simply a circle.

If you think about all of this for a while, you should be able to start piecing together a mental picture of the math involved in finally making all of this work. Essentially, any rotation of the viewer--which really manifests itself as a circling around of everything else--creates a shift along both the x axis and the y axis. If you take a point on a circle and slide it around on the circumference of the circle a few degrees, it will have shifted both its x and its y position. The x and y position of a point on a circle using an angle as an input is exactly what the sine and the cosine define.

Let's now illustrate an example using another lovely text description that you might want to visualize in your mind's eye or draw out on paper. Imagine that you are looking directly at a point. There's just one dot in this theoretical universe, and you're looking straight at it, so it's right in front of you. Further suppose that the distance from you to the dot is, oh, 100 units.

Now suppose that you turn around 30 degrees to the left. The point is no longer directly in front of you; it has shifted to your right. It has also come "closer" to you along one axis, although the actual distance from you to the point is the same. In fact, let's actually illustrate this with some ASCII art! What fun!

─────
     \
      \
       \
        \
         \Point
         /\
        /│ \
       / │  \
      /  │   \
     /   │    \
    /    │    │
   /     │    │  
  /      │    │   
 /       │    │
You──────┘    │

Yeah, it looks horrible, I know. But hopefully you can better visualize what I'm trying to describe. You're in the lower-left, along with an angle that changes based on which direction you turn in; let's call this angle "angle x". As you turn around, the point moves along a circle (the weird shape that looks like the corner of an octagon is supposed to represent this circle). Because you've turned left, the point has shifted to your right, but a simple slide to the right would mean that the point has actually moved farther away from you, so to preserve the equal distance, the point has also moved closer to you on what would be the vertical axis in this drawing.

You can also see that I've made a right triangle, with a hypotenuse formed of the line from you to the point. The other two sides of this right triangle give us all the clues we need to figure out how much the point has shifted: The line extending out from your right is how much the point has moved to the right (since the point started off being directly in front of you). The vertical line is how far the point is from you on the "vertical" axis; this distance used to be 100, because we started off with the point being directly in front of you 100 units away, so if we discern what the length of that side of the triangle is now and subtract that value from 100, that will then tell us how much the point has shifted along that axis.

We now have all the numbers we need to make this calculation, so let's go ahead and do it! First, let's see how far the point has shifted to the right. For this, we want to figure out the length of the bottom side of that triangle; this is, of course, the "side adjacent" to angle x. Angle x is 60 degrees, because you turned to the left by 30 degrees, and when the point was right in front of you, angle x was 90 degrees. Also recall that the ratio of the side adjacent to the hypotenuse is the cosine. Knowing this, let's find out the cosine for 60 degrees; a calculator will tell you this happens to be exactly 0.5. This means that the bottom side of the triangle, in this case, is 0.5 times whatever the hypotenuse is. Since the hypotenuse will always be 100 (because that's the distance the point is from you, and this never changes as long as neither you nor the point makes any movement other than you turning around), we find that the length of the bottom side of the triangle is 100 * 0.5, which equals 50. By you turning 30 degrees to the left, the point has shifted to your right by 50 units.

Okay, so that's the movement of the point along one axis. What about the other axis? We need to find the length of that vertical line. This is the "side opposite" angle x, and the ratio of the side opposite to the hypotenuse is the sine. What's the sine for 60? A calculator will tell you it's about 0.866. This means that the vertical line is 100 * 0.866, which equals 86.6. That's all well and good, but in this case, the point didn't actually shift by 86.6 units; that vertical line was 100 units before you started your turn, so the point has actually shifted towards you on that axis by 100 - 86.6 units. The point has shifted "downward" by 13.4 units, and is now 86.6 units away from you on the "vertical" axis.

Now that you're hopefully getting the idea of how we calculate the motion of a point along two axes because of a rotation, let's take some time out and set up our four points that we're actually going to use for our program. As I mentioned, we'll want to make a simple square shape out of these points, so let's imagine that we're going to make a 10x10 unit square. Further suppose that we're about 100 units away from this square. Sound reasonable?

Given these dimensions, then, let's go ahead and create a variable type called Point and give it three properties: x, y, and z, which of course will be the point's 3D coordinates. We create this data structure in C with the following line:

typedef struct {double x; double y; double z;} Point;

Now let's actually create 4 points for us to work with. I'll call these points a, b, c, and d.

Point a,b,c,d;

Now we can set the initial locations of these points in the square. First, a quick reminder: In a 3D engine, you, the viewer, form the origin of the coordinate system, meaning that your location is always (0,0,0). Okay, since you're looking directly at the center of the square, and the square itself is 10 units on each side, each point will be offset from the center by 5 units, so I'll use an initial value of 5 for the x and y values. We also need to define how far we are from the points; to do this, I'm going to use 100 for the z value. BEWARE! If you're good at following along, you've already realized that this doesn't mean we're actually 100 units away from the points; it only means that the points are shifted 100 units "ahead" of us, but the actual distance from us to the points is slightly greater, because the points will also be shifted 5 units along the x and y axes. This is a correct observation, but our goal isn't actually to make sure that we're exactly 100 units distant from the points; we just want to make sure that when we turn around, the points are rotated correctly. In light of this, I'll go ahead and use 100 for the z axis. Our lines to initialize our 4 points, then, look like this:

a.x = -5;
a.y = 5;
a.z = 100;
b.x = 5;
b.y = 5;
b.z = 100;
c.x = -5;
c.y = -5;
c.z = 100;
d.x = 5;
d.y = -5;
d.z = 100;

a will be the top-left point in the square, b will be the top-right point in the square, c will be the bottom-left point in the square, and d will be the bottom-right point in the square. At this point, it's worth pointing out (since I haven't yet) that what look like the x and y axes in the trigonometric drawing above become the x and z axes in 3D. This is because the trigonometric drawing is a bird's-eye view; the "y" axis in the trig drawing is forward/backward motion in 3D, and we use the "z" axis to represent this. Our 3D "y" axis represents vertical motion, but since you're just turning around in this program, our 4 points will never experience any change in height, so their y value will never change. Thus, calculations on x and y in trig will become changes to the x and z values in 3D.

Okay, we're all set! We have 4 points already placed in the world, and we know their exact x, y, and z locations. Now all we need to do is put in place the algorithms for figuring out how much the x and z values will change when we rotate. Recall that we'll rig this program so that you rotate 1 degree at a time. Before we can work out the new values that these points take on after a rotation, we need some way of determining what the current angle of these points is, so let's spend some time talking about that.

We know how far the points are shifted along each axis away from us. What we don't know is each point's total distance from the origin. We can figure this out using the most famous contribution by good old Pythagoras: The Pythagorean Theorem. Let's think about point a, and how we would find out its distance from the origin right off the top, when its x, y, z coordinates are (-5, 5, 100). The Pythagoren Theorem only works in 2D, but we don't actually need to calculate this in 3D, because we know that the 3D "y" coordinate of all the points never changes. So in actuality, all we need to know is the 2D distance from the origin. (Incidentally, you can work around this 2D limitation of the Pythagorean Theorem by simply using it twice, but that's another subject for another time.) Let's go back to the bird's-eye view again; in this view, if we draw a line from us to point a and use this as the hypotenuse for a right triangle, then x = 5 and y = 100. Since the length of the hypotenuse is sqrt(x^2 + y^2), we find our distance from the point in this plane is 100.124921972503928638486060741613. (Hey, precision is good!)

What, then, is the angle of the point? To calculate this, let's use the ratio of the side adjacent to the hypotenuse, which of course is the cosine. Assuming that we're setting up the triangle so the angle extends out from our left, then the length of the side adjacent is 5 (since the point is shifted along the x axis 5 units left of the origin). The cosine, then, is 5 divided by 100.124921972503928638486060741613, which equals 0.0499376169438922337349057659559232. That's the cosine, and to get the angle from that, just use the inverse cosine function, which gives you the angle. We find that the angle in this case is 87.1375947738882524673066552715158.

It might be appropriate at this point to write a block of code to do these calculations. Let's create a variable called aangle (for the angle for point a). The code would probably look something like this:

double aangle;
aangle = acos( a.x / sqrt(a.x^2 + a.z^2) )

(C generally uses the "acos" function to do an inverse cosine.)

We can use this same format to create the initial angles for all 4 points. Then, from that point onward, we can simply add or subtract 1 to these variables and re-calculate the points' coordinates.

How do we re-calculate the points' coordinates? Well, the x coordinate for any point will be the side adjacent, meaning it'll be the cosine times the hypotenuse. Before we figure out how to do this, it would be useful to point out, once again, that the hypotenuse never changes, because any one point is always the same distance away from you since all you're doing is rotating. In fact, all 4 points always have exactly the same distance from you, because they're all offset by the same amounts: 5 units up or down, and 5 units to the side. The reason I point this out is because it means we actually only need to calculate the hypotenuse once, and after we've done this, we can dump it into a static variable that we can call on whenever we need to use that value. This can be another of our initial startup lines; I'll call this variable "pointdistance":

double pointdistance = sqrt(a.x*a.x + a.z*a.z);

With this, let's set out to show how we'd work out the new x value for point a:

a.x = cos(aangle) * pointdistance;

That's it! pointdistance is the hypotenuse of the right triangle in question, so all we need to do is calculate the ratio of the side to that value, and we're set!

Similarly, the z value for point a:

a.z = sin(aangle) * pointdistance;

Now we can make functions like this for all 4 of the points, and package them into an infinite loop that keeps checking for user input, and increments or decrements the angles when you press the left or right arrow keys.

That's about all you need to know. I'll also add a trap for pressing the "q" (for quit) key to exit the program. It's time for the code!

#include <stdio.h> // just because every C program in the world has this
#include <math.h> // needed for the trigonometry functions
#include <graphics.h> // needed for putpixel()
#include <bios.h> // needed for _bios_keybrd(_NKEYBRD_READ)

//Create a structure type called Point, containing the three 3D coordinates
typedef struct {double x; double y; double z;} Point;

main() {

/* The two lines below just init the graphics mode. */
int gdriver=VGA, gmode=2;
initgraph(&gdriver, &gmode, "C:\\TC\\BGI");

/* The 4 lines below set the screen_center variables */
int x_resolution = 640;
int y_resolution = 480;
int screen_center_x = x_resolution/2;
int screen_center_y = y_resolution/2;
/* --------------------- */

int distance=256;

//Create the four points of the square
Point a,b,c,d;

//Each of these points will also need an angle
double aangle, bangle, cangle, dangle;

//Let's set the initial locations of our four points
a.x = -5;
a.y = 5;
a.z = 100;
b.x = 5;
b.y = 5;
b.z = 100;
c.x = -5;
c.y = -5;
c.z = 100;
d.x = 5;
d.y = -5;
d.z = 100;

//Define the permanent variable pointdistance, which should never change
double pointdistance = sqrt(a.x*a.x + a.z*a.z);

//Set the initial angle values for all our points
//Note that we need the reciprocal of aangle for bangle, and the reciprocal
//of cangle for dangle. This is made even more unpleasant by the fact that
//most C compilers default to using radians for trigonometric functions, so
//we convert from degrees to radians on-the-fly
aangle = acos(a.x / pointdistance);
bangle = (90 * M_PI / 180) + ( acos(b.x / pointdistance) - (90 * M_PI / 180) );
cangle = acos(c.x / pointdistance);
dangle = (90 * M_PI / 180) + ( acos(d.x / pointdistance) - (90 * M_PI / 180) );

//Now we enter an infinite loop where it always checks for a keypress
while(1) {

cleardevice(); //clears the screen

//Draw the points
putpixel (screen_center_x + distance * (a.x / a.z), screen_center_y - distance * (a.y / a.z), 9);
putpixel (screen_center_x + distance * (b.x / b.z), screen_center_y - distance * (b.y / b.z), 9);
putpixel (screen_center_x + distance * (c.x / c.z), screen_center_y - distance * (c.y / c.z), 9);
putpixel (screen_center_x + distance * (d.x / d.z), screen_center_y - distance * (d.y / d.z), 9);

switch(_bios_keybrd(_NKEYBRD_READ)) {

//Based on whether we pressed left arrow, right arrow, or q, react
//appropriately. Again, since most C compilers expect radians instead of
//degrees, we convert the value of 1 degree into radians.

    case 19424: /*left arrow*/
                aangle-=1*M_PI/180;
                a.x = cos(aangle) * pointdistance;
                a.z = sin(aangle) * pointdistance;
                bangle-=1*M_PI/180;
                b.x = cos(bangle) * pointdistance;
                b.z = sin(bangle) * pointdistance;
                cangle-=1*M_PI/180;
                c.x = cos(cangle) * pointdistance;
                c.z = sin(cangle) * pointdistance;
                dangle-=1*M_PI/180;
                d.x = cos(dangle) * pointdistance;
                d.z = sin(dangle) * pointdistance;
		break;
    case 19936: /*right arrow*/
                aangle+=1*M_PI/180;
                a.x = cos(aangle) * pointdistance;
                a.z = sin(aangle) * pointdistance;
                bangle+=1*M_PI/180;
                b.x = cos(bangle) * pointdistance;
                b.z = sin(bangle) * pointdistance;
                cangle+=1*M_PI/180;
                c.x = cos(cangle) * pointdistance;
                c.z = sin(cangle) * pointdistance;
                dangle+=1*M_PI/180;
                d.x = cos(dangle) * pointdistance;
                d.z = sin(dangle) * pointdistance;
		break;
    case 4209: /* q */
	       return(0);
    }

  }

}

Back to Project 3D

Back to the main page