Core Techniques and Algorithms in Game Programming2003
Imagine you are working on a space shooter and need to draw a fleet of starships. You have a ship model and need to render it several times to give the impression of a large fleet. Each individual ship is just an instance of the master ship, rotated and translated to a point in space. As your first option, you could try to individually transform each vertex in the ship. You would need to implement rotation and translation matrices, and then manually apply them to the geometry. Although this task would not be very complex, it is hard to get the code right, especially if you plan to do transform chains. Luckily, OpenGL does this for you, so you can concentrate on other, more rewarding tasks. Transforms in OpenGL are really simple to use yet extremely powerful and flexible. The key concept you must understand is that during the geometry processing, OpenGL applies several matrices to the incoming data. Data is initially transformed by a first matrix, which specifies model coordinates. This is called the modelview matrix and is used to perform object-level transforms such as in the previous starship example. To place each ship in its real location, we will specify a different modelview matrix for each one, so geometry is affected by that change as it crosses the pipeline. A second matrix is used to specify how transformed (that is, affected by the modelview matrix) geometry is mapped to the screen. This is the projection matrix and is used to achieve perspective effects as well as complex camera lens deformations. If we change the projection matrix prior to sending a geometry block to OpenGL, the said geometry will be projected accordingly. The last matrix we can modify is the texture matrix. This matrix is applied to texture coordinates right before rasterization. Thus, by applying a nonstandard texture matrix we can get animated texture effects: rotations, zooms, and so on. Clearly, we need two types of calls. First, we need calls to specify which matrix we want to work with. Second, we will need calls that actually modify the selected matrix. For the matrix selection, the following call is used: glMatrixMode(gluint matrixmode) ; This call receives only one parameter, which can be any of the following symbolic constants:
Once the right matrix has been selected, we can apply a wide range of transforms. This first call clears the currently selected matrix, so data traversing it remains unchanged: glLoadIdentity(); To achieve this, the current matrix is initialized with an identity matrix (a diagonal of ones with all other positions initialized to zero). Remember that this call does not multiply the values of the current matrix but simply overwrites them. The following call multiplies the currently selected matrix with a rotation matrix: glRotatef(GLdouble angle, GLdouble axisx, GLdouble axisy, GLdouble axisz); The rotation matrix is specified by a rotation angle (in degrees) and an axis. You can see the geometric interpretation of this call in Figure B.3. Figure B.3. How rotations work in OpenGL.
Specifying rotation with an angle and an axis might seem unintuitive. But once you master glRotated, you will discover its flexibility. If you need to perform simpler axis-oriented rotations, you can always simplify the call as follows: glRotated (xangle, 1, 0, 0); glRotated (yangle, 0, 1, 0); glRotated (zangle, 0, 0, 1); Translations are performed in a very similar way, using the glTranslatef call: glTranslated(GLdouble dispx, GLdouble dispy, GLdouble dispz); Here we only need to specify the displacement we want to apply, and the currently selected matrix is multiplied by this new translation matrix. Then, we can apply a scaling matrix with the following call: glScaled(GLdouble scalex, GLdouble scaley, GLdouble scalez); Notice how the scaling syntax allows both homogeneous and nonhomogeneous transforms. Also, keep in mind that you can actually scale by 0 (thus, eliminating one coordinate set). This is sometimes used in shadow algorithms: flattening the geometry to the ground level. However, avoid scaling by zero in all directions because your geometry would totally collapse. Most transforms are specified using translations, rotations, or scales. But sometimes we will need extra flexibility. For example, imagine that you want to implement a shearing matrix or, even worse, that you need to manually apply a matrix you have computed. For these situations, OpenGL offers the following call: glMultMatrixd (GLdouble *m); This function passes a pointer to 16 consecutive values that are used as the elements of a 4x4 column-major matrix. This new matrix will multiply the currently selected matrix, so you can implement any transform that can be expressed as a matrix. Remember that matrices in OpenGL are represented in homogeneous coordinates. Concatenation of Transforms
OpenGL transforms can be freely concatenated, so you can apply several transforms to the same geometry. This can be achieved by typing a series of transform calls. There are only two potential issues that you should be aware of. First, OpenGL post-multiplies matrices. What this actually means for programmers is that you have to type transforms in reverse order. For example, if you need to place a starship in a 3D game and the ship has both a position and a yaw angle, you would do this:
In OpenGL, you must reverse these two matrices, so the code sequence would be glTranslatef(...); glRotatef(...); This reverse ordering applies to all transforms that multiply the current matrix (glTranslate, glRotate, glScale, glMultMatrix). Thus, glLoadIdentity is not affected by this rule and should always be the first call in a transform chain. Second, transforms are applied on a per-object basis and must thus be specified outside glBegin-glEnd sequences. Transforms placed inside geometry sections will probably be ignored or yield incorrect results. Hierarchical Transforms
OpenGL provides functions to implement hierarchical transforms. For example, think of a tank with a turret and a cannon on top of it. The turret inherits the transforms for the tank body, and the cannon inherits both the transforms for the body and the transforms for the turret. These complex systems require chaining several transforms and being able to easily select which transforms are applied and where. OpenGL implements hierarchical transforms through the glPushMatrix/glPopMatrix pair, whose use resembles the scope brackets in C code. PushMatrix opens a new scope or transform node, and PopMatrix closes it. A more involved explanation would be that PushMatrix makes a duplicate of the current matrix, so we can modify it freely while keeping a clean copy. PopMatrix eliminates the matrix at the top of the stack (usually a matrix we previously pushed in), so we can work with the matrix below that one. As an example, here is the source for the tank renderer. I assume that the tank body must be translated and rotated with a yaw angle, that the turret only has a yaw angle, and that the cannon has a pitch: glPushMatrix(); glTranslatef(tankpos.x, tankpos.y, tankpos.z); glRotatef(tankyaw,0,1,0); // here I paint the tank (...) glPushMatrix(); glTranslatef(turretpos.x, turretpos.y, turretpos.z); glRotatef(turretyaw,0,1,0); // here I paint the turret (...) glPushMatrix(); glTranslatef(cannonpos.x, cannonpos.y, cannonpos.z); glRotatef(cannonpitch,1,0,0); // here I paint the cannon (...) glPopMatrix(); glPopMatrix(); glPopMatrix(); The PushMatrix/PopMatrix paradigm offers all the flexibility you need to create hierarchical models. All you have to do is remember two very basic rules of thumb: Never place these constructs inside Begin/End sections, and always make sure you pop every matrix you push or, in other words, that you have exactly the same number of pushes and pops. |