Based on what we learned about textures last day, we are now going to create a drawable texture object. Today the focus will be on coding. The difference between today and previous days is less of general knowledge, and more of a guide to the implementation of one of the classes in our engine. And unlike Stage, this class will be very close to the final version. Before we begin, I would like to express my best Christmas wishes for all!
Constructing texture
Bring your project My Game into view and create a class named Texture in your project’s main package. Today’s work involves filling in this class. The initial structure of the class will be as follows:
public class Texture { private boolean mipmaps; int resourceId; Texture(int resourceId, boolean mipmaps) { this.resId = resourceId; this.mipmaps = mipmaps; } Texture(int resourceId) { this(resourceId, false); } }
Our constructor has two arguments. The first argument is the resource id of the image (for example R.drawable.blast). The second argument is whether or not we want to generate mip maps. If this argument is not specified (second constructor) we take it as false
. Our constructors simply save these options for later use.
Dimension properties
Our Texture class will be a stand-alone image container that draws itself onto stage. So it should have basic characteristics of an image. Width and height are important properties of an image and should be retrieved readily when required. OpenGL ES does not provide a straightforward way to get the dimensions of a texture. That’s why we should rely on our own code to keep them. We introduce the following fields to our texture:
protected int width, height;
We also provide the interface for external objects to have read-only access to these fields:
public final int getWidth() { return width; } public final int getHeight() { return height; }
When should these values be assigned to? Obviously when they are known; and that’s definitely not at the constructor.
Generating texture identifiers
Before we begin working with a texture, we must generate a texture identifier. The following OpenGL ES call is used to acquire these texture ids (known as texture names in OpenGL jargon):
GLES10.glGenTextures(n, textures, offset)
The first argument is the number of names we require. The second argument is an array of integers. It will be filled in with the generated numbers. The third argument is the offset from the beginning of the array. Since we always want to start from the first element of the array, we set this value to 0. To make things easier, we encapsulate this functionality into a static method in Texture:
private static final int newTextureID() { int[] temp = new int[1]; GLES10.glGenTextures(1, temp, 0); return temp[0]; }
What this method does is to return a single texture identifier to be used for a newly created texture. OpenGL guarantees that an unused texture name is returned each time we call this function. As each of our textures should have one of these integers, we also add a private field to the class to hold it:
protected int textureId = -1;
We set this value to -1 since glGenTextures will never return -1. So, it is a good initial value.
Loading the texture
Next we are going to load the texture. This will be a part of your game’s “loading” process. We have to read the image from the resource file and then load it into the OpenGL ES texture.
Reading from the resource
It is pretty straightforward to load an image from a resource. Android’s Bitmap
class represents a memory bitmap image. There is also a class called BitmapFactory
you can use to create Bitmap objects from various sources. Take a look at the following code snippet:
BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inScaled = false; Bitmap bmp = BitmapFactory.decodeResource(context.getResources(), resourceId, opts);
The decodeResource
method in BitmapFactory is used to create bitmaps from application resources. We should pass the Resources object in which our resource is contained, the resource identifier of the image (e.g. R.drawable.blast), and optionally some options!
In our code we do use this third argument to disable one of BitmapFactory’s default features. BitmapFactory automatically resizes image resources to match the density of loaded images into that of the screen (for example it enlarges images for screens with higher resolution). This will cause troubles in our fixed-dimension platform, which has always 600 units for the smallest of width and height. With inScaled option set to false
, the dimensions of the loaded image will match those in the file.
After the image is decoded from resource, it is the perfect time to also save its dimensions into our private fields:
width = bmp.getWidth(); height = bmp.getHeight();
Creating OpenGL ES texture
Up to here we have loaded the bitmap into a variable called bmp and now we want to make a OpenGL ES texture out of it. This is what we are going to do:
- Generate a new texture id.
- Read the bitmap from resources.
- Load it into the texture.
So, we start with the following:
textureId = newTextureID(); GLES10.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
You are familiar with our newTextureID() method. So, the first line saves the new texture id into textureId. The second line binds the texture to GL_TEXTURE_2D. In other words it tells OpenGL: the texture identified by textureId is now your “2D texture”. Any call to the 2D texture from this point will affect our texture. This call is necessary whenever we want to manipulate or use a texture. Now that our texture is selected, we can proceed:
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bmp, 0);
This line of code causes OpenGL ES to load our bitmap (bmp) into the 2D texture (which is our texture at the moment). The first and third arguments are self-explanatory. The second argument is the level-of-detail number. Level 0 is the base image level and consecutive numbers are next mipmap reduction image levels. Using this function you can fill in different mipmap levels for a single texture. Finally, the fourth argument must always be zero according to OpenGL documentation.
Mipmaps and texture filtering
Next we get to how the image will scale (due to screen resolution or as a part of an animation). We spoke about mipmaps as a method for handling minified (down-scaled) images. We can manually load different mipmap levels using texImage2D. But unless you have a very good reason for that, you are better off using the function provided by OpenGL ES to generate all of them for you. This way you save a lot of work on rescaling the image multiple times and loading it to the texture. A single function call will do it all:
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
Some might argue that the quality of the manually generated mipmap images can be better than those automatically generated. But the truth is often that’s not the case. You will have to have an amazing function for which the visual difference justifies the extra work done to replace this simple method.
Now let’s get a little picky. Each mipmap level adds some load to the memory. If you are not really down-scaling the image that much, it stands to reason not to use mipmaps. Besides, the automatic mipmap generation above was introduced in OpenGL ES 2.0 while some devices are still running 1.0 and 1.1. So we have two reasons not to use mipmaps. That’s why we left it as an option in the constructor. The question now is, how does the image scale without mipmaps? What are the other options?
Texture filtering is the process of choosing what color a single pixel would be based on the texture and the point itself. If an object is too small, the texture needs to be downscaled to wrap the object. There are several ways to evaluate the color of each point. Mipmaps are only one solution and only for down-scaling (minification). Below are some possible methods for both magnification and minification:
- Nearest-neighbor interpolation is the fastest and crudest filtering method. It simply uses the color of the texel closest to the pixel center for the pixel color. While fast, this results in texture ‘blockiness’ during magnification, and aliasing and shimmering during minification.
- Nearest-neighbor with mipmapping takes advantage of mipmaps; in this method, the closest mipmap level is chosen based on distance, and then the same strategy is used as nearest-neighbor.
- Bilinear filtering is the next step up. In this method, the four nearest texels to the pixel center are sampled at the closest mipmap level, and their colors are combined by weighted average according to distance. This removes the ‘blockiness’ seen during magnification, as there is now a smooth gradient of color change from one texel to the next, instead of an abrupt jump as the pixel center crosses the texel boundary.
- Trilinear filtering is even better than bilinear filtering in that it doesn’t use a single mipmap level, but rather two closest level (one just higher and one just lower quality). It applies bilinear filtering on both and then interpolates between them. The outcome is to remove the noticeable change in quality at boundaries where the renderer switches from one mipmap level to the next. When you get closer than level 0, as there is only one mipmap level available, the algorithm reverts back to bilinear filtering.
Based on these definitions, trilinear filtering is the best choice when we are mipmaping. Otherwise, linear interpolation, while a bit slower than nearest neighbor, has a noticeably higher quality. So, let’s speak code now. OpenGL allows us to assign different algorithms to magnification and minification:
GLES10.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); if(mipmaps) { GLES10.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR_MIPMAP_LINEAR); GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); } else GLES10.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
In the above code, we first set the magnification filtering to linear interpolation as more sophisticated methods are only useful (and only available) for minification. For minification, our choice of algorithm depends on whether we use mipmaping or not. If so, we will use trilinear filtering (that’s what GL_LINEAR_MIPMAP_LINEAR stands for). Otherwise we use the basic linear interpolation.
In the end of our loading function, we recycle our bitmap to free the memory allocated to it. Bitmaps are huge and you don’t want too many of them piled up in memory:
bmp.recycle();
Life-cycle considerations
Android application life-cycle plays an important role for OpenGL ES context. Whenever the application is paused, the OpenGL ES context is completely destroyed. This means that navigating away from the app may cause loss of all OpenGL ES textures. To anticipate this, we make sure to unload and invalidate our textures whenever the app is paused. The method below is used to unload the texture:
public final void destroy() { GLES10.glDeleteTextures(1, new int[] {textureId}, 0); textureId = -1; }
The glDeleteTextures
method is used exactly like glGenTextures
, and deletes all textures listed in the array. When we are through with that, textureId will be invalid. So we set it back to its original value -1. Now you remember that this value was -1 before we generated the identifier using glGenTextures
, and after we delete it using glDeleteTextures
, we revert to -1. So, it is a good indication that the texture is loaded. Such an indication is necessary for our stage to determine when it has to reload textures. External classes can know the status of the texture through the following method:
public final boolean isLoaded() { return textureId >= 0; }
Creating mapping coordinates
Before we can draw the texture, OpenGL should also know how to map the texture onto the vertices. We should assign each vertex its corresponding coordinates in the texture. This is done in a very similar fashion to creating vertex buffers. Before anything else, we need to define a field that will hold the buffer:
private FloatBuffer tempTextureBuffer = null;
This buffer will be filled in with an array of texture coordinates. We do that in a separate function:
private void buildTextureMapping() { final float texture[] = { 0, 0, 1, 0, 0, 1, 1, 1, }; final ByteBuffer ibb = ByteBuffer.allocateDirect(texture.length * 4); ibb.order(ByteOrder.nativeOrder()); tempTextureBuffer = ibb.asFloatBuffer(); tempTextureBuffer.put(texture); tempTextureBuffer.position(0); }
Each row in the definition of the texture array corresponds to a vertex in our rectangle, and we associate each of the vertices to one of our texture’s corners. Thus we will have the whole texture mapped onto our rectangle. The lines that follow create a native buffer from the array, just like we did for vertex buffers. Now you need to add the following to the end of your load method to create texture mapping:
if(tempTextureBuffer == null) buildTextureMapping();
Texture mapping is preserved unless the application is completely taken out of the memory, in which case onCreate
will be called again, doing everything from scratch. Hence we do not bother destroying and recreating the texture buffer on each pause/resume (that is why we don’t destroy it in destroy and have this existence check here).
Drawing the texture
Now everything is ready to draw. First we need to prepare the OpenGL ES context for this specific texture. This will be done through the following method:
public final void prepare(GL10 gl, int wrap) { // Enable 2D texture gl.glEnable(GL10.GL_TEXTURE_2D); // Bind our texture to the 2D texture gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId); // Set texture wrapping method for both dimensions gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, wrap); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, wrap); // Set texture coordinates gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, tempTextureBuffer); }
This method first tells OpenGL ES to enable 2D textures and binds our texture. Then it sets the wrapping method for both horizontal (S) and vertical (T) dimensions of the texture. The wrap method is given to this method as an argument, to give us freedom when using this class. After calling the above method, subsequent calls to the following method will draw the texture:
public final void draw(GL10 gl, float x, float y, float w, float h, float rot) { gl.glPushMatrix(); gl.glTranslatef(x, y, 0); gl.glRotatef(rot, 0, 0, 1); gl.glScalef(w, h, 0); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4); gl.glPopMatrix(); }
This is exactly what we did in day 6, except all transform values are now provided as arguments. We have also added rotation. Note that rotation and scaling should both be applied before translation if we want the rectangle to rotate around its center; hence the order.
Next steps
Next day we will first organize our project and create our library. As the engine is forming, we do not want to end up in a library/game mash-up. We will also see how our texture class can be used. Meanwhile, you can download the full code for texture here (additional comments are added for better readability):