2D Transformations: Fun with the Model-View Matrix

All we've done so far is define static geometries in the form of triangle lists. There was nothing moving, rotating, or scaling. Also, even when the vertex data itself stayed the same (that is, the width and height of a rectangle composed of two triangles along with texture coordinates and color), we still had to duplicate the vertices if we wanted to draw the same rectangle at different places. Look back at Listing 7–11 and ignore the color attributes of the vertices for now. The two rectangles only differ in their y-coordinates by 200 units. If we had a way to move those vertices without actually changing their values, we could get away with defining the rectangle of Bob only once and simply draw him at different locations— and that's exactly how we can use the model-view matrix.

World and Model Space

To understand how this works, we literally have to think outside of our little orthographic view frustum box. Our view frustum is in a special coordinate system called the world space. This is the space where all our vertices are going to end up eventually.

Up until now, we have specified all vertex positions in absolute coordinates relative to the origin of this world space (compare with Figure 7–5). What we really want is to make the definition of the positions of our vertices independent from this world space coordinate system. We can achieve this by giving each of our models (for example, Bob's rectangle, a spaceship, and so forth.) its own coordinate system.

This is what we usually call model space, the coordinate system within which we define the positions of our model's vertices. Figure 7–19 illustrates this concept in 2D, and the same rules apply to 3D as well (just add a z-axis).

images

Figure 7–19. Defining our model in model space, re-using it, and rendering it at different locations in the world space

In Figure 7–19, we have a single model defined via a Vertices instance—for example, like this:

Vertices vertices = new Vertices(glGraphics, 4, 12, false, false);
vertices.setVertices(new float[] { -50, -50,
                                    50, -50,
                                    50,  50,
                                   -50,  50 }, 0, 8);
vertices.setIndices(new short[] {0, 1, 2, 2, 3, 0}, 0, 6);

For our discussion, we just leave out any vertex colors or texture coordinates. Now, when we render this model without any further modifications, it will be placed around the origin in the world space in our final image. If we want to render it at a different position—say, its center being at (200,300) in world space—we could redefine the vertex positions like this:

vertices.setVertices(new float[] { -50 + 200, -50 + 300,
                                    50 + 200, -50 + 300,
                                    50 + 200,  50 + 300,
                                   -50 + 200,  50 + 300 }, 0, 8);

On the next call to vertices.draw(), the model would be rendered with its center at (200,300), but this is a tad bit tedious isn't it?

Matrices Again

Remember when we briefly talked about matrices? We discussed how matrices can encode transformations, such as translations (moving stuff around), rotations, and scaling. The projection matrix we use to project our vertices onto the projection plane encodes a special type of transformation: a projection.

Matrices are the key to solving our previous problem more elegantly. Instead of manually moving our vertex positions around by redefining them, we simply set a matrix that encodes a translation. Since the projection matrix of OpenGL ES is already occupied by the orthogonal graphics projection matrix we specified via glOrthof(), we use a different OpenGL ES matrix: the model-view matrix. Here's how we could render our model with its origin moved to a specific location in eye/world space:

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslatef(200, 300, 0);
vertices.draw(GL10.GL_TRIANGLES, 0, 6);

We first have to tell OpenGL ES which matrix we want to manipulate. In our case, that's the model-view matrix, which is specified by the constant GL10.GL_MODELVIEW. Next, we make sure that the model-view matrix is set to an identity matrix. Basically, we just overwrite anything that was in there already—we sort of clear the matrix. The next call is where the magic happens.

The method glTranslatef() takes three arguments: the translation on the x-, y-, and z-axes. Since we want the origin of our model to be placed at (200,300) in eye/world space, we specify a translation by 200 units on the x-axis and a translation by 300 units on the y-axis. As we are working in 2D, we simply ignore the z-axis and set the translation component to zero. We didn't specify a z-coordinate for our vertices, so these will default to zero. Adding zero to zero equals zero, so our vertices will stay in the x-y plane.

From this point on, the model-view matrix of OpenGL ES encodes a translation by (200,300,0), which will be applied to all vertices that pass through the OpenGL ES pipeline. If you refer back to Figure 7–4, you'll see that OpenGL ES will multiply each vertex with the model-view matrix first and then apply the projection matrix. Until now, the model-view matrix was set to an identity matrix (the default of OpenGL ES); therefore, it did not have an effect on our vertices. Our little glTranslatef() call changes this, and it will move all vertices first before they are projected.

This is, of course, done on the fly; the values in our Vertices instance do not change at all. We would have noticed any permanent change to our Vertices instance, because, by that logic, the projection matrix would have changed it already.

An Initial Example Using Translation

What can we use translation for? Say we want to render 100 Bobs at different positions in our world. Additionally, we want them to move around on the screen and change direction each time they hit an edge of the screen (or rather, a plane of our parallel projection view frustum, which coincides with the extents of our screen). We could do this by having one large Vertices instance that holds the vertices of the 100 rectangles—one for each Bob—and re-calculate the vertex positions of each frame. The easier method is to have one small Vertices instance that only holds a single rectangle (the model of Bob) and reuse it by translating it with the model-view matrix on the fly. Let's define our Bob model.

Vertices bobModel = new Vertices(glGraphics, 4, 12, false, true);
bobModel.setVertices(new float[] { -16, -16, 0, 1,  
                                    16, -16, 1, 1,
                                    16,  16, 1, 0,
                                   -16,  16, 0, 0, }, 0, 8);
bobModel.setIndices(new short[] {0, 1, 2, 2, 3, 0}, 0, 6);

So, each Bob is 32×32 units in size. We also texture map him—we'll use bobrgb888.png to see the extents of each Bob.

Bob Becomes a Class

Let's define a simple Bob class. It will be responsible for holding a Bob's position and advancing his position in his current direction based on the delta time, just like we advanced Mr. Nom (with the difference being that we don't move in a grid anymore). The update() method will also make sure that Bob doesn't escape our view volume bounds. Listing 7–12 shows the Bob class.

Listing 7–12. Bob.java

package com.badlogic.androidgames.glbasics;

import java.util.Random;

class Bob {
    static final Random rand = new Random();
    public float x, y;
    float dirX, dirY;

    public Bob() {
        x = rand.nextFloat() * 320;
        y = rand.nextFloat() * 480;
        dirX = 50;
        dirY = 50;
    }

    public void update(float deltaTime) {
        x = x + dirX * deltaTime;
        y = y + dirY * deltaTime;

        if (x < 0) {
            dirX = -dirX;
            x = 0;
        }

        if (x > 320) {
            dirX = -dirX;
            x = 320;
        }

        if (y < 0) {
            dirY = -dirY;
            y = 0;
        }

        if (y > 480) {
            dirY = -dirY;
            y = 480;
        }
    }
}

Each Bob will place himself at a random location in the world when we construct him. All the Bobs will initially move in the same direction: 50 units to the right and 50 units upward per second (as we multiply by the deltaTime). In the update() method, we simply advance Bob in his current direction in a time-based manner and then check if he left the view frustum bounds. If that's the case, we invert his direction and make sure he's still in the view frustum.

Now let's assume we are instantiating 100 Bobs, like this:

Bob[] bobs = new Bob[100];
for(int i = 0; i < 100; i++) {
    bobs[i] = new Bob();
}

To render each of these Bobs, we'd do something like this (assuming we've already cleared the screen, set the projection matrix, and bound the texture):

gl.glMatrixMode(GL10.GL_MODELVIEW);
for(int i = 0; i < 100; i++) {
    bob.update(deltaTime);
    gl.glLoadIdentity();
    gl.glTranslatef(bobs[i].x, bobs[i].y, 0);
    bobModel.render(GL10.GL_TRIANGLES, 0, 6);
}

That is pretty sweet, isn't it? For each Bob, we call his update() method, which will advance his position and make sure he stays within the bounds of our little world. Next, we load an identity matrix into the model-view matrix of OpenGL ES so we have a clean slate. We then use the current Bob's x- and y-coordinates in a call to glTranslatef(). When we render the Bob model in the next call, all the vertices will be offset by the current Bob's position—exactly what we wanted.

Putting It Together

Let's make this a full-blown example. Listing 7–13 shows the code.

Listing 7–13. BobTest.java; 100 Moving Bobs!

package com.badlogic.androidgames.glbasics;

import javax.microedition.khronos.opengles.GL10;

import com.badlogic.androidgames.framework.Game;
import com.badlogic.androidgames.framework.Screen;
import com.badlogic.androidgames.framework.gl.FPSCounter;
import com.badlogic.androidgames.framework.gl.Texture;
import com.badlogic.androidgames.framework.gl.Vertices;
import com.badlogic.androidgames.framework.impl.GLGame;
import com.badlogic.androidgames.framework.impl.GLGraphics;

public class BobTest extends GLGame {

    @Override
    public Screen getStartScreen() {
        return new BobScreen(this);
    }
    
    class BobScreen extends Screen {
        static final int NUM_BOBS = 100;
        GLGraphics glGraphics;
        Texture bobTexture;
        Vertices bobModel;
        Bob[] bobs;

Our BobScreen class holds a Texture (loaded from bobrbg888.png), a Vertices instance holding the model of Bob (a simple textured rectangle), and an array of Bob instances. We also define a little constant named NUM_BOBS so that we can modify the number of Bobs we want to have on the screen.

        public BobScreen(Game game) {
            super(game);
            glGraphics = ((GLGame)game).getGLGraphics();
            
            bobTexture = new Texture((GLGame)game, "bobrgb888.png");
            
            bobModel = new Vertices(glGraphics, 4, 12, false, true);
            bobModel.setVertices(new float[] { -16, -16, 0, 1,  
                                                16, -16, 1, 1,
                                                16,  16, 1, 0,
                                               -16,  16, 0, 0, }, 0, 16);
            bobModel.setIndices(new short[] {0, 1, 2, 2, 3, 0}, 0, 6);

            
            bobs = new Bob[100];
            for(int i = 0; i < 100; i++) {
                bobs[i] = new Bob();
            }            
        }

The constructor just loads the texture, creates the model, and instantiates NUM_BOBS Bob instances.

@Override
        public void update(float deltaTime) {         
            game.getInput().getTouchEvents();
            game.getInput().getKeyEvents();
            
            for(int i = 0; i < NUM_BOBS; i++) {
                bobs[i].update(deltaTime);
            }
        }

The update() method is where we let our Bobs update themselves. We also make sure our input event buffers are emptied.

@Override
        public void present(float deltaTime) {
            GL10 gl = glGraphics.getGL();
            gl.glClearColor(1,0,0,1);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
            gl.glMatrixMode(GL10.GL_PROJECTION);
            gl.glLoadIdentity();
            gl.glOrthof(0, 320, 0, 480, 1, -1);
            
            gl.glEnable(GL10.GL_TEXTURE_2D);
            bobTexture.bind();
            
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            for(int i = 0; i < NUM_BOBS; i++) {
                gl.glLoadIdentity();
                gl.glTranslatef(bobs[i].x, bobs[i].y, 0);
                gl.glRotatef(45, 0, 0, 1);
                gl.glScalef(2, 0.5f, 0);
                bobModel.draw(GL10.GL_TRIANGLES, 0, 6);
            }            
        }

In the render() method, we clear the screen, set the projection matrix, enable texturing, and bind the texture of Bob. The last couple of lines are responsible for actually rendering each Bob instance. Since OpenGL ES remembers its states, we have to set the active matrix only once; in this case, we are going to modify the model-view matrix in the rest of the code. We then loop through all the Bobs, set the model-view matrix to a translation matrix based on the position of the current Bob, and render the model, which will be translated by the model view-matrix automatically.

        @Override
        public void pause() {            
        }

        @Override
        public void resume() {            
        }

        @Override
        public void dispose() {            
        }
    }
}

That's it. Best of all, we employed the MVC pattern we used in Mr. Nom again. It really lends itself well to game programming. The logical side of Bob is completely decoupled from his appearance, which is nice, as we can easily replace his appearance with something more complex. Figure 7–20 shows the output of our little program after running it for a few seconds.

images

Figure 7–20. That's a lot of Bobs!

That's not the end of all of our fun with transformations yet. If you remember what we said a couple of pages ago, you'll know what's coming: rotations and scaling.

More Transformations

Besides the glTranslatef() method, OpenGL ES also offers us two methods for transformations: glRotatef() and glScalef().

Rotation

Here's the signature of glRotatef():

GL10.glRotatef(float angle, float axisX, float axisY, float axisZ);

The first parameter is the angle in degrees by which we want to rotate our vertices. What do the rest of the parameters mean?

When we rotate something, we rotate it around an axis. What is an axis? Well, we already know three axes: the x-axis, the y-axis, and the z-axis. We can express these three axes as so-called vectors. The positive x-axis would be described as (1,0,0), the positive y-axis would be (0,1,0), and the positive z-axis would be (0,0,1). As you can see, a vector actually encodes a direction; in our case, in 3D space. Bob's direction is also a vector, but in 2D space. Vectors can also encode positions, like Bob's position in 2D space.

To define the axis around which we want to rotate the model of Bob, we need to go back to 3D space. Figure 7–21 shows the model of Bob (with a texture applied for orientation), as defined in the previous code in 3D space.

images

Figure 7–21. Bob in 3D

Since we haven't defined z-coordinates for Bob's vertices, he is embedded in the x-y plane of our 3D space (which is actually the model space, remember?). If we want to rotate Bob, we can do so around any axis we can think of: the x-, y-, or z-axis, or even a totally crazy axis like (0.75,0.75,0.75). However, for our 2D graphics programming needs, it makes sense to rotate Bob in the x-y plane; hence, we'll use the positive z-axis as our rotation axis, which can be defined as (0,0,1). The rotation will be counterclockwise around the z-axis. A call to glRotatef(), like this would cause the vertices of Bob's model to be rotated as shown in Figure 7–22.

gl.glRotatef(45, 0, 0, 1);
images

Figure 7–22. Bob, rotated around the z-axis by 45 degrees

Scaling

We can also scale Bob's model with glScalef(), like this:

glScalef(2, 0.5f, 1);

Given Bob's original model pose, this would result in the new orientation depicted in Figure 7–23.

images

Figure 7–23. Bob, scaled by a factor of 2 on the x-axis and a factor of 0.5 on the y-axis . . . ouch.

Combining Transformations

Now, we also discussed that we can combine the effect of multiple matrices by multiplying them together to form a new matrix. All the methods—glTranslatef(), glScalef(), glRotatef(), and glOrthof()—do just that. They multiply the current active matrix by the temporary matrix they create internally based on the parameters we pass to them. So, let's combine the rotation and scaling of Bob.

gl.glRotatef(45, 0, 0, 1);
gl.glScalef(2, 0.5f, 1);

This would make Bob's model look like Figure 7–24 (remember, we are still in model space).

images

Figure 7–24. Bob, first scaled and then rotated (still not looking happy)

What would happen if we applied the transformations the other way around?

gl.glScalef(2, 0.5, 0);
gl.glRotatef(45, 0, 0, 1)

Figure 7–25 gives you the answer.

images

Figure 7–25. Bob, first rotated and then scaled

Wow, this is not the Bob we used to know. What happened here? If you look at the code snippets, you'd actually expect Figure 7–24 to look like Figure 7–25, and Figure 7–25 to look like Figure 7–24. In the first snippet, we apply the rotation first and then scale Bob, right?

Wrong. The way OpenGL ES multiplies matrices with each other dictates the order in which the transformations the matrices encode are applied to a model. The last matrix with which we multiply the currently active matrix will be the first that gets applied to the vertices. So if we want to scale, rotate, and translate Bob in that exact order, we have to call the methods like this:

glTranslatef(bobs[i].x, bobs[i].y, 0);
glRotatef(45, 0, 0, 1);
glScalef(2, 0.5f, 1);

If we changed the loop in our BobScreen.present() method to the following code:

gl.glMatrixMode(GL10.GL_MODELVIEW);
for(int i = 0; i <NUM_BOBS; i++) {
    gl.glLoadIdentity();
    gl.glTranslatef(bobs[i].x, bobs[i].y, 0);
    gl.glRotatef(45, 0, 0, 1);
    gl.glScalef(2, 0.5f, 0);
    bobModel.draw(GL10.GL_TRIANGLES, 0, 6);
}

The output would look like Figure 7–25.

images

Figure 7–26. A hundred Bobs scaled, rotated, and translated (in that order) to their positions in world space

It's easy to mix up the order of these matrix operations when you first start out with OpenGL on the desktop. To remember how to do it correctly, use the mnemonic device called the LASFIA principle: last specified, first applied (Yeah, this mnemonic isn't all that great huh?).

The easiest way to get comfortable with model-view transformations is to use them heavily. We suggest you take the BobTest.java source file, modify the inner loop for some time, and observe the effects. Note that you can specify as many transformations as you want for rendering each model. Add more rotations, translations, and scaling. Go crazy.

With this last example, we basically know everything we need to know about OpenGL ES to write 2D games . . . or do we?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.2.23