ATTENTION READERS! Lucky's VB Gaming Site is no longer active. For updated game programming information and tutorials, please visit The Game Programming Wiki!
There comes a point in most 2D game projects using DirectDraw when you start
wondering how you can do all those cool alpha blending and rotation effects
you've seen in [insert your favorite 2D game here]. Now, you could switch
over to DirectGraphics8 to do all your 2D
graphics, but the major drawback with
that is that a.) It is more complicated, and you have a learn a whole different
structure (No DirectDraw!), and b.) You can't easily do surfaces of any
size - D3D surfaces need to be square and a power of 2 size no bigger than
256x256 (this is due to hardware limitations of your users). But!
What if you could use DirectDraw for the parts of your game that need big
surfaces, and use Direct3D in conjunction for the parts that need alpha
blending, rotation, scaling, etc.? You can! This tutorial will
explain (hopefully) simply how to do this.
Initializing DirectX
You initialize DirectX almost the same as a normal DirectDraw project, (See
how in this tutorial) with
a few exceptions:
1.) Declaring the D3D variables:
Dim d3d As Direct3D7
Public dev As Direct3DDevice7
2.) Initializing the primary surface to be D3D capable:
ddsdPrimary.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE Or DDSCAPS_3DDEVICE Or DDSCAPS_FLIP Or DDSCAPS_COMPLEX Or DDSCAPS_VIDEOMEMORY
3.) Initializing Direct3D:
Set d3d = dd.GetDirect3D
Set dev = d3d.CreateDevice("IID_IDirect3DHALDevice", BackBuffer)
Loading Surfaces
For loading surfaces, you will now have to take into account two kinds of
surfaces:
Normal DirectDraw Surfaces:
Can be any size - they don't need to be square or have power of 2
dimensions
Can't use alpha-blending, scaling, rotation, or vertex coloring
Direct3D Surfaces
Must be square, must have a power of 2 size, and can't be bigger than
256x256 (Possible sizes: 2x2, 4x4, 8x8, 16x16, 32x32, 64x64, 128x128,
256x256)
You can blit different sized portions from the square surface, if you need
a non-square sprite.
Supports Alpha Blending - This makes the the surface semi-transparent, for
window or smoke type effects.
Supports Rotation - You can manipulate the vertices of a sprite to rotate
it.
Supports Scaling - You can manipulate the vertices of a sprite to scale it
bigger or smaller. In fact, you can manipulate the vertices to do all
kinds of things, like skewing, distorting, and stretching.
Supports Vertex Coloring - You can assign any color to each of the four
corners of a D3D sprite, to "colorize" it.
So you will need a surface-loading routine that can load both types of
surfaces. For the sample project, I first create a dynamic array of
surfaces:
'The element type of the Surfaces() array
Public Type Surface
Surface As DirectDrawSurface7
Width As Integer
Height As Integer
D3DSurface As Boolean
End Type
'Array of surfaces
Public Surfaces() As Surface
'Current number of surfaces in Surfaces()
Public NumSurfaces As Long
This organizes the surfaces you use nicely. Here is my routine to load
surfaces:
Public Sub LoadSurface(File As String, Optional D3DSprite As Boolean)
Dim CKey As DDCOLORKEY
Dim SurfaceDesc As DDSURFACEDESC2
'If this is a D3D
sprite, load it accordingly
If D3DSprite = True Then
SurfaceDesc.lFlags = DDSD_CAPS Or DDSD_WIDTH Or DDSD_HEIGHT Or DDSD_CKSRCBLT
SurfaceDesc.ddsCaps.lCaps = DDSCAPS_TEXTURE
SurfaceDesc.ddsCaps.lCaps2 = DDSCAPS2_TEXTUREMANAGE
'Set the color key
SurfaceDesc.ddckCKSrcBlt.high = ColorKey
SurfaceDesc.ddckCKSrcBlt.low = ColorKey
'Create the surface
Set Surfaces(NumSurfaces).Surface = dd.CreateSurfaceFromFile(File, SurfaceDesc)
'Set the information for this surface
Surfaces(NumSurfaces).Width = SurfaceDesc.lWidth
Surfaces(NumSurfaces).Height = SurfaceDesc.lHeight
Surfaces(NumSurfaces).D3DSurface = True
'Normal DirectDraw surface
Else
SurfaceDesc.lFlags = DDSD_CAPS Or DDSD_WIDTH Or DDSD_HEIGHT
SurfaceDesc.ddsCaps.lCaps = DDSCAPS_VIDEOMEMORY Or DDSCAPS_OFFSCREENPLAIN
'Create the surface
Set Surfaces(NumSurfaces).Surface = dd.CreateSurfaceFromFile(File, SurfaceDesc)
'Set up the color key
CKey.low = ColorKey
CKey.high = ColorKey
Surfaces(NumSurfaces).Surface.SetColorKey DDCKEY_SRCBLT, CKey
'Set the information for this surface
Surfaces(NumSurfaces).Surface.SetForeColor vbBlack
Surfaces(NumSurfaces).Width = SurfaceDesc.lWidth
Surfaces(NumSurfaces).Height = SurfaceDesc.lHeight
End If
ErrOut:
Exit Sub
End Sub
So whether or not the surface created is a D3D surface is determined by the
second argument passed to the LoadSurface subroutine. An example of using
this routine would be:
Private Sub LoadSurfaces()
'Load the sprite surface - D3D Surface is true so we can use alpha blending, etc.
LoadSurface App.Path & "\object.bmp", True
'Load the background image surface - Not D3D surface so it can be any size
(This one is 640x480)
LoadSurface App.Path & "\stars.bmp", False
End Sub
Drawing Surfaces
Now that we have loaded our surfaces, we need to display them on the
screen! To do this, we must again take into account the two surface
kinds. Drawing D3D surfaces is going to be much more complicated than
normal DirectDraw surfaces. Bltting a normal DirectDraw surface is simple:
BackBuffer.Blt DestRect, Surfaces(SurfIndex).Surface, SrcRect, DDBLT_KEYSRC Or DDBLT_WAIT
D3D surfaces work differently. They are actually made up of two
triangles that form the rectangle of the surface:
The corners are called vertices (Singular, vertex), and these are what allow
us to do rotation and scaling. To set up for drawing a D3D sprite, we must
first set up some vertices:
'The vertices that will
define this sprite
Dim TempVerts(3) As D3DTLVERTEX
I created a beefy little subroutine called SetUpGeom() that will set up a supplied D3DTLVERTEX
array according to the passed information:
Public Sub SetUpGeom(Verts() As D3DTLVERTEX, SurfIndex As Integer, Src As RECT, Dest As RECT, R As Single, G As Single, B As Single, A As Single, Angle As Single)
'This sub sets up the vertices for a sprite, taking into account
'width, height, vertex color, and rotation angle
'NOTE: R, G, and B dictate the color that the sprite will be -
'1, 1, 1 is normal, lower values will colorize the vertices
Dim SurfW As Single
Dim SurfH As Single
Dim XCenter As Single
Dim YCenter As Single
Dim Radius As Single
Dim XCor As Single
Dim YCor As Single
'Width of the surface
SurfW = Surfaces(SurfIndex).Width
'Height of the surface
SurfH = Surfaces(SurfIndex).Height
'Center coordinates on screen of the sprite
XCenter = Dest.Left + (Dest.Right - Dest.Left - 1) / 2
YCenter = Dest.Top + (Dest.Bottom - Dest.Top - 1) / 2
'Calculate screen coordinates of sprite, and only rotate if necessary
If Angle = 0 Then
XCor = Dest.Left
YCor = Dest.Bottom
Else
XCor = XCenter + (Dest.Left - XCenter) * Sin(Angle) + (Dest.Bottom - YCenter) * Cos(Angle)
YCor = YCenter + (Dest.Bottom - YCenter) * Sin(Angle) - (Dest.Left - XCenter) * Cos(Angle)
End If
To do scaling, simply supply a bigger or smaller destination width and/or
height to this
sub.
Now let's go through one of the vertices, step by step, to understand what's
going on:
'Calculate screen coordinates of sprite, and only rotate if necessary
If Angle = 0 Then
XCor = Dest.Left
YCor = Dest.Bottom
Else
XCor = XCenter + (Dest.Left - XCenter) * Sin(Angle) + (Dest.Bottom - YCenter) * Cos(Angle)
YCor = YCenter + (Dest.Bottom - YCenter) * Sin(Angle) - (Dest.Left - XCenter) * Cos(Angle)
End If
This part calculates the correct destination coordinates of this vertex and puts
them in the XCor and YCor variables. This is where rotation
calculations are made. If angle is 0, then no rotation calculations are
made. If a non-zero angle is supplied the destination coordinates will be
rotated by the amount in the Angle variable, which is in radians. This is just a simple matter of trigonometry
calculations, which, if you don't understand them, don't worry, you don't have to
for them to work :) (BTW, thanks to W-Buffer for the trig. code here ;)
Remember, if you want to rotate a sprite to a specific angle in degrees,
multiply the degree angle by (pi / 180) to get the correct radian angle to
supply to this subroutine.
The R, G, and B variables sent to this sub determine what color the sprite
will be made. If they are all 1, the color will be normal. If R is
1, and the rest are 0, the sprite will be tinted red, and so on, according to
RGB color standards. The A variable determines what level of visibility
the sprite will have for alpha blending, 1 being completely solid, and 0 being
invisible. So .5 would be a half-translucent state.
These are the TU and TV values. These show where the source coordinates
for this sprite are on it's surface - TU is the X coordinate, and TV is the Y
coordinate. This is calculated by dividing the source coordinate by the
size of the surface, which will give a number between 0 and 1. So for this
vertex, which is vertex 0; the bottom left corner, the TU will be 0
(0 / 64 = 0), and the TV will be 1 (64 / 64 = 1). If the sprite
started half-way across the surface, the TU would be .5 (32 / 64 = .5) If
this is a little confusing, you probably don't have to worry about it, because
you don't need to understand it to use the subroutine.
This is the vertex array that this information will all be stored in.
Whew! Now that the geometry is all set up, we can actually draw the
surface (Remember to call Dev.BeginScene before drawing any D3D surfaces, and
call Dev.EndScene when you're all done):
'Enable alpha-blending
dev.SetRenderState D3DRENDERSTATE_ALPHABLENDENABLE, True
'Enable color-keying (ColorKey is drawn transparent)
dev.SetRenderState D3DRENDERSTATE_COLORKEYENABLE, True
dev.SetRenderState D3DRENDERSTATE_COLORKEYBLENDENABLE, True
'Use Alpha Blend One alpha blending
if the ABOne variable is true (you can use any variable)
If ABOne = True Then
dev.SetRenderState D3DRENDERSTATE_SRCBLEND, D3DBLEND_ONE
dev.SetRenderState D3DRENDERSTATE_DESTBLEND, D3DBLEND_ONE
'Or Alpha blend to a certain fade value (0 - 1)
Else
dev.SetRenderState D3DRENDERSTATE_SRCBLEND, D3DBLEND_SRCALPHA
dev.SetRenderState D3DRENDERSTATE_DESTBLEND, D3DBLEND_INVSRCALPHA
dev.SetRenderState D3DRENDERSTATE_TEXTUREFACTOR, dx.CreateColorRGBA(1, 1, 1, Alpha)
Dev.SetTextureStageState 0, D3DTSS_ALPHAOP, D3DTA_TFACTOR
End If
'Set the texture on the D3D device
dev.SetTexture 0, Surfaces(SurfIndex).Surface
dev.SetTextureStageState 0, D3DTSS_MIPFILTER, 3
'Draw the triangles that make up our square texture
dev.DrawPrimitive D3DPT_TRIANGLESTRIP, D3DFVF_TLVERTEX, TempVerts(0), 4, D3DDP_DEFAULT
'Turn off alphablending after we're done
dev.SetRenderState D3DRENDERSTATE_ALPHABLENDENABLE, False
The alpha blending is specified here - either Alpha Blend One, or blending to
a certain fade value (Determined by the Alpha variable - a fraction from 0 to 1,
0 being invisible, and 1 being solid). You can check the sample
project to see how these different techniques look. Then the texture
is set on the device, the triangles that make up the surface are drawn, and
we're done!
Check out the sample project with source code, which
demonstrates all these concepts in action, using a cool sprite engine demo! :)