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:
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:
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:
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:
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.
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.