DirectX:Direct3D:Tutorials:VB:DX7:Hybrid Engine

From GPWiki
Jump to: navigation, search

Personal pronouns refer to the original author of the document, ViX44

If you're one of those learn-as-you-go programmers, moving up to DirectX from throwing Line statements at innocent picture boxes is a bit of an adjustment. An adjustment that is personified by the words Automation Error and rather negative error codes that Google hasn't much to say about, other than that some other poor schmuck saw it once, too. A great number of tutorials have been written on both DirectDraw7 and its Direct3D alter ego, but few let you know what you need to go beyond the tutorial; and asking people for more info usually is met with did you read the SDK! use search function or look at the tutorial or sumthin and other, similarly encouraging and enlightening remarks. Well, I'll leave the details to the SDK, but here I'll cover the more interesting options.

I've managed to scrape together a working DD7/D3D hybrid engine for a puzzle game I'm working on, so I figured I'd share it. Most of it is based on existing tutorials, such as those availiable here and at DX4VB, but with alterations throughout and a couple helper functions, so if you have any basic questions, seek out and compare to the existing tutorials, then return here to see how I bent them to work for me.

Declarations and Initialization

Public SrcRect As DxVBLib.RECT Public DestRect As DxVBLib.RECT Public dX As DirectX7 Public DD As DirectDraw7 Public Type Surface

 s As DirectDrawSurface7 ' Surface itself
 d As DDSURFACEDESC2     ' Surface descriptor flags
 t As Boolean            ' Boolean flag I set for D3D textures, not at all vital.
 End Type

Public Primary As Surface ' The actual image on screen Public Buffer As Surface ' Where screen data comes from in Fullscreen mode Public RenderTarget As Surface ' Where I draw to and copy from, to make my life simpler. Public TextColl() As Surface ' Surfaces I'll be drawing from. Public ddClipper As DirectDrawClipper Public D3D As Direct3D7 Public dev As Direct3DDevice7 Public D3DSprite(3) As D3DTLVERTEX ' This will become a rectangle for D3D's consumption. Private Const GWL_STYLE As Long = (-16) ' Identifies the standard frame + titlebar and control buttons window Public Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long) As Long Public Declare Function AdjustWindowRect Lib "user32" (lpRect As RECT, ByVal dwStyle As Long, ByVal bMenu As Long) As Long

Note I created a type, Surface, to contain both the surface data and its related description flags. You can make them seperate if you're doing something simple, like a windowed level editor, but my game uses a good number of surfaces, and this works for me. There are also a few Win API calls in there, we'll look at them in a moment. A few other wide-scoped variables will pop up, explained as we go.

The rectangles use DxVBLib.RECT instead of simply RECT as their types. If you're using just DX7, you don't need the DxVBLib part, but if you use DX7 and 8 together, you will have issues without the DxVBLib specification, since both 7 and 8 think they know what a RECT is, and they do, and they're the same, but nonetheless, they have a fight, Triangle wins, and you lose unless you say DxVBLib first.

Set dX = New DirectX7 Set DD = dX.DirectDrawCreate("") If videoWINDOWED Then

 Call DD.SetCooperativeLevel(aForm.hwnd, DDSCL_NORMAL)
 Else
 Call DD.SetCooperativeLevel(aForm.hwnd, DDSCL_EXCLUSIVE Or DDSCL_FULLSCREEN Or DDSCL_ALLOWREBOOT)
 End If

Set Primary.s = Nothing Set Buffer.s = Nothing

We've created DirectX, a DirectDraw instance, and then attached it to our form of choice, aForm. videoWINDOWED is a Boolean I use to implement the option to choose between windowed and fullscreen mode. SetCooperativeLevel is what lets DirectDraw operate on the screen or section thereof. Note the difference in flags between windowed/fullscreen; and that a lot of these go together exclusively, so don't change things without looking them up.

If videoWINDOWED Then

 SrcRect = pushRect(0, 0, VideoModeDataX(videoRESOLUTION), VideoModeDataY(videoRESOLUTION))
 AdjustWindowRect SrcRect, GetWindowLong(aForm.hwnd, GWL_STYLE), False
 aForm.Width = (SrcRect.Right - SrcRect.Left) * 15
 aForm.Height = (SrcRect.Bottom - SrcRect.Top) * 15
 aForm.picbox.Width = VideoModeDataX(videoRESOLUTION)
 aForm.picbox.Height = VideoModeDataY(videoRESOLUTION)
 Set ddClipper = DD.CreateClipper(0)
 ddClipper.SetHWnd aForm.picbox.hwnd
 Else
 Call DD.SetDisplayMode(VideoModeDataX(videoRESOLUTION), VideoModeDataY(videoRESOLUTION), videoBITS, 0, DDSDM_DEFAULT)
 End If

Much code for windowed mode, huh? Because windowed mode means rendering to a picture box on the form and leaving the rest of the screen memory untouched, we have to slug through Windows to find out where we are on the screen so everything is drawn correctly. If you don't go through this, you might see some interesting effects, like things not working the way you expected. For example, knocking out AdjustWindowRect in my game causes the bottom of the screen to be clipped off, because the form height includes the menu bar, which should be in addition to my rendering space. The calls get the window position, and adjust for boarder and title bar; setting it to be sized to fit a picbox that matches the resolution we desire. The * 15 is pixel-to-twip conversion, btw.

pushRect is simply a function that plugs in origin X/Y, width, and height into a rectangle. I don't like using a bunch of Withs, and the rectangle's Left, Top, Right, Bottom format leads me to create silly errors, and writing .Left + whateverWidth all the time makes me nauseous. VideoModeDataD are arrays (or function calls, whatever you like) to return screen resolution dimensions. Mix it with your favorite enumerator, or replace with raw numbers.

The fullscreen section is more obvious, since it's only one line. We have resolutions X and Y. videoBITS is the bitdepth. Remember that older systems may reject 32 and accept 24, while new ones accept 32 but reject 24; if you don't plan to enumerate. The 0 is placeholding where you can command a refresh rate; 0 is default, a number like 60 or 75 is the refresh you request. Last parameter is a cool tool, leave it be.

If videoWINDOWED Then

 Primary.d.lFlags = DDSD_CAPS
 Primary.d.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE
 Else
 Primary.d.lFlags = DDSD_CAPS Or DDSD_BACKBUFFERCOUNT
 Primary.d.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE Or DDSCAPS_3DDEVICE Or DDSCAPS_FLIP _
                           Or DDSCAPS_COMPLEX Or (DDSCAPS_VIDEOMEMORY And videoUSEVRAM)
 Primary.d.ddsCaps.lCaps2 = DDCAPS2_FLIPNOVSYNC And videoNOVSYNC
 Primary.d.lBackBufferCount = 1
 End If

Set Primary.s = DD.CreateSurface(Primary.d) If videoWINDOWED = False Then ' Only Fullscreen uses backbuffer

 Dim caps As DDSCAPS2
 caps.lCaps = DDSCAPS_BACKBUFFER
 Set Buffer.s = Primary.s.GetAttachedSurface(caps)
 End If

Again, two faces for Windowed and Fullscreen. Here, we set our capibility flags. videoUSEVRAM is a Bool indicating if we want to force video memory use, or let it use system. Older systems, like my laptop and its dreadful Savage S3 video did not agree with the video memory flag, so you might plan around that, or just not force video memory at all. I also include a videoNOVSYNC option Bool, because my game uses fixed-interval physics, and I can't abide it being held up by waiting around on the screen. This is an enabler, not a setting; you can enable no vsync, and still sync all your fullscreen .Flips.

Everything else should be pretty well covered by other tutorials.

RenderTarget.d.lFlags = DDSD_CAPS Or DDSD_WIDTH Or DDSD_HEIGHT RenderTarget.d.ddsCaps.lCaps = (DDSCAPS_VIDEOMEMORY And videoUSEVRAM) Or DDSCAPS_OFFSCREENPLAIN Or DDSCAPS_3DDEVICE videoRENDERx = VideoModeDataX(videoRESOLUTION) videoRENDERy = VideoModeDataY(videoRESOLUTION) RenderTarget.d.lWidth = videoRENDERx RenderTarget.d.lHeight = videoRENDERy Set RenderTarget.s = DD.CreateSurface(RenderTarget.d) RenderTarget.s.SetForeColor vbBlack RenderTarget.s.setDrawStyle 5 If videoWINDOWED Then Primary.s.SetClipper ddClipper Set D3D = DD.GetDirect3D Set dev = D3D.CreateDevice("IID_IDirect3DHALDevice", RenderTarget.s)

I use plenty of surfaces. You can get away with one, but I like three. I draw to RenderTarget; then either copy that to the BackBuffer if I'm using fullscreen so it can flip, or copy it to the Primary directly in windowed. The purpose for the RenderTarget step is that I can render everything onto it at a larger or smaller resolution, then scale it when copying to the backbuffer to create smooth (DD7 bi-lin filtered) resized graphics. (Assuming the video card supports filtering.) Just multiplying pixel values to correct for resolution change and drawing to backbuffer caused some misalignments I simply couldn't abide. (Aside: I wanted to render a 37.5 pixel square, and it just wasn't happening; this way, I can render large, scale down, and blend the 38th pixel with its neighbor) Feel free to set dev to the backbuffer surface if you aren't obsessive compulsive about image quality, or just want to save some processor. Also note that Windowed mode doesn't use a normal backbuffer at all. I draw to a buffer (RenderTarget) and copy all at once to the primary, but you can draw straight to the primary if you like, though this can cause minor tearing since you're affecting the video memory while the screen may be drawing it.

So far, we've told DirectX how we want to use the screen (Windowed or Full), pointed it to a Primary surface, given it a backbuffer if it's fullscreen, and if so desired, created an additional surface to goof around with. Now, we're ready to actually do something fun.

Creating Surfaces

Here's a stripped down version of my standard surface creator, chunked and formed like a chicken nugget.

Public Sub DD_SurfaceMaker(ByRef surf As Surface, ByVal fName As String, bD3D As Boolean) Dim surfpic As Picture Dim targetS As String fName = aPath(fName) targetS = aPath(tempFName) Set surfpic = LoadPicture(fName) SavePicture surfpic, targetS

surf is the surface I'm loading an image to, fName is the source graphic file, bD3D tells if I want a D3D style texture, or just a surface. aPath is just my fancy function for attaching App.Path unto a directory string, such that it checks for already being a full path, and makes sure it doesn't spit out c:\apppathfilename.ext or c:\apppath\\filename.ext. tempFName is, obviously, a temporary file name, I usually start a program with something like tempFName = "temp" & (Timer mod 1000), feel free to go wild...just don't do what I did... (Aside: I kept wondering why, when I loaded a localhost netgame with four instances of my game, some would die during the game image loading; no pattern to it, just one or two would always keel over. Turns out, they were all using the same temporary filename, and one instance would Kill the temp while another was loading, causing File Not Founds.) LoadPicture and SavePicture are VB functions that will open a BMP, JPG, or GIF, and save them as a BMP which DX will load. surfpic holds the image between these steps. If you have a custom loader, that's fine, too.

surf.d.lFlags = DDSD_CAPS Or DDSD_WIDTH Or DDSD_HEIGHT Or DDSD_CKSRCBLT If bD3D Then

 surf.d.ddsCaps.lCaps = DDSCAPS_TEXTURE Or (DDSCAPS_VIDEOMEMORY And videoUSEVRAM)
 surf.d.ddsCaps.lCaps2 = (DDSCAPS2_TEXTUREMANAGE And Not (videoUSEVRAM))
 surf.t = True
 Else
 surf.d.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN 'Or (DDSCAPS_VIDEOMEMORY And videoUSEVRAM)
 surf.t = False
 End If

Setting capibility flags again. Note for the Direct3D surface flag, you can't use DDSCAPS2_TEXTUREMANAGE if you use DDSCAPS_VIDEOMEMORY. TEXTUREMANAGE lets DX decide between video and system memory for the textures. Obviously, you can't force video memory while telling it to decide for you. Well, not so obviously, because I kept trying and getting errors until I saw a tiny sentence in the SDK. That one line in the DD7 side has its video flag commented out...I don't remember why. I'll uncommment it in my code, and if a freindly error box reminds me why I did that, I'll edit this. Again, feel free to play with it yourself, a quick test seemed not to mind uncommenting that.

surf.d.lWidth = 0 surf.d.lHeight = 0 Set surf.s = DD.CreateSurfaceFromFile(targetS, surf.d) surf.s.SetForeColor vbBlack Dim ddpf As DDPIXELFORMAT, CKey As DDCOLORKEY surf.s.GetPixelFormat ddpf CKey.low = ddpf.lBBitMask Or ddpf.lRBitMask CKey.high = CKey.low surf.s.SetColorKey DDCKEY_SRCBLT, CKey Kill targetS Set surfpic = Nothing End Sub

Setting lDimension to 0 is important. Set to Zero, and DirectX will fill the parameters with the dimensions of what it loaded. Leave it with a value, and DirectX adjusts the image to fit those sizes. Hence, reloading a surface with a function like this without setting to Zero causes issues.

DDPIXELFORMAT is the solution to all your why won't colorkey work in 16-bit OMGWTF?! woes. This pulls up the bit format the surface is using, creates the proper colorkey value for Magenta, and sets the colorkey. Obviously you can edit this to use colors other than Magenta, or snip it entirely if you don't need colorkey. Finally, delete the temp file and release the surfpic we created.

Configuring the Device

Since we've bothered to know how to create a color key, let's make it actually work for even more fun...

dev.SetRenderState D3DRENDERSTATE_COLORKEYENABLE, True dev.SetRenderState D3DRENDERSTATE_COLORKEYBLENDENABLE, True dev.SetRenderState D3DRENDERSTATE_ALPHABLENDENABLE, True dev.SetRenderState D3DRENDERSTATE_SRCBLEND, D3DBLEND_SRCALPHA dev.SetRenderState D3DRENDERSTATE_DESTBLEND, D3DBLEND_INVSRCALPHA dev.SetTextureStageState 0, D3DTSS_ALPHAOP, D3DTA_TFACTOR dev.SetTextureStageState 0, D3DTSS_MAGFILTER, D3DTFG_LINEAR dev.SetTextureStageState 0, D3DTSS_MINFILTER, D3DTFP_LINEAR

SetRenderState calls are basically more capibility flags. They turn on and off renderer options. Of course, old video hardware may not support them, but I haven't received an error for trying. Here I've activated colorkey and alpha channel blending for visual effects. I use colorkey all the time, and a lot of alpha in my game, so I turn them on at once; but if you don't use alpha or colorkey much, you should turn it/them off when unneeded and on when you do, since doing so saves the card a bit of effort working alpha math or hunting colorkey pixels that aren't there.

SetTextureStateState lets you do fun texture tricks, but for our needs (or mine, at least), the most important is setting the filtering. Note that D3DTSS_MAGFILTER and D3DTSS_MINFILTER take different parameters, D3DTFG_LINEAR and D3DTFP_LINEAR. I had them backwards and it didn't work well. There are many other filters you can try, but Linear is fast and easy.

Note that if you recall your video initialization (to change resolution in game, for example) and reSet dev, you will probably need to call these again.

Finally, Drawing!

DirectX7 is super simple...

SrcRect = pushRect(.sX, .sY, .sW, .sH) DestRect = pushRect(.dX, .dY, .dW, .dH) RenderTarget.s.Blt DestRect, TextColl(index).s, SrcRect, DDBLT_WAIT

TextColl() is whichever surface you're getting your source image from. Just plug in your source data's rectangle, where you want it to go, and Blt until you're blue in the face.

For a Direct3D draw, we have to warm it up a bit... dev.BeginScene must be called before we make Direct3D go to work, and it's mate, dev.EndScene when we're done. With the scene started, we can define a simple rectangle and draw an image on it.

dev.SetTexture 0, TextColl(index).s DD_SetD3DSprite .lX, .lY, .lW, .lH, iC, .tX, .tY, .tW, .tH dev.DrawPrimitive D3DPT_TRIANGLESTRIP, D3DFVF_TLVERTEX, D3DSprite(0), 4, D3DDP_DEFAULT

.SetTexture loads the source texture such that D3D can draw with it. .DrawPrimitive does the actuall Blting. The parameters here are the ones most analogous to DirectDraw's Blt. Proper 3D uses the others, and you can do fun stuff, even just for Hybrid work, with them, but that's for the SDK and other, more well-written tutorials to cover. Right now, we just want hardware accelerated sprites with clean rotation, and alpha blending.

DD_SetD3DSprite is another of my helper functions...

Sub DD_SetD3DSprite(lX As Long, lY As Long, lW As Long, lH As Long, coloring As Long, _

                   tX As Single, tY As Single, ByVal tW As Single, ByVal tH As Single)

dX.CreateD3DTLVertex lX, lY + lH, 0, 1, coloring, 0, tX, tY + tH, D3DSprite(0) dX.CreateD3DTLVertex lX, lY, 0, 1, coloring, 0, tX, tY, D3DSprite(1) dX.CreateD3DTLVertex lX + lW, lY + lH, 0, 1, coloring, 0, tX + tW, tY + tH, D3DSprite(2) dX.CreateD3DTLVertex lX + lW, lY, 0, 1, coloring, 0, tX + tW, tY, D3DSprite(3) End Sub

Feed it a place on the screen to draw to (just like a Dest rectangle), lighting color, and texture locations (by ratio per dimension, not pixel locations, unlike a Src rectangle) and it sets the D3DSprite array of vertices such that you get a simple two-poly form that works like our Dest rectangle in DD7. (Aside: Looking at this now, perhaps those As Long things are why I had scaling issues before and resorted to my crazy three-layer "Blt to RenderTarget, scale to BackBuffer, Flip to Primary" scheme...oh well, more for you to play with. I'll stick with what I have so I don't break it.) Anyway, lighting color is how much of each channel you want to show through, it's explained elsewhere, so when in doubt, use white. BTW, if you need to make colors and are afraid of bit-depth madness causing problems, use dX.CreateColorRGBA and feed it values from 0.0 to 1.0 for each channel, and it should return what you need.

Showtime!

All that's left is to get it on screen.

SrcRect = pushRect(0, 0, videoRENDERx, videoRENDERy) DestRect = pushRect(0, 0, VideoModeDataX(videoRESOLUTION), VideoModeDataY(videoRESOLUTION)) If videoWINDOWED Then

 GetWindowRect aForm.picbox.hwnd, DestRect
 DestRect.Right = VideoModeDataX(videoRESOLUTION) + DestRect.Left
 DestRect.Bottom = VideoModeDataY(videoRESOLUTION) + DestRect.Top
 Primary.s.Blt DestRect, RenderTarget.s, SrcRect, DDBLTFAST_WAIT
 Else
 Buffer.s.Blt DestRect, RenderTarget.s, SrcRect, DDBLTFAST_WAIT
 Primary.s.Flip Nothing, (DDFLIP_NOVSYNC And videoNOVSYNC) Or DDFLIP_WAIT
 End If

In fullscreen, all that's needed is to copy from RenderTarget to the backbuffer (if you even bother with it), then flip the backbuffer to the screen. Note by enabling the no-sync option at initialzation, we can now choose to skip the sync dynamically and save some time if we can forgive tearing to preserve framerate. (Assuming ATi Control Panel doesn't push us around.) For windowed, we get the screen position of our picture box, ensure everything's lined up, and Blt from the rendering surface to the Primary.

Afterword

When using DD7 and D3D calls together, I noticed that DD7 calls seem to happen immediately, while D3D waited until calling dev.EndScene. I was using DrawText and to get the text over a sprite, I had to end scene, then do my text drawing. I don't know the technical explanation, but if you seem to have layering issues between your DD7 and D3D stuff, give that consideration.

I hope this walk-through format is a bit more insightful than the example format of many tutorials, and together you're able to get graphics where you want them. I know I spent way too much time with the regular tutorials and had no clue why I couldn't make them work in my project until that moment of clarity struck me, and an hour later I had a complete (though still a bit buggy) graphics system coded out.

Good luck; you'll need it.