Perspective
For 2D scenes, the function gluOrtho2D helped setup the coordinate system. Now, we need a function which can setup a 3D coordinate system; we need the function gluPerspective. Documentation for it can be found at http://www.opengl.org/sdk/docs/man2/xhtml/gluPerspective.xml, but basically it takes 4 arguments: field of view, aspect ratio, near distance, and far distance.
The field of view is the amount of the world that the camera can see at any moment. It is an angle measured in degrees. The aspect ratio is the window width divided by its height. This is also accounted for in previous 2D examples, but here we only need to provide one value. The near distance is the closest distance at which shapes are drawn, and the far distance is the maximum distance for drawing shapes. These are both adjustable, but its best to keep them above zero and not ridiculously far apart.
This function will create a 3D camera at the position (0, 0, 0), so in order to see shapes drawn, we must move them in front of the camera. When using gluPerspective, negative values of Z are in front of the camera (into the monitor) while positive Z is behind the camera (out of the monitor). The following example will draw a plane and place it in front of the camera:
First, three float variables are declared which are used later for glTranslatef. The shiftZ is negative in order to move the shape in front of the camera. These variables will form the position that the shape is drawn around, so the point (0, -1, -3) is at the center of this square. Also, the point (-1, -1, -4) is at the far, left corner since the square's point (-1, 0, -1) will be shifted by -1 in the Y direction and -3 in the Z.
The function gluPerspective is given the field of view 70. This is a personal preference, so feel free to change this value and observe the result. The given aspect ratio (1.333) is accurate for a 640 by 480 window, so if you use different window dimensions, just divide the width by the height.
The near and far values are also personal preferences, but if the near value is changed to 3, then the closer half of the plane is no longer visible. This is because nothing closer than the near distance is drawn, and this is called clipping. Alternatively, if the far value is set to 3, then the farther half of the plane is removed. This is because it is farther than the given far distance.
Rotations
Honestly, the previous example is just a roundabout way of drawing a trapezoid. So, one way to showcase the 3D aspect of it would be to include some interactive rotation. The following example will rotate the plane based on SDL mouse position:
/*thanks to tohtml.com for syntax highlighting*/ #include <SDL/SDL.h> #include <SDL/SDL_opengl.h> #include <stdio.h> SDL_Surface* screen; SDL_Event input; int loop = 1; /*SDL mouse coordinates*/ int mouseX, mouseY; /*rotation angles*/ float angleX = 0.0, angleY = 0.0; int main(int argc, char** argv) { SDL_Init(SDL_INIT_EVERYTHING); /*initialize SDL*/ screen = SDL_SetVideoMode(640, 480, 32, SDL_OPENGL); /*set clear color to dark teal*/ glClearColor(0, 0.5, 0.5, 1.0); glClear(GL_COLOR_BUFFER_BIT); /*create 3D camera*/ glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(70, 1.333, 1, 100); glMatrixMode(GL_MODELVIEW); /*for transforming shapes*/ while (loop) { /*glClear(GL_COLOR_BUFFER_BIT); /*clear screen*/ while (SDL_PollEvent(&input)) { if (input.type == SDL_QUIT) loop = 0; if (input.type == SDL_KEYDOWN) { /*exit with escape key*/ if (input.key.keysym.sym == SDLK_ESCAPE) loop = 0; } } SDL_GetMouseState(&mouseX, &mouseY); /*move shape directly in front of camera*/ glLoadIdentity(); glTranslatef(0, 0, -3); /*rotate about Y axis based on mouseX*/ angleY = 360.0 * (float)mouseX / 640.0; glRotatef(angleY, 0, 1, 0); /*rotate about X axis based on mouseY*/ angleX = 180.0 * (float)mouseY / 480.0; glRotatef(angleX, 1, 0, 0); /*printf rotations*/ printf("X-axis: %g degrees, Y-axis: %g degrees\n", angleX, angleY); /*draw quadrilateral*/ glBegin(GL_QUADS); /*multi-colored square*/ glColor3f(1, 1, 0); glVertex3f(-1, 0, -1); glColor3f(1, 0, 0); glVertex3f(-1, 0, 1); glColor3f(0, 1, 0); glVertex3f(1, 0, 1); glColor3f(0, 0, 1); glVertex3f(1, 0, -1); glEnd(); SDL_GL_SwapBuffers(); SDL_Delay(20); /*wait 20ms*/ } /*perform final commands then exit*/ SDL_Quit(); /*close SDL*/ fflush(stdout); /*update stdout*/ return 0; } | ||
prompt/stdout: X-axis: 16.125 degrees, Y-axis: 73.6875 degrees X-axis: 25.5 degrees, Y-axis: 102.375 degrees X-axis: 27 degrees, Y-axis: 106.875 degrees X-axis: 28.125 degrees, Y-axis: 110.813 degrees X-axis: 28.875 degrees, Y-axis: 113.625 degrees X-axis: 30 degrees, Y-axis: 117 degrees X-axis: 30.75 degrees, Y-axis: 120.375 degrees X-axis: 31.5 degrees, Y-axis: 122.625 degrees X-axis: 32.625 degrees, Y-axis: 126.563 degrees X-axis: 33 degrees, Y-axis: 128.25 degrees X-axis: 34.125 degrees, Y-axis: 131.625 degrees X-axis: 34.875 degrees, Y-axis: 135 degrees X-axis: 36.375 degrees, Y-axis: 139.5 degrees X-axis: 37.875 degrees, Y-axis: 144 degrees X-axis: 38.625 degrees, Y-axis: 146.813 degrees X-axis: 40.125 degrees, Y-axis: 150.75 degrees X-axis: 40.875 degrees, Y-axis: 152.438 degrees X-axis: 42.375 degrees, Y-axis: 156.938 degrees X-axis: 43.5 degrees, Y-axis: 160.875 degrees X-axis: 45 degrees, Y-axis: 165.375 degrees X-axis: 45.375 degrees, Y-axis: 167.625 degrees X-axis: 46.5 degrees, Y-axis: 171 degrees X-axis: 46.875 degrees, Y-axis: 173.813 degrees X-axis: 47.625 degrees, Y-axis: 177.188 degrees X-axis: 48 degrees, Y-axis: 180 degrees X-axis: 48.75 degrees, Y-axis: 184.5 degrees X-axis: 49.125 degrees, Y-axis: 188.438 degrees X-axis: 49.125 degrees, Y-axis: 190.125 degrees X-axis: 51 degrees, Y-axis: 197.438 degrees X-axis: 51.375 degrees, Y-axis: 201.375 degrees X-axis: 52.125 degrees, Y-axis: 203.063 degrees X-axis: 52.5 degrees, Y-axis: 208.688 degrees X-axis: 52.875 degrees, Y-axis: 212.063 degrees X-axis: 53.25 degrees, Y-axis: 215.438 degrees X-axis: 53.625 degrees, Y-axis: 216.563 degrees X-axis: 53.625 degrees, Y-axis: 218.25 degrees X-axis: 54 degrees, Y-axis: 219.375 degrees X-axis: 54 degrees, Y-axis: 222.188 degrees X-axis: 54.375 degrees, Y-axis: 224.438 degrees X-axis: 54.75 degrees, Y-axis: 226.688 degrees X-axis: 54.75 degrees, Y-axis: 226.688 degrees X-axis: 54.75 degrees, Y-axis: 227.25 degrees X-axis: 54.75 degrees, Y-axis: 228.938 degrees X-axis: 54.75 degrees, Y-axis: 230.625 degrees X-axis: 54.75 degrees, Y-axis: 231.75 degrees X-axis: 54.75 degrees, Y-axis: 232.313 degrees X-axis: 54.75 degrees, Y-axis: 232.313 degrees X-axis: 54.75 degrees, Y-axis: 232.313 degrees X-axis: 54.75 degrees, Y-axis: 232.313 degrees X-axis: 54.75 degrees, Y-axis: 232.313 degrees X-axis: 54.75 degrees, Y-axis: 232.875 degrees X-axis: 54.75 degrees, Y-axis: 232.875 degrees X-axis: 54.75 degrees, Y-axis: 232.875 degrees X-axis: 54.75 degrees, Y-axis: 232.875 degrees X-axis: 54.75 degrees, Y-axis: 232.875 degrees X-axis: 54.75 degrees, Y-axis: 233.438 degrees X-axis: 54.75 degrees, Y-axis: 233.438 degrees X-axis: 54.75 degrees, Y-axis: 233.438 degrees X-axis: 55.125 degrees, Y-axis: 233.438 degrees X-axis: 55.125 degrees, Y-axis: 233.438 degrees X-axis: 55.125 degrees, Y-axis: 233.438 degrees X-axis: 55.125 degrees, Y-axis: 233.438 degrees X-axis: 55.875 degrees, Y-axis: 233.438 degrees X-axis: 55.875 degrees, Y-axis: 233.438 degrees X-axis: 55.875 degrees, Y-axis: 233.438 degrees X-axis: 55.875 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees X-axis: 56.25 degrees, Y-axis: 233.438 degrees | ||
output inside window:
|
Here glClear has been turned off again, so if you don't want the after images seen in the picture above, be sure to uncomment the glClear line. The picture above is based on me moving the mouse toward the yellow corner until they overlap. Notice that the example exits if the escape key is pressed; this was to stop the printf statements without having to click the upper-right close button (which still works).
SDL_GetMouseState is used to get the mouse coordinates and save them to the mouseX and mouseY variables. For every frame, these coordinates are used to calculate the angleX and angleY values. Notice that mouseX and mouseY are divided by the window width and height respectively. These will give us values between 0 and 1 which will give us angles between 0 and 360 degrees or 0 and 180 degrees when multiplied.
For this example, the shape is first rotated around the X axis, then the Y axis, and finally translated into position. Remember that OpenGL performs transformations opposite the order we write them. If you were to switch the order of the X and Y rotations, the result would be changed. It is hard to explain, but the "feel" of the rotation is different, so I highly recommend running this example and swapping the order of rotations to try it out.
When working with rotations, be cautious of gimbal lock. This example rotates about the X and Y axes, and the order of the rotations affects the result. I believe the axis of the second rotation operation is rotated by the first, so if I rotate 90 degrees on the Y axis, any following X axis rotation would actually be a Z axis rotation. For more information about gimbal lock, see http://en.wikipedia.org/wiki/Gimbal_lock.
Moving the Camera
So far, shapes have been placed in front of the point (0, 0, 0) so that they can be visible. However, we can also have a "moving" camera by applying the same set of transformations to every shape. For a large, 3D scene, every shape must be rotated and translated in terms of the camera's position and angles. While we could use glRotatef and glTranslatef, there are situations where the function gluLookAt can help us more.
This function takes 9 arguments, which are grouped into sets of three. The first three are the x, y, and z of the camera's position. The second three are the x, y, and z of the point which the camera is focused on. The last three make up a three-dimensional direction for the camera, the up vector. This is usually (0, 1, 0), but can be any other direction if you wanted to tilt the camera sideways or upside down.
We still need a way to save and restore the current matrix. If we setup a matrix which puts everything around the camera, we still need to be able to scale, rotate and translate individual shapes without them affecting each other. The matrix created from gluLookAt can be saved with "glPushMatrix();" and can become the current modelview matrix again by using "glPopMatrix();".
The following example will use gluLookAt to move the camera and draw rectangles at (0, 0, 0) and (-5, 0, 0):
/*thanks to tohtml.com for syntax highlighting*/ #include <SDL/SDL.h> #include <SDL/SDL_opengl.h> #include <stdio.h> SDL_Surface* screen; SDL_Event input; int loop = 1; /*camera position*/ float camX = 2, camY = 2, camZ = 2; int main(int argc, char** argv) { SDL_Init(SDL_INIT_EVERYTHING); /*initialize SDL*/ screen = SDL_SetVideoMode(640, 480, 32, SDL_OPENGL); /*print camera position*/ printf("camera position = (%g, %g, %g)\n", camX, camY, camZ); /*set clear color to dark teal*/ glClearColor(0, 0.5, 0.5, 1.0); glClear(GL_COLOR_BUFFER_BIT); /*create 3D camera*/ glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(70, 1.333, 1, 100); glMatrixMode(GL_MODELVIEW); /*for transforming shapes*/ while (loop) { glClear(GL_COLOR_BUFFER_BIT); /*clear screen*/ while (SDL_PollEvent(&input)) { if (input.type == SDL_QUIT) loop = 0; } /*move camera away from shape, rotate to see shape*/ glLoadIdentity(); gluLookAt(camX, camY, camZ, 0, 0, 0, 0, 1, 0); /*transformations which work in place of gluLookAt*/ /*glRotatef(35.26, 1, 0, 0); /*x-axis rotation*/ /*glRotatef(-45, 0, 1, 0); /*y-axis rotation*/ /*glTranslatef(-2, -2, -2); /*move to position*/ /*scale and translate the shape*/ glPushMatrix(); /*save current modelview*/ glTranslatef(-5, 0, 0); glScalef(2, 1, 2); /*draw quadrilateral*/ glBegin(GL_QUADS); /*light teal square*/ glColor3ub(0, 255, 255); glVertex3f(-1, 0, -1); glVertex3f(-1, 0, 1); glVertex3f(1, 0, 1); glVertex3f(1, 0, -1); glEnd(); glPopMatrix(); /*restore previous modelview*/ /*scale the shape*/ glPushMatrix(); /*save current modelview*/ glScalef(2, 1, 1); /*draw quadrilateral*/ glBegin(GL_QUADS); /*multi-colored square*/ glColor3f(1, 1, 0); glVertex3f(-1, 0, -1); glColor3f(1, 0, 0); glVertex3f(-1, 0, 1); glColor3f(0, 1, 0); glVertex3f(1, 0, 1); glColor3f(0, 0, 1); glVertex3f(1, 0, -1); glEnd(); glPopMatrix(); /*restore previous modelview*/ SDL_GL_SwapBuffers(); SDL_Delay(20); /*wait 20ms*/ } /*perform final commands then exit*/ SDL_Quit(); /*close SDL*/ fflush(stdout); /*update stdout*/ return 0; } | ||
prompt/stdout: camera position = (2, 2, 2) | ||
output inside window:
|
Every frame, the modelview matrix is set to the identity matrix and changed using the gluLookAt function. Then, each shape is transformed using the result of gluLookAt and some unique transformations (scaling and translating in this case). The unique transformations are applied after glPushMatrix and are removed by calling glPopMatrix.
In this example, gluLookAt is told that the camera is at position (2, 2, 2) and looking at point (0, 0, 0). This same transformation can be accomplished with two glRotatef calls and a glTranslatef as seen by the commented lines. To test this, you can comment the gluLookAt line and un-comment the three OpenGL functions below it. Note that these three lines translate then rotate (and don't scale), reversing the usual order of transformations.
After the camera transformations are in the modelview matrix, we can still apply transformations to individual shapes. The teal rectangle is scaled then translated in order to have a 4x4 plane centered at (-5, 0, 0). The multi-colored rectangle is scaled to be 4x2 and remains centered at (0, 0, 0). These individual transformations can be applied after the camera transformations.
However, we don't want to move the multi-colored rectangle to (-5, 0, 0) or scale it beyond 4x2. That means we need a way to remove the "glTranslatef(-5, 0, 0);" and "glScalef(2, 1, 2);" calls used for the teal rectangle. We could apply "glScalef(0.5, 1, 0.5);" and glTranslatef(+5, 0, 0);", the opposite transformations in the opposite order.
Since OpenGL provides the functions glPushMatrix and glPopMatrix, we can avoid calculating the reverse transformation needed to restore the result of gluLookAt. glPushMatrix can store the current modelview matrix and glPopMatrix will remove the current modelview matrix and replace it with what was saved earlier. Without the first call to glPopMatrix, the multi-colored rectangle would be transformed using gluLookAt, glTranslatef, and every call to glScalef.
To wrap up this post, here are better explanations of the way matrices help transform shapes: http://robertokoci.com/world-view-projection-matrix-unveiled/ and http://3dgep.com/?p=1700. For OpenGL, there are separate projection and modelview matrices, but the modelview can also be split into a world matrix (individual transformations) and a view matrix (camera transformations). Also, here is the documentation for gluLookAt: http://www.opengl.org/sdk/docs/man2/xhtml/gluLookAt.xml. Finally, another tutorial page can be found at http://www.swiftless.com/tutorials/opengl/rotation.html, and it is written in C++ using GLUT rather than C using SDL.
Thank you for reading, and future posts should discuss lighting and depth testing.