Wednesday, August 10, 2011

OpenGL, Android, and gluUnProject

So, as you know, no one really reads what I put here. And I'm completely alright with that. This isn't a blog to entertain users so much as a place for me to put my nifty little experiments, even if they're slow to come out.

Back on topic. I've recently been toying with OpenGL more and more, and it's known that I have Android games in the works. Now, I've come across my share of annoyances that Google has been... well, not the best resource for once. This has lead me to solving a lot of my Android based problems, particularly with OpenGL.

This post is to go over the annoyance of gluUnProject, which can be found in the GLU package in the Android SDK. I'll post my example of what I was trying to achieve, and how I managed to do it.

So, I'm working on a 2D game which may end up being graphic-intensive, and as such, the Canvas method of rendering has been inadequate -- and allocating objects and deleting them, I've found, does not lend itself to speedy code. But those kind of optimizations have been discussed in a Google I/O video which I'm sure you've seen (you know the one -- it's based on game dev on Android).

The problem I had was trying to convert a screen coordinate (X, Y, touch location) to its corresponding point on a specific plane (z = some value) within the screen (in my case, the "camera_zoom" variable, which is something like z = -50). This was an issue as I had no way to judge the 'game bounds' at that z-value, so I couldn't simply do a ratio. I had to convert window coordinates to object coordinates, which gluUnProject does, but I also had to tell it, "Oh by the way, it's at z = -50.". So I'll get right to it.

Working in the Renderer subclass (the one that your GLSurfaceView has set):

First, I initialized a few static arrays to hold the ModelView matrix, Projection matrix, and Viewport matrix. Because of my application, the only place these values change where it'd impact my gluUnProject is in the "onSurfaceChanged" function.
 public static int[] viewport = new int[16];  
 public static float[] modelViewMatrix = new float[16];  
 public static float[] projectionMatrix = new float[16];  
 public static float[] pointInPlane = new float[16];  
Now, as you can see (clearly) they're all initialized to length 16. This was the first irksome thing. I AM aware that the viewport need only be length 4, and the pointInPlane (the array that will hold the output) be of length 3. But when I tried to do that, I would only get an IllegalArgumentException error from a Matrix Multiplication class stating, "length - offset < n". So, they all get to be 16. Now, again, because my application is how it is, I only alter the matrices and viewport in the "onSurfaceChanged" function. After all the settings code, simply add this block:
 gl.glLoadIdentity();
((GL11) gl).glGetIntegerv(GL11.GL_VIEWPORT, viewport, 0);
((GL11) gl).glGetFloatv(GL11.GL_MODELVIEW_MATRIX, modelViewMatrix, 0);
((GL11) gl).glGetFloatv(GL11.GL_PROJECTION_MATRIX, projectionMatrix, 0);
Please note that I use GL11, which you should check if your device supports. You can do this by calling:
 gl instanceof GL11  
in a conditional block.

Now, again, one of the things that kept messing up my gluUnProject is that I would forget to load the identity matrix before getting the ModelView and Projection matrix. So, now that this is done, the rest is pretty easy. In order to get the bounds of the game at a certain plane, here's the gluUnProject code:
 GLU.gluUnProject(0, viewport[3], camera_zoom, modelViewMatrix, 0, projectionMatrix, 0, viewport, 0, pointInPlane, 0);  
 gameBounds[0] = pointInPlane[0] * -camera_zoom;  
 gameBounds[1] = pointInPlane[1] * -camera_zoom;  
 GLU.gluUnProject(width, 0, camera_zoom, modelViewMatrix, 0, projectionMatrix, 0, viewport, 0, pointInPlane, 0);  
 gameBounds[2] = pointInPlane[0] * -camera_zoom;  
 gameBounds[3] = pointInPlane[1] * -camera_zoom;  
Now, since I'm working in onSurfaceChaged, width and height are going to be the same as viewport[2] and viewport[3]. The first call gets the (x, y) of the top left of the screen as OpenGL coordinates at z = camera_zoom, and stores those values into gameBounds (defined as a float[4]). The second call gets the (x, y) at the lower right. Simple enough. The last thing you need to do on the output is multiply by the zoom level * -1. That's it. Now gameBounds is filled with OpenGL coordinates for the edges of your surface at z = camera_zoom.

Now, to get a screen coordinate into an OpenGL coordinate at z = camera_zoom. This code is called from "drawFrame", since that was the only place that made sense at the time. You can probably guess it by now.
 GLU.gluUnProject(GameSurface.mouse[0], viewport[3] - GameSurface.mouse[1], camera_zoom, modelViewMatrix, 0, projectionMatrix, 0, viewport, 0, pointInPlane, 0);  
 pointInPlane[0] *= -camera_zoom;  
 pointInPlane[1] *= -camera_zoom;  
If you can't tell, GameSurface.mouse is an int[] that has the screen's mouse x at [0] and mouse y at [1]. Simple enough.

Oh, you do the "viewport[3] - winY" thing to make sure the coordinates are the same -- the top of the window is at y = 0, whereas the bottom of the window in OpenGL is y = 0.

Site note: Someone pointed out that the emulator doesn't handle OpenGL calls that well -- this should work on an actual device, however.

9 comments:

  1. You may think that no one reads this, but I stumbled across this post as I was googling for OpenGL help, and it's about EXACTLY what I was trying to accomplish. Thanks to your clear examples and discussion (way better than many of the other half-baked ones out there), I was able to get my code working within minutes, not hours or days as it probably would have been otherwise. I am extremely grateful, and if you choose to write more about OpenGL, know that you'll have at least one person reading.

    ReplyDelete
  2. This was really useful. Thanks for the explanation. I've spent hours looking for a good tutorial on this subject and I found yours easy to follow. One point I would make though is that after implementing the code in your tutorial I was left scratching my head as to why it wasn't working in the emulator. Turns out that the emulator doesn't handle some aspects of OpenGL that well but when I tried it on an actual device it worked perfectly. Thanks again.

    ReplyDelete
    Replies
    1. I'll add that as a note -- thanks! I'm glad I could help.

      Delete
  3. To fix IllegalArgumentException you only need to make pointInPlane of size 4, not 3 as stated in documentation. (http://code.google.com/p/android/issues/detail?id=25143)

    ReplyDelete
  4. THIS IS THE BEST POST I FOUND EVER!!!!!

    ReplyDelete