DirectDraw7 / Direct3D Hybrid Engine
Sample Project

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:

Direct3D Surfaces

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

On Error GoTo ErrOut

NumSurfaces = NumSurfaces + 1
ReDim Preserve Surfaces(NumSurfaces)

'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

'0 - Bottom left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
(Src.Bottom + 1) / SurfH, _
Verts(0)

'Calculate screen coordinates of sprite, and only rotate if necessary
If Angle = 0 Then
    XCor = Dest.Left
    YCor = Dest.Top
Else
    XCor = XCenter + (Dest.Left - XCenter) * Sin(Angle) + (Dest.Top - YCenter) * Cos(Angle)
    YCor = YCenter + (Dest.Top - YCenter) * Sin(Angle) - (Dest.Left - XCenter) * Cos(Angle)
End If

'1 - Top left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
Src.Top / SurfH, _
Verts(1)

'Calculate screen coordinates of sprite, and only rotate if necessary
If Angle = 0 Then
    XCor = Dest.Right
    YCor = Dest.Bottom
Else
    XCor = XCenter + (Dest.Right - XCenter) * Sin(Angle) + (Dest.Bottom - YCenter) * Cos(Angle)
    YCor = YCenter + (Dest.Bottom - YCenter) * Sin(Angle) - (Dest.Right - XCenter) * Cos(Angle)
End If

'2 - Bottom right vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
(Src.Right + 1) / SurfW, _
(Src.Bottom + 1) / SurfH, _
Verts(2)

'Calculate screen coordinates of sprite, and only rotate if necessary
If Angle = 0 Then
    XCor = Dest.Right
    YCor = Dest.Top
Else
    XCor = XCenter + (Dest.Right - XCenter) * Sin(Angle) + (Dest.Top - YCenter) * Cos(Angle)
    YCor = YCenter + (Dest.Top - YCenter) * Sin(Angle) - (Dest.Right - XCenter) * Cos(Angle)
End If

'3 - Top right vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
(Src.Right + 1) / SurfW, _
Src.Top / SurfH, _
Verts(3)

End Sub

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.

'0 - Bottom left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
Src.Bottom / SurfH, _
Verts(0)

These are the coordinates on the screen of this vertex, calculated previously to take into account rotation.

'0 - Bottom left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
Src.Bottom / SurfH, _
Verts(0)

These will just always be set this way, don't ask why :)

'0 - Bottom left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
Src.Bottom / SurfH, _
Verts(0)

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.

'0 - Bottom left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
Src.Bottom / SurfH, _
Verts(0)

Always gonna be zero :)

'0 - Bottom left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
Src.Bottom / SurfH, _
Verts(0)

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.

'0 - Bottom left vertex
dx.CreateD3DTLVertex _
XCor, _
YCor, _
0, _
1, _
dx.CreateColorRGBA(R, G, B, A), _
0, _
Src.Left / SurfW, _
Src.Bottom / SurfH, _
Verts(0)

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_ALPHAARG1, 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! :)

-Matt Hafermann