|
Drawing & Animation
Using the Win32 GDI #3
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.
| As you read through this and other
sections of the book you may find it very useful to use
the Debug menu to Step Into (F8) your code using
the Debug menu. This will enable you to get a clearer
picture of the program flow. |
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 glue 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
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
corner of the circle, the second defining the
lower-right corner.
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
Loop 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
normal 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:
'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 < 1 Then
GenerateDC = 0
'Raise error
Err.Raise vbObjectError + 1
Exit Function
End If
'Load the image....BIG NOTE: This function is not supported under NT, there you can not
'specify the LR_LOADFROMFILE flag
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
'''DeleteObject hBitmap do not delete the handle
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 bitmapsbmWidth: 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 cheat 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 high and 3 bytes wide, 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 is 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 didnt 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 (isnt 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, lets 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.
[Drawing & Animation #1] [Drawing & Animation #2] [Drawing & Animation #3] [Download All Samples]
These tutorials were originally developed by
Soren Christensen and Burt Abreu as part of a book which we were working on.
The book idea didn't come to fruition and so we decided to post the completed
chapters here in the hopes that you would find them useful. We retain copyright
to this material and you may not reproduce it, post it, or otherwise disseminate
it in any fashion without our express written consent with the exception of making
copies for your personal use.
|