Logo
RuTh's  RuThLEss  HomEpAgE

 

sTaRt
 
 
 
 
 
 
 
 
FuN
 
 
 
 
fAcTs
 
 
>
3D Game DesignEnglish
>
PlanetrisEnglishDeutschCesky
>
PHP-SkripteDeutsch
>
Programming Links
 
 
 
 
 
 
* INDEX * 3D game to-do list * 3D Engine to-do list ( chapter 1, 2, 3, 4, 5, 6, 7 ) *
* projection formula * transformation matrices * Bresenham algorithm * Scanline Polygonfill algorithm *
* Spieldesign / game design * Troubleshooting 3D * Irrlicht 3D engine * Blender for Beginners *

Chapter 4 -- Overwriting the Drawing Method

This chapter is a preparation for the optimization we'll do in chapter 5, hidden-surface-removal. The hidden-surface problem comes up as soon as there's more than one object in our game world: We then have to define what to draw if objects overlap. To do that, we will need access to every individual pixel to decide whether it has to be drawn or not.

Cocoa's NSBezierPath, however, keeps us from "bothersome details" such as individual pixels. That's why we will need to write our own drawWireframePolygon: method (in place of Cocoa's [NSBezierPath stroke]), and our own drawFilledPolygon: method (in place of Cocoa's [NSBezierPath fill]). From now on, we will exclusively use our drawing methods instead of the built-in ones, although it's very likely that Cocoa's NSBezierPath drawing method internally uses the same Bresenham line algorithm and scan-line polygonfill algorithm that we'll implement.

Your Very Own Image Representation With MYDraw

NSBezierPath is Cocoa's drawing library that we used for the Rotating Cube demo earlier. You may have created a NSBezierPath object bp, added some polygon vertices to it using [bp moveToPoint:p] and [bp lineToPoint:p]. Then you completed the polygon by calling [bp closePath], and drew either its wireframe with [bp stroke] or drew it filled with [bp fill]. Then you reseted the NSBezierPath by calling [bp removeAllPoints].

We want MYDraw to support the same features ([mydraw moveToPoint:p] etc), but it's supposed to draw the pixels into a custom framebuffer over which we have complete control. So first we need a framebuffer in MYDraw. We create an NSBitmapImageRep containing 4 "planes", one of which contains our NSMutableData pixel buffer named pixels. The NSBitmapImageRep can later be wrapped into an NSImage that can be drawn to the screen by MYGameView.

NSImage* image
NSBitmapImageRep* bitmap
unsigned char* planes[4]
NSMutableData* pixels
baseAddr --> RGB RGB RGB RGB...
NULL NULL NULL

Implementation: Initializer

The init function creates a new MYDraw object. The object contains an NSMutableData framebuffer of bytes (chars!) that can hold values from 0-255 each (8 bit). Three bytes (three samples) make one RGB-pixel. The framebuffer is called pixels.

- (id) init:(NSRect)cliprect {
    self = [super init];
    if( self )
    {
      bps = 8; // 8 Bits Per Sample = 1 byte/sample (0-255)
      spp = 3; // Samples Per Pixel (R, G, B, no alpha = 3) 
      vertexNum = 0;
      rect = cliprect;
      bitmap = [NSBitmapImageRep alloc];
      vertexListX = [[NSMutableArray alloc] init]; // x-coordinates
      vertexListY = [[NSMutableArray alloc] init]; // y-coordinates
      ppr  = (int)rect.size.width;      // pixels per row
      bpr  = spp*(int)rect.size.width;  // bytes per row (a.k.a. RowBytes)
      size = spp*(int)rect.size.width*(int)rect.size.height; // size of bitmap in bytes
      samplerange = pow(2,bps)-1;       // RGB colors from 0-255
      pixels = [[NSMutableData alloc] initWithLength:size];  // create new framebuffer
      baseAddr = [pixels mutableBytes]; // where the framebuffer's content starts
      [self reset];                     // Fill framebuffer with white pixels
    }
    return self;
}

This method instantiates MYDraw's instance variable bitmap with a new NSBitmapImageRep object. It will be used by MYGameView to create the NSBitmapImageRep bitmap when the application starts, and to renew the NSBitmapImageRep framebuffer when the window is resized.

- (NSBitmapImageRep*) imgRep {
    unsigned char* planes[4]= { [pixels mutableBytes], NULL, NULL, NULL };
    [bitmap release]; 
     bitmap = [NSBitmapImageRep alloc];
    [bitmap initWithBitmapDataPlanes:planes
           pixelsWide:rect.size.width pixelsHigh:rect.size.height
           bitsPerSample:bps          samplesPerPixel:spp
           hasAlpha:NO                isPlanar:NO
           colorSpaceName:NSCalibratedRGBColorSpace
           bytesPerRow:bpr            bitsPerPixel:(bps*spp) ];
    return bitmap;
}

Implemetation: Drawing Routines

The two arrays vertexListX and vertexListY contain respectively the x and y coordinates of the polygon to be drawn. The arrays are filled with coordinates with every call of a drawing routine.

- (void) moveToPoint:(NSPoint)p
{
    [self removeAllPoints];
    [vertexListX addObject:[NSNumber numberWithFloat:p.x]];
    [vertexListY addObject:[NSNumber numberWithFloat:p.y]];
    vertexNum+=1;
}

- (void) lineToPoint:(NSPoint)p
{
    [vertexListX addObject:[NSNumber numberWithFloat:p.x]];
    [vertexListY addObject:[NSNumber numberWithFloat:p.y]];
    vertexNum+=1;
}

- (void) closePath
{
    [vertexListX addObject:[vertexListX objectAtIndex:0]];
    [vertexListY addObject:[vertexListY objectAtIndex:0]];
    vertexNum+=1;
}

- (void) removeAllPoints
{
    [vertexListX removeAllObjects];
    [vertexListY removeAllObjects];
    vertexNum=0;
}

This is finally the method that puts an actual pixel into the framebuffer. It multiplies the pixeloffset loc by the number of samples per pixel, because each RGB-pixel needs three bytes (there are three samples per pixel, Red, Green and Blue). It sets these three bytes to the corresponding RGB values to light a pixel.

- (void) setPixel:(int)loc to:(NSColor*)f
{
    // TODO: Clipping
    baseAddr = [pixels mutableBytes];
    loc*=spp;
    baseAddr[loc]  =(int)(samplerange*[f redComponent]);
    baseAddr[loc+1]=(int)(samplerange*[f greenComponent]);
    baseAddr[loc+2]=(int)(samplerange*[f blueComponent]);
}

Integrating MYDraw Into MYGameView

Look back at our trivial Rotating Cube demo: MYGameView's drawRect:-method called MYGameWorld's draw:-method. MYGameWorld then looped over its MYEntitys and MYPolygons, calling their draw:-methods. Each MYPolygon then loops over the screen coordinates of its vertices and finally draws stroked or filled NSBezierPaths.

[gameworld draw:clipRect];                         /* MYGameView*/
   |
   V
[entity draw:clipRect];                            /* MYGameWorld */
   |
   V
[polygon draw:clipRect];                           /* MYEntity */
   |
   V
NSBezierPath* polygon = [NSBezierPath bezierPath]; /* MYPolygon */
...
[polygon fill];                                    /* MYPolygon (filled) */
or
[polygon stroke];                                  /* MYPolygon (wireframe) */

To accomodate our own framebuffer MYDraw instead of the default graphic context, we have to duplicate all drawing functions in these objects with an additional (MYDraw*)mybuffer as argument.

[gameworld draw:clipRect toBuffer:mybuffer];       /* MYGameView */
   |
   V
[entity draw:clipRect toBuffer:mybuffer];          /* MYGameWorld */
   |
   V
[polygon draw:clipRect toBuffer:mybuffer];         /* MYEntity */
   |
   V
[mybuffer drawFilledPolygon];                      /* MYPolygon (filled) */
or
[mybuffer drawWireframePolygon];                   /* MYPolygon (wireframe) */

Initialize MYDraw's framebuffer in MYGameView's initWithFrame:.

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
       mybuffer = [[MYDraw alloc] init:frame];
       image    = [[NSImage alloc] init];
    }
    return self;
}

Edit MYGameView's drawRect to call [gameworld draw:clipRect toBuffer:mybuffer]; instead of [gameworld draw:clipRect]; from now on.

- (void)drawRect:(NSRect)clipRect
{
    if( ! gameworld ) return;
    ...
    /* fill framebuffer with white pixels */
    [mybuffer reset]; 
    /* project game world into framebuffer */
    [gameworld draw:clipRect toBuffer:mybuffer]; 
    /* draw framebuffer to the screen */
    [image drawRepresentation:[mybuffer imgRep] inRect: clipRect]; 
}

Implementation of drawFilledPolygon and drawWireframePolygon

[mybuffer drawWireframePolygon] is implemented by Bresenham's Line Algorithm. This algorithm is one of the essentials in 3D game development and deserves a whole page of its own.

- (void) drawWireframePolygon
{
  ...
  [Bresenham drawLineX1:x1 y1:y1 x2:x2 y2:y2
             buffer:self ppr:ppr color:color];
}
+ (void) drawLineX1:(short)x1 y1:(short)y1 x2:(short)x2 y2:(short)y2
             buffer:(MYDraw*) mybuffer ppr:(int)ppr color:(NSColor*)color
{
  ... Bresenham line algorithm for Cocoa (Objective C) ...
}

Intermediate result: A wireframe rendering of a lone three-dimensional rotating cube, drawn with Cocoa's built-in drawing routines (NSBezierPath).

Intermediate result: A wireframe rendering of a lone three-dimensional rotating cube,
drawn with custom drawing routines.

For [mybuffer drawFilledPolygon], we use the Scanline Polygonfill Algorithm. This is another very essential algorithm that needs a separate page to be fully explained.

- (void) drawFilledPolygon {
  ... Scanline Polygonfill algorithm for Cocoa (Objective C) ...
}

Turning the Buffer Right-Side-Up

While we were using our game world data with the built-in drawing methods, everything was fine, but now when we want to use our own framebuffer, Cocoa draws it upside-down — this is because our coordinates' origin is located 'top left' in the buffer, whereas Cocoa internally fill the buffer from bottom to top with the origin being the buffer's bottom left corner. That's why every instance of NSView can implement a Boolean isFlipped to specify whether the framebuffer needs to be flipped before drawing or not.

- (BOOL)isFlipped { return YES; }

I recommend to create a BOOL variable flipped that you can set to YES or NO with an accessor method.

An Efficient Reset Method

The C function memset() can set a whole bunch of pixels to a value very quickly. It is very usefull for rapidly clearing the framebuffer and filling it all with white pixels, RGB(255,255,255). The variable samplerange holds the sample range which is the value we want to fill the framebuffer with, in this case 255; baseAddr is a pointer to the start of the framebuffer, and size is the framebuffer's length.

- (void) reset
{
    memset(baseAddr,samplerange,size);
}

Other Methods

Of course you need Accessor methods for (NSMutableData*) pixels and to get and set your framebuffer's current drawing color. Don't forget to free the memory by releasing vertexListX, vertexListY, the pixels buffer, and the bitmap wrapper object.

Useful Links

 
   
2008.08.26

http://www.ruthless.zathras.de/