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