!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> » Look Ma, No Stencil Buffer -- Mobile Perspectives

Look Ma, No Stencil Buffer

By paul ~ July 30th, 2009. Filed under: Resources.

OK, show of hands, who else was surprised to discover that there is no stencil buffer on the iPhone, even though it’s supported by OpenGL-ES 1.1? Well, I was, but I’m a noob. I’m also a bit of a hacker, so when I need something, I just go schlepping around the Internet until I find a way to do it. When I needed a stencil-type behavior for iPunt , I went googling and discovered this wonderful thing called a "stencil buffer", and tried to use it. Only it didn’t work. I did some more homework and discovered that the iPhone doesn’t have a stencil buffer. That left me scatching my head until I saw a brief comment somewhere that suggested that many applications for a stencil could be accomplished by depth culling and transparency. Ah-Ha, I exclaimed, and came up with the following. Pehaps this will help some other noob out there, or maybe some expert will show me a better way.

Here’s the effect I was trying to achieve:

Desired effect of playbox with mirrored floor suspended in mid-air.

What we have here is the iPunt playing box suspended in mid-air. The floor is a broken mirror (although that’s a little hard to see from this angle.) The camera is allowed to move freely around the inside of the skybox. When the camera is below the foor, the floor appears transparent (like broken glass.) The walls of the playbox are fractured glass.

The problem, really, is the reflection. Without any stenciling, you get this…

The problem.  Scene drawn as a reflection is visible from some angles.

We somehow need to prevent the reflected scene that is not viewed through the mirrored floor from showing. A stencil buffer would have worked well for this purpose. Without a stencil buffer, you have to rely on depth culling and transparency to achieve the desired result.

In brief, what I did was to create a stencil shape (a planar shape made up of triangles) that had a hole in the middle where the mirrored floor was going to be and was large enough to obscure the reflected scene from all camera angles above the plane of the floor. This shape will be drawn transparent (alpha = 0) so that the skybox will show through, but the draw order and the depth culling will prevent the unwanted portions of the reflected scene from showing through.

Here’s a picture of the skybox, reflection, and stencil (with the alpha turned up so that it can be seen.)

Skybox, reflection, and stencil.

Because the draw order is skybox, then stencil, then reflected scene, those portions of the reflected scene that are occluded by the stencil are depth-culled and won’t be drawn. The real scene and floor can then be drawn.

Here is the code, with comments, that does this…

First, the definition of the stencil and floor triangles.

// The shape here is a large plane
// with a hole in it where the mirror will be.
const GLfloat stencilVerts[] = {
-100.0, -15.0, 100.0,
-10.0, -15.0, 10.0,
100.0, -15.0, 100.0,
10.0, -15.0, 10.0,
100.0, -15.0, -100.0,
10.0, -15.0, -10.0,
-100.0, -15.0, -100.0,
-10.0, -15.0, -10.0,
-100.0, -15.0, 100.0,
-10.0, -15.0, 10.0

const GLfloat floorVerts[] = {
-10.0, -15.0, 10.0,
10.0, -15.0, 10.0,
10.0, -15.0, -10.0,
-10.0, -15.0, -10.0,

const GLfloat floorUV[] = {
0.0, 0.0,
0.0, 1.0,
1.0, 1.0,
1.0, 0.0

Then in my render() function…


  // Clear the space
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

  // Draw the skybox

  // Only draw the reflection if the view is above the plane of the floor.
  if (pWorld->camera()->location().y() > -14.5) {

    // Draw the stencil.  Anything that is drawn after this that is occluded by the
    // stencil will not be drawn because it fails the depth test.  The skybox, which
    // has already been drawn, will show through everywhere because of the transparency.
    pWorld->material("StencilMaterial")->installMaterial();  // Sets the material properties, including alpha
    glVertexPointer(3, GL_FLOAT, 0, stencilVerts);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 10);

    // Draw reflection w/out floor -- Reflection will be depth culled everwhere but hole
    // Draw everything else normally (including the mirrored floor)
    glPushMatrix ();
    // Mirror lies in XZ plane, so scale by -1.0 in Y axis
    glScalef(1.0, -1.0, 1.0);
    // Mirror is 15.0 units from origin, so translate by 30.0 units
    glTranslatef(0.0, 30.0, 0.0);

  // Draw the real scene

  // Draw the floor
  pWorld->material("Material_010")->installMaterial(); // Sets the material properties
  glVertexPointer(3, GL_FLOAT, 0, floorVerts);
  glTexCoordPointer(2, VERTTYPEENUM, 0, floorUV);
  glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

Note that the BMW functions are functions in our framework that encapsulate OpenGL functionality. You can pretty much treat them as pseudo-code in this example.

So, there you have it. A use of depth culling and transparency to implement functionality that I originally though required a stencil buffer. I’d be intereste in hearing about any examples you have of similar effects!

2 Responses to Look Ma, No Stencil Buffer

  1. paul

    Here’s a great thread from iDevGames on the same subject that uses texture combining (the iPhone has two texture units) and suggests some other methods to achieve similar results. At some point, I’ll try this alternative and see which performs better for my application. http://www.idevgames.com/forum/showthread.php?t=16049

  2. itunedownload

    interesting web log. It would be great if you can provide more contingents about it. Thanks you.