If you've played around with our previous examples and let the cube move further away from the camera, you might have noticed that the texture starts to looks grainy and full of little artifacts the smaller the cube gets. This effect is called aliasing, a prominent effect in all types of signal processing. Figure 11–8 shows you the effect on the right side and the result of applying a technique called mipmapping on the left side.
We won't go into the details of why aliasing happens; all you need to know is how to make objects look better. That's where mipmapping comes in.
The key to fixing aliasing problems is to use lower-resolution images for parts of an object that are smaller on screen or further away from the view point. This is usually called a mipmap pyramid or chain. Given an image in its default resolution, say 256×256 pixels, we create smaller versions of it, dividing the sides by two for each level of the mipmap pyramid. Figure 11–9 shows the crate texture with various mipmap levels.
To make a texture mipmapped in OpenGL ES, we have to do two things:
GL_XXX_MIPMAP_XXX
constants, usually GL_LINEAR_MIPMAP_NEAREST
.To resize the base image for the mipmap chain, we can simply use the Bitmap
and Canvas
classes that the Android API provides. Let's modify the Texture
class a little. Listing 11–8 shows you the code.
package com.badlogic.androidgames.framework.gl;
import java.io.IOException;
import java.io.InputStream;
import javax.microedition.khronos.opengles.GL10;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.opengl.GLUtils;
import com.badlogic.androidgames.framework.FileIO;
import com.badlogic.androidgames.framework.impl.GLGame;
import com.badlogic.androidgames.framework.impl.GLGraphics;
public class Texture {
GLGraphics glGraphics;
FileIO fileIO;
String fileName;
int textureId;
int minFilter;
int magFilter;
public int width;
public int height;
boolean mipmapped;
We add only one new member, called mipmapped
, which stores whether the texture has a mipmap chain or not.
public Texture(GLGame glGame, String fileName) {
this(glGame, fileName, false);
}
public Texture(GLGame glGame, String fileName, boolean mipmapped) {
this.glGraphics = glGame.getGLGraphics();
this.fileIO = glGame.getFileIO();
this.fileName = fileName;
this.mipmapped = mipmapped;
load();
}
For compatibility, we leave the old constructor in, which calls the new constructor. The new constructor takes a third argument that lets us specify whether we want the texture to be mipmapped.
private void load() {
GL10 gl = glGraphics.getGL();
int[] textureIds = new int[1];
gl.glGenTextures(1, textureIds, 0);
textureId = textureIds[0];
InputStream in = null;
try {
in = fileIO.readAsset(fileName);
Bitmap bitmap = BitmapFactory.decodeStream(in);
if (mipmapped) {
createMipmaps(gl, bitmap);
} else {
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
setFilters(GL10.GL_NEAREST, GL10.GL_NEAREST);
gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);
width = bitmap.getWidth();
height = bitmap.getHeight();
bitmap.recycle();
}
} catch (IOException e) {
throw new RuntimeException("Couldn't load texture '" + fileName
+ "'", e);
} finally {
if (in != null)
try {
in.close();
} catch (IOException e) {
}
}
}
The load()
method stays essentially the same as well. The only addition is the call to createMipmaps()
in case the texture should be mipmapped. Non-mipmapped Texture instances are created as before.
private void createMipmaps(GL10 gl, Bitmap bitmap) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
width = bitmap.getWidth();
height = bitmap.getHeight();
setFilters(GL10.GL_LINEAR_MIPMAP_NEAREST, GL10.GL_LINEAR);
int level = 0;
int newWidth = width;
int newHeight = height;
while (true) {
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, level, bitmap, 0);
newWidth = newWidth / 2;
newHeight = newHeight / 2;
if (newWidth <= 0)
break;
Bitmap newBitmap = Bitmap.createBitmap(newWidth, newHeight,
bitmap.getConfig());
Canvas canvas = new Canvas(newBitmap);
canvas.drawBitmap(bitmap,
new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()),
new Rect(0, 0, newWidth, newHeight), null);
bitmap.recycle();
bitmap = newBitmap;
level++;
}
gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);
bitmap.recycle();
}
The createMipmaps()
method is fairly straightforward. We start off by binding the texture so that we can manipulate its attributes. The first thing we do is to keep track of the bitmap's width and height and set the filters. Note that we use GL_LINEAR_MIPMAP_NEAREST
for the minification filter. If we don't use that filter, mipmapping will not work and OpenGL ES will fall back to normal filtering, only using the base image.
The while
loop is straightforward. We upload the current bitmap as the image for the current level. We start at level 0, the base level with the original image. Once the image for the current level is uploaded, we create a smaller version of it, dividing its width and height by 2. If the new width is less than or equal to zero, we can break out of the infinite loop, since we have uploaded an image for each mipmap level (the last image has a size of 1×1 pixels). We use the Canvas class to resize the image and to store the result in newBitmap
. We then recycle the old bitmap so that we clean up any of the memory it used and set the newBitmap
as the current bitmap. This process is repeated until the image is smaller than 1×1 pixels.
Finally, we unbind the texture and recycle the last bitmap that got created in the loop.
public void reload() {
load();
bind();
setFilters(minFilter, magFilter);
glGraphics.getGL().glBindTexture(GL10.GL_TEXTURE_2D, 0);
}
public void setFilters(int minFilter, int magFilter) {
this.minFilter = minFilter;
this.magFilter = magFilter;
GL10 gl = glGraphics.getGL();
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
minFilter);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
magFilter);
}
public void bind() {
GL10 gl = glGraphics.getGL();
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
}
public void dispose() {
GL10 gl = glGraphics.getGL();
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
int[] textureIds = { textureId };
gl.glDeleteTextures(1, textureIds, 0);
}
}
The rest of the class is the same as in the previous version. The only difference in usage is how we call the constructor. Since this is perfectly simple, we won't write an example just for mipmapping. We'll use mipmapping on all of our textures that we used for 3D objects. In 2D, mipmapping is less useful. A few final notes on mipmapping:
NOTE: Once again, because this is really important, remember that mipmapping will only work with square textures! A 512×256 pixel image will not work.
3.148.112.79