Loading OpenGL Textures in Android

There is a thousand pages on this already, but many of them are needlessly slow, incorrect, outdated, or poorly written, so I thought I'd throw my little bit of code into the mix, since I haven't posted in a while:

The following code will load a Drawable resource into OpenGL, and will return the integer you will bind to use this texture, while handling all mipmaping. Note: the texture's dimensions MUST be powers of two:

// Get a new texture id:
private static int newTextureID(GL10 gl) {
int[] temp = new int[1];
gl.glGenTextures(1, temp, 0);
return temp[0];
}

// Will load a texture out of a drawable resource file, and return an OpenGL texture ID:
private int loadTexture(GL10 gl, Context context, int resource) {

// In which ID will we be storing this texture?
int id = newTextureID(gl);

// We need to flip the textures vertically:
Matrix flip = new Matrix();
flip.postScale(1f, -1f);

// This will tell the BitmapFactory to not scale based on the device's pixel density:
// (Thanks to Matthew Marshall for this bit)
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inScaled = false;

// Load up, and flip the texture:
Bitmap temp = BitmapFactory.decodeResource(context.getResources(), resource, opts);
Bitmap bmp = Bitmap.createBitmap(temp, 0, 0, temp.getWidth(), temp.getHeight(), flip, true);
temp.recycle();

gl.glBindTexture(GL10.GL_TEXTURE_2D, id);

// Set all of our texture parameters:
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR_MIPMAP_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR_MIPMAP_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

// Generate, and load up all of the mipmaps:
for(int level=0, height = bmp.getHeight(), width = bmp.getWidth(); true; level++) {
// Push the bitmap onto the GPU:
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, level, bmp, 0);

// We need to stop when the texture is 1x1:
if(height==1 && width==1) break;

// Resize, and let's go again:
width >>= 1; height >>= 1;
if(width<1) width = 1;
if(height<1) height = 1;

Bitmap bmp2 = Bitmap.createScaledBitmap(bmp, width, height, true);
bmp.recycle();
bmp = bmp2;
}

bmp.recycle();

return id;
}

This bit of code is fairly quick, and it won't accidentally flip the texture vertically. (Lot's of the examples I've seen will do this, since the Android's 2D API, which we use to parse the resource, has Y increasing downwards, whereas OpenGL has Y increasing upwards.)

Enjoy. That code is public domain; I don't care what you do with it. Enjoy! ;)

EDIT (27 Sep 2010): Applied Matthew Marshall's fix for a screen density bug.


14 Responses to Loading OpenGL Textures in Android

  1. 336 Ron 2010-08-28 01:32:13

    Any work around to draw non-power-of-two dimensional bitmap?

  2. 341 ray 2010-08-30 22:29:51

    You can try padding the image with blank data, so the image has dimensions that are powers of 2. Then, you would just draw the non-blank stuff. Alternatively, you can use OpenGL ES 2.0, because it seems that 2.0 allows for textures with non-power of 2 dimensions, whereas 1.0 / 1.1 mandates the power-of-two thing.

    See:
    http://www.khronos.org/opengles/sdk/1.1/docs/man/glTexImage2D.xml
    http://www.khronos.org/opengles/sdk/2.0/docs/man/glTexImage2D.xml

  3. 441 fluffy 2010-09-20 14:04:31

    Note that while ES2.0 supports non-power-of-2 textures, on most Android devices (at least ones based on the PowerVR SGX), they are INSANELY slow to render - usually by a factor of 20. It's much better to either pad them and alter the texture coordinates, or scale the bitmap (using Bitmap.createScaledBitmap) to an appropriate size for your device.

    Meanwhile, the issue I seem to be having is that Android's Bitmap loader (via BitmapFactory.decodeStream(InputStream) is loading in BGR format, whereas OpenGL ES expects them in RGB. Any ideas about how to get Bitmap to do the byte swapping (ideally at load time, but as a postprocess is okay I guess), rather than having to do a slow byte-swapping loop of my own in userland? (I suppose that using a color matrix transform is probably the easiest approach...)

  4. 442 fluffy 2010-09-20 14:08:29

    Oh, never mind, it's just the specific device I'm working with that's screwing it up. Driver bug report time.

  5. 443 ray 2010-09-20 16:04:30

    Wow, I've heard that non-power-of-2 sized textures were slower, but not 20x slower. I will have to keep that in mind...

    As for the whole BGR issue, that's definitely odd... Since there isn't a BGR entry in the PixelFormat enum, meaning that the BitmapFactory is totally producing malformed Bitmaps... Which device is doing that, so I know to avoid it. ;P

  6. 483 Matthew Marshall 2010-09-27 09:15:48

    I found one improvement for this code... BitmpaFactory seems to scale images for different screen pixel densities, so on my HTC evo my 32x32 image was loaded as 48x48.

    There may be a way to prevent this application wide, but I got by with this change to loadTexture:

    BitmapFactory.Options opts = new BitmapFactory.Options();
    opts.inScaled = false;

    // Load up, and flip the texture:
    Bitmap temp = BitmapFactory.decodeResource(getResources(), resource, opts);

    Hope that helps the next person wondering why their textures aren't working.

    MWM

  7. 484 ray 2010-09-27 14:26:01

    Hunh, good to know. I guess Google used the standard density for the N1, it being their flagship phone and all. That would explain why the BitmapFactory was loading 32x32 as 32x32 for me. So thanks for that fix, cause I don't think I would have ever found that bug alone. (Or at least it would have been hell finding it... :X )

    Anyway, I altered the code above to include your fix, and once again, thanks. :)

  8. 597 Idris DW 2010-10-21 17:23:09

    Thanks for this, very handy.

    I had to do a couple of things to get it working though:

    1) Added anywhere: gl.glEnable(GL10.GL_TEXTURE_2D);

    (From http://www.andengine.org/forums/development/problem-with-opengl-t685.html )

    2) Added before the for loop:

    int[] crop = {0, bmp.getWidth(), bmp.getHeight(), -bmp.getHeight()};

    ((GL11) gl).glTexParameteriv(GL10.GL_TEXTURE_2D,
    GL11Ext.GL_TEXTURE_CROP_RECT_OES, crop, 0);

    (From http://stackoverflow.com/questions/3553244/android-opengl-es-and-2d )

    Without these it just showed a blank square or, what seemed to be a 1x1 scaled down version of the image - I assume this because a half green, half red PNG came out as a yellow square :-)

    ---

    (For any other noobs needing a framework to try this code out with, I started with the very minimal SimpleGLSurfaceView application from http://developer.android.com/resources/articles/glsurfaceview.html with a couple of lines added to the onDrawFrame method:

    gl.glBindTexture(GL10.GL_TEXTURE_2D, thing);
    ((GL11Ext)gl).glDrawTexfOES(10, 10, 0, 64, 64);

    And with this added to onSurfaceCreated:

    thing = loadTexture(gl, context, R.drawable.thing);

    Where 'thing' is a 64x64 PNG image)

  9. 602 ray 2010-10-23 12:44:10

    @Idris

    1: Yeah, you need to enable 2d textures. I should have probably mentioned how to do that...

    2: That's odd. My code doesn't use that cropping code, and it works just fine. I'm thinking that you are using the actual pixel dimensions when drawing the texture. Note that OpenGL, by default, will have you reference the pixel data with floating point values between 0.0 and 1.0. (Where 0.0 is the left/bottom, and 1.0 is the right/top.) If you have tiling turned on, then that would explain the color mixing... cause the entire texture is being squeezed into one pixel, and is then being tiled... That's just a theory, though...

    Anyway, thanks for the comment. :)

  10. 1010 Faris 2010-12-12 17:32:21

    From http://developer.android.com/guide/practices/screens_support.html

    "... Pre-scaling and auto-scaling of bitmaps and nine-patches

    When a bitmap or nine-patch image is loaded from the application's resources, the platform attempts to pre-scale it to match the display's density. For instance, if you placed a 100x100 icon in the res/drawable/ directory and loaded that icon as a bitmap on a high-density screen, Android would automatically scale up the icon and produce a 150x150 bitmap.

    This pre-scaling mechanism works independently of the source. For instance, an application targeted for a high-density screen may have bitmaps only in the res/drawable-hdpi/ directory. If one of the bitmaps is a 240x240 icon and is loaded on a medium-density screen, the resulting bitmap will measure 160x160."

    Different density devices need to be accommodated for. A couple apps I was working on designed for medium density devices and high density devices (eg the Evo) had menu icons which were slightly fuzzy. After making another icon set 50% larger hdpi devices display them fine. Lately, I have been simply using hdpi resolution images and letting the BitmapFactory auto scale them down at runtime without quality degradation. I have not tested how this translates to rendering into a GLSurfaceView though.

  11. 1024 Tim 2011-05-26 20:46:18

    It's a bad practice to use local arrays- garbage collector will come for you

  12. 1025 ray 2011-05-28 10:38:24

    @Tim:

    I don't see the problem. I create a local array, use it within the local scope, and then return a value out of it, NOT the array itself. The array is then free to be garbage collected, since I already have the value...

  13. 1027 Different Tim 2011-08-03 16:16:05

    @ray:

    The problem Tim is describing is not so much a graphics issue, or a question of whether you will get your value before the garbage collector eats your array... the issue is that every time you allocate memory, the GC gets a chance to run. The GC will cheerfully "stop the world" while it does its thing, which means your app can appear to hang for anywhere from 100-500ms while the GC runs, killing the framerate of your app.

    - A different Tim

  14. 1203 Charlie 2012-12-07 21:51:11

    @Tim

    I don't do android applications professionally, but I am a professional java programmer that knows the details of JVMs and GCs are implemented.

    First off, his array will not be de-allocated by the GC. In java, all objects (arrays are considered objects) are allocated to the heap and only their reference is on the stack. The GC keeps tracks of the number of references to an object, and only collects the object if the number of references is zero. There is such a thing as weak or volatile references, but he is not using them.

    Second, GC only hangs the application when it does a major garbage collection -- which is only executed when the heap is x% full (where by x is a configurable number). The only other time that a major GC is called is when you programmically call System.gc() -- which is why it is bad practice since it locks all your threads.

    More commonly, a minor garbage collection is called to clean up unused objects. This is less intensive, and usually only looks are recently defined references (reference that were perhaps used only in a local scope). A minor collection will execute on it's own thread, and will NOT lock other threads in the application. If references survive the minor collection, they usually get promoted to a difference space in the heap -- there are difference types of spaces on the heap but I wont get into that; basically it works on seniority (with the exception of the perm space), and the more older the object is the less it gets checked.

    Then again, android's implementation may be different from an actual JVM.

    -Charlie.

Leave a Reply



About

User