Chapter 8

Chapter 8 covers texturing, the process of applying an image to a surface, instead of simply making it a solid color. This can improve an image by adding a lot of detail without adding a huge number of triangles with their attendant supply of vertexes, normals and color data.

The first sample program applies a stone pattern to a pyramid. I adjusted the proportions of the pyramid to match the Great Pyramid at Giza, and then modified it to look like the dollar bill pyramid, with the apex floating above the base, like this:

The basic pyramid has five points and six triangles (four faces and two on the square base), but the floating-apex pyramid requires a lot more effort. I wrote a triangle-drawing method that takes three vertex index values, which then set the normal to the triangle, calculated the texture coordinates for each vertex, and then drew the three vertexes. That made the drawPyramid method much clearer: it just had to draw triangles, and the heavy lifting got done behind the scenes. Here’s the code:

- (void)drawTrianglePointA:(int)iA 
                    pointB:(int)iB 
                    pointC:(int)iC {
    M3DVector3f vNormal;
    m3dFindNormal(vNormal, vCorners[iA], vCorners[iB], vCorners[iC]);
    // NSLog(@"Triangle %i, %i, %i with normal (%f, %f, %f)", 
    //      iA, iB, iC, vNormal[0], vNormal[1], vNormal[2]);
    int sAxis, tAxis;
    float sFactor, tFactor;
    float sBias, tBias;
    float s, t;
    if (vNormal[1] > 0.8 | vNormal[1] < -0.8) { // top of apex
        sAxis = 0;
        tAxis = 2;
        sFactor = tFactor = vNormal[1]/(2.0f*width);
        sBias = tBias = 0.5f;
    } else if (vNormal[0] > 0.7f) {
        sAxis = 2;
        tAxis = 1;
        sFactor = -0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    } else if (vNormal[0] < -0.7f) {
        sAxis = 2;
        tAxis = 1;
        sFactor = +0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    } else if (vNormal[2] > 0.7f) {
        sAxis = 0;
        tAxis = 1;
        sFactor = +0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    } else {
        sAxis = 0;
        tAxis = 1;
        sFactor = -0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    }
    // NSLog(@"sAxis = %i, tAxis = %i, sFactor, sBias = %f, %f; tFactor, tBias = %f, %f",
    //      sAxis, tAxis, sFactor, sBias, tFactor, tBias);
    glNormal3fv(vNormal);
    s = vCorners[iA][sAxis]*sFactor + sBias;
    t = vCorners[iA][tAxis]*tFactor + tBias;
    // NSLog(@"s = %f, t = %f", s, t);
    glTexCoord2f(s, t);
    glVertex3fv(vCorners[iA]);

    s = vCorners[iB][sAxis]*sFactor + sBias;
    t = vCorners[iB][tAxis]*tFactor + tBias;
    // NSLog(@"s = %f, t = %f", s, t);
    glTexCoord2f(s, t);
    glVertex3fv(vCorners[iB]);

    s = vCorners[iC][sAxis]*sFactor + sBias;
    t = vCorners[iC][tAxis]*tFactor + tBias;
    // NSLog(@"s = %f, t = %f", s, t);
    glTexCoord2f(s, t);
    glVertex3fv(vCorners[iC]);
}

- (void)drawPyramid {
    // bottom of apex
    [self drawTrianglePointA:2 pointB:4 pointC:1];
    [self drawTrianglePointA:2 pointB:3 pointC:4];

    // faces of apex
    [self drawTrianglePointA:0 pointB:4 pointC:3];
    [self drawTrianglePointA:0 pointB:1 pointC:4];
    [self drawTrianglePointA:0 pointB:2 pointC:1];
    [self drawTrianglePointA:0 pointB:3 pointC:2];

    // Top of base
    [self drawTrianglePointA:6 pointB:5 pointC:8];
    [self drawTrianglePointA:8 pointB:7 pointC:6];

    // faces of base
    [self drawTrianglePointA:8 pointB:12 pointC:7];
    [self drawTrianglePointA:7 pointB:12 pointC:11];
    [self drawTrianglePointA:5 pointB:9  pointC:8];
    [self drawTrianglePointA:8 pointB:9  pointC:12];
    [self drawTrianglePointA:6 pointB:10 pointC:5];
    [self drawTrianglePointA:5 pointB:10 pointC:9];
    [self drawTrianglePointA:7 pointB:11 pointC:6];
    [self drawTrianglePointA:6 pointB:11 pointC:10];

    // bottom of base
    [self drawTrianglePointA:10 pointB:12 pointC:9 ];
    [self drawTrianglePointA:10 pointB:11 pointC:12];
}

Here are the coordinates:

vCorners = {{    0.000000,   280.000000,     0.000000},
            {  -52.380951,   213.333344,   -52.380951},
            {   52.380951,   213.333344,   -52.380951},
            {   52.380951,   213.333344,    52.380951},
            {  -52.380951,   213.333344,    52.380951},
            {  -83.809525,   173.333344,   -83.809525},
            {   83.809525,   173.333344,   -83.809525},
            {   83.809525,   173.333344,    83.809525},
            {  -83.809525,   173.333344,    83.809525},
            { -220.000000,     0.000000,  -220.000000},
            {  220.000000,     0.000000,  -220.000000},
            {  220.000000,     0.000000,   220.000000},
            { -220.000000,     0.000000,   220.000000}};

The apex really needs to be casting a shadow to make the image more believable.

Now I need to add an eye texture to one face of the apex, and maybe “Novus Ordo Seclorum” or “MDCCLXXVI” at the bottom of the base. Or I could add it to SphereWorld and make the apex rotate…

Advertisements
Posted in OpenGL | Leave a comment

Querying OpenGL: Version and Extensions

OpenGL exists in a variety of versions, and different vendors have created extensions that other vendors don’t support. So how do you tell what verions of OpenGL you have, and what extensions can you use?

OpenGL lets you query its capabilities via the function glGetString(), which returns a string you can examine. Set the argument to the function to determine what OpenGL will tell you. GL_VERSION and GL_EXTENSIONS are the ones we’re interested in right now.

The single line of code NSLog(@"OpenGL Version %s", glGetString(GL_VERSION)); will tell you the version of OpenGL you have. My machine returns OpenGL Version 2.1 NVIDIA-1.6.18, so I’ve got an NVIDIA graphics card.

Here’s the code to develop the list of extensions.

   NSLog(@"Now checking extensions...");
   NSString *extensionString = [NSString 
     stringWithCString:(const char*)glGetString(GL_EXTENSIONS) 
              encoding:NSASCIIStringEncoding];
   NSArray *extensions = [extensionString componentsSeparatedByString:@" "];
   NSString *extensionsInHTML;
   extensionsInHTML = [extensions componentsJoinedByString:@"</li>\n<li>"];
   NSLog(@"<ol>\n<li>%@</li>\n</ol>", extensionsInHTML);

Yes, I was in full nerd mode writing code to create HTML markup. My machine has 122 extensions, so I’ll spare you my list: you’re interested in your machine, of course.

Posted in OpenGL | Leave a comment

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