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
.
ReadMe.txt
main.m
OpenGLAppDelegate
EAGLView
ESRenderer.h
declares the
ESRenderer
protocol.ESRenderer
protocol
ES1Renderer
ES2Renderer
GLES2Sample-Info.plist
MainWindow.xib
GLES2Sample.xcodeproj
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.
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;
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} };
render
method of classes
ES1Renderer
and
ES2Renderer
,
change
glClear(GL_COLOR_BUFFER_BIT);to
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
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;
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
ES1Renderer.m
,
after the import.
//Convert degrees to radians. #define RADIANS(degrees) ((degrees) * M_PI / 180.0)
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 );
ES1Renderer
in
ES1Renderer.h
.
GLfloat theta; //bounce the box; in degreesIn 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;
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 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 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.
To allow the app to use OpenGL ES 2.0, comment the following statement back in.
renderer = [[ES2Renderer alloc] init];
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
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
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
.
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);
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 };