Texture Atlas Because Sharing Is Caring

Up until this point we have only ever used a single texture in our programs. What if we not only want to render Bob, but other superheroes or enemies or explosions or coins as well? We could have multiple textures, each holding the image of one object type. But OpenGL ES wouldn't like that much, since we'd need to switch textures for every object type we render (e.g., bind Bob's texture, render Bobs, bind the coin texture, render coins, etc.). We can do better by putting multiple images into a single texture. And that's a texture atlas: a single texture containing multiple images. We only need to bind that texture once, and we can then render any entity types for which there is an image in the atlas. That saves some state change overhead and increases our performance. Figure 8-23 shows such a texture atlas.

Figure 8-23. A texture atlas o

There are three objects in Figure 8-23: a cannon, a cannonball, and Bob. The grid is not part of the texture; it's only there to illustrate how I usually create my texture atlases.

The texture atlas is 64x64 pixels in size, and each grid is 32x32 pixels. The cannon takes up two cells, the cannonball a little less than one-quarter of a cell, and Bob a single cell. Now, if you look back at how we defined the bounds (and graphical rectangles) of the cannon, cannonball, and targets, you will notice that the relation of their sizes to each other is very similar to what we have in the grid here. The target is 0.5x0.5 meters in our world, and the cannon is 0.2x0.2 meters. In our texture atlas, Bob takes up 32x32 pixels and the cannonball a little under 16x16 pixels. The relationship between the texture atlas and the object sizes in our world should be clear: 32 pixels in the atlas equals 0.5 meters in our world. Now, the cannon was 1 x1 meters in our original example, but we can of course change that. According to our texture atlas, in which the cannon takes up 64x32 pixels, we should let our cannon have a size of 1 x0.5 meters in our world. Wow, that is exceptionally easy isn't it?

So why did I choose 32 pixels to match 1 meter in our world? Remember that textures must have power-of-two widths and heights. Using a power-of-two pixel unit like 32 to map to 0.5 meters in our world is a convenient way for the artist to cope with the restriction on texture sizes. It also makes it easier to get the size relations of different objects in our world right in the pixel art as well.

Note that there's nothing keeping you from using more pixels per world unit. You could choose 64 pixels or 50 pixels to match 0.5 meters in our world just fine. So what's a good pixel-to-meters size, then? That again depends on the screen resolution our game will run at. Let's do some calculations.

Our cannon world is bounded by (0,0) in the bottom-left corner and (9.6,4.8) in the top-left corner. This is mapped to our screen. Let's figure out how many pixels per world unit we have on the screen of a Hero (480x320 pixels in landscape mode):

pixelsPerUnitX = screenWidth / worldWidth = 480 / 9.6 = 50 pixels / meter pixelsPerUnitY = screenHeight / worldHeight = 320 / 6.4 = 50 pixels / meter

Our cannon, which will now take up 1 x0.5 meters in the world, will thus use 50x25 pixels on the screen. We'd use a 64x32-pixel region from our texture, so we'd actually downscale the texture image a little when rendering the cannon. That's totally fine— OpenGL ES will do this automatically for us. Depending on the minification filter we set for the texture, the result will either be crisp and pixelated (GL_NEAREST) or a little smoothed out (GL_LINEAR). If we wanted a pixel-perfect rendering on the Hero, we'd need to scale our texture images a little. We could use a grid size of 25x25 pixels instead of 32x32. However, if we just resized the atlas image (or rather redraw everything by hand), we'd have a 50x50-pixel image—a no-go with OpenGL ES. We'd have to add padding to the left and bottom to obtain a 64x64 image (since OpenGL ES requires power-of-two widths and heights). I'd say we are totally fine with OpenGL ES scaling our texture image down on the Hero.

How's the situation on higher-resolution devices like the Nexus One (800x480 in landscape mode)? Let's perform the calculations for this screen configuration via the following equations:

pixelsPerUnitX = screenWidth / worldWidth = 800 / 9.6 = 83 pixels / meter pixelsPerUnitY = screenHeight / worldHeight = 480 / 6.4 = 75 pixels / meter

We have different pixels per unit on the x- and y-axes because the aspect ratio of our view frustum (9.6 / 6.4 = 1.5) is different from the screen's aspect ratio (800 / 480 = 1.66). We already talked about this in Chapter 4 when we outlined a couple of solutions. Back then we targeted a fixed pixel size and aspect ratio; now we'll adopt that scheme and target a fixed frustum width and height for our example. In the case of the Nexus One, the cannon, the cannonball, and Bob would get scaled up a little and stretched, due to the higher resolution and different aspect ratio. We accept this fact since we want all players to see the same region of our world. Otherwise, players with higher aspect ratios could have the advantage of being able to see more of the world.

So, how do we use such a texture atlas? We just remap our rectangles. Instead of using all of the texture, we just use portions of it. To figure out the texture coordinates of the corners of the images contained in the texture atlas, we can reuse the equations from one of the last examples. Here's a quick refresher:

Here, u and v are the texture coordinates and x and y are the pixel coordinates. Bob's top-left corner in pixel coordinates is at (32,32). If we plug that into the preceding equation, we get (0.5,0.5) as texture coordinates. We can do the same for any other corners we need, and based on this set the correct texture coordinates for the vertices of our rectangles.

+1 0

Post a comment