In the last installment, we went through the AppController of GLPaint and saw that there was minimal OpenGL ES code in it. This time around, I'm going to tackle the files PaintingView.h and initWithFrame in PaintingView.m so let's dive right in.
The first thing we come across is a set of defines:
#define kBrushOpacity (1.0 / 3.0)
#define kBrushPixelStep 3
#define kBrushScale 2
#define kLuminosity 0.75
#define kSaturation 1.0
which itemize the characteristics of the "paint brush" we are using. If you want to see what they do, try changing some of these values and rerunning the sample.
The actual interface comes next:
@interface PaintingView : EAGLView
{
GLuint brushTexture;
GLuint drawingTexture;
GLuint drawingFramebuffer;
CGPoint location;
CGPoint previousLocation;
Boolean firstTouch;
}
@property(nonatomic, readwrite) CGPoint location;
@property(nonatomic, readwrite) CGPoint previousLocation;
- (void) erase;
@end
As you can see, this is a subclass of EAGLView which we went over in the first set of walkthroughs. The PaintingView here, intruduces a set of properties, the first three are OpenGL ES related. The first two define brushTexture, and drawingTexture as GLuints (again, this is an OpenGL ES-specific data type, namely an unsigned integer.) This probably has to do with the "fuzziness" you see when drawing in GLPaint.
The next two are iPhone CoreGraphics (CG) point definitions. These aren't OpenGL ES. When you draw on the screen you have to draw by keeping track of the points your "touch" passes over in the view. Specifically, we need to keep track of the last point, and the current point. We draw a line between the two points. We then set the last point to be the current point and as the finger moves, it writes to a new point. We then connect these two points, and continue as long as the finger is touching the screen. I'm betting that's what these two properties are used for.

We have the property declarations for these properties, and finally a single method to erase the display. That's it for PaintingView.h
The .m file starts normally with the synthesis of the declared properties:
@implementation PaintingView
@synthesize location;
@synthesize previousLocation;
The first method is initWithFrame
- (id) initWithFrame:(CGRect)frame
{
NSMutableArray* recordedPaths;
CGImageRef brushImage;
CGContextRef brushContext;
GLubyte *brushData;
size_t width, height;
This declares some local properties. The first property is interesting. It's a mutable array called "recordedPaths". I bet this will keep track of the points we move through (see image above) or the opening animation--we'll see which in a moment.. The brush image which is declared as a CGImageRef, and then a brush context. These are again CoreGraphics objects.
This is followed by brushData which is an OpenGL ES unishined byte. At this point, I'm not sure what this is used for yet.
We then get some dimensions for managing the width and height--of what? Again, we don't have enough to guess at. So let's look at the code and find out.
The first line is an "if" statement:
if((self = [super initWithFrame:frame pixelFormat:GL_RGB565_OES depthFormat:0 preserveBackbuffer:YES])) {
[self setCurrentContext];
What this does is call our parent's initWithFrame and passes in a specific pixel format: GL_RGB565_OES. Doing a little digging this turns out to be a constant that allows your Open GL ES program to deal with textures (rather than solid colors). Textures are managed and stored in their own separate memory areas known as "texture memory" by Open GL. We then make our PaintingView the current drawing context.
// Create a texture from an image
// First create a UIImage object from the data in a image file, and then extract the Core Graphics image
brushImage = [UIImage imageNamed:@"Particle.png"].CGImage;
// Get the width and height of the image
width = CGImageGetWidth(brushImage);
height = CGImageGetHeight(brushImage);
This block of code pulls the "Particle.png" image from our resources and makes it our brush image.

But when you run the program your brush doesn't look like this, what's happening? Well it appears the program doesn't use the Particle.png as a brush but rather as a texture to draw with.
Note the next comment:
// Texture dimensions must be a power of 2. If you write an application that allows users to supply an image,
// you'll want to add code that checks the dimensions and takes appropriate action if they are not a power of 2.
So what are the width and height? Run the app in the debugger and you see that width = height = 64, which is a power of 2. What happens if it's not? Change width or height and run it again and see! If width is set to 65 we get:

Is this wrong? Does it crash? No. The comment is just a warning that the results you get may not be perfect unless the dimensions are a power of 2. Maybe you want this effect.
The next block does a lot.:
// Make sure the image exists
if(brushImage) {
// Allocate memory needed for the bitmap context
brushData = (GLubyte *) malloc(width * height * 4);
// Use the bitmatp creation function provided by the Core Graphics framework.
brushContext = CGBitmapContextCreate(brushData, width, width, 8, width * 4, CGImageGetColorSpace(brushImage), kCGImageAlphaPremultipliedLast);
// After you create the context, you can draw the image to the context.
CGContextDrawImage(brushContext, CGRectMake(0.0, 0.0, (CGFloat)width, (CGFloat)height), brushImage);
// You don't need the context at this point, so you need to release it to avoid memory leaks.
CGContextRelease(brushContext);
If the image exists we draw the image to a temporary context that we set up in these lines. Now we get to some OpenGL ES.
// Use OpenGL ES to generate a name for the texture.
glGenTextures(1, &brushTexture);
glGenTextures is an OpenGL ES call that creates a texture name and returns it in the brushTexture variable. The 1 parameter says generate one name. Again, the way we do things in OpenGL is we create a name and then we bind/attach the actual thing to the generated name. Just so you know, the name that's returned is not really a name, but a unique integer.
// Bind the texture name.
glBindTexture(GL_TEXTURE_2D, brushTexture);
This associates our name with a 2D texture. The other option is GL_TEXTURE_1D. Once you execute the bind command, you can work with the texture.
// Specify a 2D texture image, providing the a pointer to the image data in memory
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, brushData);
The next call to glTexImage2D actually takes a portion our brush data (Particle.png drawn in our context) and makes it a drawable texture. That is, we don't have to use our whole image as a texture. The first parameter specifies the type of texture (2D), 0 indicates base image data (this deals with something known as "mipmaps" which we may discuss in a future blog entry), GL_RGBA indicates the number of color components (R,G,B, and alpha). The width and height indicate the width and height of the texture image. Both of these are supported up to 64 textels (texture pixels) each. The second 0 indicates we don't want a border, and the second GL_RGBA states the format of the image data (which can be different than the number of color components.) GL_UNSIGNED_BYTE indicates the size of the pixel data--each pixel in the image is an unsigned byte. And finally, brushData is a pointer to our texture image to use.
// Release the image data; it's no longer needed
free(brushData);
We release our source texture data. Our texture is set up.
// Set the texture parameters to use a minifying filter and a linear filer (weighted average)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
You can think of glTexParameteri as a call that determines how your texture is applied to various surfaces. Here we set it up to work in our view. The first parameter is just to specify this is a 2D texture, the second GL_TEXTURE_MIN_FILTER specifies that we want to specify how to map a texture pixel (textel) from our texture map onto an area larger than a single pixel. This is known as minifying filter. The actual rule for minifying is GL_LINEAR. GL_LINEAR takes the weighted average of the 4 closest textels from the texture and uses that to map the pixel. So, this allows OpenGL ES to interpolate a pixel that needs to be mapped.
// Enable use of the texture
glEnable(GL_TEXTURE_2D);
We then enable (make active) the texture so we can use it.
// Set a blending function to use
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
glBlendFunc sets up how we the texture map is blended onto the target view. GL_SRC_ALPHA says we want to use the alpha component of the source (the texture) to override the alpha of the destination (the view). The second parameter GL_ONE says use the color components of the destination. The first parameter therefore deals with how the source is blended, the second how the destination is blended.
// Enable blending
glEnable(GL_BLEND);
}
We then enable blending when we draw. To see how this works, draw a red line, then a green line that crosses it, and then a blue one that crosses the two.

You end up with white due to the blending. If you draw slowly, you'll also get white, if you draw fast you get a purer color due to less blending taking place.
The next block takes care of actually setting up most of the OpenGL state to use.
//Set up OpenGL states
glDisable(GL_DITHER);
glMatrixMode(GL_PROJECTION);
glOrthof(0, frame.size.width, 0, frame.size.height, -1, 1);
glMatrixMode(GL_MODELVIEW);
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_VERTEX_ARRAY);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
glEnable(GL_POINT_SPRITE_OES);
glTexEnvf(GL_POINT_SPRITE_OES, GL_COORD_REPLACE_OES, GL_TRUE);
glPointSize(width / kBrushScale);
Some of these I've already gone over so I won't explain them again. Check the previous installments for more information.
The new calls here are to disable dithering. What happens if you comment this out?
glMatrixMode specifies that the glOrthof call will work on the GL_PROJECTION matrix (remember the matrix discussion?) rather than on the texture matrix, etc. The second glMatrixMode says that the remainder of the calls will work on the modelview. We enable texturing, and the vertex array (remember our square?) via the call to glEnableClientState. We enable blending set up a blending function to use,
The next two lines:
glEnable(GL_POINT_SPRITE_OES);
glTexEnvf(GL_POINT_SPRITE_OES, GL_COORD_REPLACE_OES, GL_TRUE);
use OpenGL's particle system to create a sprite (think of a graphical sprite as a rubber stamp, except that once you use the stamp, you can move the image around.) The particle system is used to deal with things like explosions, stars, etc. That is, things that look like points. The second line is similar to the previous glTexParameteri call except that this one deals with setting up the texturing environment as a whole. The first parameter says we want to deal with the sprite's environment, the second parameter indicates the actual parameter in the environment we want to modify, and GL_TRUE is the value of GL_COORD_REPLACE_OES. So what does GL_COORD_REPLACE_OES do? It replaces the part of the view being drawn (mapped) with our particle. If you set this to GL_FALSE nothing will be drawn.
The final line in this block sets the particle size to draw to 32:
glPointSize(width / kBrushScale)
Essentially, this sets the size of the brush that will be drawn with.
Next is some interesting stuff:
//Make sure to start with a cleared buffer
[self erase];
//Playback recorded path, which is "Shake Me"
recordedPaths = [NSMutableArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Recording" ofType:@"data"]];
if([recordedPaths count])
[self performSelector:@selector(playback:) withObject:recordedPaths afterDelay:0.2];
}
return self;
}
We clear our view, and then get the data block we looked at in the previous installment "Recording". This is the data that draws "Shake Me" on the screen when it first launches. If there is data to be played back, we invoke the method "playback:" passing it the recorded data to play back. We then come to the end of initWithFrame.
So what have we learned? We've learned about how to load an image as a texture, how to set up the texturing environment, and also how to use the particle system to define a particle as a point "sprite" as a brush.
One thing that is pretty evident to me is that there is a lot of setup that you have to do with OpenGL to get anything to happen. You may be wondering, is it worth it? Yes if you want your graphics to look good, work efficiently, and across multiple platforms. Could you do this in a simpler manner? Probably. Something as relatively simple as GLPaint could be done easier using CoreGraphics, with less code. But once you get to full blown applications and games, OpenGL gives you the quality you need.
What's next? We'll finish up our dissection of PaintingView.m and GLPaint and get into how the actual drawing code works. Again, feel free to comment.
0 Comments