DirectX Graphics for Visual Basic Part 2
Welcome back to part 2 of this mini-series, hopefully you’ve read and learnt the information covered in the first part (thanks to all the complements I received about the first article), this article starts where the last one left off - things will not be covered twice, so make sure you know what happened in the first article… It can be found here
By the time you’ve read and learnt the things I am about to cover you should be perfectly capable of creating a simple game/demo - which shows you how quickly you can get started in Direct3D. Having said this, don’t expect to finish this article (or this series) and go on to write the next big 3D engine - it wont happen, I’ve had many emails from people who’ve only read the first couple of tutorials on the basics of D3D and want to get straight on with a "Simple" quake clone… try something like pong/tetris/snakes first.
This article is going to be quite steep - the things covered may well not come to you easily, if not, re-read the article until it does or seek out other beginners guides to 3D graphics / theory. Today we’ll be covering:
1) Setting up the sample application to go full-3D.
The above 3 things would usually be covered by several articles, as they are deceptively big topics, anyway, onwards and upwards:
Reconfiguring our Direct3D application
The sample at the end of the last article was very simplistic - not much use for anything really, before we go into full-3D we’ll need to add a few parameters and configure a few new things.
I also want to take the time to introduce full screen mode, this is the main display format used by games, where, funnily enough, your game occupies the entire screen. Full screen mode is much faster, and isn’t held back by windows (which is effectively suspended in the background). Full screen mode requires you to pick a resolution that the hardware/monitor combination can handle - open up the windows display properties and see what settings you can set the resolution slider to - these (but not always) will be the display modes that Direct3D can use on your hardware. 800x600, 1024x768 are examples of full screen modes. The important thing about this is that the resolutions available will differ from one computer to another - 640x480, 800x600, 1024x768 all tend to be standard resolutions, but there is no guarantee that they will be available (only 1 of my 2 computers supports 1024x768 display modes). To solve this problem we must use enumeration.
Dim tmpDispMode As D3DDISPLAYMODE '//used during the enumeration of avail. modes Dim I As Long '//so we can loop through the avail. display modes For I = 0 To D3D.GetAdapterModeCount(0) - 1 'primary adapter D3D.EnumAdapterModes 0, I, tmpDispMode Debug.Print tmpDispMode.Width & "x" & tmpDispMode.Height Next I
The previous piece of code will output a list of all the display modes supported by your hardware to VB’s immediate (debug) window. The code will need to go at the start of the Initialise() function, but after the Dx and D3D objects have been initialised. The output of the above code, for my GeForce 256 + generic 15" monitor was:
320x200 320x240 400x300 480x360 512x384 640x400 640x480 800x600 960x720 1024x768 1152x864 1280x960 1280x1024 320x200 320x240 400x300 480x360 512x384 640x400 640x480 800x600 960x720 1024x768 1152x864 1280x960 1280x1024
So why are there two of each resolution? Its not a mistake, it’s down to the format of the display mode. Anyone paying any attention to games will know that you can have 32 bit and 16 bit rendering (amongst various other formats) - the above list does not contain that data, but the first copy of the resolutions will be in 16 bit format, the second set will be in 32 bit format. This requires some discussion:
D3DFMT_A1R5G5B5 D3DFMT_A4R4G4B4 D3DFMT_A8R3G3B2 D3DFMT_A8R8G8B8 D3DFMT_DXT1 D3DFMT_DXT2 D3DFMT_DXT3 D3DFMT_DXT4 D3DFMT_DXT5 D3DFMT_R5G6B5 D3DFMT_R8G8B8 D3DFMT_X1R5G5B5 D3DFMT_X4R4G4B4 D3DFMT_X8R8G8B8
Above is a selection of members from the enumeration type "CONST_D3DFORMAT" - which we’ll be using later. All of the above describe a format that the display mode should be in, such as 32 bit/16 bit - but it’s not as simple as saying 16 or 32 bit… you can work it out by counting the number of bits in the description:
Add up the 8’s and you get 32 - which indicates that D3DFMT_X8R8G8B8 is a 32 bit mode, all of the ones above are either 16 or 32 bit format (except the D3DFMT_DXT* ones). The next part to notice is the lettering - indicating the channel, all of them will have an RGB triplet - Red, Green and Blue - you should all know that colours on a computer screen are made up of these 3 colours, even using paintbrush you could probably see this in action. We also have an optional X or A channel, the X just means unused, there are bits allocated, but they wont have anything in them, and wont be used for anything. The A channel is alpha, something that will be covered later on. If you don’t need alpha blending/transparencies then you’ll be okay using an X****** format, if you need alpha but accidentally use an X******* format nothing will happen - or at least what you want to happen wont.
A word on accuracy, the more bits to a channel the more colours you can represent, which is why the 32 bit formats will looks substantially better than the 16 bit formats. A channel can represent 2^n (where n is the number of bits) colours. An 8 bit channel can therefore represent 2^8 colours = 256 colours, a 4 bit channel can only represent 2^4 channels = 16 colours. You may have noticed that there is an R5G5B5 format and an R5G6B5 format - this is down to the fact that our eyes are more sensitive to green light, so being able to represent more colours in the green channel is better. On a connected, but not particularly useful note - the total number of colours supported by a display mode will be 2^n again, where n is the total number of bits being used, not including the X channel. I tend not to include the A channel in my calculations, but you can if you want. Therefore a 16 bit colour will have 2^16 colours = 65536, and a 32 bit colour will have 2^32 colours = 4,294,967,296 - roughly 4.3 billion…
Now (hopefully) we have an understanding of display formats, we can go about setting one up. I’ve written a small function that will check for support of a specified display mode - it works fine for this tutorial, but you’ll probably need a more rigid function for a proper project:
Private Function CheckDisplayMode(Width As Long, Height As Long, Depth As Long) As CONST_D3DFORMAT '//0. any variables Dim I As Long Dim DispMode As D3DDISPLAYMODE '//1. Scan through For I = 0 To D3D.GetAdapterModeCount(0) - 1 D3D.EnumAdapterModes 0, I, DispMode If DispMode.Width = Width Then If DispMode.Height = Height Then If DispMode.Format = D3DFMT_R5G6B5 Or D3DFMT_X1R5G5B5 Or D3DFMT_X4R4G4B4 Then '16 bit mode If Depth = 16 Then CheckDisplayMode = DispMode.Format: Exit Function ElseIf DispMode.Format = D3DFMT_R8G8B8 Or D3DFMT_X8R8G8B8 Then '32bit mode If Depth = 32 Then CheckDisplayMode = DispMode.Format: Exit Function End If End If End If Next I CheckDisplayMode = D3DFMT_UNKNOWN End Function
Fairly simple really, it assumes that the D3D object has been created, and will use the global copy of the object - if it hasn’t been created this piece of code will error-out. The only slightly complicated part is the format selection, it defines formats as either 16 bit or 32 bit - whilst the 32 bit selection technically includes a 24 bit format, colour wise it’s identical. I’ve not checked against formats with alpha components - the display mode wont use an alpha channel, that’s for texturing - you can set up a display mode with an alpha channel, but there’s little point, on top of that the formats listed here are more likely to be supported than the equivalent with an alpha channel. We can extend the usage of this function to help select the samples display mode - we’ll hard code this part rather than spend time building a user interface in to allow the user to select the resolution - it’s not hard to do that, and you can probably work it out as you need it.
DispMode.Format = CheckDisplayMode(640, 480, 32) If DispMode.Format > D3DFMT_UNKNOWN Then '640x480x32 is supported DispMode.Width = 640: DispMode.Height = 480 Else DispMode.Format = CheckDisplayMode(640, 480, 16) If DispMode.Format > D3DFMT_UNKNOWN Then '640x480x16 is supported DispMode.Width = 640: DispMode.Height = 480 Else 'hmm, neither are supported. oh well... MsgBox "Your hardware does not appear to support" _ & " 640x480 display modes in either 16 bit or 32 bit modes. Exiting" _ , vbInformation, "Error" Unload Me end End If End If
Not too complicated really. We could check for higher resolutions, but for this sample it isn’t really necessary. By the time this little segment of code has been executed we will have a properly initialised, valid D3DDISPLAYMODE structure that we can use when setting up our device. If it could not create the structure it will exit out of the program.
We aren’t done yet though, we need to fill out the D3DPRESENT_PARAMETERS structure differently from the windowed mode example. This is mostly down to the device creation requiring more data than in the last sample. The new configuration looks like this:
D3DWindow.BackBufferCount = 1 D3DWindow.BackBufferFormat = DispMode.Format D3DWindow.BackBufferWidth = DispMode.Width D3DWindow.BackBufferHeight = DispMode.Height D3DWindow.hDeviceWindow = frmMain.hWnd D3DWindow.AutoDepthStencilFormat = D3DFMT_D16 D3DWindow.EnableAutoDepthStencil = 1 D3DWindow.SwapEffect = D3DSWAPEFFECT_COPY_VSYNC
Notice that we’re copying the display mode format information to the structure at this point, you could avoid this by writing straight to this structure - but for clarity I left it separate.
The first section deals with configuring a backbuffer. Anyone who’s done any work with DirectDraw/Direct3D in DirectX7 will already know what one of these is (you should do). When Direct3D does the actual rendering of 3D geometry onto a 2D surface it will do it in parts, usually as each piece of geometry is rendered. If we rendered straight to the screen in all but the highest frame rate situations you would be able to see the screen being drawn piece by piece - even if it was going quite quickly you’d be able to pick up on strange artefacts appearing - a tree appearing and quickly being overwritten by the house in front of it (for example). To solve this problem we use a secondary buffer, the image is composed on this surface (an identical size to the screen), and then the whole contents of the backbuffer are copied to the screen in one quick operation (its not actually copied, the addresses/pointers are switched around). This removes the possibility of any drawing artefacts appearing - or at least it should do… We configure our backbuffer here using our display mode, we never actually configure the screen surface, D3D will take the measurements specified in the backbuffer members and use those - saves on any simple data mis-matching errors.
The second part deals with configuring the depth buffer. This is another concept that you’ll need to grasp when dealing with 3D environments. In 3D we, obviously, have 3 dimensions, XYZ, and in 2D we only have X and Y, when we want to project our 3D scene onto our 2D screen we need to know what happens to this 3rd dimension. As things are converted into 2D in any given order we will need to check if the current part we are drawing is in front of, or behind the current piece of scene in the frame buffer (the screen/backbuffer). Or in more technical language, when we draw a pixel in 2D we check it’s depth coordinate against that stored for the same location in depth buffer (which will hold the depth for the pixel currently in the frame buffer), if the depth is greater (the new pixel is behind the old one) then it wont be drawn, if the depth is les (the new pixel is in front of the old one) then it will draw it over the top of existing pixel. The depth buffer, is therefore a surface identical in dimensions to the screen and backbuffer. The only involvement we’ll ever have with it is telling D3D we want to use it, turning it on and clearing it before each frame (to remove the depth information from the previous frame). The only important part at this stage is specifying what format the depth buffer will be in - similar to the way we specified what format the screen/backbuffer will be in. The more bits allocated to each pixel the more accurate the depth testing will be, typically they come as standard at 16bits per pixel, newer hardware is allowing 24 bit or 32 bit depth buffers (usually as combinations with a stencil buffer). We can enumerate what depth buffer modes are available using the following code:
'In order of preference: 32, 24, 16 If D3D.CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, DispMode.Format, _ D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, D3DFMT_D32) = D3D_OK Then '//Enable a pure 32bit Depth buffer D3DWindow.AutoDepthStencilFormat = D3DFMT_D32 D3DWindow.EnableAutoDepthStencil = 1 Debug.Print "32 bit Depth buffer selected" Else '//search for a 24 bit depth buffer If D3D.CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, DispMode.Format, _ D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, D3DFMT_D24X8) = D3D_OK Then '//Enable a 24 bit depth buffer D3DWindow.AutoDepthStencilFormat = D3DFMT_D24X8 D3DWindow.EnableAutoDepthStencil = 1 Debug.Print "24 bit Depth buffer selected" Else If D3D.CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, DispMode.Format, _ D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, D3DFMT_D16) = D3D_OK Then '//Enable a 16 bit depth buffer D3DWindow.AutoDepthStencilFormat = D3DFMT_D16 D3DWindow.EnableAutoDepthStencil = 1 Debug.Print "16 bit Depth buffer selected" Else '//hmm. No depth buffers available... Dont use one then :) D3DWindow.EnableAutoDepthStencil = 0 Debug.Print "no Depth buffer selected" End If End If End If
But for clarity this sample just uses a 16 bit depth buffer, in about 99% of cases the hardware will support a 16 bit depth buffer - which is perfectly acceptable for most 3D environments. If one is not available in hardware you may find that Direct3D will emulate ones existence - which is much slower, but it will still function.
The last two things to discuss are the .hDeviceWindow member, and the .SwapEffect member. These are not complicated, the first parameter, hDeviceWindow needs to be the hWnd property of the form that you are using - this is so that Direct3D can keep track of your application/window, if the form is closed, minimised or moved Direct3D can find out. The SwapEffect member indicates how Direct3D should draw to the screen, there are two main choices here, V-Sync or not. Hopefully you are aware of the monitors refresh rate - or how it works, if you use V-Sync then Direct3D will wait until a vertical refresh event occurs before drawing the next frame, therefore, if you only have a 70hz monitor the maximum frame rate will be around 70fps (not always exactly) - using a v-sync is usually better quality than without as it performs the copy at the same time as the monitor and no artefacts should appear. Disabling V-Sync forces Direct3D to copy the frame buffers to the screen as soon as it’s finished rendering - using this method you can achieve considerably higher frame rates (if it’s being locked to the refresh rate that is…) at the cost of visual artefacts; whilst it doesn’t affect some hardware (mine is fine), on others you can get a visible tear line across the screen - where one frame is above and one frame (usually the previous) is below, if this happens it will look pretty ugly! Also note that the drivers have the ability to override these settings, my drivers are setup to ignore v-sync, and I cant, programmatically, force it to use the V-sync; vice-versa when I enable v-sync in the driver properties. The available choices for the SwapEffect member are:
D3DSWAPEFFECT_COPY D3DSWAPEFFECT_COPY_VSYNC D3DSWAPEFFECT_DISCARD D3DSWAPEFFECT_FLIP D3DSWAPEFFECT_FORCE_DWORD
The 3 bolded entries are the ones that you should be using. The _COPY member is for a single backbuffer (like ours) that ignores V-Sync where possible, the _COPY_VSYNC option is the same, but will lock drawing to the monitors refresh rate. The _FLIP member is the same as the _COPY member except for multiple backbuffers (usually when you have 2).
Now we’ve covered the redesigned initialisation process. At last… There are two final lines that we should add after the device has been created:
D3DDevice.SetRenderState D3DRS_ZENABLE, 1
That will enable our depth buffer (AKA Z-Buffer) for rendering. And this next line goes in place of the current line in the Render() function:
D3DDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, &HCCCCFF, 1#, 0
All that’s changed there is that it’s clearing the depth buffer as well as the frame buffers. If you leave this line out you’ll start getting some strange artefacts appearing (you can use it to your advantage) where the new scene is drawn according to the depth information of the previous scene…
If you now run the program (F5 or Ctrl+F5 in the IDE) you should be greeted with a 640x480 light blue screen and a small triangle in the top left corner - identical to the sample in the last example - but in fullscreen!
Getting Started With Basic 3D Geometry
Now things start to get more fun. What we’ve done so far could be seen as being fairly boring, just setting up the foundations for bigger things really…
Luckily, now that the initialisation process is sorted out quite nicely adding the ability to use 3D geometry will be quite straight forward, if you read the first article properly you’ll find that a lot of the ground work for geometry has already been laid down. To summarise the relevent key points brought up in the first article:
Simple as that really, the only major change is going to be the switch from using 2D vertex coordinates into using 3D vertex coordinates, which cant be too hard can it? You also need to remember that it’s all relative to the camera viewpoint rather than to the screen (2D) coordinates.
The next part of the sample program we’re going to design will render a small 3D cube on the screen, then we’ll use this as a base to learning more of the manipulation features that Direct3D exposes for us (Matrices). First, take the user defined type and FVF description from the last article:
Const FVF_LVERTEX = (D3DFVF_XYZ Or D3DFVF_DIFFUSE Or D3DFVF_SPECULAR Or D3DFVF_TEX1) Private Type LITVERTEX X As Single Y As Single Z As Single Color As Long Specular As Long tu As Single tv As Single End Type
This particular vertex is pre-lit by us, that means that we must specify the colour of the vertex rather than let Direct3D light it for us (lighting will be covered later). We’ll also use a little helper function CreateLitVertex() to help us initialise the structures, this is very similar to the one in the previous article, if you are unclear as to the exact nature of it have a look at the sample code for this article…
Now that we have the vertex descriptor/UDT set up we need to define the vertex structures for our cube. For this sample we’re going to use 36 vertices:
Dim CubeVerts(0 To 35) As LITVERTEX
Why are we using 36 vertices for a 6 sided/8 cornered object? A cube has 6 square faces, to make up a square we require 2 triangles, therefore we have 12 triangles in total. Each triangle requires 3 vertices, hence the 36 figure. Later on we’ll learn how to reduce the number of vertices in the cube to 8, but for a first example this will do fine…
Private Sub InitialiseGeometry() '//0. Any Variables '//1. Define the colours at each corner Const Corner000 As Long = &HFF0000 'red Const Corner001 As Long = &HFF00 'green Const Corner010 As Long = &HFF 'blue Const Corner011 As Long = &HFF00FF 'magenta Const Corner100 As Long = &HFFFF00 'yellow Const Corner101 As Long = &HFFFF 'cyan Const Corner110 As Long = &HFF8000 'orange Const Corner111 As Long = &HFFFFFF 'white '//2. Define the faces 'top CubeVerts(0) = CreateLitVertex(-1, 1, -1, Corner010, 0, 0, 0) CubeVerts(1) = CreateLitVertex(1, 1, -1, Corner110, 0, 0, 0) CubeVerts(2) = CreateLitVertex(-1, 1, 1, Corner011, 0, 0, 0) CubeVerts(3) = CreateLitVertex(1, 1, -1, Corner110, 0, 0, 0) CubeVerts(4) = CreateLitVertex(1, 1, 1, Corner111, 0, 0, 0) CubeVerts(5) = CreateLitVertex(-1, 1, 1, Corner011, 0, 0, 0) 'bottom CubeVerts(6) = CreateLitVertex(-1, -1, -1, Corner000, 0, 0, 0) CubeVerts(7) = CreateLitVertex(1, -1, -1, Corner100, 0, 0, 0) CubeVerts(8) = CreateLitVertex(-1, -1, 1, Corner001, 0, 0, 0) CubeVerts(9) = CreateLitVertex(1, -1, -1, Corner100, 0, 0, 0) CubeVerts(10) = CreateLitVertex(1, -1, 1, Corner101, 0, 0, 0) CubeVerts(11) = CreateLitVertex(-1, -1, 1, Corner001, 0, 0, 0) 'left CubeVerts(12) = CreateLitVertex(-1, 1, -1, Corner010, 0, 0, 0) CubeVerts(13) = CreateLitVertex(-1, 1, 1, Corner011, 0, 0, 0) CubeVerts(14) = CreateLitVertex(-1, -1, -1, Corner000, 0, 0, 0) CubeVerts(15) = CreateLitVertex(-1, 1, 1, Corner011, 0, 0, 0) CubeVerts(16) = CreateLitVertex(-1, -1, 1, Corner001, 0, 0, 0) CubeVerts(17) = CreateLitVertex(-1, -1, -1, Corner000, 0, 0, 0) 'right CubeVerts(18) = CreateLitVertex(1, 1, -1, Corner110, 0, 0, 0) CubeVerts(19) = CreateLitVertex(1, 1, 1, Corner111, 0, 0, 0) CubeVerts(20) = CreateLitVertex(1, -1, -1, Corner100, 0, 0, 0) CubeVerts(21) = CreateLitVertex(1, 1, 1, Corner111, 0, 0, 0) CubeVerts(22) = CreateLitVertex(1, -1, 1, Corner101, 0, 0, 0) CubeVerts(23) = CreateLitVertex(1, -1, -1, Corner100, 0, 0, 0) 'front CubeVerts(24) = CreateLitVertex(-1, 1, 1, Corner011, 0, 0, 0) CubeVerts(25) = CreateLitVertex(1, 1, 1, Corner111, 0, 0, 0) CubeVerts(26) = CreateLitVertex(-1, -1, 1, Corner001, 0, 0, 0) CubeVerts(27) = CreateLitVertex(1, 1, 1, Corner111, 0, 0, 0) CubeVerts(28) = CreateLitVertex(1, -1, 1, Corner101, 0, 0, 0) CubeVerts(29) = CreateLitVertex(-1, -1, 1, Corner001, 0, 0, 0) 'back CubeVerts(30) = CreateLitVertex(-1, 1, -1, Corner010, 0, 0, 0) CubeVerts(31) = CreateLitVertex(1, 1, -1, Corner110, 0, 0, 0) CubeVerts(32) = CreateLitVertex(-1, -1, -1, Corner000, 0, 0, 0) CubeVerts(33) = CreateLitVertex(1, 1, -1, Corner110, 0, 0, 0) CubeVerts(34) = CreateLitVertex(1, -1, -1, Corner100, 0, 0, 0) CubeVerts(35) = CreateLitVertex(-1, -1, -1, Corner000, 0, 0, 0) End Sub
Doesn’t that look so pretty! Well, not really…
There's nothing more to this that brute force calculations, I went through (took me about 10 minutes) working out which vertex would have what coordinate, and then what colour it would have. I used a set of constants to set the colours, as it makes it much easier when you want to change the colour of a given corner - saves you having to search through all 36 changing the wrong/old values for the new ones. Now that the array of vertices is completed we can render them in an almost identical way to last time:
D3DDevice.BeginScene 'All rendering calls go between these two lines D3DDevice.DrawPrimitiveUP D3DPT_TRIANGLELIST, 12, CubeVerts(0), Len(CubeVerts(0)) D3DDevice.EndScene
Hardly complicated really is it? However, if you now run the project to see what it looks like you’ll be surprised by the result - a big yellow/magenta/white square covering the entire screen. Great - not like the pretty coloured cube we were aiming for. This is because we "forgot" (or I didn’t tell you) about one major thing that needs to be set up: Projection, World and View Matrices.
If you think about it logically, we have not yet told Direct3D where we’re looking from, or where we’re looking at - or what properties our camera has. Which is what we now need to set up. Matrices are used a lot in Direct3D (as in most 3D API’s I do believe), they control transformations of world geometry (we’ll see that later), they control the configuration of the camera, and the information about where the camera is/where it’s looking at. The actual maths behind matrices is quite complicated, and you don’t really need to know why they work - or even how they do what they do, you can get along fine in Direct3D by just knowing how to use them. There are 3 types of matrix that we need to set up, they are:
The Projection Matrix
This matrix is usually configured during initialisation and left alone for the rest of the time, you can alter it other times to achieve effects such as zooming, but in general you probably wont need to.
D3DXMatrixPerspectiveFovLH( MOut as D3DMatrix, fovy as Single, Aspect as Single, zn as Single, zf as Single )
Above is the function prototype for the projection matrix altering function. There are others, but for 99% of Direct3D applications this one will be perfectly acceptable. The parameters should be used like so:
MOut: An empty (not a requirement though) D3DMatrix structure that will be filled with the relevant details of a projection matrix
fovy: This is the view angle for the camera, the wider the angle this is set at the more "stuff" you’ll see on the screen. This is often set at 45, 60 or 90 degrees - but as it’s measured in radians (not degrees) you should specify it in terms of PI, so the above list becomes Pi/4 , Pi/3 and Pi/2 - there are 2Pi radians in 360 degrees, therefore 1 Pi radians in 180 degrees, so dividing by 2 (for example) would give us the equivalent value (in radians) of 90 degrees.
Aspect: This is the aspect ratio for the projection matrix. Leave this at 1 unless you’re trying to do weird stuff ! experiment setting it to <1 or >1 value (as in 0.1 or 2, not 20000), you’ll notice that things look a little bit strange afterwards. In general, less than 1 stretches the geometry vertically, greater than 1 stretches the geometry horizontally.
zn: This is the near clipping plane. Any geometry closer than this to the camera will automatically be not-rendered, or clipped so that only the visible area is rendered. Leave this to something like 0.1 for the best results.
zf: this is the far clipping plane, as with the near plane, anything beyond this point (from the camera) will not be rendered, or will be clipped. If you are going to have a lot of things on screen it’s best to keep this as close as possible - otherwise you’ll end up rendering lots and lots of stuff that might not be visible.
The set up used in the example source code is:
D3DXMatrixPerspectiveFovLH matProj, PI / 3, 1, 0.1, 75
Finally, we’ve created the matrix structure (matProj in this case) - but Direct3D doesn’t necessarily know it exists - so we must tell it. We use the following line, which must only be called AFTER a successful device creation:
D3DDevice.SetTransform D3DTS_PROJECTION, matProj
From now until the next time this call is made (with a different matrix) all rendering will take place using this matrices configuration - 60 degrees view and a 75 meter draw depth.
The View Matrix
This is basically for configuring the camera. When we set this up we can tell Direct3D where the camera is, and where it’s looking at. Direct3D will then use this matrix in conjunction with the projection matrix to decide what is going to be rendered - and how it’s projected onto the screen.
D3DXMatrixLookAtLH ( MOut as D3DMATRIX, VEye as D3DVECTOR, VAt as D3DVECTOR, VUp as D3DVECTOR );
Not too complicated really. The MOut parameter will be the structure that’s filled with the relevant information. Veye is the position of the camera - where the player is looking from. Vat is the position of the target - the center of the screen (in 2D) will be this point in 3D Space. Vup is a proper vector (rather than a position) that tells D3D which way is up, using this you could make it render upside down…
The line from the sample code looks like:
D3DXMatrixLookAtLH matView, MakeVector(0, 5, 2), MakeVector(0, 0, 0), MakeVector(0, 1, 0)
Above we’re using a simple inline function that returns a D3DVECTOR based on the parameters, it’s quite simple and you can see it in the sample code. The actual code here tells D3D that the camera is place a little bit up and forward of the origin, and looking at the origin, and up is in the positive Y axis. As with the projection matrix you need to tell D3D that you want to use this camera set up for the duration until you change it again:
D3DDevice.SetTransform D3DTS_VIEW, matView
The World Matrix
This is a more interesting one - it is quite likely that you’ll set up and reuse this matrix 10 or more times every frame. It also matters how you set it up - whilst there are several helper functions to set a world matrix up a lot of it is still down to you.
First off then, what is a world matrix? The world matrix is applied to all geometry that is rendered, you can have translations (moving it around), scaling (bigger/smaller) and rotations. If you have a 2x scaling matrix applied then all geometry that’s rendered after that point will be twice as big as the actual vertex coordinates.
Secondly, why do we want to use them? The world matrix is an extremely fast and efficient way of manipulating your vertices - rather that you having to rotate and translate (for example) the raw vertex structures you can set up a world matrix and it will do it all for you. You also have the added advantage of being able to manipulate a single set of vertices to be many objects. For example, the cube we’ve made - you could render it in one position with one texture, then alter the world matrix and render it again with a different texture in a different position - and whilst there is only one physical set of vertices the user will see two essentially different cubes on the screen at the same time.
So they seem to be the perfect answer to what we need (they are, that’s why!) -but before we do a quick run through of how to use them I’m going to confuse you (unless you’re a good mathematician). To combine transformation matrices (The world matrix is a combination of 1 or more of these) we multiply them together, and in the normal world of maths A x B = B x A, remember? Well, in matrix land A x B does not equal B x A. In which case it matters what order we set up our world matrix. Great - things suddenly got complicated.
There are 3 main types of transformation that you will be applying to the world matrix - Rotation, Scaling and Translation. All of them can be applied around one or more of the axis’ - X, Y or Z. Whilst there’s no reason why you cant change the order, it will tend to be Rotation then Scaling and lastly translations. The reason for translating last is that the other two are calculated about the origin - all vertices are rotated around the point [0,0,0] and all vertices are scaled from the point [0,0,0]. Therefore if you translate something to [10,0,0] first, then rotate it around the Y axis you’ll get the object doing a loop (which might be what you want), but if you rotate it then translate it then you’ll get the object at [10,0,0] and spinning on the spot.
Sometimes these things are hard to visualise - but experiment with them and see what results you get from different orders. A further analogy could be the car wheel. Imagine a car wheel that, whilst driving normally, rotates around the X axis, if the car takes damage and the wheel is bent out of place we may choose to rotate it a constant 15 degrees around the Z axis (so it appears bent inwards); if we rotate it around the Z axis first then the X axis we’ll get the wheel oscillating in and out at the top and bottom as the wheel goes around - the part that’s bent outwards will rotate around with the wheel. If we rotate around the X axis first then the Z axis the wheel will revolve normally, except appear to be leaning in one direction - depending on which way the Z axis was rotated either the top of the wheel will stick out and the bottom will stick in - or vice versa. Think about it…
Whilst I’ve already touched upon this, it is often important to make all geometry relevant to the origin. Geometry is rotated around it, scaled about it and translated relative to it - if your geometry is centred around the point [10,0,0] and you translate it by 10 units along the X axis the centre will become [20,0,0]… which is why the cube that we’ve already generated has all it’s coordinates + or - 1 unit of the origin.
Before we move onto the actual sample code for these transformations we’ll quickly discuss the structure of setting up the world matrix. Matrices are held in a structure called D3DMATRIX, we will have one master matrix and one temporary matrix. The temporary matrix will be rest each time and have a new transformation applied, then it will be combined (through matrix multiplication) with the master matrix. This looks like this:
1) Define a Temporary and master matrix
3) Rotate the temporary matrix around the X axis
5) Set the temporary matrix to the identity matrix
8) Set the temporary matrix to the identity matrix
11) Set the temporary matrix to the identity matrix
14) Set the temporary matrix to the identity matrix
17) Commit the master matrix to the device so that it is applied to all subsequent rendering.
Not too complicated really - you’ll pick it up really quickly. There are two things in the above that I haven’t yet covered. The first is the identity matrix, when you create a new matrix type it will have 4x4 singles set out with a value of 0. if you made direct3D use this matrix nothing would appear - the scale Is set to 0, so at most all you’d see is a coloured dot at the origin. The identity matrix is one where the scaling values are set to 1.0, and any geometry rendered using it will not be altered at all - it appears in 3D space as it was laid out in the raw vertex data. It is important to reset the temporary matrix each time - otherwise you can get it accumulating multiple transformations in a strange way and you’ll get some truly strange results! The second thing is the multiplication part. As initially mentioned, [A][B] does not equal [B][A], so this line has to be in a specific order - you can get slightly different effects depending on which way around its done, but I strongly suggest you choose a preferred order and stick with it - I have always multiplied the world matrix by the temporary matrix - it’s worked fine for me and should do for you. The same goes with what order you rotate it - as mentioned in the wheel analogy, XYZ is very different from ZXY (or any other combination), Unless I need it to be done in another way I stick with XYZ - you can use that or change it - it’s up to you…
Okay… So I do believe we’re now ready to look at some proper code (at last!), the sample project only does rotation - you can see it as a small challenge to rewrite the sample so that the cube is translated and scaled (perhaps based on input from the user)…
There are 7 functions to perform the translations - there are lots more, but I’ll let you explore them later on (they tend to be more specialised). These are:
D3DXMatrixIdentity(MOut As D3DMATRIX) D3DXMatrixMultiply(MOut As D3DMATRIX, M1 As D3DMATRIX, M2 As D3DMATRIX) D3DXMatrixRotationX(MOut As D3DMATRIX, angle As Single) D3DXMatrixRotationY(MOut As D3DMATRIX, angle As Single) D3DXMatrixRotationZ(MOut As D3DMATRIX, angle As Single) D3DXMatrixScaling(MOut As D3DMATRIX, x As Single, y As Single, z As Single) D3DXMatrixTranslation(MOut As D3DMATRIX, x As Single, y As Single, z As Single)
Not too complicated really, you’ll see how to use them in a second, they all follow the same format. MOut is the resulting matrix, and angle is always in radians (not degrees).
D3DXMatrixIdentity matWorld D3DXMatrixIdentity matTemp D3DXMatrixRotationX matTemp, Angle * RAD D3DXMatrixMultiply matWorld, matWorld, matTemp D3DXMatrixIdentity matTemp D3DXMatrixRotationY matTemp, Angle * RAD D3DXMatrixMultiply matWorld, matWorld, matTemp D3DXMatrixIdentity matTemp D3DXMatrixRotationZ matTemp, Angle * RAD D3DXMatrixMultiply matWorld, matWorld, matTemp Angle = Angle + 1 D3DDevice.SetTransform D3DTS_WORLD, matWorld
The above excerpt is the code used in the sample program to spin the cube around on all axis by 1 degree every frame. This code is updated every frame - so any particularly complicated formula’s that you want to use may well slow things down a bit - although using the D3DX* functions are fast enough not to worry about.
If you now run the sample project (or your code if copying from the page) you should be greeted with a fairly large, colourful spinning cube… If you have turbo-charged hardware acceleration the cube may be spinning extremely fast (1 degree every frame, 360+ fps will mean it does a complete spin in 1 second!), if this is the case change the angle increment value down to 0.5 or 0.25…
Vertex Buffers and Index Buffers
Well, that was one rather large 7 A4 page section done with, things should fly by a little quicker now that the basic theory has been covered. As with most things, once you’ve jumped over the initial couple of hurdles (setting up D3D, basic 3D geometry) it’s all easy - well, sort of J
This section is dealing with enhancing your capabilities for rendering geometry, making things easier and faster. Which I’m sure you’ll appreciate…
The first stop will be vertex buffers - what are these? I hear you asking… A vertex buffer is an area of memory where you can store your vertex data, this has the advantage of being easier to render - both for you and for the driver (it can perform various optimisations, and access the data faster) and allowing you to archive certain pieces of geometry, a generic cube for example… Whilst not entirely true it can also stop most accidental overwriting/corrupting errors - once the buffer is created it’s not something you can accidentally change without realising it - whereas an array of vertex structures as a standard variable could easily be overwritten by a rogue function or library (however unlikely you think it, it will happen sometime).
Creating a vertex buffer is extremely simple once you have the basics from the last section under your belt. At the simplest level it involves copying the data you generated to a custom buffer…
The first part is allocating some space for our buffer:
'In Declarations section Dim vbCube As Direct3DVertexBuffer8 'at the end of InitialiseGeometry() sub Set vbCube = D3DDevice.CreateVertexBuffer(Len(CubeVerts(0)) * 36, 0, FVF_LVERTEX, D3DPOOL_MANAGED) If vbCube Is Nothing Then Debug.Print "ERROR: Could not create vertex buffer": Exit Sub
Not too complicated really, first we create the buffer with the function built into the Direct3DDevice8 class, then we check to make sure it was created successfully - if it wasn’t it usually generates an error on that line. The parameters for the CreateVertexBuffer() function look like:
CreateVertexBuffer( _ LengthInBytes As Long, _ Usage As Long, _ FVF As Long, _ Pool As CONST_D3DPOOL) As Direct3DVertexBuffer8
LengthInBytes: This is the amount of memory that will be set aside for your buffer, it must be at least as big as the data you want to store in it - any less and you’ll get an error when copying the data in, any more and you’ll be wasting space. This value can be computed using the Len() function to find the size of one vertex structure, then multiplying it by the number of structures that your using.
Usage: This can be left as 0 for most uses, but more specialised cases require that you let Direct3D know that your going to be doing different things with this vertex buffer. For example, specifying D3DUSAGE_DYNAMIC will tell Direct3D to let the driver know that we’re probably going to be changing the contents around quite a lot - it will then choose the most optimal place in memory to put it. If you don’t specify this, the buffer will be considered static - and the driver will place it in the most optimal memory location for that use - which will mean that any lock-write-unlock operations are slower (it assumed that you weren’t likely to be doing any).
FVF: This lets Direct3D know what sort of vertices you will be storing here, should you ask D3D to perform any operations on this buffer later on it will base them on this flag. Set this to be the same as the one you use for rendering.
Pool: This allows you to tell D3D where you would like the vertex buffer to be stored, note that it’s where you would LIKE it to be - The driver may well override this setting - should it be impractical (lack of space/lack for support for example). The 3 options are: D3DPOOL_DEFAULT - it’s left mostly up to the driver to choose where it goes - usually in video memory, but other places if the usages flags suggest otherwise. D3DPOOL_MANAGED - D3D will store a master copy in system memory and will copy the buffer to video memory as and when its required. D3DPOOL_SYSTEMMEM - the buffer will be stored in system memory, which isn’t usually accessable by the 3D device, and it will tend to be quite slow.
Now that we’ve created our vertex buffer we want to put some stuff in it! For this example we’ll copy our cube vertices into the buffer and then render it from there. For this we use a little helper function to copy the data:
D3DVertexBuffer8SetData vbCube, 0, Len(CubeVerts(0)) * 36, 0, CubeVerts(0)
That call will fill the specified vertex buffer with the specified amount of information from the array provided. A quick run through of the parameters and what they mean then:
D3DVertexBuffer8SetData( _ VBuffer As Direct3DVertexBuffer8, _ Offset As Long, _ Size As Long, _ Flags As Long, _ Data As Any) As Long
Vbuffer: obviously this is the vertex buffer object that you want the data put into.
Offset: The offset in bytes from the beginning of the buffer that we start placing new data - useful if your only wanting to change vertices 10-19 (for example).
Size: The amount of data that’s going to be copied in, also the amount that D3D expects to find in the data array. This is the same calculation as used in creating the buffer. Try to make sure beforehand that this doesn’t exceed the data source size, or the remaining/total space in the vertex buffer
Flags: A set of flags defining how you want the data to be replaced/copied - look up CONST_D3DLOCKFLAGS in the object browser for more details.
Data: This is the source array, it can be of any data type - whilst you could trick D3D into storing any type of data it’s supposed to be vertex data only (make sure you don’t accidentally include any state variables/non-vertex variables).
Okay, you now have all the information required to create a vertex buffer - but currently you cant actually do anything with it. For this sample all we’ll do is render it - there are a few other things you can do, but I’ll let you explore those yourself…
'replace: D3DDevice.DrawPrimitiveUP D3DPT_TRIANGLELIST, 12, CubeVerts(0), Len(CubeVerts(0)) 'With: D3DDevice.SetStreamSource 0, vbCube, Len(CubeVerts(0)) D3DDevice.DrawPrimitive D3DPT_TRIANGLELIST, 0, 12
As you can see, rendering gets considerably easier when using vertex buffers - tell it where the vertex buffer is, and then what part you want rendering. As a side note, the stream number (The first parameter in SetStreamSource) should be left as 0 in most cases - things such as vertex shaders manipulate these further.
Now that you have vertex buffers under your belt we can take a look at index buffers, these require vertex buffers (not technically true, but I’ll explain later), which is why they came last. In fact, this is the last section in this article - we are definitely homeward bound…
If you look at our cube geometry you’ll notice that it’s extremely inefficient - we use 36 vertices to describe an object that (in real life) only has 8 vertices. Wouldn’t it be extremely useful if we could only place 1 vertex in each corner, rather than have 3-6 vertices sharing the same coordinate, whilst not always the case, in our example they have the same colour as well - several vertices that are all identical.
This is where indices and index buffers come into play - they allow us to use vertices more than once (to put it simply). We can create 8 vertices, and then, using an index list say that triangle 1 uses vertices 1,2 and 3 and triangle 2 uses vertices 2,4 and 3 - reusing vertices. This has the advantage of being relatively fast, and also saves on the amount of data involved - the index list will always be smaller than the vertex list, and therefore the amount of data that’s processed and copied also decreases.
Unfortunately they aren’t quite the miracle invention I’m making them out to be - two topics that we haven’t yet discussed: Lighting and textures can become significantly worse/harder when using indices - it’s not always the case, but in cases such as the cube example it would be very annoying. The exact reasoning will be discussed in the relevant sections later on…
To set up and use indices/index buffers we first need to create a vertex buffer with the key vertices in it - the following code does this for the cube sample:
'In the declarations section: Dim vbCubeIdx As Direct3DVertexBuffer8 Dim ibCube As Direct3DIndexBuffer8 Dim vList(0 To 7) As LITVERTEX 'the 8 vertices required Dim iList(0 To 35) As Integer 'the 36 indices required (note that the number is the 'same as the vertex count in the previous version). 'in the create geometry section: vList(0) = CreateLitVertex(-1, -1, -1, &HFFFFFF, 0, 0, 0) vList(1) = CreateLitVertex(-1, -1, 1, &HFF0000, 0, 0, 0) vList(2) = CreateLitVertex(-1, 1, -1, &HFF00, 0, 0, 0) vList(3) = CreateLitVertex(-1, 1, 1, &HFF, 0, 0, 0) vList(4) = CreateLitVertex(1, -1, -1, &HFF00FF, 0, 0, 0) vList(5) = CreateLitVertex(1, -1, 1, &HFFFF00, 0, 0, 0) vList(6) = CreateLitVertex(1, 1, -1, &HFFFF, 0, 0, 0) vList(7) = CreateLitVertex(1, 1, 1, &HFF8000, 0, 0, 0) Set vbCubeIdx = D3DDevice.CreateVertexBuffer(Len(vList(0)) * 8, 0, FVF_LVERTEX, D3DPOOL_MANAGED) If vbCubeIdx Is Nothing Then Debug.Print "ERROR: Could not create vbCubeIdx": Exit Sub D3DVertexBuffer8SetData vbCubeIdx, 0, Len(vList(0)) * 8, 0, vList(0)
I’m not going to cover this part again - reread the previous section if you’re still unclear as to setting up a vertex buffer.
The next part is the new part, we must set up an index list. All this requires is that we fill an array of 16 bit integers (you can use 32 bit integers if you need to) and then copy them to the new array. We’ll be rendering our cube using a triangle list again - so we’re going to need to describe 12 triangles again, because of this we can pretty much copy exactly what we did for the original triangle. This is what it’s going to look like:
'top iList(0) = 2: iList(1) = 6: iList(2) = 3 iList(3) = 6: iList(4) = 7: iList(5) = 3 'bottom iList(6) = 0: iList(7) = 4: iList(8) = 1 iList(9) = 4: iList(10) = 5: iList(11) = 1 'left iList(12) = 2: iList(13) = 3: iList(14) = 0 iList(15) = 3: iList(16) = 1: iList(17) = 0 'right iList(18) = 6: iList(19) = 7: iList(20) = 4 iList(21) = 7: iList(22) = 5: iList(23) = 4 'front iList(24) = 3: iList(25) = 7: iList(26) = 1 iList(27) = 7: iList(28) = 5: iList(29) = 1 'back iList(30) = 2: iList(31) = 6: iList(32) = 0 iList(33) = 6: iList(34) = 4: iList(35) = 0
I’ve grouped them together into their triangles - each line is a triangle, and there are two triangles to each face - so two lines under each comment/heading.
Hopefully it’s all fairly explanatory; if we take the first triangle as an example, it uses the vertices 2,6 and 3 to be rendered, these are in the standard clockwise order.
The next part we need to cover is creating an index buffer, and then copying the data into it. Luckily this is almost identical to the creation of a vertex buffer - many of the parameters are the same. To copy our index list into an index buffer we use this piece of code:
Set ibCube = D3DDevice.CreateIndexBuffer(Len(iList(0)) * 36, 0, D3DFMT_INDEX16, D3DPOOL_MANAGED) If ibCube Is Nothing Then Debug.Print "ERROR: Could not create the index buffer": Exit Sub D3DIndexBuffer8SetData ibCube, 0, Len(iList(0)) * 36, 0, iList(0)
Straight away you can see the similarities; the only major different when creating the index buffer is that we need to specify what format the indices are going to be in. There are only two valid options here (despite it listing 30-40 formats): D3DFMT_INDEX16 and D3DFMT_INDEX32, these refer to the actual data type that we use to store our indices, a 16 bit integer (The integer data type) can only hold up to 32,767 indices (the maximum + value for an integer), and a 32 bit integer (the long data type) can only hold up to 2,147,483,647 (the maximum + value for a long). Obviously using 32 bit indices take up double the amount of space (4 bytes per value, rather than 2 bytes per value in 16 bit), so unless you are using an extremely large number of indices you should try and stick to using 16 bit indices.
Now that we have our index data stored in a buffer we need to be able to do something with it, for this example all we’re going to do is render it - there is very little else you can do with them anyway. The following code segment will render our cube using our vertex buffer/index buffer combo:
D3DDevice.SetStreamSource 0, vbCubeIdx, Len(vList(0)) D3DDevice.SetIndices ibCube, 0 D3DDevice.DrawIndexedPrimitive D3DPT_TRIANGLELIST, 0, 36, 0, 12
All it involves is setting the current index buffer and vertex buffer - make sure they match up though, then using DrawIndexedPrimative() to render from them. The parameters are fairly straight forward for the rendering call, you can use the third and fourth to set a range that you want rendering (12-24 for example), you can use the second parameter to make an offset in the vertex buffer (all values in the index list get this number added to them, if it were 10, and iList(3) was 12 the actual vertex used would be 22). The last value states how many primitive we’re rendering - in this case the whole cube, 12 triangles.
There are two final things to be covered about the usage of indices and buffers; the first one is about rendering without using the buffers. It is perfectly possible to render our indexed cube without storing the index/vertex data in a buffer, but it is often a great deal slower. The call for doing this is quite a lengthy one - you need to specify the details of both the vertex and index buffers:
D3DDevice.DrawIndexedPrimitiveUP D3DPT_TRIANGLELIST, 0, 8, 12, _ iList(0), D3DFMT_INDEX16, vList(0), Len(vList(0))
A very quick summary of what goes where:
PrimitiveType: What type of primitives we’re rendering
MinVertexIndex: The Starting vertex to render from
NumVertexIndices: The number of vertices that we’re going to render.
IndexDataArray: The first element in the array storing indices
IndexDataFormat: What format the index data is in, 16 or 32 bit
VertexStreamZeroDataArray: The first element of the vertex data array
VertexStreamZeroStride: The size of one vertex element
If you have the SDK help file you may realise that the 2nd and 3rd parameters have different names - This is a typing error in the help file, the parameters I’ve listed above come straight from the data-tip that appears when you type the line into VB.
Over the course of this article we have covered a total of 4 different ways of rendering our cube:
'##RENDERING METHOD 1## D3DDevice.DrawPrimitiveUP D3DPT_TRIANGLELIST, 12, CubeVerts(0), Len(CubeVerts(0)) '##RENDERING METHOD 2## D3DDevice.SetStreamSource 0, vbCube, Len(CubeVerts(0)) D3DDevice.DrawPrimitive D3DPT_TRIANGLELIST, 0, 12 '##RENDERING METHOD 3## D3DDevice.SetStreamSource 0, vbCubeIdx, Len(vList(0)) D3DDevice.SetIndices ibCube, 0 D3DDevice.DrawIndexedPrimitive D3DPT_TRIANGLELIST, 0, 36, 0, 12 '##RENDERING METHOD 4## D3DDevice.DrawIndexedPrimitiveUP D3DPT_TRIANGLELIST, 0, 8, 12, _ iList(0), D3DFMT_INDEX16, vList(0), Len(vList(0))
With these 4 methods, and the knowledge of how to set them up you will be perfectly capable of generating most forms of geometry in the best possible way. Experiment with different complexity models to see what advantages of space/speed you can find…
Finally, absolutely finally, I want to explain how to get data from the buffers after you have put it there. Whilst it is quite likely you’ll have the original vertex structures around to manipulate there will be times when you need to access the vertex or index data and change it. It is through this method that you can do key frame animation (I have a tutorial on my site about this - see the link in the summary). Also when using indexed rendering you need only change the vertex in the vertex buffer for it to be instantly reflected in the final rendering - all indices that point to that vertex will use the updated copy.
D3DIndexBuffer8GetData( _ IBuffer As Direct3DIndexBuffer8, _ Offset As Long, _ Size As Long, _ Flags As Long, _ Data As Any) As Long D3DVertexBuffer8GetData( _ VBuffer As Direct3DVertexBuffer8, _ Offset As Long, _ Size As Long, _ Flags As Long, _ Data As Any) As Long
As you can see both functions are almost identical - the only difference being their name and the first parameter. A quick run through of the common parameters then:
Offset: offset in bytes indicating where the data should be read from
Size: The size of the destination buffer in bytes
Flags: Not relevant in 99% of cases, examine CONST_D3DLOCKFLAGS if you think you need something special
Data: This is the first element in an array that is going to store the data - it must be in the correct format, and must be the correct size.
Should there be any problem with knowing how big the vertex/index buffer is - or how many vertices/indices are stored inside then you can use the .GetDesc member of either the vertex buffer or index buffer. This will retrieve either a D3DVERTEXBUFFER_DESC or a D3DINDEXBUFFER_DESC structure, you can then use the data provided to work out the size, in the case of index buffers the following code will tell you how many indices there are in the buffer:
'D3DFMT_INDEX16 = 101 'D3DFMT_INDEX32 = 102 Dim ibDesc As D3DINDEXBUFFER_DESC Dim IndexCount As Long 'how many indices are in the buffer ibCube.GetDesc ibDesc If ibDesc.Format = 101 Then '16 bit indices IndexCount = ibDesc.Size / 2 ElseIf ibDesc.Format = 102 Then '32 bit indices IndexCount = ibDesc.Size / 4 Else 'no idea whats stored here! End If Debug.Print IndexCount, " indices in the buffer."
And this code will tell you how many vertices there are in a vertex buffer:
Dim vbDesc As D3DVERTEXBUFFER_DESC Dim DummyVertex As LITVERTEX Dim VertexCount As Long Dim TotalDivider As Long 'the size of the vertex structure vbCube.GetDesc vbDesc If vbDesc.FVF = FVF_LVERTEX Then 'The type stored is the LVertex type VertexCount = vbDesc.Size / Len(DummyVertex) Else 'it's some other type of vertex, lets find out: If Not (vbDesc.FVF And D3DFVF_XYZ) = 0 Then Debug.Print "D3DFVF_XYZ" TotalDivider = TotalDivider + 12 End If If Not (vbDesc.FVF And D3DFVF_XYZRHW) = 0 Then Debug.Print "D3DFVF_XYZRHW" TotalDivider = TotalDivider + 16 End If If Not (vbDesc.FVF And D3DFVF_NORMAL) = 0 Then Debug.Print "D3DFVF_NORMAL" TotalDivider = TotalDivider + 12 End If If Not (vbDesc.FVF And D3DFVF_DIFFUSE) = 0 Then Debug.Print "D3DFVF_DIFFUSE" TotalDivider = TotalDivider + 4 End If If Not (vbDesc.FVF And D3DFVF_SPECULAR) = 0 Then Debug.Print "D3DFVF_SPECULAR" TotalDivider = TotalDivider + 4 End If If Not (vbDesc.FVF And D3DFVF_TEX1) = 0 Then Debug.Print "D3DFVF_TEX1" TotalDivider = TotalDivider + 8 End If If Not (vbDesc.FVF And D3DFVF_TEX2) = 0 Then Debug.Print "D3DFVF_TEX2" TotalDivider = TotalDivider + 8 End If VertexCount = vbDesc.Size / TotalDivider End If Debug.Print VertexCount, " Vertices in the buffer"
Quite lengthy you’ll see, but that’s just if the format is unknown, in which case we undo the "or-ing" to generate an FVF by "And-ing" it with the relevant components - the ones listed above will detect most common types, but if you start using more dynamic flags then you’ll need to add them to this list.
Well, I’ve finally completed this article! All 20 A4 pages of it and nearly 10,000 words of it, it even took me almost dead on 7 hours to write the whole thing… and I haven’t made a penny/dollar from it J Maybe I should write a book about this stuff…
Once you have this massive amount of information safely behind you then you should be perfectly capable of getting started with your own game, the next article will tidy up all the loose ends and introduce you to loading models and direct3D lighting, by which point you’ll be a fairly competent DirectXGraphics developer.
Please feel free to send any comments, criticisms and questions to me: Jollyjeffers@Greenonions.netscapeonline.co.uk, I always like hearing from people who’ve read my work…
Also, you can visit my main page - where a lot of this information can be found in different formats/examples, along with plenty of other articles (just over 100 in total), go along to http://www.vbexplorer.com/directx4vb/ and have a look around…
Till next time…