Loading TGA Files

Chapter 7 covers how to deal with bit-mapped images, used as textures that cover images. The first program, Bitmap, uses a small data array to create a 16 by 16 black and white image of a campfire, only one bit deep (each pixel is either on or off).

Next up was ImageLoad, which loads a TARGA or tga file. The TARGA format is fairly old, but it is simple to use, so the OpenGL SuperBible makes use of it.

I started off having a hard time getting my Cocoa programs to load the TGA files. I tried integrating the GLEE tools, but kept having compiling and linking problems, and eventually I gave up and found some code online to load PNG files. I had been doing this on my trip home to Michigan using my soft copy of the fifth edition. When I got home and looked in my hard-copy fourth edition, I found that it had the code to load the TGA files. It was almost complete—I simply had to find the definition for the TGAHEADER in the sample code files.

I started out just adding a loadTGA method to my view class, and when I got that working, I decided to design an Objective-C TGAImage class, capable of loading and saving TGA files. Wikipedia has a description of the file format, here, which proved useful in designing the class.

Rather than loading files using C FILE pointers, I used NSData’s dataWithContentsOfURL:options:error: method to load the file and writeToURL:atomically:, like a proper Cocoa class.

My class allows setting the image ID field, which is not available in the book’s code. The TGA standard does not require any information, but the Wikipedia article suggests that it’s common for it to hold the date and time the image was created, or a serial number. I plan on using it to store the name of the app that created the image, plus the date and time. Unfortunately, the book’s code can’t load a file with an ID field, so storing this commits myself to using my own code. Of course, “eating your own dogfood” is generally good advice when creating tools, so if my class has any bugs, I will find out about them.

Here’s the header file, TGAImage.h:

//
//  TGAImage.h
//  ImageLoad
//
//  Created by Norm Hecht on 10/2/10.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import <Cocoa/Cocoa.h>

@interface TGAImage : NSObject {
    NSURL *fileURL;
    NSString *IDInformation;
    GLbyte *pixelData;
    GLint xOrigin;
    GLint yOrigin;
    GLint width;
    GLint height;
    GLint components;
    GLenum format;
}

-(id)initWithURL:(NSURL *)name;
-(id)initFromOpenGLContext;
-(BOOL)saveToURL:(NSURL *)name;

-(GLvoid *)bitmapData;
-(NSString *)IDInformation;
-(void)setIDInformation:(NSString *)theInfo;
-(GLint)xOrigin;
-(GLint)yOrigin;
-(GLint)width;
-(GLint)height;
-(GLint)components;
-(GLenum)format;
-(GLbyte *)pixelData;

@end

Here’s the implementation file, TGAImage.m:

//
//  TGAImage.m
//  ImageLoad
//
//  Created by Norm Hecht on 10/2/10.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import "TGAImage.h"
#import <OpenGL/OpenGL.h>


@implementation TGAImage

-(id)initWithURL:(NSURL *)name {
    if (!self) {
        return self;
    }
    fileURL = name;
    [fileURL retain];
    unsigned char header[18];
    NSData *tgaData = [NSData dataWithContentsOfURL:fileURL];
    NSRange headerRange = NSMakeRange(0, 18);
    if (!tgaData) {
        NSLog(@"TGA file %@ not found", fileURL);
        return NULL;
    }
    [tgaData getBytes:header range:headerRange];
    int iCount;
    for (iCount = 0; iCount < 18; iCount++) {
        NSLog(@"Header byte %2i = %i", iCount, header[iCount]);
    }
    if (header[1] != 0) {
        NSLog(@"TGA file uses a color map, which this method can't handle");
        if (fileURL) {
            [fileURL release];
        }
        return NULL;
    }
    char imageType = header[2];
    if (imageType >= 8 ) {
        NSLog(@"TGA file uses run-length encoding, which this method can't handle");
        if (fileURL) {
            [fileURL release];
        }
        return NULL;
    }
    switch (imageType) {
        case 0:
            NSLog(@"TGA file has no image data, what's up with that?");
            if (fileURL) {
                [fileURL release];
            }
            return NULL;
            break;
        case 1:
            NSLog(@"TGA file has color-mapped image data, aborting");
            if (fileURL) {
                [fileURL release];
            }
            return NULL;
            break;
        case 3:
            NSLog(@"TGA file has black and white image data");
            break;
        default:
            break;
    }
    int IDLength = header[0];
    if (IDLength > 0) { // Then read the ID information
        NSLog(@"TGA file has an ID area");
        NSRange IDRange = NSMakeRange(18, IDLength);
        char *IDInfoCString = (char *)malloc(IDLength+1);
        if (IDInfoCString == NULL) {
            NSLog(@"Couldn't allocate memory for TGA file's ID information");
            if (fileURL) {
                [fileURL release];
            }
            return NULL;
        }
        [tgaData getBytes:IDInfoCString range:IDRange];
        IDInfoCString[IDLength] = 0; // make last byte zero so it's a string
        IDInformation = [NSString stringWithCString:IDInfoCString 
                                           encoding:NSUTF8StringEncoding];
        free(IDInfoCString);
    } else {
        NSLog(@"TGA file has no ID area");
        IDInformation = NULL;
    }
    xOrigin = header[9]*256 + header[8];
    yOrigin = header[11]*256 + header[10];
    width = header[13]*256 + header[12];
    height = header[15]*256 + header[14];
    NSLog(@"TGA image's origin, width and height are (%i, %i), %i and %i", 
          xOrigin, yOrigin, width, height);
    int pixelDepth = header[16];
    switch (pixelDepth) {
        case 24:
            format = GL_BGR_EXT;
            components = GL_RGB8;
            break;
        case 32:
            format = GL_BGRA_EXT;
            components = GL_RGBA8;
            break;
        case 8:
            format = GL_LUMINANCE;
            components = GL_LUMINANCE8;
            break;
        default:
            NSLog(@"TGA pixel depth invalid for OpenGL");
            return NULL;
            break;
    }
    int imageDescriptor = header[17];
    NSLog(@"TGA image's pixel depth is %i, with descriptor of %i",
          pixelDepth, imageDescriptor);
    unsigned long lImageSize = width*height*pixelDepth/8;
    NSLog(@"Image size in bytes should be %i", lImageSize);
    pixelData = (GLbyte *)malloc(lImageSize*sizeof(GLbyte));
    if (pixelData == NULL) {
        NSLog(@"Failed to allocate memory for image data");
        if (fileURL) {
            [fileURL release];
        }
        return NULL;
    }
    NSRange pixelRange = NSMakeRange(18+IDLength, lImageSize);
    [tgaData getBytes:pixelData range:pixelRange];
    NSLog(@"Successfully read TGA file!");
    return self;
}

-(id)initFromOpenGLContext {
    GLint iViewPort[4]; // viewport in pixels
    glGetIntegerv(GL_VIEWPORT, iViewPort);
    unsigned long lImageSize = iViewPort[2]*iViewPort[3]*3;
    pixelData = (GLbyte *)malloc(lImageSize);
    if (!pixelData) { // couldn't allocate memory, so quit
        return NULL;
    }
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
    glPixelStorei(GL_PACK_ROW_LENGTH, 0);
    glPixelStorei(GL_PACK_SKIP_ROWS, 0);
    glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
    
    // Get the current read buffer setting and save it.  Switch to
    // the front buffer and do the read operation.  Finally, restore 
    // the read buffer state.
    GLint lastBuffer; // storage for the current read buffer setting
    glGetIntegerv(GL_READ_BUFFER, &lastBuffer);
    glReadBuffer(GL_FRONT);
    glReadPixels(0, 0, iViewPort[2], iViewPort[3], GL_BGR,
                 GL_UNSIGNED_BYTE, pixelData);
    glReadBuffer(lastBuffer);
    
    xOrigin = 0;
    yOrigin = 0;
    width = iViewPort[2];
    height = iViewPort[3];
    format = GL_BGR_EXT;
    components = GL_RGB8;
    return self;
}

-(BOOL)saveToURL:(NSURL *)name {
    NSMutableData *tgaData = [[[NSMutableData alloc] init] autorelease];
    unsigned char header[18];
    header[1] = 0; // No color map
    header[2] = 2; // Uncompressed, true-color image
    header[3] = header[4] = 0; // offset into color map table, not used
    header[5] = header[6] = 0; // number of color map entries, not used
    header[7] = 0; // number of bits in (non-existant) color map entries
    header[8]  = xOrigin % 256;
    header[9]  = xOrigin / 256;
    header[10] = yOrigin % 256;
    header[11] = yOrigin / 256;
    header[12] = width % 256;
    header[13] = width / 256;
    header[14] = height % 256;
    header[15] = height / 256;
    header[16] = 24; // bits per pixel
    header[17] = 0; // image descriptor:
    // bits 0-3 alpha depth, bits 5-4 direction
    
    if (IDInformation) {
        int IDLength = [IDInformation length];
        if (IDLength < 256) {
            header[0] = IDLength;
            [tgaData appendBytes:header length:18];
            NSData *IDData = [IDInformation 
                              dataUsingEncoding:NSUTF8StringEncoding];
            [tgaData appendData:IDData];
        } else {
            NSLog(@"Truncating IDInformation");
            header[0] = 255;
            [tgaData appendBytes:header length:18];
            NSRange r = NSMakeRange(0, 255);
            NSData *IDData = [[IDInformation substringWithRange:r] 
                              dataUsingEncoding:NSUTF8StringEncoding];
            [tgaData appendData:IDData];
         }
    } else {
        header[0] = 0;
        [tgaData appendBytes:header length:18];
    }
    NSUInteger imageSize = width*height*3;
    [tgaData appendBytes:pixelData length:imageSize];
    return [tgaData writeToURL:name atomically:NO];
}

- (void)dealloc {
    [super dealloc];
    if (pixelData) {
        free(pixelData);
    }
    if (fileURL) {
        [fileURL release];
    }
    if (IDInformation) {
        [IDInformation release];
    }
}

-(NSString *)IDInformation {
    return IDInformation;
}

-(void)setIDInformation:(NSString *)theInfo {
    if (IDInformation) {
        [IDInformation release];
    }
    IDInformation = theInfo;
    [IDInformation retain];
}

-(GLvoid *)bitmapData {
    return pixelData;
}

-(GLint)xOrigin {
    return xOrigin;
}

-(GLint)yOrigin {
    return yOrigin;
}

-(GLint)width {
    return width;
}

-(GLint)height {
    return height;
}

-(GLint)components {
    return components;
}

-(GLenum)format {
    return format;
}

-(GLbyte *)pixelData {
    return pixelData;
}
@end
Posted in OpenGL | Tagged , | Leave a comment

SphereWorld with Fog

Adding fog to Sphere World is another fast and easy upgrade. In my version, pressing the space bar toggles the fog effect off and on, making the effect of fog very apparent. Adding fog is simply a matter of adding a few lines to the OpenGL initialization:

	// Set up fog parameters
	glEnable(GL_FOG); // Turn fog on
	glFogfv(GL_FOG_COLOR, fLowLight); // Set fog color to match background
	glFogf(GL_FOG_START, 5.0f); // How far away the fog starts
	glFogf(GL_FOG_END, 30.0f); // How far away the fog stops
	glFogi(GL_FOG_MODE, GL_LINEAR); // Which fog equation to use

In addition, inside the drawing code, the shadow color needed changing to include the alpha channel:

	glColor4f(0.0f, 0.0f, 0.0f, 0.5);

SphereWorld with Fog
SphereWorld without Fog

Note how the fog darkens the green spheres that are farther back in the scene, and that the shadows get less distinct.

Posted in OpenGL | Leave a comment

Chapter 6, More on Colors and Materials

This chapter explains using color blending, accumulation buffers and multisampling. Blending is useful for a variety of effects, including reflections and fog. Multisampling can reduce aliasing, by sampling each primitive several times per pixel, a sort of averaging.

Multisampling is particularly easy to do: simply select the NSOpenGLView in Interface Builder, open the inspector, and change the sampling dropdown from the default of none to 2, 4, 6, 8, 9 or 16. OpenGL takes care of the rest behind the scenes, so no developer effort is required. The view does get drawn again for each sample, so there is a performance penalty.

Reflection, the first app I worked on, is Sphere World with a checkerboard ground plane which reflects the objects above the plane. The technique is to draw reflected versions of the objects, then draw the ground using blending, and finally draw the objects a second time right side up. The reflections look great.

SphereWorld with reflections and shadows

I kept the shadow effect from the last version of Sphere World, and it didn’t quite work. The original shadow code simply drew solid black shadows. That totally wiped out any reflection at that spot, which is unrealistic because the reflected object does illuminate that spot, so there should be something there.

Blending the black shadow helped, but there was one bit of flakiness: if two parts of the torus blocked the same spot, the shadow was extra-dark. Turning depth testing back on eliminated the shadow entirely, instead of only casting the shadow once. I discovered that I could fix that by using the stencil buffer. When drawing the shadows, increment the stencil buffer by one every time you draw a fragment, and then only draw if the stencil buffer for that spot is zero. The second time the algorithm reaches the same spot, it skips drawing the shadow.

Doing this with the stencil buffer required…

  1. Adding a stencil buffer to the OpenGL view in Interface Builder
  2. Adding GL_STENCIL_BUFFER_BIT in the glClear() call
  3. Adding these lines to the OpenGL initialization:
    glStencilOp(GL_INCR, GL_INCR, GL_INCR);
    glClearStencil(0);
    glStencilFunc(GL_EQUAL, 0x0, 0x01);
    
  4. Enabling GL_STENCIL_TEST just before drawing the shadows, and
  5. Disabling GL_STENCIL_TEST just after drawing the shadows

This took perhaps 15 minutes to accomplish.

Posted in OpenGL | Leave a comment

Sphere World, Chapter 5 Version

Sphere World now has upgraded from wireframes to solid objects, and also has shadows!
Sphere World upgraded to solid bodies with shadows!

There weren’t any terribly instructive errors, but I did upgrade the book’s version by increasing the number of panels on the sphere, and added multi-sampling to reduce aliasing.

Next up is chapter 6, which covers more about colors and materials. One of the topics is multi-sampling, so I will soon know more about the details of using it.

Posted in OpenGL | Leave a comment

Chapter 5, Color, Materials and Lighting: The Basics

The first program in chapter 5 is LitJet, which draws a very simple airplane constructed from a small number of triangles. So the user could rotate the jet to any arbitrary orientation, I added sliders to set the roll, pitch and yaw angles. A horizontal slider sets the yaw angle, a vertical slider sets the pitch angle, and a circular slider sets the roll angle.

The LitJetAppDelegate class took care of monitoring the sliders and passing the angles to JetView, the NSOpenGLView subclass. Here’s the app delegate header file:

//
//  LitJetAppDelegate.h
//  LitJet
//
//  Created by Norm Hecht on 9/9/10.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import 
@class JetView;

@interface LitJetAppDelegate : NSObject  {
    NSWindow *window;
	JetView *jetView;
	NSSlider *pitchSlider;
	NSSlider *yawSlider;
	NSSlider *rollSlider;
}

@property (assign) IBOutlet NSWindow *window;
@property (assign) IBOutlet JetView *jetView;
@property (assign) IBOutlet NSSlider *pitchSlider;
@property (assign) IBOutlet NSSlider *yawSlider;
@property (assign) IBOutlet NSSlider *rollSlider;

-(IBAction)changeRotation:(id)sender;

@end

Here’s the implementation file:

//
//  LitJetAppDelegate.m
//  LitJet
//
//  Created by Norm Hecht on 9/9/10.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import "LitJetAppDelegate.h"
#import "JetView.h"

@implementation LitJetAppDelegate

@synthesize window;
@synthesize jetView;
@synthesize pitchSlider;
@synthesize yawSlider;
@synthesize rollSlider;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
	// Insert code here to initialize your application 
}

-(IBAction)changeRotation:(id)sender {
	GLfloat yAngle, pAngle, rAngle;
	yAngle = [yawSlider floatValue];
	pAngle = [pitchSlider floatValue];
	rAngle = [rollSlider floatValue];
	[jetView setYawRotation:yAngle];
	[jetView setPitchRotation:pAngle];
	[jetView setRollRotation:rAngle];
	NSLog(@"%s: angles are %f, %f, %f", __PRETTY_FUNCTION__, 
		  yAngle, pAngle, rAngle);

	[jetView setNeedsDisplay:YES];
}

@end

The chapter’s second program is ShinyJet, which adds specular reflection and shininess to the airplane model. Instead of creating a separate program, I just added that feature to my LitJet.

Posted in OpenGL | Leave a comment

Creating a New OpenGL Project

I’ve typically proceeded by creating a new project and its files from scratch, copying only a few larger and more tedious chunks (list of vertices, code I’ve already typed into a previous project, and the like). Along the way I’ve made some modifications to my typical project, and I think it’s now ready for public debut.

For my OpenGL studies, I always start with the basic Mac OS X app template, so leave the boxes for “Create document-based application” and “Use Core Data for storage” unchecked. Next, I add the OpenGL and GLUT frameworks. To do this, right-click the Linked Frameworks group, select Add→ and then Existing Frameworks… to bring up the dialog box listing the available frameworks.

Next up is creating a subclass of NSOpenGLView. Create a new file, select the NSView template, and click Next. That brings up the dialog box to let you choose the name and location for the new file. I usually pick a name like BitmapView. Be sure that the box for “Also create BitmapView.h” is checked, and click Finished or hit return to create the files. This should bring up the .h file to edit.

Now change the superclass from NSView to NSOpenGLView and add any instance variables that the class will need. These might include things like xRotation and yRotation to control the orientation of the viewpoint or an NSTimer for controlling animation. Also add any IBActions, so you’ll be ready to work in Interface Builder, which is your next stop.

Under the resources group, double-click the MainMenu.xib to start IB (Interface Builder). Select the you app’s window, click cmd-1 to bring up the attributes inspector, and uncheck One Shot (You’ll get a warning when you try to save if you forget this).

Now drag an OpenGL View onto the window, move it into place and adjust its size. Set its resizing behavior if you want to. Type cmd-6 to change the inspector view and set the view’s class to your new subclass. Now it’s time to set up the attributes for the view.

Type cmd-1 to bring up the attributes pane of the inspector. This is where you will determine the capabilities of your OpenGL view, largely by choosing the types and bit-depths of the view’s buffers.

  • Color: the default buffer will be fine. The float buffers don’t work, at least on my machine.
  • Depth: defaults to none, but you need to include one to do depth testing.
    The minimum of 16 bits has given adequate performance for everything I’ve done so far.
  • Stencil: Either off (the default) or 8 bits. Useful for drawing shadows and other effects.
  • Accum: Accumulation buffers are used to accumulate composite images. Chapter 6 uses the accumulation buffer to create a motion blur for a “moving” sphere.
  • Aux. Buffers: for anything else you might think of.

I’ve also started to split apart the various initializations and setup code, so it’s not all one big blur. For programs that do animation, I have a setupTimer method, and for all programs I have a setupGL method. These are called from a commonInit method that I call from both initWithFrame and initWithCoder, because I haven’t figured out which one of the init methods will get called.

– (void)setupTimer

As noted in the comments, you need to set the selector to the name of the method that updates your scene, and then set the class name that sets the signature.

- (void)setupTimer {
    SEL theSelector = @selector(moveObjects); 
    // replace "moveObjects" with the method you want the timer to call
    NSMethodSignature *aSignature;
    NSInvocation *anInvocation;

    NSLog(@"theSelector loaded");
    aSignature = [WorldView instanceMethodSignatureForSelector: theSelector]; 
    // replace "WorldView" with the appropriate class name
    anInvocation = [NSInvocation invocationWithMethodSignature:aSignature];
    [anInvocation setSelector:theSelector];
    [anInvocation setTarget: self];
    NSLog(@"aSignature created");

    // The time interval is in seconds, so 0.025 is 40 Hz
    timer = [NSTimer scheduledTimerWithTimeInterval: 0.025
                                         invocation: anInvocation
                                            repeats: YES];
    [timer retain];
}

– (void)setupGL

This routine is for setting up things like the background color, drawing settings (the example shows typical settings), lighting, fog parameters, etc.

- (void)setupGL {
    // Green background
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);

    // Object drawing settings
    glCullFace(GL_BACK);
    glEnable(GL_DEPTH_TEST);
    glFrontFace(GL_CCW);
    glEnable(GL_CULL_FACE);

    // Set up light parameters...
    // Set up fog parameters...
}

– (void)commonInit

If you don’t set the swap interval as shown at the start of this routine, your program will fall immediately into the debugger. From what I’ve been able to figure out, the swap interval controls how many vertical retraces of the screen should occur before a buffer swap takes place. A value of 1 works, but the default of zero doesn’t. You could use two, which would limit drawing to half of your monitor’s refresh rate, but I don’t see the point of that. I set it here because it seems more system-related than a part of you OpenGL setup process. The rest of the method simply calls the other setup methods.

- (void)commonInit {
    GLint swapInterval = 1;
    [[self openGLContext] setValues: &swapInterval
                       forParameter: NSOpenGLCPSwapInterval];

    [self setupTimer];
    [self setupGL];
}

– (void)logGLErrorQuietly

This isn’t part of initialization or set up, but I always add this error logging routine so I can conveniently check for OpenGL errors. Set the Boolean input to YES if you don’t want to see any message if there are no errors. It’s usually the last method in the file.

- (void)logGLErrorQuietly:(BOOL)quietly {
	GLenum errorFlag = glGetError();
	if (errorFlag == GL_NO_ERROR) {
		if (!quietly) {
			NSLog(@"No OpenGL errors found");
		}
		return;
	}
	do {
		NSLog(@"OpenGL Error: %s", gluErrorString(errorFlag));
		errorFlag = glGetError();
	} while (errorFlag != GL_NO_ERROR);
}
Posted in OpenGL | Leave a comment

Introducing Sphere World

Chapter 4 introduces Sphere World, an app that progressively improves as the chapters roll by. This first version simply draws the ground as a gridded plane, with a spinning torus at the center, orbited by a small sphere. Fifty other spheres are distributed randomly across the plane (they’re all at the same height), and the user can use the arrow keys to move around.The torus and all the spheres are drawn as wireframes in this version.

Getting it working required fixing the usual minor bugs, plus three interesting issues. I had to look up how to deal with keyboard events, and in doing so discovered that the constants for the arrow keys (like NSUpArrowFunctionKey) didn’t match the keycodes that came in when I pressed the arrow keys. I hard-coded the relevant values, but I need to find out how it’s actually done to get away from having “magic numbers” in my code. I also forgot at first to override acceptsFirstResponder to return YES in WorldView. That resulted in the app beeping at me with every keystroke, very annoying.

Next up, the randomly drawn spheres didn’t appear. The problem turned out to be that the ApplyActorTransformation method of the book’s GLFrame class was producing an all-zero transformation matrix. That turned out to result from the GLFrame instance having zero vectors for the up and forward vectors.

Those should have been initialized to unit vectors in the constructor, which got called once, apparently without setting the up and forward vectors for any of the spheres. Since the spheres were in a static array, I thought that they would be initialized automatically, without using new like you would do for a pointer-based instance. At any rate, I set the vectors (to their defaults!) when initializing the locations of the spheres, and that took care of things.

I also spent some time figuring out how to make the animation look smooth. The first idea was just to make the incremental change in position or angle smaller, which helped with forward and back motion, but didn’t do much for rotation. The second idea was to increase the frame rate, which didn’t do much except change the speed at which the torus and orbiting sphere moved.

The idea that worked was to move or rotate a bit with each frame. I checked how often the drawRect: and keyDown: methods were being called when holding down a key for autorepeat. That turned out to be five (occasionally four) calls to drawRect: per call to keyDown:.

Based on that, I had keyDown: set a value for a counter (positive for forward or right, negative for backward or left), and then moveObjects (called from a timer) would check the counters. If they were non-zero, it would decrement or increment the counter toward zero and move or rotate appropriately. That gave swift smooth motion: very nice!

Now on to chapter 5…

Posted in OpenGL | Leave a comment