One use for blending in a 3D scene is overlaying a reflection on top of a surface, as shown on the left of Figure 6-2. Remember, computer graphics is often about cheating! To create the reflection, you can redraw the object using an upside-down projection matrix. Note that you need a way to prevent the reflection from “leaking” outside the bounds of the reflective surface, as shown on the right in Figure 6-2. How can this be done?
It turns out that third-generation iPhones and iPod touches have support for an OpenGL ES feature known as the stencil buffer, and it’s well-suited to this problem. The stencil buffer is actually just another type of renderbuffer, much like color and depth. But instead of containing RGB or Z values, it holds a small integer value at every pixel that you can use in different ways. There are many applications for the stencil buffer beyond clipping.
To check whether stenciling is supported on the
iPhone, check for the
GL_OES_stencil8 extension using
the method in Dealing with Size Constraints. At the time
of this writing, stenciling is supported on third-generation devices and
the simulator, but not on first- and second-generation devices.
The reflection trick can be achieved in four steps (see Figure 6-3):
Note that the reflection is drawn before the textured podium, which is the reason for the front-to-back blending. We can’t render the reflection after the podium because blending and depth-testing cannot both be enabled when drawing complex geometry.
The complete code for this sample is available from this book’s website, but we’ll go over the key snippets in the following subsections. First let’s take a look at the creation of the stencil buffer itself. The first few steps are generating a renderbuffer identifier, binding it, and allocating storage. This may look familiar if you remember how to create the depth buffer:
GLuint stencil; glGenRenderbuffersOES(1, &stencil); glBindRenderbufferOES(GL_RENDERBUFFER_OES, stencil); glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_STENCIL_INDEX8_OES, width, height);
GLuint framebuffer; glGenFramebuffersOES(1, &framebuffer); glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, color); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depth); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_STENCIL_ATTACHMENT_OES, GL_RENDERBUFFER_OES, stencil); glBindRenderbufferOES(GL_RENDERBUFFER_OES, color);
To save memory, sometimes you can interleave
the depth buffer and stencil buffer into a single renderbuffer. This is
possible only when the
extension is supported. At the time of this writing, it’s available on
third-generation devices, but not on the simulator or older devices. To
see how to use this extension, see Example 6-2.
Relevant portions are highlighted in bold.
Example 6-2. Using packed depth stencil
GLuint depthStencil; glGenRenderbuffersOES(1, &depthStencil); glBindRenderbufferOES(GL_RENDERBUFFER_OES, depthStencil); glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH24_STENCIL8_OES, width, height); GLuint framebuffer; glGenFramebuffersOES(1, &framebuffer); glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, color); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depthStencil); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_STENCIL_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depthStencil); glBindRenderbufferOES(GL_RENDERBUFFER_OES, color);
glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
Next you need to tell OpenGL to enable writes to the stencil buffer, and you need to tell it what stencil value you’d like to write. Since you’re using an 8-bit buffer in this case, you can set any value between 0x00 and 0xff. Let’s go with 0xff and set up the OpenGL state like this:
glEnable(GL_STENCIL_TEST); glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); glStencilFunc(GL_ALWAYS, 0xff, 0xff);
The first line enables
GL_STENCIL_TEST, which is a somewhat misleading name
in this case; you’re writing to the stencil buffer,
not testing against it. If you don’t enable
GL_STENCIL_TEST, then OpenGL assumes you’re not
working with the stencil buffer at all.
void glStencilOp(GLenum fail, GLenum zfail, GLenum zpass);
Again, this may seem like way too much
flexibility, more than you’d ever need. Later in this book, you’ll see
how all this freedom can be used to perform interesting tricks. For now,
all we’re doing is writing the shape of the disk out to the stencil
buffer, so we’re using the
void glStencilFunc(GLenum func, GLint ref, GLuint mask);
This specifies the comparison function to use for the stencil test, much like the depth test Creating and Using the Depth Buffer.
Getting back to the task at hand, check out Example 6-3 to see how to render the disk to the stencil buffer only. I adjusted the indentation of the code to show how certain pieces of OpenGL state get modified before the draw call and then restored after the draw call.
Example 6-3. Rendering the disk to stencil only
// Prepare the render state for the disk. glEnable(GL_STENCIL_TEST); glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); glStencilFunc(GL_ALWAYS, 0xff, 0xff); // Render the disk to the stencil buffer only. glDisable(GL_TEXTURE_2D); glTranslatef(0, DiskY, 0); glDepthMask(GL_FALSE); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); RenderDrawable(m_drawables.Disk); // private method that calls glDrawElements glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDepthMask(GL_TRUE); glTranslatef(0, -DiskY, 0); glEnable(GL_TEXTURE_2D);
Two new function calls appear in Example 6-3:
glColorMask. Recall that we’re interested in
affecting values in the stencil buffer only. It’s actually perfectly
fine to write to all three renderbuffers (color, depth, stencil), but to
maximize performance, it’s good practice to disable any writes that you
The four arguments to
glColorMask allow you to toggle each of the
individual color channels; in this case we don’t need any of them. Note
glDepthMask has only one argument, since it’s a
single-component buffer. Incidentally, OpenGL ES also provides a
glStencilMask function, which we’re not using
Step 2 renders the reflection of the object and uses the stencil buffer to clip it to the boundary of the disk. Example 6-4 shows how to do this.
Example 6-4. Rendering the reflection
glTranslatef(0, KnotY, 0); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); glStencilFunc(GL_EQUAL, 0xff, 0xff); glEnable(GL_LIGHTING); glBindTexture(GL_TEXTURE_2D, m_textures.Grille); const float alpha = 0.4f; vec4 diffuse(alpha, alpha, alpha, 1 - alpha); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse.Pointer()); glMatrixMode(GL_PROJECTION); glLoadMatrixf(m_mirrorProjection.Pointer()); RenderDrawable(m_drawables.Knot); // private method that calls glDrawElements glLoadMatrixf(m_projection.Pointer()); glMatrixMode(GL_MODELVIEW);
This time we don’t need to change the values
in the stencil buffer, so we use
GL_KEEP for the
glStencilOp. We changed the stencil
comparison function to
GL_EQUAL so that only the
pixels within the correct region will pass.
There are several ways you could go about
drawing an object upside down, but I chose to do it with a
quick-and-dirty projection matrix. The result isn’t a very accurate
reflection, but it’s good enough to fool the viewer! Example 6-5 shows how I did this using a
mat4 method from the C++ vector library in the
appendix. (For ES 1.1, you could simply use the provided
Example 6-5. Computing two projection matrices
The next step is rather mundane; we simply
need to render the actual floating object, without doing anything with
the stencil buffer. Before calling
the object, we turn off the stencil test and disable the depth
Remember, the reflection is drawn just like any other 3D object, complete with depth testing. Allowing the actual object to be occluded by the reflection would destroy the illusion, so it’s a good idea to clear the depth buffer before drawing it. Given the fixed position of the camera in our demo, we could actually get away without performing the clear, but this allows us to tweak the demo without breaking anything.
The final step is rendering the marble disk underneath the reflection. Example 6-6 sets this up.
Example 6-6. Render the disk to the color buffer