Clipping Rectangular Sprites

Table of Contents:
What good is a clipping sprite routine?
Okay, so how do we do it?
Chopping off the left and right sides...
And into assembly...
An example program


What good is a clipping sprite routine?

Wow. Yet another installment on the topic of sprites. Here, we will discuss what is involved in the clipping of sprites. All that we will worry about clipping is the DrawSprite routine; it doesn't make much sense to clip the GetSprite routine. If you don't understand what a clipping DrawSprite routine is good for, then you must consider the edges of the viewable area of the screen. What happens to a sprite that is half way off of the right edge of the screen for example?

Well, what ends up happening is that the sprite wraps around from the right edge onto the left edge. This is a very bad thing to happen, because it destroys it looks ugly, and it can be confusing for the user of the program. Another case is when the sprite falls off of the bottom of the screen. Here, it goes on to overwrite portions of memory... this is really bad. One thing that can become of this is that some of your data is corrupted. Another WORSE thing that can happen is that some of your code gets corrupted. This could (and probably will) lead to a crash.


Okay, so how do we do it?

So now that we are ready and determined to get the job done, how do we do it? Well, it turns out that clipping these rectangular sprites is really pretty easy. First, we will start by clipping the top and bottom of the sprite, and then we will move on to clip the left and right.

Clipping the top is easily done. All that we have to do is skip over the sprite data that we don't need. Then we adjust the sprite height to compensate for those lines lost, and move the Y value down to a position that is inside of the clipping area. Here is the code prior to the modification:

PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Index, XI, YI : INTEGER;
BEGIN
  Index := 0;
  FOR YI := 0 TO Sprite.Height-1 DO
    FOR XI := 0 TO Sprite.Width-1 DO
    BEGIN
      SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
      INC(Index);
    END;
END;
Here it is with the changes to make it clip the top:

PROCEDURE DrawSpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Height,
  Index, XI, YI : INTEGER;
BEGIN
  Index := 0;
  Height := Sprite.Height;                { Make a local copy of this    }

  IF Screen.Clip.Y1 > Y THEN
  BEGIN
    XI := Screen.Clip.Y1-Y;               { Calculate the difference     }
    DEC(Height, XI);                      { Lower the height by the diff }
    INC(Y, XI);                           { Increase the Y by the diff   }
    INC(Index, XI*Sprite.Width);          { Increase the pointer to the  }
  END;                                    {   sprite data appropriately  }

  FOR YI := 0 TO Height-1 DO
    FOR XI := 0 TO Sprite.Width-1 DO
      ...
As you can see above, although this added code, it did not add code to the inner loop of the draw routine. In fact, it did not change the looping code at all except the value which it loops to. The bottom portion of the sprite is even easier to clip than the top. To clip the top of the sprite, we had to modify three variables. To clip the bottom, we only have to change one, the Height variable.

  Height := Sprite.Height;                { Make a local copy of this    }

  IF Screen.Clip.Y2 < Y+Sprite.Height-1 THEN { Clip the bottom           }
    DEC(Height, Y+Sprite.Height-1-Screen.Clip.Y2);

  IF Screen.Clip.Y1 > Y THEN
Believe it or not, that is all that there is! Only two lines needed to be added to clip the bottom. Whoa! Is it possible that we are already half way done? Amazing! This isn't as hard as we thought. Now if we could just get the left and right sides to clip...


Chopping off the left and right sides

To clip the sides of the sprite, we do have to make a small change to the loop of our routine. Luckily, it doesn't change the inner loop of the routine, but it will slow us down slightly. Here is the new inner loop:
  FOR YI := 0 TO Height-1 DO
  BEGIN
    FOR XI := 0 TO Width-1 DO
    BEGIN
      SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
      INC(Index);
    END;
    INC(Index, SkippedPixels);
  END;
What this does, is to skip over a number of pixels (in the sprite) at the end of the scanline. If we needed to clip four pixels off the right edge, for example, we would set SkippedPixels to 4, and Width to Sprite.Width-4. All this variable does is specify how many pixels to skip over. If the left and right sides are not clipped, then it is set to zero. Luckily, it is a very simple statement, and will translate into only one instruction in assembly. Here is the code to clip the left side:

  Height := Sprite.Height;                { Make a local copy of these   }
  Width  := Sprite.Width;

  IF Screen.Clip.X1 > X THEN
  BEGIN
    XI := Screen.Clip.X1-X;               { Calculate the difference     }
    INC(Index, XI);                       { Increase the sprite offset   }
    INC(SkippedPixels, XI);               { Make sure we skip these      }
    DEC(Width, XI);                       { The sprite is now narrower   }
    INC(X, XI);                           { Start it at the clip boundary}
  END;

  IF Screen.Clip.Y2 < Y+Sprite.Height-1 THEN { Clip the bottom           }
Just like the bottom, the right is also easy to clip. In this case, we only have to modify two variables, instead of the four that were required for the left clipper. We have to make the sprite narrower (the Width variable) and increase the number of SkippedPixels to make sure that we only display the pixels we want. Here's all of the code!:

PROCEDURE DrawSpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
  Height, Width,
  SkippedPixels,
  Index, XI, YI : INTEGER;
BEGIN
  Index := 0; SkippedPixels := 0;
  Height := Sprite.Height;                { Make a local copy of these   }
  Width  := Sprite.Width;

  IF Screen.Clip.X2 < X+Width-1 THEN      { Clip the Right side          }
  BEGIN
    XI := X+Width-1-Screen.Clip.X2;       { Calculate the difference     }
    DEC(Width, XI);                       { Reduce the width             }
    INC(SkippedPixels, XI);               { Increase the number skipped  }
  END;

  IF Screen.Clip.X1 > X THEN              { Clip the Left side           }
  BEGIN
    XI := Screen.Clip.X1-X;               { Calculate the difference     }
    INC(Index, XI);                       { Increase the sprite offset   }
    INC(SkippedPixels, XI);               { Make sure we skip these      }
    DEC(Width, XI);                       { The sprite is now narrower   }
    INC(X, XI);                           { Start it at the clip boundary}
  END;

  IF Screen.Clip.Y2 < Y+Height-1 THEN     { Clip the bottom              }
    DEC(Height, Y+Height-1-Screen.Clip.Y2);{Reduce the height            }

  IF Screen.Clip.Y1 > Y THEN              { clip the Top                 }
  BEGIN
    XI := Screen.Clip.Y1-Y;               { Calculate the difference     }
    DEC(Height, XI);                      { Lower the height by the diff }
    INC(Y, XI);                           { Increase the Y by the diff   }
    INC(Index, XI*Sprite.Width);          { Increase the pointer to the  }
  END;                                    {   sprite data appropriately  }

  FOR YI := 0 TO Height-1 DO
  BEGIN
    FOR XI := 0 TO Width-1 DO
    BEGIN
      SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
      INC(Index);
    END;
    INC(Index, SkippedPixels);            { Skip invisible pixels        }
  END;
END;


And into assembly...

This piece of code turns out to be an excellent candidate for assemblerization. The inner loop turns into the following assembly code:

@MainLoop:                      { FOR YI := 0 TO Height-1 DO                 }
  mov CX, AX                    {  FOR XI := 0 TO Width-1 DO                 }
  rep movsb                     {   SetPixel(XI+X, YI+Y, Sprite.Data^[Index])}
                                {   INC(Index)                               }
  add SI, BX                    { INC(Index, SkippedPixels)                  }
  add DI, DX                                  { Skip invisible pixels        }
  dec YI
  jnz @MainLoop
One optimization we can make is to expand it to write to memory a word at a time... like this:

@MainLoop:                      { FOR YI := 0 TO Height-1 DO                 }
  mov CX, AX                    {  FOR XI := 0 TO Width-1 DO                 }
  shr CX, 1
  rep movsw                     {   SetPixel(XI+X, YI+Y, Sprite.Data^[Index])}
                                {   INC(Index)                               }
  adc CX, CX
  rep movsb
  add SI, BX                    { INC(Index, SkippedPixels)                  }
  add DI, DX                                  { Skip invisible pixels        }
  dec YI
  jnz @MainLoop
As you can see above, the code uses rep movsw for as much of the sprite as possible, and cleans up with a movsb if neccesary.

The complete code is below, but most of it turns out to be the silly initialization code. This code is very fast, and the inner loop is almost as fast as the non-clipped version of the code. Here it is:

PROCEDURE DrawSpriteC(VAR Sprite : SpriteType; X, Y : INTEGER); ASSEMBLER;
VAR
  Height, Width,
  SkippedPixels,
  Index, YI : INTEGER;
ASM
  xor AX, AX
  mov Index, AX
  mov SkippedPixels, AX

  mov CX, X
  mov DX, Y
  cmp CX, Screen.Clip.X2
  jg @TrivialReject
  cmp DX, Screen.Clip.Y2
  jg @TrivialReject

  les DI, Sprite
  mov AX, ES:[DI+SpriteType.Height]           { Make a local copy of these   }
  mov BX, ES:[DI+SpriteType.Width]
  mov Height, AX
  mov Width, BX
  add CX, BX
  add DX, AX

  cmp CX, Screen.Clip.X1
  jl @TrivialReject
  cmp DX, Screen.Clip.Y1
  jl @TrivialReject

  mov AX, Screen.Clip.Y1                      { Clip the Top                 }
  cmp AX, Y                     { IF Screen.Clip.Y1 > Y THEN         }
  jle @NoTopClip                              { Calculate the difference     }
  sub AX, Y                     {   XI := Screen.Clip.Y1-Y           }
  sub Height, AX                {   DEC(Height, XI)                  }
  add Y, AX                     {   INC(Y, XI)                       }
  mul Width                                   { Increase the pointer to the  }
  add Index, AX                               {   sprite data appropriately  }
@NoTopClip:                     {   INC(Index, XI*Sprite.Width)      }

  mov AX, X                                   { Clip the Right side          }
  add AX, Width
  dec AX
  cmp Screen.Clip.X2, AX        { IF Screen.Clip.X2 < X+Width-1 THEN }
  jge @NoRightClip              { BEGIN                              }
  sub AX, Screen.Clip.X2        {   XI := X+Width-1-Screen.Clip.X2   }
  sub Width, AX                               { Reduce the width             }
  add SkippedPixels, AX                       { Increase the number skipped  }
@NoRightClip:                   { END;                               }

  mov AX, Screen.Clip.X1                      { Clip the Left side           }
  cmp AX, X                     { IF Screen.Clip.X1 > X THEN         }
  jle @NoLeftClip               { BEGIN                              }
  sub AX, X                     {   XI := Screen.Clip.X1-X           }
  add Index, AX                               { Increase the sprite offset   }
  add SkippedPixels, AX                       { Make sure we skip these      }
  sub Width, AX                               { The sprite is now narrower   }
  add X, AX                                   { Start it at the clip boundary}
@NoLeftClip:

  mov AX, Y
  add AX, Height
  dec AX                                      { Clip the bottom              }
  cmp Screen.Clip.Y2, AX        { IF Screen.Clip.Y2 < Y+Height-1 THEN }
  jge @NoBottomClip
  sub AX, Screen.Clip.Y2                      { Reduce the height            }
  sub Height, AX                {   DEC(Height, Y+Height-1-Screen.Clip.Y2)   }
@NoBottomClip:

  les DI, Screen.Buffer
  add DI, X
  mov BX, Y       { Y is an index into the ScreenY array        }
  add BX, BX      { Multipy by two because each entry is a word }
  add DI, DS:[BX+OFFSET Screen.YTable]

  push DS
  mov DX, Screen.Width
  lds SI, Sprite
  mov AX, Height
  lds SI, DS:[SI+SpriteType.Data]
  mov YI, AX
  mov AX, Width
  mov BX, SkippedPixels
  add SI, Index
  sub DX, AX
@MainLoop:                      { FOR YI := 0 TO Height-1 DO                 }
  mov CX, AX                    {  FOR XI := 0 TO Width-1 DO                 }
  shr CX, 1
  rep movsw                     {   SetPixel(XI+X, YI+Y, Sprite.Data^[Index])}
                                {   INC(Index)                               }
  adc CX, CX
  rep movsb
  add SI, BX                    { INC(Index, SkippedPixels)                  }
  add DI, DX                                  { Skip invisible pixels        }
  dec YI
  jnz @MainLoop
  pop DS
@TrivialReject:
END;
Notice how nasty and ugly looking the initialization code is, but now nice and clean the inner loops are. It's a good thing that all of the processor time is spent in the loops, huh? Anyway, the moral of the story is that this code is not as bad as it looks.


Example Program

Wow, now that we have a super fast clipped sprite drawer, what are we going to do with it? The following program displays a bunch of clipped sprites. It moves them around so that various parts of the sprite get clipped, and there are some command line parameters. Just play with it a little bit and have some fun.
-----------------] Example Starts here [-----------------
PROGRAM SpriteClippingExample;
USES CRT, GraphPro, Sprite, FPS;

CONST
  ClipX1 = 50;
  ClipY1 = 40;
  ClipX2 = 270;
  ClipY2 = 160;
VAR
  Size,
  I : INTEGER;
  Clip : BOOLEAN;
  Font : FontType;
  Spr  : SpriteType;
  X, Y : INTEGER;
  Radius, Theta : REAL;
  Flag : STRING;
BEGIN
  Size := 63;
  Clip := TRUE;
  Randomize;

  FOR X := 1 TO ParamCount DO
  BEGIN
    Flag := ParamStr(X);
    FOR Y := 1 TO Length(Flag) DO
      Flag[Y] := Upcase(Flag[Y]);

    IF Flag = '-C' THEN
      Clip := FALSE
    ELSE
    BEGIN
      Val(Flag, I, Y);
      IF Y = 0 THEN Size := I;
      IF Size < 19 THEN Size := 19;
    END;
  END;


  InitGraph;                          { Initialize the graphics    }
  LoadBIOSFont(Font, 16);             { Get a 16 point font        }

  ClrScr(0);

  FOR I := 0 TO (Size SHR 1)-9 DO     { Create a dounut ball sprite      }
  BEGIN
    Line(Size+1+Size SHR 1  , I,      Size+1+I,      Size SHR 1,   40-I);
    Line(Size+1+Size SHR 1+1, I,      Size+1+Size-I, Size SHR 1,   40-I);
    Line(Size+1+Size SHR 1  , Size-I, Size+1+I,      Size SHR 1+1, 40-I);
    Line(Size+1+Size SHR 1+1, Size-I, Size+1+Size-I, Size SHR 1+1, 40-I);
  END;
  GetSprite(Spr, Size+1, 0, Size+Size+1, Size);

  ClrScr(0);

  SetClipBoundary(ClipX1, ClipY1, ClipX2, ClipY2);{ Set the clipping boundary }

  Line(ClipX1, ClipY1, ClipX2, ClipY1, 15);               { Draw the clipping boundary }
  Line(ClipX1, ClipY1, ClipX1, ClipY2, 15);
  Line(ClipX2, ClipY1, ClipX2, ClipY2, 15);
  Line(ClipX1, ClipY2, ClipX2, ClipY2, 15);

  WriteG1(Font, 1, 1, 'Press almost any key to continue', 7);
  WriteG1(Font, 0, 0, 'Press almost any key to continue', 15);

  DrawSprite(Spr, 160-Size DIV 2, 0);
  DrawSprite(Spr, 160-Size DIV 2, 200-Size);
  Theta := 0.0;
  StartFPS;
  WHILE NOT KeyPressed DO
  BEGIN
    Radius := Cos(Theta);
    X := Round(Cos(Theta)*110)+160 - Size DIV 2;
    Y := Round(Sin(Theta)*Radius*120)+100 - Size DIV 2;
    Theta := Theta + 0.02;
    IF Clip THEN
      DrawSpriteC(Spr, X, Y)
    ELSE
      DrawSprite(Spr, X, Y);

    INC(Frames);
  END;
  EndFPS;
  CloseGraph;                              { Deinit graphics                }
  Writeln('RLE Sprite Clipping Demo - (Frames = Sprites below...)');
  ShowFPS('1996');
  WRITELN;
  WRITELN('Format of command: RLEDemo [-C] [XXX]');
  WRITELN('   -C turns clipping off');
  WRITELN('   XXX changes the size of the sprite from the default of 63');
  WRITELN('      (Odd values work best, the minimum is 19)');
  WRITELN;
  WRITELN('Example: RLEDemo -C 31')
END.
-----------------] Example Ends here [-----------------


  • Created by Chris Lattner