Monday, February 22, 2010

Drawing a HUD Display in OpenGL ES

One thing that I've been asked by a number of people is how to do a "heads up display" (or HUD) when using OpenGL to display three-dimensional objects inside a perspective viewport. Many games and other programs need to present certain data to the user as if it were written right on the screen in front of the three-dimensional world. But, if you've used glFrustum() to set up your viewport it won't look like that, since any drawing done in a perspective viewport will experience some amount of distortion when filtered through the projection matrix.

The answer to this problem is relatively straightforward and simple. You just don't let the HUD drawing go through the same projection matrix as the three-dimensional objects. Under the hood, the project view matrix is just a 4x4 matrix of GLfloats, exactly like the model view matrix we use to transform objects in our virtual world. And, just like the model view matrix, we can make changes to the projection matrix between drawing calls.

To illustrate the point, I'm going to take the old icosahedron project from the OpenGL ES from the Ground Up series and do some two-dimensional drawing on top of the icosahedron, exactly the same way you would draw a HUD. To keep things simple, I'm just going to draw a few squares and a bit of text, like so:
Screen shot 2010-02-22 at 2.35.14 PM.png

The three squares and text will be drawn using an orthographic projection matrix, so no matter how the object behind it is transformed, moved, or projected, the three squares and the text will be drawn exactly the same with no distortion or movement. You can use this exact same technique to draw the player's score, the crosshairs of a gun, a spaceship's controls, or any other drawing that needs to appear as if it was on a piece of glass in front of the virtual world.

Before we begin, let's look at the setupView: function where we set up the viewport in the old icosahedron project:

-(void)setupView:(GLView*)view
{
GLfloat size;
const GLfloat zNear = 0.1,
zFar = 1000.0,
fieldOfView = 60.0;
size = zNear * tanf(DEGREES_TO_RADIANS(fieldOfView) / 2.0);
CGRect rect = self.view.bounds;
glMatrixMode(GL_PROJECTION);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glFrustumf(-size, size, -size / (rect.size.width / rect.size.height), size /
(rect.size.width / rect.size.height), zNear, zFar);

glViewport(0, 0, rect.size.width, rect.size.height);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
colors = [[NSMutableArray alloc] initWithCapacity:20];
for (int i =0; i < 20; i++)
[colors addObject:[UIColor randomColor]];
}

It's pretty much a bog-standard perspective viewport setup that will allow our three dimensional objects to get smaller as they move away from the viewer. Everything we draw will be distorted to represent perspective, which we don't want for our HUD elements. So… let's create a couple of convenience methods to get out of perspective mode and into orthographic mode and vice versa

First, to switch the projection to orthographic mode, we'll use this method:

-(void)switchToOrtho 
{
glDisable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrthof(0, self.view.bounds.size.width, 0, self.view.bounds.size.height, -5, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}

This is pretty simple, but let's break it down line-by-line. The first line is important. We don't want depth testing if we're drawing in two dimensions because we want all of our drawing done on the same plane with no depth.

Next, we call glMatrixMode() to make the projection matrix active. After that, we push the existing projection matrix onto the matrix stack so that we can restore it later. When this gets called, the projection matrix contains a matrix that was generated by our call to glFrustum(), and we need to be able to get back to it later. We then load the identify identity and then set up an orthographic projection where every screen pixel equates to one OpenGL unit, which will make our two-dimensional drawing easier since each unit will equal one pixel.

After that, we load the identity matrix into our model view matrix so we start drawing our HUD with a clean slate, unaffected by any previously used transformations.

In addition to a method to get into orthographic mode, we also need a method to restore our previous projection matrix, the one that we created in setupView:. Since we pushed the existing projection matrix onto the matrix stack earlier, All we have to do here is re-eneable depth-testing and pop our old projection matrix off the matrix stack:

-(void)switchBackToFrustum 
{
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
}

Now we have the ability to switch back and forth between perspective and orthographic mode, so let's do some drawing. Here's the drawing method from the icosahedron project with the new HUD drawing code drawn in bold. I've used Apple's Texture2D class to handle the text drawing. I've done this just for simplicity - it's far from the most efficient way to draw text in OpenGL ES. In fact, it's quite inefficient, but it's easy and free. Perhaps I'll focus on better ways to draw text in a future posting, but this post is about HUDs, not about text, so we can live with a little inefficiency.

- (void)drawView:(GLView*)view;
{


static GLfloat rico;
static const GLfloat icosahedronVertices[]= {
0, -0.525731, 0.850651,
0.850651, 0, 0.525731,
0.850651, 0, -0.525731,
-0.850651, 0, -0.525731,
-0.850651, 0, 0.525731,
-0.525731, 0.850651, 0,
0.525731, 0.850651, 0,
0.525731, -0.850651, 0,
-0.525731, -0.850651, 0,
0, -0.525731, -0.850651,
0, 0.525731, -0.850651,
0, 0.525731, 0.850651,
}
;
static const GLubyte icosahedronFaces[] = {
1, 2, 6,
1, 7, 2,
3, 4, 5,
4, 3, 8,
6, 5, 11,
5, 6, 10,
9, 10, 2,
10, 9, 3,
7, 8, 9,
8, 7, 0,
11, 0, 1,
0, 11, 4,
6, 2, 10,
1, 6, 11,
3, 5, 10,
5, 4, 11,
2, 7, 9,
7, 1, 0,
3, 9, 8,
4, 8, 0,
}
;
static const GLubyte icosahedronNumberOfFaces = 60;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Icosahedron
glLoadIdentity();
glEnableClientState(GL_VERTEX_ARRAY);
glTranslatef(0.0f,0.0f,-2.0f);
glRotatef(rico,1.0f,1.0f,1.0f);

glVertexPointer(3, GL_FLOAT, 0, icosahedronVertices);
for(int i = 0; i < icosahedronNumberOfFaces; i += 3)
{
UIColor *oneColor = [colors objectAtIndex:i/3];
[oneColor setOpenGLColor];

glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, &icosahedronFaces[i]);
}


glDisableClientState(GL_VERTEX_ARRAY);

static NSTimeInterval lastDrawTime;
if (lastDrawTime)
{
NSTimeInterval timeSinceLastDraw = [NSDate timeIntervalSinceReferenceDate] - lastDrawTime;
rico+=50 * timeSinceLastDraw;
}


// ------------------------------------------------
// Draw HUD ---------------------------------------
// ------------------------------------------------
[self switchToOrtho];

static const GLfloat squareVertices[] = {
5.0f, 150.0f,
5.0f, 250.0f,
100.0f, 250.0f,
100.0f, 150.0f
}
;

glLineWidth(3.0);
glColor4f(0.0, 0.0, 1.0, 1.0); // blue
glTranslatef(5.0, 0.0, 0.0);
glVertexPointer(2, GL_FLOAT, 0, squareVertices);
glEnableClientState(GL_VERTEX_ARRAY);

glDrawArrays(GL_LINE_LOOP, 0, 4);
glTranslatef(100.0, 0.0, 0.0);
glColor4f(1.0, 0.0, 0.0, 1.0); // Red
glDrawArrays(GL_LINE_LOOP, 0, 4);
glTranslatef(100.0, 0.0, 0.0);
glColor4f(1.0, 1.0, 0.0, 1.0); // Yellow
glDrawArrays(GL_LINE_LOOP, 0, 4);


glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc (GL_ONE, GL_ONE);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glColor4f(1.0, 1.0, 1.0, 1.0);
glLoadIdentity();
Texture2D *textTex = [[Texture2D alloc] initWithString:@"Text"
dimensions:CGSizeMake(100., 40.0)
alignment:UITextAlignmentCenter
font:[UIFont boldSystemFontOfSize:36.0]
]
;
[textTex drawAtPoint:CGPointMake(160.0, 440.0) depth:-1];
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);

[self switchBackToFrustum];

lastDrawTime = [NSDate timeIntervalSinceReferenceDate];
}

We start off by calling our switchToOrtho method. We then define vertices for a square that will be drawn using GL_LINE_LOOP and submit those vertices using glVertexPointer(). After that, we call glDrawArrays() three times, changing the color and doing a translate transform between the calls so that each square gets drawn to the right of the previous one and in a different color.

After that, we use Texture2D to draw the word "Text" on the screen. Because this class essentially draws the text into a graphic context then creates a texture out of it, we have to enable (and then disable) a bunch of texture-related options.

After we're all done drawing, we call the other method we created, switchBackToFrustum, to switch back to perspective mode so that the next time our drawView: method is called, we're back in three-dimensional mode.

And that is all there is to drawing a HUD over a perspective viewport. You can download the Xcode project right here and play with it yourself.

No comments:

Post a Comment