Drawing & Animation III

类别:VB语言 点击:0 评论:0 推荐:
Drawing & Animation Using the Win32 GDI #3 This three part tutorial first appeared some years ago on the old VBExplorer.com . Since then several errors and bugs have been discovered by various users on the VBExplorer.com Forums. This version reflects the changes made to overcome the bugs and erros. The text will note where a bug correction and / or update has been done, and of course also why. 
A big 'Thank you' goes out to all the people reading and reporting the bugs in the previous version of this tutorial. Please provide comments, questions, bug reports etc. at the VBExplorer.com forums in the Graphics & Game Programming section. Backgrounds

So far we have looked into the techniques of drawing bitmaps and sprites which serve as the foundation of our game programming efforts. Now we'll take a look at some of the actual game programming techniques, which we'll use to build our game. The first thing we'll explore is the subject of backgrounds. Backgrounds aid in creation of a realistic or entertaining gaming experience and are an extremely important part of most sprite-based games.

There are several possible ways to work with backgrounds. In this section we will discuss the use of what we'll identify as static backgrounds. This distinction is somewhat artificial in that you may eventually combine different background techniques, but the distinction is useful here as we are just starting out. Static means that they do not form an interactive part of the game. This doesn't mean that they can't simulate motion, rather that they don't actively interact with the sprite. Their main purpose is to provide an appealing backdrop for the sprite or the playing field. We'll discuss techniques for using backgrounds that play an active role in a game, by interacting with the sprite, later in this book.

Side scrolling backgrounds

One of the most often seen background effects, is side scrolling. The technique is very simple, and is based on wrapping techniques. The basis of side scrolling, is that you have a bitmap, which is longer than the actual play area, as illustrated on the following illustrations:

 

This is the total length of the background image. Below you can see the size of the display window, which will be shown to the user:

 

 

This window continuously moves, making it appear that it is the actual background, which is passing by. This technique is as if we were wrapping the background image around itself. We'll call this the soup can effect. Picture the background above taped around a large soup can. If you could join the edges of the picture perfectly and spin the can it would seem as if the scene was continuously passing by.

 

 

Since we cannot blit to the right end of the image and wrap the blit around to the beginning again, we have to Blit twice to achieve the wrapping effect. To use another simplified example we basically we are basically going to copy the area of the picture that has passed out of our viewing window and paste it on the backend of the picture still remaining to be viewed.

The sample project, which is found in SIDESCROLL1.ZIP, demonstrates this background scrolling. From this point on we'll refrain from using picture boxes to store images, because of the extra overhead they present, and instead load them directly into memory device contexts created with our GenerateDC function.

We create the window, which will function as our main display area, in the Form_Load event assigning it a width of 250 pixels (1/3 of the actual size of the background image). The height is the same as the background image.

In this simple example we'll place all the animation code in the TimerScroll_Timer event:

Private Sub TimerScroll_Timer()

Static X As Long
Dim GlueWidth As Long, EndScroll As Long

If X + ScrollWidth > BackLength Then 'We need to glue at the beginning again
    'Calculate the remaining width
    GlueWidth = X + ScrollWidth - BackLength
    EndScroll = ScrollWidth - GlueWidth
   
    'Blit the first part
    BitBlt Me.hdc, 0, 0, EndScroll, BackHeight, BackDC, X, 0, vbSrcCopy
    'Now draw from the beginning again
    BitBlt Me.hdc, EndScroll, 0, GlueWidth, BackHeight, BackDC, 0, 0, vbSrcCopy
Else
    BitBlt Me.hdc, 0, 0, ScrollWidth, BackHeight, BackDC, X, 0, vbSrcCopy
End If

Me.Refresh

X = (X Mod BackLength) + 10

End Sub

 

We have a static X variable, which keeps track on the actual position on the background image. The first thing we'll do is test whether the window has moved enough to the right that it has to be wrapped around. If it has, we calculate the two lengths that are needed to perform the two blits.

The first (GlueWidth) is the length of the window that has moved past the right most edge of the image. This is logically also the width of the part which we want blitted from the start of the image again. The second variable holds the remaining length, the length from the X position to the right most edge of the background image. To better illustrate what is actually going on have a look at this illustration:

From the above illustration it is apparent that we actually 慻lue?the two separate parts into one, and thus makes it appear as though we keep moving around the background image.

If the X position + the length of the display window is still inside the bounds of the background image, an ordinary Blit is used, starting from the X position, with a width of the display window.

 

Multiple side scrolling backgrounds

One background might suffice in many situations, but what if you want your sprite to be able to walk behind certain objects and in front of others? Asked another way, how can we create the illusion of depth for our 2D side-scroller? We do this by creating several layers, in fact we could call this type of game approach a 'Multi-Layer Side-Scroller'. This is one method of giving a 2D game a little bit of a 3D look.

Study this illustration:

 

The drawing order must be very specific when it comes to drawing in a multiple background situation. You must start with the Absolute background, then all the secondary background layers. Then the sprite layer, which can cover the entire background layer (but does not have to) and lastly the foreground layer, is drawn.

The sample project in MULTIBACK.ZIP, demonstrates this kind of scenario.

The sample project has four layers, as illustrated above. The drawing technique is the same as with single scrolling backgrounds, the X position in the map is moved on each timer event, and the background is wrapped if necessary. The next background to draw is the secondary background layer. The technique is the same, but since this layer must be transparent so that we can see the background, we have to draw both the mask and the sprite. The Y position of the background is also changed so that the layer is drawn towards the bottom of the display. The sprite is drawn statically to the same position (why should it move when everything else is moving?). Lastly the foreground is drawn, using the same method as with the secondary background layer.

The whole drawing event looks like this:


Private Sub TimerScroll_Timer()


Static X As Long, XBack1 As Long, XFore As Long
Dim GlueWidth As Long, EndScroll As Long

'Draw the absolute background
If X + ScrollWidth > ABBAckWidth Then 'We ned to glue at the beginnig again
    'Calculate the remaining width
    GlueWidth = X + ScrollWidth - ABBAckWidth
    EndScroll = ScrollWidth - GlueWidth
   
    'Blit the first part
    BitBlt picBack.hdc, 0, 0, EndScroll, ABBackHeight, DCABBAck, X, 0, vbSrcCopy
    'Now draw from the beginning again
    BitBlt picBack.hdc, EndScroll, 0, GlueWidth, ABBackHeight, DCABBAck, 0, 0, vbSrcCopy
Else
    BitBlt picBack.hdc, 0, 0, ScrollWidth, ABBackHeight, DCABBAck, X, 0, vbSrcCopy
End If

'Draw the first back ground
If XBack1 + ScrollWidth > Back1Width Then 'We ned to glue at the beginnig again

    'Calculate the remaining width
    GlueWidth = XBack1 + ScrollWidth - Back1Width
    EndScroll = ScrollWidth - GlueWidth

    'Blit the first part
    BitBlt picBack.hdc, 0, ABBackHeight - Back1Height, EndScroll, Back1Height, _
           DCBack1M, XBack1, 0, vbSrcAnd

    BitBlt picBack.hdc, 0, ABBackHeight - Back1Height, EndScroll, Back1Height, _
           DCBack1, XBack1, 0, vbSrcPaint

    'Now draw from the beginning again
    BitBlt picBack.hdc, EndScroll, ABBackHeight - Back1Height, GlueWidth, _
           Back1Height, DCBack1M, 0, 0, vbSrcAnd

    BitBlt picBack.hdc, EndScroll, ABBackHeight - Back1Height, GlueWidth, _
           Back1Height, DCBack1, 0, 0, vbSrcPaint
Else

    BitBlt picBack.hdc, 0, ABBackHeight - Back1Height, ScrollWidth, Back1Height, _
           DCBack1M, XBack1, 0, vbSrcAnd

    BitBlt picBack.hdc, 0, ABBackHeight - Back1Height, ScrollWidth, Back1Height, _
           DCBack1, XBack1, 0, vbSrcPaint
End If

'Draw the sprite
BitBlt picBack.hdc, XSprite, ABBackHeight - SpriteHeight, SpriteWidth, _
           SpriteHeight, DCSpriteM, 0, 0, vbSrcAnd

BitBlt picBack.hdc, XSprite, ABBackHeight - SpriteHeight, SpriteWidth, _
           SpriteHeight, DCSprite, 0, 0, vbSrcPaint

'Draw the fore ground
If XFore + ScrollWidth > ForeWidth Then 'We ned to glue at the beginnig again

    'Calculate the remaining width
    GlueWidth = XFore + ScrollWidth - ForeWidth
    EndScroll = ScrollWidth - GlueWidth
    'Blit the first part

    BitBlt picBack.hdc, 0, ABBackHeight - ForeHeight, EndScroll, ForeHeight, _
           DCForeM, XFore, 0, vbSrcAnd

    BitBlt picBack.hdc, 0, ABBackHeight - ForeHeight, EndScroll, ForeHeight, _
           DCFore, XFore, 0, vbSrcPaint

    'Now draw from the beginning again
    BitBlt picBack.hdc, EndScroll, ABBackHeight - ForeHeight, GlueWidth, ForeHeight, _
           DCForeM, 0, 0, vbSrcAnd

    BitBlt picBack.hdc, EndScroll, ABBackHeight - ForeHeight, GlueWidth, ForeHeight, _
           DCFore, 0, 0, vbSrcPaint
Else
    BitBlt picBack.hdc, 0, ABBackHeight - ForeHeight, ScrollWidth, ForeHeight, _
           DCForeM, XFore, 0, vbSrcAnd

    BitBlt picBack.hdc, 0, ABBackHeight - ForeHeight, ScrollWidth, ForeHeight, _
           DCFore, XFore, 0, vbSrcPaint
End If

'Draw the back buffer onto the display
BitBlt Me.hdc, 0, 0, ScrollWidth, ABBackHeight, picBack.hdc, 0, 0, vbSrcCopy

Me.Refresh

'Modify the positions.
X = (X Mod ABBAckWidth) + 1
XBack1 = (XBack1 Mod Back1Width) + 8
XFore = (XFore Mod ForeWidth) + 25

End Sub

The X position on the different layers is controlled by a separate variable. This enables us to move the layers with different speeds, which further enhances the illusion of depth and distance.

Star field background

Another often-used background is a star field background, which as the name implies simulates a moving star field. The most efficient method when it comes to speed is of course to make a bitmap and then draw it directly onto the gaming as the absolute background. Another and more fun method is to randomly generate dots and small circles on the drawing area.

The sample project STARFIELD in STARFIELD.ZIP demonstrates how to create a simple star field of small circles and dots. To represent a star we create a type called Star and declare it as follows:


Private Type Star
    X As Long
    Y As Long
    Speed As Long
    Size As Long
    Color As Long
 End Type

 

The X and Y members are the position of the star. The size is the diameter of the star in pixels. The Speed is the amount of pixels the star moves each turn. The color is the color of the star.

To represent the stars in this sample project we have an array of Star Types (called Stars). This array is initialized in the Load event of the form:

Private Sub Form_Load()

Dim I As Long

Randomize
'Generate the 100 stars
For I = LBound(Stars) To UBound(Stars)
   
    Stars(I).X = Me.ScaleWidth * Rnd + 1
    Stars(I).Y = Me.ScaleHeight * Rnd + 1
    Stars(I).Size = MaxSize * Rnd + 1
    Stars(I).Speed = MaxSpeed * Rnd + 1
    Stars(I).Color = RGB(Rnd * 255 + 1, Rnd * 255 + 1, Rnd * 255 + 1)
Next I

End Sub

 


Each individual star is initialized to a random value within the bounds of the window.

The stars are drawn in the timer event, using the Ellipse API function.


Private Sub TimerStarField_Timer()

Dim I As Long

'clear the form
BitBlt Me.hdc, 0, 0, Me.ScaleWidth, Me.ScaleHeight, 0, 0, 0, vbBlackness

For I = 0 To UBound(Stars)
   
    'Move the star
    Stars(I).Y = (Stars(I).Y Mod Me.ScaleHeight) + Stars(I).Speed
    'Relocate the X position
    If Stars(I).Y > Me.ScaleHeight Then
      Stars(I).X = Me.ScaleWidth * Rnd + 1
    End If
    'Set the color
    Me.FillColor = Stars(I).Color
    Me.ForeColor = Stars(I).Color
    'Draw the star
    Ellipse Me.hdc, Stars(I).X, Stars(I).Y, Stars(I).X + Stars(I).Size, Stars(I).Y + Stars(I).Size

Next I

End Sub

 

The BitBlt outside the loop makes the background of the window black, using the vbBlackness raster operation. The stars are moved using the usual operation. If a star goes out of bounds, compared to the scale height of the window, the X position of the star will be changed, in order to created more randomness. The Ellipse function simply takes two parameters, the first pair defining the upper-left 慶orner?of the circle, the second defining the lower-right 慶orner?

  The Game Loop

So far we have been using the timer control to continuously execute the so-called game loop. The Game Loop is where everything is being controlled and drawn in the game. But as you also might have noticed, the Timer does not have a very good resolution. Put the somewhat irregular firing of the timer event on top of that, and you have a very bad scenario for games.

A much better scenario is to let the computer run as fast as it possible can, and then slow the game loop down with a defined time interval. This way we get maximum speed, and are still able to control the timing of the loop. You may sometimes hear game programmers refer to this as a throttle.

This can easily be accomplished in much the same way as we defined the frame ratio of the animated sprites in the previous project. The sample project in GAMELOOP.ZIP, demonstrates a possible implementation of such a scheme. The actual game loop is a procedure called RunGameLoop. The frame of the game loop is as such:


Private Sub RunGameLoop()

Const TickDifference As Long = 20
Dim LastTick As Long
Dim CurrentTick As Long

'Show the form
Me.Show

Do
    CurrentTick = GetTickCount()
      
    If CurrentTick - LastTick > TickDifference Then
                
        'Do the game drawing and calculation here
        
        'Update the frame variable
        LastTick = GetTickCount()                
    Else        
        'It is not time yet, do something else here
    End If
   
   
    DoEvents
   
Loop

'If we are here we are finished
Unload Me
Set frmGameLoop = Nothing

End Sub


We have two very important variables here, the LastTick, which keeps the time of the last time the game functions were executed and the CurrentTick, which keeps track the current time. The constant, TickDifference, is the actual time interval, which must pass before a frame update will occur.

So the trick is to simply subtract the CurrentTick amount from the LastTick, and test whether the difference between the variables is greater than the required time interval. If the difference is greater, then the game drawing and calculation must proceed, if not, then other things can be done. This comparison is tightly situated inside a Do匧oop which apparently never ends. So you somehow need an outside breaking of the loop. In the chapter about user input you will see several methods of testing whether a given key has been pressed, and based on that, exit the program.

In the sample project you can observe how much more effective this scheme is compared to the use of timers. The project simply draws a sprite and moves it over the screen. Run the sample project and push the Start normal Timer button. Then press the Start Loop button and observe the difference.

There are many other ways to implement a game loop. The one shown here is the one which, we will use throughout this book. The reason for this should become apparent in later chapters. You will notice that some programmers control the loop with the Sleep API function. This is not always a good method, since it freezes the whole process, and thus makes any other events in the application impossible until the specified time has elapsed.

More on Bitmaps

Bitmaps are the essential graphical element in VB game programming. That is the reason that a solid understanding of bitmapped graphics is so important for the game-programmer. Unfortunately bitmaps in Win32 is a major subject, which could probably fill a whole book by itself, so only the most common and elemental subjects will be covered here. If you decide to continue your game-programming career you will want to continue to explore advanced graphics programming theories and techniques.

Types of Bitmaps

There are three basic types of bitmaps that we will concern ourselves with, the three most common bitmaps, 1, 8 and 24 bit bitmaps.

The number of bits in a bitmap represents the possible number of colors the given bitmap can contain. This means that a 1-bit bitmap can have two possible colors, which are always black and white. The 1 bit bitmap is also known as a monochrome bitmap.

The 8-bit bitmap can contain up to 256 colors (28 = 256) and the 24-bit bitmap can have up to 16.7 millions colors (224 = 16.7 million). The value X-bit defines the size of each pixel in a bitmap. So an 8-bit bitmap with a width of 100 and a height of 200 takes up 100*200*8 bit = 160 KBits = 20 KB.

This sounds pretty easy, but unfortunately there is a bit more to it. A color in our bitmaps (8 & 24 bits. 1-bits bitmap are either black or white), is represented by a 24-bit value of the type RGB. The RGB type color is divided into 3 x 8 bit sets. Each of these 8-bit sets (which we might conveniently call a byte) represent the actual amount of color for Red, Green or Blue, hence the name RGB. Take a look at the following illustration:

 

So in a 24-bit bitmap, each single pixel is actually a 24-bit structure representing one of the many possible colors.

But what about 8-bit bitmaps how can each pixel be a 24-bit color, when they only take up 8-bit of space? Well, to put it simply, by using color tables. A color table is an array of 24-bit colors. In 8-bit bitmaps the array has 256 posts (from 0 to 255). So each pixel in an 8-bit bitmap does not represent an actual physical color, but instead an index to the associated color table. In a color table you only have two colors you can depend on (and this is actually not always the truth either), namely black and white. Black is index 0 and white is index 255 in the color table.

The use of color tables is a very unfortunate thing, since it makes the process of doing things to 8-bit bitmaps a bit more complicated than the 憂ormal?24-bit bitmaps. But since computers are still comparatively slow, you do not have many choices if you want super fast graphics.

Getting to the Bits

This next section is for those with a strong heart. This information is not that important in the simple samples which we have shown so far, but it is very important if you want to access the actual bits and bytes and do some interesting manipulation of the bitmaps. The function which will return the actual bytes of the bitmap pixels to us, is the GetBitmapBits API function, and the function which will return the bytes back to the bitmap is the SetBitmapBits API function. Let us start by examining the GetBitmapBits function. This function is declared as such:

Private Declare Function GetBitmapBits Lib "gdi32" (ByVal hBitmap As Long, _
                    ByVal dwCount As Long, lpBits As Any) As Long

 

The first parameter (hBitmap) is a handle to a bitmap in memory. If you recall we got a handle to a bitmap before, in the GenerateDC function. We'll use this handle when we load the bitmap now. To do this we have to change the GenerateDC function a bit. We have to pass a new parameter, which will receive the handle (it is by default passed by reference so this is no problem), and then of course we cannot delete the handle in the function. After this modification the GenerateDC will look like this (This corresponds to the updated way of doing this. Since this in actuality didn't contain any bugs - nothing has been changed).


'IN: FileName: The file name of the graphics
'    BitmapHandle: The receiver of the loaded bitmap handle
'OUT: The Generated DC
Public Function GenerateDC(FileName As String, ByRef BitmapHandle As Long) As Long

Dim DC As Long
Dim hBitmap As Long

'Create a Device Context, compatible with the screen
DC = CreateCompatibleDC(0)

If DC = 0 Then
    GenerateDC = 0
    'Raise error
    Err.Raise vbObjectError + 1
    Exit Function
End If

'Load the image.
hBitmap = LoadImage(0, FileName, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE Or LR_CREATEDIBSECTION)

If hBitmap = 0 Then 'Failure in loading bitmap
    DeleteDC DC
    GenerateDC = 0
    'Raise error
    Err.Raise vbObjectError + 2
    Exit Function
End If

'Throw the Bitmap into the Device Context
SelectObject DC, hBitmap

'Return the device context and handle
BitmapHandle = hBitmap
GenerateDC = DC

End Function


So now we have a handle to the loaded bitmap, we need to get the next parameter in the GetBitmapBits function. This parameter is a dwCount, which is the to number of bytes the function should return. In order to get this, we have to extract some info from the bitmap and into a special BITMAP structure, using the GetObjectAPI function . The bitmap structure is declared as such:

Private Type BITMAP
        bmType As Long 
        bmWidth As Long
        bmHeight As Long
        bmWidthBytes As Long
        bmPlanes As Integer
        bmBitsPixel As Integer
        bmBits As Long
End Type

 

Members of BITMAP structure:


bmType: The type of the bitmap. Must be 0 for logical bitmaps
bmWidth: The width of the bitmap in pixels. Must be greater than 0
bmHeight: The height of the bitmap. Must be greater than 0
bmWidthBytes: The width of the bitmap in bytes. This number must be even
bmPlanes: The number of color planes
bmBitsPixel: The number of bits per pixel
bmBits: A pointer to the bits.


 

The important members of this structure, that concern us here, are the bmWidth, bmHeight and bmWidthBytes members. To calculate how many bytes we need we simply multiply the bmHeight with bmWidthBytes. The bmWidth can not be used in this instance, since it only contains width values in pixels, and not in bytes.

The last parameter in the GetBitmapBits function lpBits is a pointer to the buffer, which will hold the bits. Since we are not eligible to work with pointers directly, we have to 慶heat?the system a bit. The lpBits parameter is passed By Reference, which means that the actual variable will not be passed, but instead a reference (pointer) to the variable is made and passed to the function. So the trick is to pass the first index in a byte array to the function. This address of this first index will be passed to the function, and it will take it from there. So the only thing we have to do, is to initialize a Byte array with at least the same amount of space as the value passed in the dwCount. This way the function will copy all the bits into our buffer (byte array), and we will be able to work with them as any normal array.

So in order to get the bits and bytes of a given bitmap, we could do the following:


Dim BitmapImage As Long 'The DC of the bitmap
Dim bm As BITMAP  'The bitmap structure
Dim hbm As Long   'The bitmap handle
Dim OriginalBits() As Byte

'Load the bitmap and get the handle on it
BitmapImage = GenerateDC(App.Path & "\bitmap.bmp", hbm)

'Get the information into the BITMAP structure
GetObjectAPI hbm, Len(bm), bm

'Redimension the byte array to fit into the size of the bitmap
ReDim OriginalBits(1 To bm.bmWidthBytes, 1 To bm.bmHeight)

'Retrieve the bits into the byte array
GetBitmapBits hbm, bm.bmWidthBytes * bm.bmHeight, OriginalBits(1, 1)

 

Setting the Bits

After manipulating with the bits and bytes of a bitmap, you have to set them back into position before any updates are shown. This is done with the SetBitmapBits function. It is declared as such:

Private Declare Function SetBitmapBits Lib "gdi32" (ByVal hBitmap As Long, _
                ByVal dwCount As Long, lpBits As Any) As Long

It is almost identical with the GetBitmapBits, and is also used in the same way. The hBitmap parameter is the handle of the given bitmap. The dwCount parameter is the size of the byte array, which contains the new bytes, and the lpBits is the first index into the byte array with the pixel information.

 

24-bit bitmaps

So now that we know all about getting and setting the bits of a bitmap, let us take a look at what can actually be done with it. The sample project in BITMAPS.ZIP , demonstrates some simple image processing algorithms. Before actually showing you how these are implemented we should address the problem with using 24-bit bitmaps and manipulating these in an array of bytes. There is no data structure in Visual Basic, which is a 24-bit variable, so we cannot directly make a value and set it. Instead we have to deal with 24-bit in pieces of 8 bit (1 byte).

Take a look at the following illustration, which shows the byte layout of small part of a 24-bit bitmap:

As you can see each pixel is one byte 慼igh?and 3 bytes 憌ide? which is equal to (3*8 bits + 1*8 bits) = 24 bits. This is relatively simple. The problem occurs when you think about how our byte array is organized. It just represents each byte with a separate index and not each pixel as a separate index. So an index of (3,1) into the byte array will not produce the third pixel in the first row, but instead produce the color value of blue of the first pixel. Keep this mind.

Our sample project is built around some effects applied to a picture. The picture is loaded in the Form_Load event. In the same event we extract the information we need to do the pixel manipulations:

Private Sub Form_Load()

'Load the image
BitmapImage = GenerateDC(App.Path & "\bitmap.bmp", hbm)

'Get the bitmap structure
GetObjectAPI hbm, Len(bm), bm

'Preinitialize the byte array
ReDim OriginalBits(1 To bm.bmWidthBytes, 1 To bm.bmHeight)

BitmapWidth = bm.bmWidth
BitmapHeight = bm.bmHeight

'Get the bits
GetBitmapBits hbm, bm.bmWidthBytes * bm.bmHeight, OriginalBits(1, 1)
  
'Draw the bitmap
BitBlt Me.hdc, 0, 0, BitmapWidth, BitmapWidth, BitmapImage, 0, 0, vbSrcCopy

End Sub

 

We use the same methods as described above to extract the pixels from the bitmap. Lastly we display the image with the BitBlt function. Note that the OriginalBits() array is declared at global level, and is used as the reference to the bits in the image processing functions.

Graying

Graying an image is very simple, you simply add the value of each color together and divide this value with 3. This will get you the medium value of the three colors. The three colors are then set to this medium value, and you have a gray pixel.

The code looks like this:


Private Sub cmdGrey_Click()

Dim BitmapWidthBytes As Long
Dim ByteArray() As Byte
Dim I As Long, J As Long
Dim TempColor As Long

ReDim ByteArray(1 To bm.bmWidthBytes, 1 To bm.bmHeight)

For I = 1 To bm.bmWidthBytes Step 3
    For J = 1 To bm.bmHeight
       
        TempColor = OriginalBits(I, J)
        TempColor = TempColor + OriginalBits(I + 1, J)
        TempColor = TempColor + OriginalBits(I + 2, J)
        TempColor = TempColor / 3
       
        ByteArray(I, J) = TempColor
        ByteArray(I + 1, J) = TempColor
        ByteArray(I + 2, J) = TempColor
           

    Next J
Next I

SetBitmapBits hbm, bm.bmWidthBytes * bm.bmHeight, ByteArray(1, 1)

BitBlt Me.hdc, 0, 0, BitmapWidth, BitmapHeight, BitmapImage, 0, 0, vbSrcCopy
Me.Refresh

End Sub

--------------------------------------------------------------------------------

As you can see we loop through the local ByteArray() array and set each byte with the appropriate color. The problem with the one byte vs. three bytes pr pixel we discussed above is overcome by using a loop with the Step 3 option. This makes the outer loop able to set the variable-counter I, to first byte of each pixel in the byte array. We then simply add 1 or 2 to this value and we can access the other colors of the same pixel.

We add each of the individual color values of one pixel together into the TempValue variable. Then we will divide the value in the TempValue variable by 3, and the each color in the pixel is set to this color, which makes the pixel gray.

Blue, Red, Green

Making the bitmap one of either color is actually very simple. If you want to make a pixel blue, you simply set the red and green color values to 0 (black), and you have a pixel dominated by the blue color. Of course if the pixel didn抰 have any blue color in it, then it would turn colorless (black). The same thing goes when a pixel red or green.

The code for this procedure (only Blue shown here) is:


Private Sub cmdBlue_Click()

Dim BitmapWidthBytes As Long
Dim ByteArray() As Byte
Dim I As Long, J As Long

ReDim ByteArray(1 To bm.bmWidthBytes, 1 To bm.bmHeight)

For I = 1 To bm.bmWidthBytes Step 3
    For J = 1 To bm.bmHeight
       
        ByteArray(I, J) = 0
        ByteArray(I + 1, J) = 0
        ByteArray(I + 2, J) = OriginalBits(I + 2, J)
       
    Next J
Next I

SetBitmapBits hbm, bm.bmWidthBytes * bm.bmHeight, ByteArray(1, 1)

BitBlt Me.hdc, 0, 0, BitmapWidth, BitmapHeight, BitmapImage, 0, 0, vbSrcCopy
Me.Refresh

End Sub


Notice again that we use the Step 3 option to set the counter-variable to the start of a pixel.

Brightness

To make a pixel brighter we simply add a value to each of the colors in the pixel. To make it darker we subtract a value from each of the colors in the pixel. It is really as simple as that (isn抰 it disappointing?).

The way we implement it here is to first make a so-called LookUp table. Such a beast is actually no more than an array of calculated values. The lookup table we will generate contains values from 0 to 255, just like a typical byte. The values in the lookup table will increase as the index increases, so any given index will always have a value at least equal to or greater than any lower indexes.

The values in the lookup table are calculated by multiplying the current index with a variable lighting factor. This means that a variable lighting factor of less than 1 will darken the picture instead of lighting it up. The code used in the sample project to build a lookup table is like this:


For I = 0 To 255
    TempValue = I * Val(txtBright.Text)
   
    If TempValue > 255 Then
        BrightTable(I) = 255
    Else
        BrightTable(I) = TempValue
    End If
Next I

The lookup table in this sample code is the BrightTable. The variable lighting factor is entered in the text box next to the Brightness button on the form.

Now that we have this lookup table, let take a peek at the actual code, which manipulates the pixels:

For I = 1 To bm.bmWidthBytes Step 3
    For J = 1 To bm.bmHeight
       
        ByteArray(I, J) = BrightTable(OriginalBits(I, J))
        ByteArray(I + 1, J) = BrightTable(OriginalBits(I + 1, J))
        ByteArray(I + 2, J) = BrightTable(OriginalBits(I + 2, J))
       
    Next J
Next I


We set each of the bytes to the corresponding value in the lookup table, and thus either makes it lighter or darker, depending on the variable lighting factor.

Ripple

The ripple effect is a bit different than any of the other effect we have applied. This is because the rippling does not directly change the color of a specific pixel, but instead set the color of a given pixel equal to the color of another pixel.

As with the Brightness effect, we build a lookup table to store intermediate values in. This lookup table must not be longer than the width of the bitmap, since the index of the table is considered an X-position into the bitmap. So by defining the actual index in the lookup table as a position in the bitmap, the actual value in the index is the distortion pixel, i.e. the pixel that the index X position will be moved to.

So how do we calculate these distortion pixel values? Since we want a ripple (wave like) effect, so why not use the Sin function which define values in waves (I know this is not correct, just accept it in our usage).

So by using a formula with a Sine calculated value we can distort (move) the pixel in the lookup table by adding the index from the table to result of the formula, and thus get a wave like result. The building of the lookup table looks like this:


 

For I = 1 To BitmapWidth
    TempValue = I + Sin(I / 5) * Val(txtRipple.Text)
    If TempValue > BitmapWidth Then
        RippleTable(I) = BitmapWidth
    ElseIf TempValue < 1 Then
        RippleTable(I) = 1
    Else
        RippleTable(I) = TempValue
    End If
Next I

 

We still have to keep things from moving out of bounds, so we make sure that any values over 200 and below 1 are correct to either the least (1) or the greatest (200) value.

So the actual manipulation is then quite simple and looks like this:


For I = 1 To bm.bmWidthBytes Step 3
    For J = 1 To bm.bmHeight
        ByteArray(I, J) = OriginalBits(I, RippleTable(J))
        ByteArray(I + 1, J) = OriginalBits(I + 1, RippleTable(J))
        ByteArray(I + 2, J) = OriginalBits(I + 2, RippleTable(J))
    Next J
Next I


We simply replace the horizontal pixel with the color of the distorted pixel, which is stored in the lookup table, and we have a wave like ripple on the bitmap.

Please note that there are many different algorithms and implementations to achieve each these different effects, the ones shown in this section is just our way of demonstrating pixel manipulation in VB. So do not complain if you discover a nicer or better algorithm, we were just demonstrating pixel manipulation and not algorithm effectiveness and implement.

8-Bit bitmaps

So how do we apply the same effects to 8-bit bitmaps? Well, the actual algorithms are the same, but the implement is not. As stated previously then the pixels in 8-bit bitmaps are not color values, but instead indexes into a color table. The color table holds 256 different 24-bit colors. So what we have to do in order to obtain the effects (all beside the ripple algorithm) is to manipulate this color table, instead of the pixels.

A Color Table in 8-bit bitmaps is an array of 256 RGBQUAD structures. The RGBQUAD structure is defined as such:

Private Type RGBQUAD
        rgbBlue As Byte
        rgbGreen As Byte
        rgbRed As Byte
        rgbReserved As Byte
End Type


The rgbRed, rgbBlue and rgbGreen members combine to make the 24-bit color value of the given index. The rgbReserved member is reserved and must therefore always be 0.

To get the color table of an 8-bit bitmap, we will use the GetDIBColorTable API function. This function is declared as such:


Private Declare Function GetDIBColorTable Lib "gdi32" (ByVal hDC As Long, _
                ByVal un1 As Long, ByVal un2 As Long, pRGBQuad As RGBQUAD) As Long

The first parameter, hDC, is the device context to retrieve the color table from. The second parameter un1, is the start index of the color table you wish to get. The third parameter un2, is the number of indexes you want to retrieve. The last parameter pRGBQuad is the first index into an array of RGBQUAD structures, which will be filled with the color table.

The function to set a color table back to a device context is the SetDIBColorTable API function. It looks like this:

Private Declare Function SetDIBColorTable Lib "gdi32" (ByVal hDC As Long, _
                 ByVal un1 As Long, ByVal un2 As Long, pcRGBQuad As RGBQUAD) As Long

 

The parameters are the same as with the GetDIBColorTable.

Now that we know how to set and get the color table of an 8-bit bitmap, let抯 get to work on applying the effects. The sample project in BIT8BITMAPS.ZIP , demonstrates the same effects as the BITMAP project did with the 24-bit bitmaps.

Since we are now working with color tables instead of pixels, we can do much of the hard work in the initializing phase of the application. This means that we can build the color tables for the Gray, Red, Blue, Green and Invert tables, before applying any of the effects.

The tables are generated in the CreateColorTables() procedure:

 

Private Sub CreateColorTables()


Dim I As Long
Dim TempValue As Long

For I = LBound(GrayTable) To UBound(GrayTable)
       
    'Create Gray Color table
    'Add the values together
    TempValue = OriginalTable(I).rgbBlue
    TempValue = TempValue + OriginalTable(I).rgbGreen
    TempValue = TempValue + OriginalTable(I).rgbRed
   
    'Get the medium value
    TempValue = TempValue / 3
   
    'Set the color in the gray table
    GrayTable(I).rgbBlue = TempValue
    GrayTable(I).rgbGreen = TempValue
    GrayTable(I).rgbRed = TempValue
   

    'Create the rest of the color tables
    RedTable(I).rgbBlue = 0
    RedTable(I).rgbGreen = 0
    RedTable(I).rgbRed = OriginalTable(I).rgbRed
   
    GreenTable(I).rgbBlue = 0
    GreenTable(I).rgbRed = 0
    GreenTable(I).rgbGreen = OriginalTable(I).rgbGreen
   
    BlueTable(I).rgbBlue = OriginalTable(I).rgbBlue
    BlueTable(I).rgbGreen = 0
    BlueTable(I).rgbRed = 0
   
    InvertTable(I).rgbBlue = 255 - OriginalTable(I).rgbBlue
    InvertTable(I).rgbGreen = 255 - OriginalTable(I).rgbGreen
    InvertTable(I).rgbRed = 255 - OriginalTable(I).rgbRed

Next I

End Sub

 

As you can see the actual algorithms are the same. The gray effect is still implemented by adding the three color-values and dividing the result by three. The same thing goes for the color effects. The color, which should be dominant, is kept, while the rest is set to 0.

The ripple effect is implemented in the same way as for 24-bit bitmaps, by manipulating the actual positions of the pixels instead of the color of the pixels. But since each pixel is only 8-bits we do not have to implement any special iteration when we set the pixel positions.

The brightness effect is generated in the same fashion as with the 24-bit bitmaps, by first making a lookup table and then applying these effects into the color table.

This concludes our little expedition into bitmapped country. There is much more to it than what is written here, but you should now have a general idea on how to manipulate with bitmap pixels in order to get some special effects into your game.

End of Part III of the three part Drawing and Animation Tutorial by Burt Abreu & S鴕en Skov.

本文地址:http://com.8s8s.com/it/it6448.htm