Part Sixteen: The Depth Buffer

In the last chapter, we saw how back-face culling can solve some depth-related rendering issues, but we also determined that we can’t always use back-face culling if we need to see the back faces of our models for some reason.

The way to fix this is to implement a depth renderbuffer so OpenGL can keep track of what’s behind what for us.

First, let’s remove that back-face culling code we put in for the last chapter and have another look at our problematic cube.

    //glCullFace(GL_BACK); // ED: Removed
    //glEnable(GL_CULL_FACE); // ED: Removed

Building and running the program without the face culling brings us back to this.

 

 

Still strange. This time, we’ll fix this problem with a depth renderbuffer. Unlike our many previous changes to our rendering code, the addition of a depth renderbuffer requires that we modify the code that creates the framebuffer, and that’s in the EAGLView class. Let’s start with the EAGLView.h file.

#import <UIKit/UIKit.h>

#import <OpenGLES/ES1/gl.h>
#import <OpenGLES/ES1/glext.h>
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>

@class EAGLContext;

// This class wraps the CAEAGLLayer from CoreAnimation into a convenient UIView subclass.
// The view content is basically an EAGL surface you render your OpenGL scene into.
// Note that setting the view non-opaque will only work if the EAGL surface has an alpha channel.
@interface EAGLView : UIView {
@private
    // The pixel dimensions of the CAEAGLLayer.
    GLint framebufferWidth;
    GLint framebufferHeight;
   
    // The OpenGL ES names for the framebuffer and renderbuffer used to render to this view.
    GLuint defaultFramebuffer, colorRenderbuffer;
    GLuint depthRenderbuffer; // ED: Added
}

@property (nonatomic, retain) EAGLContext *context;

- (void)setFramebuffer;
- (BOOL)presentFramebuffer;

@end

Here we do nothing more than add a new variable to hold the name of the depth renderbuffer. OpenGL tracks these renderbuffers with a GLuint (unsigned integer) type.

Next, in the EAGLView.m file, we modify the createFramebuffer method to create and attach a new depth renderbuffer to the framebuffer.

- (void)createFramebuffer
{
    if (context && !defaultFramebuffer) {
        [EAGLContext setCurrentContext:context];
       
        // Create default framebuffer object.
        glGenFramebuffers(1, &defaultFramebuffer);
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer);
       
        // Create color render buffer and allocate backing store.
        glGenRenderbuffers(1, &colorRenderbuffer);
        glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
        [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &framebufferWidth);
        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &framebufferHeight);

        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer);
       
        // ED: Added to create a depth render buffer
        glGenRenderbuffers(1, &depthRenderbuffer);
        glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, framebufferWidth, framebufferHeight);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);
       
        glEnable(GL_DEPTH_TEST);
        // ED: End of code changes
       
        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
            NSLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}

Our new depth renderbuffer code is nearly identical in structure to the color renderbuffer code, with only a couple small differences.

In the color renderbuffer initialization code, we see the following code that isn’t in the depth renderbuffer logic.

        [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];

This line allows us to draw from the color renderbuffer to the iPhone screen. Although the color renderbuffer was never explicitly named in this message, the color renderbuffer was bound to the GL_RENDERBUFFER target by the previous glBindRenderbuffer() call. Since OpenGL is a state machine, and the color renderbuffer was bound at the time of the call, the color renderbuffer is used when the GL_RENDERBUFFER target is referenced.

The other difference is that after we create and attach our depth renderbuffer, we use the following code to enable it.

        glEnable(GL_DEPTH_TEST);

This lets OpenGL know that there’s a place it can ‘think’ about depth calculations, so it should handle proper depth-related rendering for us.

The last change in the EAGLView.m file is in the deleteFramebuffer method. Since we’ve created a new renderbuffer, we need to make sure we free its resources when we delete the framebuffer that it’s attached to.

- (void)deleteFramebuffer
{
    if (context) {
        [EAGLContext setCurrentContext:context];
       
        if (defaultFramebuffer) {
            glDeleteFramebuffers(1, &defaultFramebuffer);
            defaultFramebuffer = 0;
        }
       
        if (colorRenderbuffer) {
            glDeleteRenderbuffers(1, &colorRenderbuffer);
            colorRenderbuffer = 0;
        }

        // ED: Added block to delete new depth renderbuffer
        if (depthRenderbuffer) {
            glDeleteRenderbuffers(1, &depthRenderbuffer);
            depthRenderbuffer = 0;
        }
        // ED: And code changes
    }
}

Our new code is identical to the existing color renderbuffer code, but for the depth renderbuffer instead.

In the EDCubeDemoViewController.m file, the drawFrame method needs a few updates. The updated method will look like this.

- (void)drawFrame
{
    [(EAGLView *)self.view setFramebuffer];
   
    static const GLfloat cubeVertices[] = {
        // Front face
        -1,-1,1, 1,-1,1, -1,1,1, 1,1,1,
        // Right face
        1,1,1, 1,-1,1, 1,1,-1, 1,-1,-1,
        // Back face
        1,-1,-1, -1,-1,-1, 1,1,-1, -1,1,-1,
        // Left face
        -1,1,-1, -1,-1,-1, -1,1,1, -1,-1,1,
        // Bottom face
        -1,-1,1, -1,-1,-1, 1,-1,1, 1,-1,-1,
       
        // move to top
        1,-1,-1, -1,1,1,
       
        // Top Face
        -1,1,1, 1,1,1, -1,1,-1, 1,1,-1
    };

    static const GLfloat cubeTexCoords[] = {
        // Front face
        0,0, 1,0, 0,1, 1,1,
        // Right face
        0,1, 0,0, 1,1, 1,0,
        // Back face
        0,0, 1,0, 0,1, 1,1,
        // Left face
        0,1, 0,0, 1,1, 1,0,
        // Bottom face
        0,1, 0,0, 1,1, 1,0,
       
        1,0, 0,0,
       
        // Top face
        0,0, 1,0, 0,1, 1,1
    };
   
    static float transY = 0.0f;
    static float transX = 0.0f;
   
    GLfloat mvpMatrix[16];
   
    //glCullFace(GL_BACK); // ED: Removed
    //glEnable(GL_CULL_FACE); // ED: Removed
   
    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    //glClear(GL_COLOR_BUFFER_BIT); // ED: Removed
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // ED: Added
   
    if ([context API] == kEAGLRenderingAPIOpenGLES2) {
        glEnable(GL_TEXTURE_2D);
       
        glBindTexture(GL_TEXTURE_2D, textureName);
       
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        // Use shader program.
        glUseProgram(program);
       
        [EDMatrixTools applyIdentity:mvpMatrix];
       
        [EDMatrixTools applyScale:mvpMatrix x:0.50f y:0.50f z:0.50f];
        [EDMatrixTools applyRotation:mvpMatrix x:(transX / 6) y:(transX / 6) z:0];
        [EDMatrixTools applyTranslation:mvpMatrix x:0 y:0 z:-3]; // ED: Added
       
        //[EDMatrixTools applyProjection:mvpMatrix aspect:1.5]; // ED: Removed
        [EDMatrixTools applyProjection:mvpMatrix fov:45 aspect:1.5 near:1 far:100]; // ED: Added
               
        // Update uniform value.
        glUniformMatrix4fv(uniforms[UNIFORM_MVP_MATRIX], 1, 0, mvpMatrix);
        glUniform1i(uniforms[UNIFORM_TEXTURE], 0);
        transY += 0.075f;  
        transX += 0.075f;
       
        // Update attribute values.
        glVertexAttribPointer(ATTRIB_VERTEX, 3, GL_FLOAT, 0, 0, cubeVertices);
        glEnableVertexAttribArray(ATTRIB_VERTEX);
        glVertexAttribPointer(ATTRIB_TEXTURE_COORD, 2, GL_FLOAT, 0, 0, cubeTexCoords);
        glEnableVertexAttribArray(ATTRIB_TEXTURE_COORD);
       
        // Validate program before drawing. This is a good check, but only really necessary in a debug build.
        // DEBUG macro must be defined in your debug configurations if that's not already the case.
#if defined(DEBUG)
        if (![self validateProgram:program]) {
            NSLog(@"Failed to validate program: %d", program);
            return;
        }
#endif
    }
   
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 26);
   
    [(EAGLView *)self.view presentFramebuffer];
}

Let’s look at each of the changes individually.

    //glCullFace(GL_BACK); // ED: Removed
    //glEnable(GL_CULL_FACE); // ED: Removed

First, we’re going to remove the face culling we played with earlier. We don’t need to cull faces with proper depth handling, although if you create a scene where you never see back faces you should cull them to increase performance. I’m not going to cull them anymore in this project because we’ll be going inside the cube in the next chapter.

    //glClear(GL_COLOR_BUFFER_BIT); // ED: Removed
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // ED: Added

Now that we have a depth renderbuffer, we need that cleared between renders, too, just like the color renderbuffer.

        [EDMatrixTools applyTranslation:mvpMatrix x:0 y:0 z:-3]; // ED: Added

Now that we’re playing around in a 3D space, we need to move the object back into the screen a bit for the camera to see it. We’ll be beefing up the projection matrix in this chapter, too, and we’ll need to make sure that the object is between the near frame and the far frame.

Earlier, when our scene had no depth, the Z axis positioning didn’t matter. When you render a scene without regard for the Z-axis, it’s called orthogonal. If you look back up at the last few cubes we rendered, you can see that they don’t seem to have any depth. No matter how far the back face is from the front face, the faces are all the same size.

In real life, objects seem to get smaller as they move farther away, and that’s what we’re going to take care of in our program now.

        //[EDMatrixTools applyProjection:mvpMatrix aspect:1.5]; // ED: Removed
        [EDMatrixTools applyProjection:mvpMatrix fov:45 aspect:1.5 near:1 far:100]; // ED: Added

Now that our scene has depth, we need to add a few things. The fov variable specifies a field of vision. It’s basically the full range of vision of the camera for this scene. The wider the field of vision is, more objects can be seen at once, but they’ll be smaller. Depending on how many objects are in your scene, and where they are in relation to each other, you may need to increase or decrease the field of vision in your own scenes.

The function of the aspect ratio hasn’t changed.

The near and far variables simply define how close to you or how far away from you objects can be and still be seen. In this example, any object 1 unit away or farther can be seen, up until 100 units away. After 100 units, the object can no longer be seen.

 

 

In order for an object to be seen, it must fall between the near and far planes and be within the field of vision.

Let’s modify our applyProjection method in the EDMatrixTools class. First, we’ll update the method definition in EDMatrixTools.h.

//+ (void)applyProjection:(GLfloat *)m aspect:(GLfloat)aspect; // ED: Removed
+ (void)applyProjection:(GLfloat *)m fov:(GLfloat)fov aspect:(GLfloat)aspect near:(GLfloat)near far:(GLfloat)far; // ED: Added

Next, we’ll update the method itself.

/* ED: Removed
+ (void)applyProjection:(GLfloat *)m aspect:(GLfloat)aspect {
    GLfloat tempMatrix[16];
   
    [self applyIdentity:tempMatrix];
   
    tempMatrix[0] = 1;
    tempMatrix[5] = 1 / aspect;
   
    [self multiplyMatrix:tempMatrix by:m giving:m];
}
 */


// ED: Added method

+ (void)applyProjection:(GLfloat *)m fov:(GLfloat)fov aspect:(GLfloat)aspect near:(GLfloat)near far:(GLfloat)far {
    GLfloat tempMatrix[16];
   
    [self applyIdentity:tempMatrix];
   
    GLfloat r = fov * M_PI / 180.0f;
    GLfloat f = 1.0f / tanf(r / 2.0f);
   
    tempMatrix[0] = f;
    tempMatrix[5] = f / aspect;
    tempMatrix[10] = -((far + near) / (far - near));
    tempMatrix[11] = -1;
    tempMatrix[14] = -(2 * far * near / (far - near));
    tempMatrix[15] = 0;
   
    [self multiplyMatrix:tempMatrix by:m giving:m];
}

For this update, I just commented out the old method and created a new one.

    GLfloat tempMatrix[16];
   
    [self applyIdentity:tempMatrix];

We create a temporary matrix to use as our projection matrix, and apply the identity matrix to it.

    GLfloat r = fov * M_PI / 180.0f;
    GLfloat f = 1.0f / tanf(r / 2.0f);

Here, we’re converting our field of view from degrees into radians, and calculating the new scale of the vertex based on it’s position in the field of view.

    tempMatrix[0] = f;
    tempMatrix[5] = f / aspect;
    tempMatrix[10] = -((far + near) / (far - near));
    tempMatrix[11] = -1;
    tempMatrix[14] = -(2 * far * near / (far - near));
    tempMatrix[15] = 0;

You can see the projection matrix is actually part scale matrix, since array positions 0, 5, and 10 are the scale positions.

The -1 value in array position 11 is important because OpenGL is working with homogeneous coordinates. Remember how the gl_Position variable that we had to populate in the vertex shader was vec4 type? It represents a vertex as (X, Y, Z, W). Normally, we ignore that W when we’re moving stuff around, rotating, or scaling, but when we work with the projection matrix, we need that W valued correctly so OpenGL will render everything correctly.

When that -1 gets multiplied by our Z coordinate, which is negative here, it will turn positive, and be used as our W coordinate. Putting that 0 in array position 15 makes sure that the W coordinate stays correct.

Let’s build and run and see what happens.

 

 

Now OpenGL is handling our depth management, and the cube even appears to get smaller as it get farther away.

We’ve now played with textures, creating a 3D object and implementing the depth buffer, rotating, scaling, translating, and applying a projection matrix. Our cube is now very pretty, but it would be nicer if it were more interactive.

In the next chapter, we’re going to allow the cube to be rotated arbitrarily by touch, and throw in a zoom feature to reveal the insides of the cube as well.

Part Fifteen | Index | Part Seventeen