Part VI - GL Paint Dissected - EAGLView.m

Last installment I went through the header file for the EAGLView. We saw that it was actually fairly straightforward with the addition of a delegate declarations, a depth buffer and some utility functions to convert coordinates.  Let's dive into the implementation and see how these are used.

The first thing we see are the property synthesizers followed by a class method:


@implementation EAGLView


@synthesize delegate=_delegate, autoresizesSurface=_autoresize, surfaceSize=_size, framebuffer = _framebuffer, pixelFormat = _format, depthFormat = _depthFormat, context = _context;


+ (Class) layerClass

{

return [CAEAGLLayer class];

}



This just returns the class of the CAEAGLLayer.

Next:

- (BOOL) _createSurface

{

CAEAGLLayer*         eaglLayer = (CAEAGLLayer*)[self layer];

CGSize newSize;

GLuint oldRenderbuffer;

GLuint oldFramebuffer;

if(![EAGLContext setCurrentContext:_context]) {

return NO;

}



This method creates a surface to draw on, and returns a flag indictating whether it was created or not.


The first attribute eagllayer points to our layer.  We then have a size and buffer pointers for rendering and our display buffer.  If we can't set a current context we return false (NO).


newSize = [eaglLayer bounds].size;

newSize.width = roundf(newSize.width);

newSize.height = roundf(newSize.height);


Here we set the size of the layer we are creating to our bounds rectangle.


glGetIntegerv(GL_RENDERBUFFER_BINDING_OES, (GLint *) &oldRenderbuffer);

glGetIntegerv(GL_FRAMEBUFFER_BINDING_OES, (GLint *) &oldFramebuffer);


We cast our pointers to integers using an OpenGL utility function glGetIntegerv (v stands for value).  


glGenRenderbuffersOES(1, &_renderbuffer);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, _renderbuffer);


This creates a new rendering buffer and binds to it.


if(![_context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:(id<EAGLDrawable>)eaglLayer]) {

glDeleteRenderbuffersOES(1, &_renderbuffer);

glBindRenderbufferOES(GL_RENDERBUFFER_BINDING_OES, oldRenderbuffer);

return NO;

} 


We then see if we can get the storage for the render buffer in our layer and if we can't we delete the renderbuffer we just created, unbind it, and return false from the method.  If we can't get the storage for the renderbuffer, we can't create our layer.  As a reminder, layers are related to the depthbuffer we saw in the declarations. 

glGenFramebuffersOES(1, &_framebuffer);

glBindFramebufferOES(GL_FRAMEBUFFER_OES, _framebuffer);

If we can get the renderbuffer set up we then generate a framebuffer and bind the pointer.


if (_depthFormat) {

glGenRenderbuffersOES(1, &_depthBuffer);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, _depthBuffer);

glRenderbufferStorageOES(GL_RENDERBUFFER_OES, _depthFormat, newSize.width, newSize.height);

glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, _depthBuffer);

}


If we're using a depthbuffer (see the the last installment), we generate and bind a depth buffer in the first two lines in the 'if' statement.  Next we get the framebuffer's storage, and attach the framebuffer to the renderbuffer. This essentially associates the two buffers so we can handle them together.


_size = newSize;

if(!_hasBeenCurrent) {

glViewport(0, 0, newSize.width, newSize.height);

glScissor(0, 0, newSize.width, newSize.height);

_hasBeenCurrent = YES;

}

else {

glBindFramebufferOES(GL_FRAMEBUFFER_OES, oldFramebuffer);

}

glBindRenderbufferOES(GL_RENDERBUFFER_OES, oldRenderbuffer);


Next we save off the size of the layer, and check our one-time flag (see the last installment). If the flag is not set (first time in)  we set our viewport.  The viewport is the virtual window we are looking through onto our scene.  In this case, the viewport is the size of our layer...we see our entire layer head on.  The glScissor call creates something known as a "scissor box".  The scissor box is more commonly known as a "drawing clipping rectangle". Things will only be drawn in the bounds of the scissor box even if what we draw goes outside the box. 


If this isn't the first time, we bind the framebuffer to our "old" buffer.  In either case we bind the renderbuffer to the "old" buffer.




// Error handling here

[_delegate didResizeEAGLSurfaceForView:self];

return YES;


Strangely, there is a comment for error handling but there is none.  We then invoke our delegate method to inform it we've resized our surface.  At this point the surface is created and we return true.


Next, we have a complimentary method that destroys a surface (one we created above).



- (void) _destroySurface

{

EAGLContext *oldContext = [EAGLContext currentContext];

if (oldContext != _context)

[EAGLContext setCurrentContext:_context];

if(_depthFormat) {

glDeleteRenderbuffersOES(1, &_depthBuffer);

_depthBuffer = 0;

}

glDeleteRenderbuffersOES(1, &_renderbuffer);

_renderbuffer = 0;


glDeleteFramebuffersOES(1, &_framebuffer);

_framebuffer = 0;

if (oldContext != _context)

[EAGLContext setCurrentContext:oldContext];

}



The first thing that happens here is we save our current context.  IF the current context isn't our old previous context, we make the previous context current.  If we have a depthbuffer, we delete it.  We delete our render and framebuffers.  We then restore the context we saved off in the first line as the current context.

- (id) initWithFrame:(CGRect)frame

{

return [self initWithFrame:frame pixelFormat:GL_RGB565_OES depthFormat:0 preserveBackbuffer:NO];

}


- (id) initWithFrame:(CGRect)frame pixelFormat:(GLuint)format 

{

return [self initWithFrame:frame pixelFormat:format depthFormat:0 preserveBackbuffer:NO];

}


We have some initializers here.  These allow us to create our EAGLEView using different initializers.  The next initWithFrame is a bit more complex so lets go through it.


- (id) initWithFrame:(CGRect)frame pixelFormat:(GLuint)format depthFormat:(GLuint)depth preserveBackbuffer:(BOOL)retained

{

if((self = [super initWithFrame:frame])) {

CAEAGLLayer* eaglLayer = (CAEAGLLayer*)[self layer];


The first line here just calls one of the previous init methods passing in our layer.


eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:

[NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];

This sets up the layer's drawable properties.  The two main ones sets up the ability to save off our buffer for later use (we do this in RGBA8 (bit) form).  The kEAGLDrawablePropertyColorFormat specifies a color object to use (nil in this case).  So we aren't using an internal color buffer.


_format = format;

_depthFormat = depth;


Save off the format and depth.


_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];

if(_context == nil) {

[self release];

return nil;

}


Create an EAGL context using initWithAPI:  This call initializes an EAGL context and specifies that the context conforms to OpenGL ES 1.0 calls.  If we can't get a context clean up and return.


Finally, try creating a surface (using the method above) and return an initialized view.


if(![self _createSurface]) {

[self release];

return nil;

}

}


return self;

}


The dealloc call cleans up our view by destroying the surface and releasing the context.


- (void) dealloc

{

[self _destroySurface];

[_context release];

_context = nil;

[super dealloc];

}


The next method:


- (void) layoutSubviews

{

CGRect bounds = [self bounds];

if(_autoresize && ((roundf(bounds.size.width) != _size.width) || (roundf(bounds.size.height) != _size.height))) {

[self _destroySurface];

[self _createSurface];

}

}



checks to see if autoresize is set and if it is the size has been changed. If it has we recreate the drawing surface in our view.

- (void) setAutoresizesEAGLSurface:(BOOL)autoresizesEAGLSurface;

{

_autoresize = autoresizesEAGLSurface;

if(_autoresize)

[self layoutSubviews];

}



The setAutoresizesEAGLSurface controls whether the surface autoresize or not based on the flag passed in.  

The next three methods are utility methods for dealing with the context:

- (void) setCurrentContext

{

if(![EAGLContext setCurrentContext:_context]) {

printf("Failed to set current context %p in %s\n", _context, __FUNCTION__);

}

}


- (BOOL) isCurrentContext

{

return ([EAGLContext currentContext] == _context ? YES : NO);

}


- (void) clearCurrentContext

{

if(![EAGLContext setCurrentContext:nil])

printf("Failed to clear current context in %s\n", __FUNCTION__);

}


These are straightforward so I won't explain them.


Next we have a method called swapBuffers.


- (void) swapBuffers

{

EAGLContext *oldContext = [EAGLContext currentContext];

GLuint oldRenderbuffer;

if(oldContext != _context)

[EAGLContext setCurrentContext:_context];


The first thing this does is save off the current context and creates a local renderBuffer pointer.  It then sees if the previous context (_context) the one we just saved.  if not we make the previous one current.


// CHECK_GL_ERROR();

glGetIntegerv(GL_RENDERBUFFER_BINDING_OES, (GLint *) &oldRenderbuffer);

glBindRenderbufferOES(GL_RENDERBUFFER_OES, _renderbuffer);


Again a comment with no code (a reminder) and we get the integer value of the old renderbuffer and save it in our local variable oldRenderbuffer.  We then bind to the previous renderbuffer (_renderbuffer).  This essentially makes the previous buffer the active one and we saved the current one--we swapped the render buffers.  This might be a little confusing with "previous" and "current" all over the place.  The key to understanding this is that in this example variables with an underscore as part of the name are "previous"ly active, whereas the names without the underscore are currently active.


if(![_context presentRenderbuffer:GL_RENDERBUFFER_OES])

printf("Failed to swap renderbuffer in %s\n", __FUNCTION__);


if(oldContext != _context)

[EAGLContext setCurrentContext:oldContext];

}


We check to see if the swap worked, and then we check our context and adjust our context accordingly.


The final two methods are the utility methods which convert coordinates between the view and the surface systems:


- (CGPoint) convertPointFromViewToSurface:(CGPoint)point

{

CGRect bounds = [self bounds];

return CGPointMake((point.x - bounds.origin.x) / bounds.size.width * _size.width, (point.y - bounds.origin.y) / bounds.size.height * _size.height);

}


- (CGRect) convertRectFromViewToSurface:(CGRect)rect

{

CGRect bounds = [self bounds];

return CGRectMake((rect.origin.x - bounds.origin.x) / bounds.size.width * _size.width, (rect.origin.y - bounds.origin.y) / bounds.size.height * _size.height, rect.size.width / bounds.size.width * _size.width, rect.size.height / bounds.size.height * _size.height);

}


@end



These utility methods are not OpenGL so I'll let you figure them out.  


Ok, that's the implementation of the view.  It just creates a view and manages the buffers and surface content.  It's all the infrastructure to the drawing logic we saw in PaintingView.m


That's it for the GLPaint example. We saw how to draw based on touch, how to replay stored drawing data, and how to set up the drawing environment.    


Again this example is pretty basic; it's not really game-quality.  But, the example does provide some basic tools you might use in a game.


So what's the next installment going to cover?  I think I want to take a basic OpenGL game and take it apart to learn how a simple OpenGL game works.  I'll try to find an example to go through for next time.


As always if you have comments or suggestions, feel free to email or Twitter me to let me know what you think.


Until next time...