main.m
GLAppDelegate
GLViewController
EAGLView
GL-Info.plist
Shader.vsh
vertexShader.fsh
fragment
File → New Project…
Choose a template for your new project: OpenGL ES Application
Do not remove the
MainWindow.xib
file from the project.
Do not remove the “Main nib file base name”
from the
Info.plist
file.
The
DEBUG
macro in
GLViewController.m
is already defined;
it will send any error messages to the Xcode Console window.
You can see its definition (as the empty string) in
Project →
Edit Active Target "GL" →
Build →
GCC 4.2 - Preprocessing →
Preprocessor Macros
Project →
Edit Active Target "GL" →
General → Linked Libraries
You should have four linked libraries:
Foundation.framework
UIKit.framework
OpenGLES.framework
QuartzCore.framework
The view controller is usually created by the application delegate.
But this view controller is unarchived from the
MainWindow.xib
file,
leaving the application delegate with little to do except call the
startAnimation
and
stopAnimation
methods of the view controller.
The view controller contains an instance variable named
view
that points to the view it controls.
This view is of class
EAGLView
.
The view controller creates an
EAGLContext
.
This context is a souped-up version of the humble
CGContextRef
we used for 2D graphics in
Japan.
The view controller prefers to create an OpenGL ES 2.0 context,
but it will settle for an OpenGL ES 1.0 context.
As a C programmer,
I have a rage to do as much as possible in a single expresion.
I would like to change
EAGLContext *aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!aContext) { aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; } if (!aContext) NSLog(@"Failed to create ES context"); else if (![EAGLContext setCurrentContext:aContext]) NSLog(@"Failed to set ES context current");in the
awakeFromNib
method of the view controller
to the following,
making the parallelism between 1.0 and 2.0 as conspicuous as possible.
EAGLContext *aContext; if ((aContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2]) == nil && (aContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES1]) == nil) { NSLog(@"Failed to create ES context"); } else if (![EAGLContext setCurrentContext: aContext]) { NSLog(@"Failed to set ES context current"); }
On my iOS 4.1, the first call to
initWithAPI:
produces the following Console output,
after which
NSLog
no longer works.
Detected an attempt to call a symbol in system libraries that is not present on the iPhone: open$UNIX2003 called from function _ZN4llvm12MemoryBuffer7getFileEPKcPSsx in image libLLVMContainer.dylib. etc.
When the application delegate calls
startAnimation
,
the view controller will start calling its own
drawFrame
method 60 times per second.
To get the 60 hertz cycle,
the view controller will create either a
CADisplayLink
(as in Pong)
or an
NSTimer
.
It would prefer to create a
CADisplayLink
.
As a C programmer,
I would like to change
displayLinkSupported = FALSE; // Use of CADisplayLink requires iOS version 3.1 or greater. // The NSTimer object is used as fallback when it isn't available. NSString *reqSysVer = @"3.1"; NSString *currSysVer = [[UIDevice currentDevice] systemVersion]; if ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending) displayLinkSupported = TRUE;to
// Use of CADisplayLink requires iOS version 3.1 or greater. // The NSTimer object is used as fallback when it isn't available. NSString *reqSysVer = @"3.1"; NSString *currSysVer = [[UIDevice currentDevice] systemVersion]; //Let displayLinkSupported be YES if currSysVer >= reqSysVer. displayLinkSupported = [currSysVer compare: reqSysVer options: NSNumericSearch] != NSOrderedAscending;in the
awakeFromNib
method of the view controller.
The
drawFrame
method of the view controller,
called 60 times per second,
draws the picture.
The
glClearColor
designates the gray background color.
Each pair of
GLfloat
s
in the array
squareVertices
represent a vertex.
That’s why
drawFrame
passes the arguments
2
and
GL_FLOAT
to the functions
glVertexAttribPointer
(OpenGL ES 2.0)
and
glVertexPointer
(OpenGL ES 1.1).
OpenGL ES puts the origin (0, 0) at the center of the picture,
with the X axis pointing right and the Y axis pointing up.
The first point in the array is therefore the lower left corner of the square.
As we are about to see,
it is colored yellow.
Each quartet of
GLubyte
s
in the array
squareColors
represents an rgbα color.
That’s why
drawFrame
passes the arguments
4
and
GL_UNSIGNED_BYTE
to a second call to
glVertexAttribPointer
(OpenGL ES 2.0)
and to
glColorPointer
.
We start at the beginning of each array (at element zero)
and draw four points.
That’s why
drawFrame
passes the arguments
0
and
4
to
glDrawArrays
.
As a C programmer,
I’d rather not count the 4 points in
squareVertices
myself.
I’d rather say
#define D 2 //number of dimensions #define V (sizeof squareVertices / (D * sizeof squareVertices[0])) //number of vertices glDrawArrays(GL_TRIANGLE_STRIP, 0, V);
The
drawFrame
method
draws a strip of triangles consisting of two triangles
determined by four vertices.
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);Our array
squareVertices
contains four vertices:
black 2-----3 purple |\ | | \ | | \ | | \ | | \| yellow 0-----1 cyan (a mixture of green and blue)But we could easily have an array of more of them:
6-----7 |\ | | \ | | \ | | \ | | \| 4-----5 |\ | | \ | | \ | | \ | | \| 2-----3 |\ | | \ | | \ | | \ | | \| 0-----1
The variables passed to the
“vertex shader”
function in
Shader.vsh
are called
uniforms.
We have one example:
the variable
transY
in
drawFrame
is passed to the shader,
arriving there under the name of
translate
.
static float transY = 0.0f; //angle in radians
Here’s where we plug in the names
transY
and
translate
.
The array
uniforms
at the top of
GLViewController.m
contains one element for each uniform.
There is only one uniform
(transY
),
so the array contains only one element.
The enumeration
UNIFORM_TRANSLATE
is the subscript of the element.
// Uniform index. enum { UNIFORM_TRANSLATE, NUM_UNIFORMS }; GLint uniforms[NUM_UNIFORMS];We put a value into the element at the end of the
loadShaders
method at the end of
GLViewController.h
:
// Get uniform locations. uniforms[UNIFORM_TRANSLATE] = glGetUniformLocation(program, "translate");We use the value of this element in
drawFrame
:
// Update uniform value. glUniform1f(uniforms[UNIFORM_TRANSLATE], (GLfloat)transY); transY += 0.075f;
Shader.vsh
and
Shader.fsh
are two simple programs that get compiled and linked by the app.
As each frame is drawn,
Shader.vsh
is executed once for each vertex,
and
Shader.fsh
is executed once for each pixel (fragment).
They are written in the
OpenGL ES
Shading Language,
which looks a lot like C.
Shaders are present only in OpenGL ES 2.0, not 1.1.
The following methods belong to the view controller.
The
awakeFromNib
method calls the
loadShaders
method,
which calls
compileShader
(twice)
and
linkProgram
.
Every
view
has a property named
layer
pointing to the view’s layer object.
For example, a plain vanilla view has a plain vanilla layer
of class
CALayer
.
UIView *view = [[UIView alloc] init]; NSLog(@"view.layer belongs to class %@.", view.layer.class);
[Session started at 2011-05-13 11:54:30 -0400.] 2011-05-13 11:54:31.297 GL[3283:207] view.layer belongs to class CALayer.
But thanks to the
layerClass
method of class
EAGLView
in
EAGLView.m
,
an
EAGLView
object will have a much more elaborate layer of class
CAEAGLayer
.
A
renderbuffer
is the area of memory where a picture will be drawn.
Each renderbuffer has an identifying number given to it by
glGenRenderbuffers
.
The identifying number of our renderbuffer is stored in the
colorRenderbuffer
instance variable of the view.
See the
createFramebuffer
method of class
EAGLView
.
Somewhat redundantly, the call to
glBindRenderbuffer
causes our color renderbuffer to be used as a renderbuffer.
The view’s layer is attached to this renderbuffer by calling
the
renderbufferStorage:fromDrawable:
method of the view.
Displaying a view will automatically display the view’s layer,
and displaying this view’s layer will now automatically display
the renderbuffer.
The view gets its width and height from
GLViewController.xib
.
The layer gets its width and height from the view,
and the renderbuffer gets its width and height from the layer.
The instance variables
framebufferWidth
and
framebufferHeight
get their values from the renderbuffer.
It takes more than a renderbuffer to draw a picture:
there might also be a
“depth buffer”
and a
“stencil buffer”.
The renderbuffer and all the other buffers are contained
in one master buffer called the
framebuffer.
Each framebuffer has an identifying number given to it by
glGenFramebuffers
.
The identifying number of our framebuffer is stored in the
defaultFramebuffer
instance variable of the view.
We insert the renderbuffer into the framebuffer by calling
glFramebufferRenderbuffer
.
drawFrame
method of the view controller drew a square consisting of a
GL_TRIANGLE_STRIP
consisting of two triangles.
The seven possibilities are listed in the documentation for
glDrawArrays
:
GL_POINTS
GL_LINE_STRIP
GL_LINE_LOOP
GL_LINES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
GL_TRIANGLES
GL_TRIANGLE_FAN
consisting of eight triangles.
Pass
D
instead of
2
to
glVertexAttribPointer
.
You might want to rename the arrays.
#define D 2 //number of dimensions static const GLfloat squareVertices[] = { 0.00f, 0.00f, //right triangle 0.50f, -0.25f, 0.50f, 0.25f, 0.25f, 0.50f, //upper right triangle -0.25f, 0.50f, //top triangle -0.50f, 0.25f, //upper left triangle -0.50f, -0.25f, //left triangle -0.25f, -0.50f, //lower left triangle 0.25f, -0.50f, //bottom triangle 0.50f, -0.25f //lower right triangle }; #define V (sizeof squareVertices / (D * sizeof squareVertices[0])) //number of vertices
static const GLubyte squareColors[] = { 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255 };
glDrawArrays(GL_TRIANGLE_FAN, 0, V);