Three Dimensions in OpenGL ES

The previous project had only two dimensions, x and y. We will now add a z dimension for depth. To prove that we have three dimensions, we will draw a cube with a triangle removed from its front face so we can see the inside.

threed.zip is the completed, three-dimensional version of this project. The name of the project contained in this zip file is GLES2Sample.

Source code in threed.zip

  1. ReadMe.txt
  2. main.m
  3. Class OpenGLAppDelegate
  4. Class EAGLView
  5. Class ESRenderer.h declares the ESRenderer protocol.
  6. Classes that adopt the ESRenderer protocol
    1. Class ES1Renderer
    2. Class ES2Renderer
  7. Shaders
    1. template.vsh
    2. template.fsh
  8. Matrix functions written in the language C
    1. matrix.h
    2. matrix.c
  9. GLES2Sample-Info.plist
  10. MainWindow.xib
  11. GLES2Sample.xcodeproj
    1. project.pbxproj
    2. scholar.mode1v3
    3. scholar.pbxuser
    4. lsang.mode1v3

Create the project

Download the project GLESSample.zip from here. It’s just like the previous project, except that the square rotates instead of bounces. It also contains the pair of files matrix.h and matrix.c. Build and Run. To avoid yellow warnings, I had to change the Overview to Simulator - 3.1.3 | Debug.

We will now add a third dimension.

Changes common to OpenGL ES 1.1 and 2.0

  1. In ES1Renderer.h and ES2Renderer.h, add the following instance variable to classes ES1Renderer and ES2Renderer.
    	//OpenGL name for the depth buffer that is attached to viewFramebuffer,
    	//if it exists (0 if it does not exist)
    	GLuint depthRenderbuffer;
    
  2. In the render method of classes ES1Renderer and ES2Renderer, change the squareVertices and squareColors arrays to the following.

    	typedef GLfloat vertex_t[3];	//A vertex is an array of 3 floats.
    	typedef vertex_t triangle_t[3];	//A triangle is an array of 3 vertices.
    
    	static const triangle_t cubeVertices[] = {
    		{{ 1, -1,  1}, { 1,  1,  1}, {-1,  1,  1}},	//front: z == 1
    		{{ 1, -1,  1}, {-1,  1,  1}, {-1, -1,  1}},
    
    		{{ 1, -1, -1}, { 1,  1, -1}, {-1,  1, -1}},	//rear: z == -1
    		{{ 1, -1, -1}, {-1,  1, -1}, {-1, -1, -1}},
    
    		{{ 1,  1,  1}, { 1,  1, -1}, {-1,  1, -1}},	//top: y == 1
    		{{ 1,  1,  1}, {-1,  1, -1}, {-1,  1,  1}},
    
    		{{ 1, -1,  1}, { 1, -1, -1}, {-1, -1, -1}},	//bottom: y == -1
    		{{ 1, -1,  1}, {-1, -1, -1}, {-1, -1,  1}},
    
    		{{ 1, -1, -1}, { 1,  1, -1}, { 1,  1,  1}},	//right: x == 1
    		{{ 1, -1, -1}, { 1,  1,  1}, { 1, -1,  1}},
    
    		{{-1, -1, -1}, {-1,  1, -1}, {-1,  1,  1}},	//left: x == -1
    		{{-1, -1, -1}, {-1,  1,  1}, {-1, -1,  1}}
    	};
    
    	typedef GLubyte color_t[4];	//A color is an array of 4 bytes: rgbα
    
    	static const color_t cubeColors[] = {
    		{255,   0,   0, 255}, {255,   0,   0, 255}, {255,   0,   0, 255}, //front red
    		{255,   0,   0, 255}, {255,   0,   0, 255}, {255,   0,   0, 255},
    
    		{  0, 255,   0, 255}, {  0, 255,   0, 255}, {  0, 255,   0, 255}, //rear green
    		{  0, 255,   0, 255}, {  0, 255,   0, 255}, {  0, 255,   0, 255},
    
    		{  0,   0, 255, 255}, {  0,   0, 255, 255}, {  0,   0, 255, 255}, //top blue
    		{  0,   0, 255, 255}, {  0,   0, 255, 255}, {  0,   0, 255, 255},
    
    		{255, 255,   0, 255}, {255, 255,   0, 255}, {255, 255,   0, 255}, //bottom yellow
    		{255, 255,   0, 255}, {255, 255,   0, 255}, {255, 255,   0, 255},
    
    		{  0, 255, 255, 255}, {  0, 255, 255, 255}, {  0, 255, 255, 255}, //right cyan
    		{  0, 255, 255, 255}, {  0, 255, 255, 255}, {  0, 255, 255, 255},
    
    		{255,   0, 255, 255}, {255,   0, 255, 255}, {255,   0, 255, 255}, //left purple
    		{255,   0, 255, 255}, {255,   0, 255, 255}, {255,   0, 255, 255}
    	};
    
  3. In the render method of classes ES1Renderer and ES2Renderer, change
    	glClear(GL_COLOR_BUFFER_BIT);
    
    to
    	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    

Changes to ES1Renderer for OpenGL ES 1.1

To force the app to use OpenGL ES 1.1, comment out the

		renderer = [[ES2Renderer alloc] init];
in the initWithCoder: method of class EAGLView. renderer will be initialized to nil. To make sure you’re getting OpenGL ES 1.1, insert
		NSLog(@"%@", [renderer class]);
immediately before the
		animating = FALSE;

  1. In the render method of class ES1Renderer, change
    	glVertexPointer(2, GL_FLOAT, 0, squareVertices);
    	glColorPointer(4, GL_UNSIGNED_BYTE, 0, squareColors);
    	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    to
    	glVertexPointer(3, GL_FLOAT, 0, cubeVertices);	//three dimensions
    	glColorPointer(4, GL_UNSIGNED_BYTE, 0, cubeColors);
    	glDrawArrays(GL_TRIANGLES, 3, 6 * 6 - 3);	//omit half of the front
    
  2. Insert the following at the top of ES1Renderer.m, after the import.
    //Convert degrees to radians.
    #define RADIANS(degrees)	((degrees) * M_PI / 180.0)
    
  3. In the render method of class ES1Renderer, change
    	glOrthof(-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f);
    
    to the following. Cast the dividend to GLfloat to avoid truncation to integer.
    	glEnable(GL_DEPTH_TEST);
    
    	GLfloat near = .1;	//distance from viewer to near face of frustum
    	//Horizontal field of view is 45 degrees.
    	//length is half the horizontal length of the near face of the frustum.
    	GLfloat length = near * tan(RADIANS(45.0f / 2.0f));
    	GLfloat ratio = (GLfloat)backingHeight / backingWidth;
    
    	glFrustumf(
    		-length,		//left
    		length,			//right
    		-length * ratio,	//bottom
    		length * ratio,		//top
    		near,			//near
    		10.0			//far
    	 );
    
  4. I think a bounce is more legible than a rotation. Add the following instance variable to class ES1Renderer in ES1Renderer.h.
    	GLfloat theta;	//bounce the box; in degrees
    
    In the render method of class ES1Renderer, change
    		glRotatef(3.0f, 0.0f, 0.0f, 1.0f);
    
    to
    		glLoadIdentity();
    		glTranslatef(0.0f, sinf(RADIANS(theta)), -5.0f);
    		theta += 3.0f;
    
  5. In the resizeFromLayer: method of class ES1Renderer, add the following statements immediately before the if statement. They initialize the instance variable we just added.
    	glGenRenderbuffersOES(1, &depthRenderbuffer);
    	glBindRenderbufferOES(GL_RENDERBUFFER_OES, depthRenderbuffer);
    
    	glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES,
    		backingWidth, backingHeight);
    
    	glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,
    		GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES,
    		depthRenderbuffer);
    

The frustum in OpenGL ES 1.1

The viewer is located at the origin. The positive X axis points to the right. The positive Y axis points up. The positive Z axis points to the rear. Objects along the positive Z axis will be behind the observer and invisible to him or her. The negative Z axis points forward, in the direction in which the viewer is looking.

A frustum is a pyramid with its top cut off. This exposes the rectangular surface called the near surface, colored yellow in this diagram. The base of the frustum is called the far surface. It is larger than the near surface and farther from the viewer.

The viewer’s horizontal field of view is 45°. His or her vertical field of view is (almost but not quite) 60°, because the near surface is taller than it is wide. It has the same 2 to 3 aspect ratio as the iPhone’s screen.

The near surface is .1 units from the viewer. To give the user a horizontal field of view of 45°, the width of the near surface must be 2 × .1 × tan(45° / 2). The height of the near surface is 3/2 of this amount.

           ---------------------------------------   ← the far surface
            \                                   /
             \                                 /
              \                               /
               \                             /
                \                           /
                 \                         /
                  \             .1tan θ   /
              left -----------+----------- right     ← the near surface
                    \         |         /
                     \        |        /
                      \       |       /
                       \      |.01   /
                        \     |     /
                         \    |    /
                          \   |   /
                           \  |  /
                            \ |θ/   θ = 22½°
                             \|/
                              viewer

The last two arguments of glFrustumf are unsigned distances from the viewer. They are always positive.

Changes to ES2Renderer for OpenGL ES 2.0

To allow the app to use OpenGL ES 2.0, comment the following statement back in.

		renderer = [[ES2Renderer alloc] init];
  1. In the render method of class ES2Renderer, change
    	glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, squareVertices);
    	glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, 1, 0, squareColors);
    	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    
    to
    	glVertexAttribPointer(ATTRIB_VERTEX, 3, GL_FLOAT, 0, 0, cubeVertices);	//3D
    	glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, 1, 0, cubeColors);
    	glDrawArrays(GL_TRIANGLES, 3, 6 * 6 - 3); //omit half of front face
    
  2. In the render method of class ES2Renderer, change
    	mat4f_LoadOrtho(-1.0f, 1.0f, -1.5f, 1.5f, -1.0f, 1.0f, proj);
    
    to the following. Cast the dividend to float to avoid truncation to integer. The second argument is the width-to-height aspect ratio of the screen.
    	glEnable(GL_DEPTH_TEST);
    	float hfield = 45.0f;	//horzontal field of view in degrees
    
    	//vertical field of view in radians
    	float vfield = 2.0f * atan2f((float)backingHeight / backingWidth,
    		1.0f / tanf((hfield / 2.0f) * M_PI / 180.0f));
    
    	mat4f_LoadPerspective(vfield, (float)backingWidth / backingHeight,
    		.1f,	//zNear
    		10.0f,	//zFar
    		proj
    	);
    

    The narrow angle (22½°) is half of the horizontal field of view. The wider angle, between the vertical line and the line of dots, is half of the vertical field of view.

                                     1      .5
                               +---------------- total length of horizontal line is
                               |         /     . backingHeight / backingWidth
                               |        /     .
                               |       /    .
    length of vertical line is |      /    .
                 1 / tan(22½°) |     /   .
                               |22½°/   .
                               |   /  .
                               |  /  .
                               | / .
                               |/.
                               viewer
    
  3. I think a bounce is more legible than a rotation. In the render method of class ES2Renderer, change
    	// setup modelview matrix (rotate around z)
    	mat4f_LoadZRotation(rotz, modelview);
    
    to
    	float axis[3] = {0.0f, sinf(rotz), -5.0f};
    	mat4f_LoadTranslation(axis, modelview);
    
    
    You could also rename rotz.
  4. In the resizeFromLayer: method of class ES2Renderer, add the following statements immediately before the if statement. They initialize the instance variable we added to ES2Renderer.
    	glGenRenderbuffers(1, &depthRenderbuffer);
    	glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
    
    	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16,
    		backingWidth, backingHeight);
    
    	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
    		GL_RENDERBUFFER, depthRenderbuffer);
    

Programming with C preprocessor macros

The X axis points right, the Y axis points up, the Z axis towards us.

                        G-----------F
                       /           /|
                      C-----------B |
                      |           | |
                      |           | |
Point H is hidden.    |           | E
                      |           |/
                      D-----------A

In ES1Renderer and ES2Renderer, you can create the two arrays as follows.

	typedef GLfloat vertex_t[3];	//A vertex is an array of 3 floats.
	typedef vertex_t triangle_t[3];	//A triangle is an array of 3 vertices.

//vertices on front face
#define A { 1.0f, -1.0f,  1.0f}
#define B { 1.0f,  1.0f,  1.0f}
#define C {-1.0f,  1.0f,  1.0f}
#define D {-1.0f, -1.0f,  1.0f}

//vertices on rear face
#define E { 1.0f, -1.0f, -1.0f}
#define F { 1.0f,  1.0f, -1.0f}
#define G {-1.0f,  1.0f, -1.0f}
#define H {-1.0f, -1.0f, -1.0f}

#define FACE(a, b, c, d) {a, b, c}, {c, d, a}

	static const triangle_t cubeVertices[] = {
		FACE(A, B, C, D),	//front:  z ==  1
		FACE(E, F, G, H),	//rear:   z == -1

		FACE(B, F, G, C),	//top:    y ==  1
		FACE(A, D, H, E),	//bottom: y == -1

		FACE(A, E, F, B),	//right:  x ==  1
		FACE(C, G, H, D)	//left:   x == -1
	};

	typedef GLubyte color_t[4];	//A color is an array of 4 bytes: rgbα

#define RED    {255,   0,   0, 255}
#define GREEN  {  0, 255,   0, 255}
#define BLUE   {  0,   0, 255, 255}

#define YELLOW {255, 255,   0, 255}
#define CYAN   {  0, 255, 255, 255}
#define PURPLE {255,   0, 255, 255}

#define COLOR(c) c, c, c, c, c, c

	static const color_t cubeColors[] = {
		COLOR(RED),	//front
		COLOR(GREEN),	//rear

		COLOR(BLUE),	//top
		COLOR(YELLOW),	//bottom

		COLOR(CYAN),	//right
		COLOR(PURPLE)	//left
	};