DirectDraw: Alpha Blending with vbDABL
By: David Goodlad [Email] [Web: The Black Hole, TheNexus]
Written: July 2, 2000
Download:
vbDABL.zip
(279kb)
Introduction
Many people wonder how to do alpha blending with DirectDraw in Visual Basic. If
they've got a fair bit of knowledge, they'll try to do it using GetLockedArray
along with CopyMemory. Yes, this method will work just fine. The only problem:
it's *sloooow*. My solution: vbDABL: visual basic Directx Alpha Blending Library.
This is an open-sourced DLL written mainly in inline assembly, with an outside
'shell' of C++, which provides various highly optimized functions to a Visual
Basic programmer which perform alpha blending. Currently, it requires an MMX processor
(most new Pentium processors, from about the P-233 up to the PIIIs) to run due
to the use of packed data functions in the 64-bit registers that MMX provides.
But, with this little restriction, which is basically negligible nowadays, a *massive*
speed increase is gained. So, with this little bit of background info, let's get
started...
Getting and Installing vbDABL
You can get vbDABL 0.20, which is the most recent release as of this writing,
from my website which is linked above. It comes in a fairly small .ZIP file. When
you unzip it, you will get two different directories: 'library' and 'sample'.
What you should really be interested in is the 'library' directory, as it contains
the precompiled DLL as well as the source code. All that you need to do if you
don't want to recompile the DLL yourself (which you shouldn't need to do), is
copy the \library\precompiled\Release\vbDABL.dll file to your \windows\system
directory on Windows9x, or your \winnt\system32 directory on Windows NT (including
2000).
Once this is done, you will have to tell Visual Basic about the exported functions,
as there is no Type Library for the DLL as there is for DirectX. Do so by adding
the following function declarations to the '(Declarations)' section of a .BAS
Module.
Public
Declare
Function
vbDABLalphablend16
Lib
"vbDABL"
(ByVal
iMode
As
Integer,
ByVal
bColorKey
As
Integer,
_
ByRef
sPtr
As
Any,
ByRef
dPtr
As
Any,
ByVal
iAlphaVal
As
Integer,
ByVal
iWidth
As
Integer,
ByVal
iHeight
As
Integer,
_
ByVal
isPitch
As
Integer,
ByVal
idPitch
As
Integer,
ByVal
iColorKey
As
Integer)
As
Integer
Public
Declare
Function
vbDABLcolorblend16555
Lib
"vbDABL"
(ByRef
sPtr
As
Any,
ByRef
dPtr
As
Any,
ByVal
alpha_val%,
_
ByVal
Width%,
ByVal
Height%,
ByVal
sPitch%,
ByVal
dPitch%,
ByVal
rVal%,
ByVal
gVal%,
ByVal
bVal%)
As
Long
Public
Declare
Function
vbDABLcolorblend16565
Lib
"vbDABL"
(ByRef
sPtr
As
Any,
ByRef
dPtr
As
Any,
ByVal
alpha_val%,
_
ByVal
Width%,
ByVal
Height%,
ByVal
sPitch%,
ByVal
dPitch%,
ByVal
rVal%,
ByVal
gVal%,
ByVal
bVal%)
As
Long
Public
Declare
Function
vbDABLcolorblend16555ck
Lib
"vbDABL"
(ByRef
sPtr
As
Any,
ByRef
dPtr
As
Any,
ByVal
alpha_val%,
_
ByVal
Width%,
ByVal
Height%,
ByVal
sPitch%,
ByVal
dPitch%,
ByVal
rVal%,
ByVal
gVal%,
ByVal
bVal%)
As
Long
Public
Declare
Function
vbDABLcolorblend16565ck
Lib
"vbDABL"
(ByRef
sPtr
As
Any,
ByRef
dPtr
As
Any,
ByVal
alpha_val%,
_
ByVal
Width%,
ByVal
Height%,
ByVal
sPitch%,
ByVal
dPitch%,
ByVal
rVal%,
ByVal
gVal%,
ByVal
bVal%)
As
Long |
|
|
|
For now, we will ignore the four 'colorblend' functions, and simply examine the
plain old 'vbDABLalphablend16' function.
vbDABLalphablend16 parameters
I think the best way to get started is to look over each of the parameters of
this function:
- iMode: The only possible values
for this parameter are '555' or '565'. It specifies how each pixel is formatted
in memory. In 16-bit mode, there are two possible ways: 555, where there are
5 red bits, 5 green bits, and 5 blue bits, and 565 where there are 5 red bits,
6 green bits, and 5 blue bits. I'll show you how to detect this a little bit
later...
- bColorKey: This parameter
tells vbDABL whether to use colorkeying (a transparent color which will not
be copied) or not. If this is a '0' (zero), colorkying will NOT be used, any
other number will enable colorkeying.
- sPtr As Any: The 'pointer'
to, or memory address of, the DirectDraw surface to be used as the source
for the alpha blending. It is passed 'ByRef', so what you actually pass as
the parameter is NOT a pointer, but what truly gets to the function IS. I'll
explain how this works in more detail later...
- dPtr As Any: The pointer to
the DirectDraw surface to be used as the destination for the alpha blending.
All the comments about sPtr apply to this parameter as well.
- iAlphaVal: This is the 'alpha'
value to use for the blend. Basically, it is a number between 0 and 255, with
0 making the resulting pixels (which end up on the destination surface) equal
to the destination pixels, so there is no change at all, and 255 making the
resulting pixels equal to the source pixels, essentially a plain blit from
source to destination. Therefore, the closer you get to 255, the more the
resulting pixel resembles the source pixel rather than the destination pixel.
This may become clearer to you after I discuss the algorithm used for the
blending later.
- iWidth: The width (in pixels)
of the area you want to blend.
- iHeight: The height (once
again, in pixels) of the area you want to blend.
- isPitch: This is the 'pitch'
of the source surface, as defined by DirectDraw. Basically it's the number
of bytes per row of pixels in memory. There is a better definition and explanation
in DirectX SDK documentation. All you need to know is that you can get it
out of the DDSURFACEDESC2 structure filled out for you when you lock your
surface.
- idPitch: The pitch of the
destination surface... All the isPitch stuff applies here as well.
- iColorKey: Hmm, similar to
the 'bColorKey' parameter, except that this one starts with an 'i'. The way
I named many of the parameters is to use the first letter of the variable
type as the first letter of the parameter; thus bColorKey is treated as a
boolean (even though it's actually an integer...), and this one is a true
integer. It is only used if bColorKey is not 0, therefore only if colorkeying
is enabled. It is the numerical value of the color that you want to be transparent
in your blend (if you aren't using colorkeying, just leave it as 0 or something,
it really doesn't matter). It behaves the same way as a colorkey in a normal
DirectDraw blit.
So How do I Use this!?
Okay, we've gotten through the boring technical stuff... But now you're wondering
about how we actually use vbDABL to do alpha blending. You can't just call the
function without setting some things up first, because it will have no way to
access whatever memory you tell it to (you can't even get a pointer to the surface
without setting stuff up properly!). So, what do we have to do to set everything
up? Lock your surfaces, that's what! :) You should probably already be aware of
this function and how it works, so I won't go into detail about it... Here's the
code that will surround your call to vbDABL:
Dim
rEmptyRect
As
RECT,
dArray()
As
Byte,
sArray()
As
Byte
ddsDestination.Lock
rEmptyRect,
ddsdDestination,
DDLOCK_WAIT,
0
ddsSource.Lock
rEmptyRect,
ddsdSource,
DDLOCK_WAIT,
0
ddsDestination.GetLockedArray
dArray()
ddsSource.GetLockedArray
sArray()
'Call
to
vbDABL
goes
here...
ddsSource.Unlock
rEmptyRect
ddsDestination.Unlock
rEmptyRect |
|
|
|
As you can see, we've also used the GetLockedArray method of each of our surfaces
to get an array of bytes which represents the raw data in the surface. Something
important to note, though, is to NEVER, EVER forget to put the calls to Unlock
in. If you do, you'll most likely end up with a crashed computer and have to physically
turn it off and then on again... Don't say I didn't warn you! :) (If you must
know, I've done this several times myself, so save yourself the trouble and learn
from my mistakes!)
Now that we've locked our surfaces, and have locked arrays for each, we have everything
that we need to make a call to vbDABLalphablend16. The locked arrays will be used
as the pointers to the surfaces (yes, that's right.), and the pitches of the surfaces
are in their respective DDSURFACEDESC2 structures. So, before I go on discussing
how to use these things in the call properly, I will give you an example to look
at and then refer back to:
Call vbDABLalphablend16 (555, 0, sArray(sX + sX, sY), dArray(dX + dX, dY), 200, 64, 32, ddsdSource.lPitch, ddsdDestination.lPitch, 0) |
|
|
|
Okay, there's a few confusing things here. Firstly, there's my arbitrary decision
to use 555 mode (the first parameter...). I will once again have to say I'm going
to leave that till later. Secondly, there's the use of the arrays as pointers.
It's a little difficult to explain why it works, but I'll explain what you have
to do to MAKE it work. You pass the (x+x,y)'th element of your array to the function.
Yes, confusing. I use x+x because there are two bytes per 16-bit pixel, right?
(and a multiplication by 2 is slower than an addition!) The y just remains unchanged.
The x and y values are the pixel coordinates of the first (ie: top left) pixel
to be blended by vbDABL. Hopefully this is clear enough, it's difficult to make
it any clearer. If you follow the example when making your calls, you will be
fine.
Deciding Which Mode to Use
Finally I get to this part! :) The problem with 16-bit mode is that you can't
divide 16 bits between three color channels evenly. Because of this, there has
been debate in the video hardware industry as to how to divide up the available
bits for each color. At first, it was generally agreed that in actuality only
15 bits should be used, to allow for an even distribution of the bits: 555 mode.
But recently, the move has been made to use all 16 bits: 565 mode, giving the
green channel double the degrees of accuracy of the two others. It doesn't give
green more saturation, it just gives the green channel basically 'in-between'
values. If you don't really understand, don't worry, I just like to give a bit
of background info about why this works. It's not really necessary to know :)
Anyways, because of the differences between the two modes, vbDABL has to know
which mode is being used by the client's video hardware. To figure this out is
actually quite easy: you use the 'bitmask' of the green channel. It will be different
for each of the different modes: &H3E0; in 555 mode, and &H7E0; in 565 mode. These
are hexadecimal numbers, that's why they're formatted weird :) So, what I recommend
doing is directly after your DirectDraw initialization, grab a DDSURFACEDESC2
structure from your surface, and store what mode the client is using in a global
variable. That way, you can simply pass the value of this variable to vbDABL;
no need to do a check each time you call it (when you're doing alphablending,
speed is something you should always be thinking about). This is a simple implementation
of the detection scheme I just described:
'Right
after
DirectDraw
initialization...
Dim ddsdTemp As DDSURFACEDESC2
ddsPrimary.GetSurfaceDesc ddsdTemp
If ddsdTemp.ddpfPixelFormat.lGBitMask = &H3E0 Then
ClientVidMode = 555
ElseIf ddsdTemp.ddpfPixelFormat.lGBitMask = &H7E0 Then
ClientVidMode = 565
Else
MsgBox "Client Video mode isn't 555 or 565, something's wrong."
End
End If |
|
|
|
Then, you would pass the ClientVidMode variable as the first parameter to vbDABL,
and you'd be set!
What about those colorblend functions?
Now that you can do basic alpha blending, you're probably wondering what else
there is to it? What are these colorblend functions that we declared earlier?
Basically, they allow you to do a basic alpha blend with a twist: instead of blending
the source and destination pixels and putting the result to the destination, you
blend the source pixels with a specified color, and put the result to the destination;
the destination pixels get overwritten and are never used in the blend! By doing
this, you can achieve some nice effects, with the added benefit of a slight speed
increase over the plain alpha blend as well as not having to have another surface
simply full of a solid color. One obvious use of this function would be to 'colorize'
a sprite: for example, in Final Fantasy 3 the characters would fade between a
greenish tinge and their regular colors when they were poisoned. I have recreated
this effect easily using the colorblend functions, as shown with the two pictures
below. The first one is the original sprite graphic (ripped from FF3, sorry Square!
hehe), and the second is with that graphic used as a replacement for the foreground.bmp
in the vbdabl sample, with the colorblend color changed to (0,255,0) - all green.
Nice effect huh? You can fade the amount of green by changing the alpha value;
simple as that! A second use for colorblend requires a bit of thought to discover
because you first have to realize that the same surface can be passed as both
the destination AND the source! In this way, you can basically do what would normally
would require a plain alpha blend with the source surface being of a solid color
and the destination being the stuff that you want the color blended on. Here's
a picture straight out of the vbDABL sample project:
The red rectangle in the bottom-right of the picture is an area that has been 'colorblended' by using the background surface as both the destination and the
source.
That's neat, but how do I use it??
It's quite simple, and very similar to the plain alpha blend. The only difference
is that instead of having a single function that allows you to choose between
the two modes, and whether or not to colorkey, there are four different functions;
one for each combination. The next release of vbDABL will rememdy this for sure
though :)
The parameters for each of the colorblend functions are very similar to the plain
alphablend one. The only differences are that the first two parameters of the
plain alphablend function are not present for the colorblend functions (they're
not needed), and there's three extra parameters for specifying the color to blend
with. These three represent the different color channels: red, green, and blue,
and each have a range of 0-255. The thing to note about them, though, is that
if you pass '5' as the red value, it will be interpreted as 0. This is because
there are only 5 bits for red, so the values are always shifted to the right 3
bits (basically divided by 2^3, or 8). Therefore, there are only 256/8=32 degrees
of accuracy for each color (with the exception of green in 565 mode; there are
64 degrees of accuracy for it in this case), so any number between 0-7 will be
treated as 0, 8-15 will be treated as 8, etc. This is a limitation placed on us
by the video mode, not by any calculations that vbDABL does. This is also the
reason for the odd 'jumps' in color that you might notice in the vbDABL sample
project, where when you've chosen to have the alpha value fade back and forth
from 0-255, you see the color suddenly jump from one tone to another, not smoothly
change.
Anyways, I apologize once again for all the theory, but it's a good thing to know
when you're dealing with 16-bit mode and alpha blending. I won't discuss in any
detail how to call the colorblend functions, as they're basically the same as
the basic alphablend function with the few differences described above. Look at
the sample that comes with vbDABL for some working code if you need it.
So how does this alpha blending actually work anyways?
Well, it's some very basic algebra actually. Here is the basic formula used for
alpha blending:
FinalPixel = (SourcePixel * AlphaValue) + (DestinationPixel * (1 - AlphaValue))
This involves an alpha value between 0 and 1, using floating point numbers, which
is SLOW. Integer math is much faster, so I use values from 0-255. Also, with some
messing around with the formula, you find that you can get rid of one of the multiplications
(which is the big bottleneck... a multiplication takes 40-50 times as long as
an addition, subtraction, or bit shift!) and end up with this:
FinalPixel = (AlphaValue * (Source - Destination)) / 256 + Destination
The only problem left is that in asm it's annoying to deal with signed numbers
(numbers that can be negative) when you are trying to maintain the right number
of bits, etc etc. So, to ensure that the subtraction in the middle there never
results in a negative, we add 256 to that inner bracket, and by algebraic rules,
we subtract (AlphaValue * 256) / 256. We can cancel out the multiplication and
division, and we finally end up with:
FinalPixel = (AlphaValue * (Source + 256 - Destination)) / 256 + Destination
- AlphaValue
Voila! That's the exact formula that I used in vbDABL to do all the alpha blending
(though with the colorblending the 'destination' color, which is the one specified
by the user, remains constant, so there are some pre-calculations that can be
done to give a slight speed boost...)
Wrapping Things Up...
Well, hopefully by now you should understand how to use vbDABL pretty well. If
you need to look at some source code, I suggest opening up the sample project
that comes in the same ZIP as the DLL. That should give you an idea of how it
all comes together, though it's not extremely well commented yet. Most of the
relevant code is in modDirectX under the 'RenderLoop' function.
If you have any questions/comments/suggestions/CONSTRUCTIVE criticism, please
feel free to email me!
David Goodlad
- webmaster: the black hole
http://blackhole.thenexus.bc.ca/
dgoodlad@junction.net
|