JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

Clipper Fundamentals

I'm going to talk about clipping over and over in this book. Pixel clipping, bitmap clipping, 2D clipping, and I'm sure I'll think of some more <BG>. Right now, though, the theme is DirectDraw. I want to focus on pixel clipping and bitmap clipping to help you ease into the subject, which I guarantee is going to get very complex when you do it in 3D!

Clipping is generally defined as "not drawing pixels or image elements that are out of bounds of the view port or window." Just like Windows clips anything you draw to the client area of your window, you need to do this in a game that runs under DirectX. Now, as far as 2D graphics go, the only thing that DirectDraw accelerates are bitmaps and bit blitting. Sure, many cards know how to draw lines, circles, and other conic sections, but DirectDraw doesn't support these primitives, so you don't get access to them (hopefully you will soon, though).

What this all means is that if you write a graphics engine that draws pixels, lines, and bitmaps, you have to do the clipping yourself for the pixel and line drawing algorithms. However, DirectDraw can help with the bitmaps—as long as the bitmaps are in the form of DirectDraw surfaces, or IDirectDrawSurface(s) to be exact.

The help that DirectDraw gives is in the form of DirectDraw clippers under the IDirectDrawClipper interface. What you do is create an IDirectDrawClipper, give it valid regions to clip to, and then attach it to a surface. Then, when you use the blitter function, Blt(), it will clip to the clipping regions and you won't have any out-of-bounds blitting or performance hits—if you have the proper hardware, of course. But first, take a look at how to clip pixels and do a rewrite of the Blit8x8() function that does clip.

Clipping Pixels to a Viewport

Figure 7.17 gives you a visual of the problem. You want to clip a pixel with coordinates (x,y) to a viewport located at (x1,y1) to (x2,y2). If (x,y) is within the rectangle defined by (x1,y1) to (x2,y2), render it; otherwise, don't. Simple enough?

Figure 7.17. A detailed view of the clipping region.

graphics/07fig17.gif

Here's the code for a 640x480 linear 8-bit mode:

// assume clipping rectangle is global
int x1,y1,x2,y2; // these are defined somewhere

void Plot_Pixel_Clip8(int x, int y,
                      UCHAR color,
                      UCHAR *video_buffer)
{
// test the pixel to see if it's in range
if (x>=x1 && x<=x2 && y>=y1 && y<=y2)
    video_buffer[x+y*640] = color;

} // end if

Of course, there's a lot of room for optimization, but you get the point—you've created a software filter on the pixel coordinates. Only pixel coordinate values that satisfy the if statement pass through the filter—interesting concept, huh? Now, the preceding clipper is very general, but in many cases, the window or viewport is located at (0,0) and has dimensions (win_width, win_height). This simplifies your code a little:

// assume clipping rectangle is global
int x1,y1,x2,y2; // these are defined somewhere

void Plot_Pixel2_Clip8(int x, int y,
                       UCHAR color,
                       UCHAR *video_buffer)
{
// test the pixel to see if it's in range
if (x>=0 && x<win_width && y>=0 && y<=win_height)
    video_buffer[x+y*640] = color;

} // end if

See? In addition, more optimizations can be made whenever zeros are around. Now that you get the point of clipping and know how to do it, I'll show you how to clip an entire bitmap.

Clipping Bitmaps the Hard Way

Clipping bitmaps is as simple as clipping pixels. There are two ways to approach it:

  • Method 1: Clip each pixel of the bitmap on a independent basis as it's generated. Simple, but slow.

  • Method 2: Clip the bounding rectangle of the bitmap to the viewport, and then only draw the portion of the bitmap that's within the viewport. More complex, but very fast, with almost no performance loss and no hit at all in the inner loop.

Obviously, you're going to use Method 2, which is shown graphically in Figure 7.18. Also, I'm going to generalize a little and assume that the screen extends from (0,0) to (SCREEN_WIDTH-1, SCREEN_HEIGHT-1), that your bitmap has its upper-left corner at (x,y), and that it's exactly so many widthxheight pixels in dimension—or in other words, the bitmap extends from (x,y) to (x+width-1, y+height-1). Please take a minute and make sure you see the reasoning for the "-1" factors. Basically, if a bitmap is 1x1, it has a width of 1 and a height of 1. Therefore, if the origin of the bitmap is at (x,y), the bitmap extends from (x,y) to (x+1-1,y+1-1) or (x,y). This is because it's only 1x1 pixels, so the "-1" factor is needed, as shown by this base case.

Figure 7.18. How to clip the bounding box of a bitmap.

graphics/07fig18.gif

The plan of attack for clipping is simple—you just clip the virtual rectangle of the bitmap to the viewport and then draw only the portions of the bitmap that are in the clipped bitmap. Here's the code for a 640x480x8 linear mode:

// dimensions of window or viewport (0,0) is origin
#define SCREEN_WIDTH  640
#define SCREEN_HEIGHT 480

v
void Blit_Clipped(int x, int y,          // position to draw bitmap
                  int width, int height, // size of bitmap in pixels
                  UCHAR *bitmap,         // pointer to bitmap data
                  UCHAR *video_buffer,   // pointer to video buffer surface
                  int   mempitch)        // video pitch per line
{
// this function blits and clips the image sent in bitmap to the
// destination surface pointed to by video_buffer
// the function assumes a 640x480x8 mode
// this function is slightly different than the one in the book
// ie, it doesn't assume linear pitch

// first do trivial rejections of bitmap, is it totally invisible?
if ((x >= SCREEN_WIDTH) || (y>= SCREEN_HEIGHT) ||
    ((x + width) <= 0) || ((y + height) <= 0))
return;

// clip source rectangle
// pre-compute the bounding rect to make life easy
int x1 = x;
int y1 = y;
int x2 = x1 + width - 1;
int y2 = y1 + height -1;

// upper left hand corner first
if (x1 < 0)
   x1 = 0;

if (y1 < 0)
   y1 = 0;

// now lower left hand corner
if (x2 >= SCREEN_WIDTH)
    x2 = SCREEN_WIDTH-1;

if (y2 >= SCREEN_HEIGHT)
    y2 = SCREEN_HEIGHT-1;

// now we know to draw only the portions of
// the bitmap from (x1,y1) to (x2,y2)
// compute offsets into bitmap on x,y axes,
// we need this to compute starting point
// to rasterize from
int x_off = x1 - x;
int y_off = y1 - y;

// compute number of columns and rows to blit
int dx = x2 - x1 + 1;
int dy = y2 - y1 + 1;

// compute starting address in video_buffer
video_buffer += (x1 + y1*mempitch);
// compute starting address in bitmap to scan data from
bitmap += (x_off + y_off*width);

// at this point bitmap is pointing to the first
// pixel in the bitmap that needs to
// be blitted, and video_buffer is pointing to
// the memory location on the destination
// buffer to put it, so now enter rasterizer loop

UCHAR pixel; // used to read/write pixels

for (int index_y = 0; index_y < dy; index_y++)
     {
     // inner loop, where the action takes place
     for (int index_x = 0; index_x < dx; index_x++)
          {
          // read pixel from source bitmap
          // test for transparency and plot
          if ((pixel = bitmap[index_x]))
              video_buffer[index_x] = pixel;

          } // end for index_x

          // advance pointers
          video_buffer+=mempitch;  // bytes per scanline
          bitmap      +=width;     // bytes per bitmap row

     } // end for index_y

} // end Blit_Clipped

As a demo of this little software clipper, I've written the crudest bitmap engine you've ever seen. Basically, I created an array of 64 bytes to hold a little happy face. Here's the declaration:

UCHAR happy_bitmap[64] = {0,0,0,0,0,0,0,0,
                          0,0,1,1,1,1,0,0,
                          0,1,0,1,1,0,1,0,
                          0,1,1,1,1,1,1,0,
                          0,1,0,1,1,0,1,0,
                          0,1,1,0,0,1,1,0,
                          0,0,1,1,1,1,0,0,
                          0,0,0,0,0,0,0,0};

Then I put the system into 320x240x8 back buffer mode and made color index RGB(255,255,0), which is yellow. Then I made the little happy face move around the screen by moving it on a constant random velocity and then wrapping the face around when it goes too far off any of the four screen edges. It goes out of the window just far enough for you to see the clipping function work. Then I got carried away and made 100 happy faces! The final program is DEMO7_8.CPP|EXE, and Figure 7.19 is a screen shot of the program in action.

Figure 7.19. DEMO7_8.EXE in action.

graphics/07fig19.gif

Here's the Game_Main() function for your review:

int Game_Main(void *parms = NULL, int num_parms = 0)
{
// this is the main loop of the game, do all your processing
// here

DDBLTFX ddbltfx; // the blitter fx structure

// make sure this isn't executed again
if (window_closed)
   return(0);

// for now test if user is hitting ESC and send WM_CLOSE
if (KEYDOWN(VK_ESCAPE))
   {
   PostMessage(main_window_handle,WM_CLOSE,0,0);
   window_closed = 1;
   } // end if

// use the blitter to erase the back buffer
// first initialize the DDBLTFX structure
DDRAW_INIT_STRUCT(ddbltfx);

// now set the color word info to the color we desire
ddbltfx.dwFillColor = 0;

// make the blitter call
if (FAILED(lpddsback->Blt(NULL, // ptr to dest RECT, NULL means all
                          NULL, // pointer to source surface
                          NULL, // pointer to source RECT
                          DDBLT_COLORFILL | DDBLT_WAIT,
                          // do a color fill and wait if you have to
                          &ddbltfx))) // pointer to DDBLTFX holding info
return(0);

// initialize ddsd
DDRAW_INIT_STRUCT(ddsd);

// lock the back buffer surface
if (FAILED(lpddsback->Lock(NULL,&ddsd,
                              DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,
                              NULL)))

    return(0);

// draw all the happy faces
for (int face=0; face < 100; face++)
    {
    Blit_Clipped(happy_faces[face].x,
                 happy_faces[face].y,
                 8,8,
                 happy_bitmap,
                 (UCHAR *)ddsd.lpSurface,
                 ddsd.lPitch);
    } // end face

// move all happy faces
for (face=0; face < 100; face++)
    {
    // move
    happy_faces[face].x+=happy_faces[face].xv;
    happy_faces[face].y+=happy_faces[face].yv;

    // check for off screen, if so wrap
    if (happy_faces[face].x > SCREEN_WIDTH)
         happy_faces[face].x = -8;
    else
    if (happy_faces[face].x < -8)
        happy_faces[face].x = SCREEN_WIDTH;

    if (happy_faces[face].y > SCREEN_HEIGHT)
         happy_faces[face].y = -8;
    else
    if (happy_faces[face].y < -8)
        happy_faces[face].y = SCREEN_HEIGHT;

    } // end face

// unlock surface
if (FAILED(lpddsback->Unlock(NULL)))
   return(0);

// flip the pages
while (FAILED(lpddsprimary->Flip(NULL, DDFLIP_WAIT)));
// wait a sec
Sleep(30);

// return success or failure or your own return code here
return(1);

} // end Game_Main

NOTE

Make sure to look at the code for Blit_Clipped() in the demo program, because I slightly modified it to work with a variable memory pitch. No big deal, but I thought you might want to know. Also, you may be wondering why I decided to use 320x240 mode. Well, the little 8x8 bitmap in 640x480 was so small, I was going blind <BG>.


Making a DirectDraw Clip with IDirectDrawClipper

Now that you see the work it takes to perform clipping via software, it's time to look at how easy it is with DirectDraw. DirectDraw has an interface called IDirectDrawClipper that's used for all 2D blitter clipping, as well as 3D rasterization under Direct3D. In essence, the buck stops here. Right now, however, you're only interested in using the clipper to clip bitmaps that are blitted using the Blt() function and the associated blitter hardware.

To set up DirectDraw clipping, you must do the following:

  1. Create a DirectDraw clipper object.

  2. Create a clipping list.

  3. Send the clipping list data to the clipper with IDIRECTDRAWCLIPPER::SetClipList().

  4. Attach the clipper to a window and/or surface with IDIRECTDRAWSURFACE7::SetClipper().

I'll begin with step 1. The function to create an IDirectDrawClipper interface is called IDIRECTDRAW7::CreateClipper() and is shown here:

HRESULT CreateClipper(DWORD dwFlags, // control flags
        LPDIRECTDRAWCLIPPER FAR *lplpDDClipper, // address of interface pointer
        IUnknown FAR *pUnkOuter); // COM stuff

The function returns DD_OK if successful.

The parameters are pretty easy. dwFlags is currently unused and must be 0. lplpDDClipper is the address of an IDirectDrawClipper interface that will point to a valid DirectDraw clipper after the function succeeds. Finally, pUnkOuter is for COM aggregation, which is something you don't care about—make it NULL. To create a clipper object, just enter this:

LPDIRECTDRAWCLIPPER lpddclipper = NULL; // hold the clipper

if (FAILED(lpdd->CreateClipper(0,&lpddclipper,NULL)))
    return(0);

If the function succeeds, lpddclipper will point to a valid IDirectDrawClipper interface and you can call the methods on it.

That's great, but how do you create the clipping list, and what does it represent? Under DirectDraw, the clipping list is a list of rectangles stored in RECT structures that indicate the valid regions that can be blitted to, as shown in Figure 7.20. As you can see, there are a number of rectangles on the display surface, but DirectDraw's blitter system can blit only within these rectangles. You can draw anywhere you want with Lock()/Unlock(), but the blitter hardware will be able to draw only within the clipping regions, more commonly called the clip list.

Figure 7.20. The relationship between the clip list and the blitter.

graphics/07fig20.gif

To create a clip list, you must fill in a rather ugly data structure called RGNDATA (Region Data), which is shown here:

typedef struct _RGNDATA
        { /* rgnd */
        RGNDATAHEADER rdh; // header info
        char Buffer[1];    // the actual RECT list
        } RGNDATA;

This is a very odd data structure. Basically, it's a variant size structure, which means that the Buffer[] part of it can be any length. The structure is generated dynamically rather than statically, and its true length is stored in the RGNDATAHEADER. What you're seeing here is the old version of the new DirectX data structure technique that sets the dwSize field of every structure. Maybe a better approach would have been to make Buffer[] a pointer rather than storage for a single byte?

Whatever the thinking was, here's the deal: All you have to do is allocate enough memory for a RGNDATAHEADER structure, along with memory to hold an array of one or more RECT structures that are contiguous in memory, as shown in Figure 7.21. Then you'll just cast it to a RGNDATA type and pass it.

Figure 7.21. The memory footprint of the RGNDATA clipping structure.

graphics/07fig21.gif

Anyway, look at what's in the RGNDATAHEADER structure:

typedef struct _RGNDATAHEADER
        { // rgndh
        DWORD dwSize;   // size of this header in bytes
        DWORD iType;    // type of region data
        DWORD nCount;   // number of RECT'S in Buffer[]
        DWORD nRgnSize; // size of Buffer[]
        RECT  rcBound;  // a bounding box around all RECTS
        } RGNDATAHEADER;

To set this structure up, set dwSize to the sizeof(RGNDATAHEADER), set iType to RDH_RECTANGLES, set nCount to the number of rectangles or RECTS in your clipping list, set nRgnSize to the size in bytes of your Buffer[] (which is equal to sizeof(RECT)*nCount), create a bounding box around all your RECTs, and store this box in rcBound. Once you've generated the RGNDATA structure, you send it to your clipper with a call to IDIRECTDRAWCLIPPER::SetClipList(), shown here:

HRESULT SetClipList(LPRGNDATA lpClipList, // ptr to RGNDATA
                    DWORD dwFlags);  // flags, always 0

There's not much more to say about this. Assuming you've already generated the RGNDATA structure for your clipping list, here's how you would set the clipping list:

if (FAILED(lpddclipper->SetClipList(&rgndata,0)))
    return(0);

Once the clip list is set, you can finally attach the clipper to the surface you want it to be associated with using IDIRECTDRAWSURFACE7::SetClipper(), which is shown here:

HRESULT SetClipper(LPDIRECTDRAWCLIPPER lpDDClipper);

And here's the function in action:

if (FAILED(lpddsurface->SetClipper(&lpddcliper)))
    return(0);

In most cases, lpddsurface would be your offscreen rendering surface, such as the back buffer surface. Usually, you don't attach a clipper to the primary surface.

Okay, I know you're probably turning purple with frustration because I've hedged about the details of creating the RGNDATA structure and setting it up. The reason is that it's too hard to explain detail by detail; it's easier to just look at the code. Hence, I've created a function called DDraw_Attach_Clipper() (part of the graphics library) that creates a clipper and a clip list, and attaches them to any surface. Here's the code:

LPDIRECTDRAWCLIPPER DDraw_Attach_Clipper(LPDIRECTDRAWSURFACE7 lpdds,
                                         int num_rects,
                                         LPRECT clip_list)
{
// this function creates a clipper from the sent clip list and attaches
// it to the sent surface

int index;                         // looping var
LPDIRECTDRAWCLIPPER lpddclipper;   // pointer to the newly
                                   // created dd clipper
LPRGNDATA region_data;             // pointer to the region
                                   // data that contains
                                   // the header and clip list

// first create the direct draw clipper
if (FAILED(lpdd->CreateClipper(0,&lpddclipper,NULL)))
   return(NULL);

// now create the clip list from the sent data

// first allocate memory for region data
region_data = (LPRGNDATA)malloc(sizeof(RGNDATAHEADER)+
               num_rects*sizeof(RECT));

// now copy the rects into region data
memcpy(region_data->Buffer, clip_list, sizeof(RECT)*num_rects);

// set up fields of header
region_data->rdh.dwSize          = sizeof(RGNDATAHEADER);
region_data->rdh.iType           = RDH_RECTANGLES;
region_data->rdh.nCount          = num_rects;
region_data->rdh.nRgnSize        = num_rects*sizeof(RECT);
region_data->rdh.rcBound.left    =  64000;
region_data->rdh.rcBound.top     =  64000;
region_data->rdh.rcBound.right   = -64000;
region_data->rdh.rcBound.bottom  = -64000;

// find bounds of all clipping regions
for (index=0; index<num_rects; index++)
    {
    // test if the next rectangle unioned with
    // the current bound is larger
    if (clip_list[index].left < region_data->rdh.rcBound.left)
       region_data->rdh.rcBound.left = clip_list[index].left;

    if (clip_list[index].right > region_data->rdh.rcBound.right)
       region_data->rdh.rcBound.right = clip_list[index].right;

    if (clip_list[index].top < region_data->rdh.rcBound.top)
       region_data->rdh.rcBound.top = clip_list[index].top;

    if (clip_list[index].bottom > region_data->rdh.rcBound.bottom)
       region_data->rdh.rcBound.bottom = clip_list[index].bottom;

    } // end for index

// now we have computed the bounding rectangle region and set up the data
// now let's set the clipping list

if (FAILED(lpddclipper->SetClipList(region_data, 0)))
   {
   // release memory and return error
   free(region_data);
   return(NULL);
   } // end if

// now attach the clipper to the surface
if (FAILED(lpdds->SetClipper(lpddclipper)))
   {
   // release memory and return error
   free(region_data);
   return(NULL);
   } // end if

// all is well, so release memory and
// send back the pointer to the new clipper
free(region_data);
return(lpddclipper);

} // end DDraw_Attach_Clipper

The function is almost trivial to use. Let's say you have an animation system with a primary surface called lpddsprimary and a secondary back buffer called lpddsback, to which you want to attach a clipper with the following RECT list:

RECT rect_list[3] = {{10,10,50,50},
                     {100,100,200,200},
                     {300,300, 500, 450}};

Here's the call to do it:

LPDIRECTDRAWCLIPPER lpddclipper =
                     DDraw_Attach_Clipper(lpddsback,3,rect_list);

Cool, huh? If you made this call, only portions of bitmaps that were within the rectangles (10,10) to (50,50), (100,100) to (200,200), and (300,300) to (500, 450) would be visible. Also, just to let you know, this function is part of a library that I'm working on as I write this chapter. Later, I'm going to show you all the functions in it so you don't have to write all this tedious DirectDraw code yourself and you can focus on game programming, my little spawn <BG>.

Anyway, based on the preceding code, I've created a demo called DEMO7_9.CPP|EXE. Basically, I took the blitter demo program DEMO7_7.CPP, converted it to 8-bit color, and added the clipper function so that only blits within the current clipping regions are displayed on the primary surface. Furthermore, to be consistent, the clipping regions are the same ones listed in the preceding paragraph. Figure 7.22 is a screen shot of the program in action. Notice that it looks like a bunch of little windows that the clipper allows bitmaps to be rendered to.

Figure 7.22. DEMO7_9.EXE in action.

graphics/07fig22.gif

Here's the code that sets up the clipper in the Game_Main() of DEMO7_9.CPP:

// now create and attach clipper
RECT rect_list[3] = {{10,10,50,50},
                     {100,100,200,200},
                     {300,300, 500, 450}};

if (FAILED(lpddclipper = DDraw_Attach_Clipper(lpddsprimary,3,rect_list)))
   return(0);

And of course, there's no difference when attaching a clipper in 16-bit or higher modes, you make the call the exact same way. The clipper doesn't care what the mode is since clipping is performed at a different level, more abstractly that is, so the bits per pixel is irrelevant.

Coolio! At this point, I'm extremely bored with gradient fills and colored rectangles. If I don't see some bitmaps, I'm going to lose my mind! Next I'll show you how to load bitmaps with Windows.

      Previous Section Next Section
    



    JavaScript EditorAjax Editor     JavaScript Editor