DirectX Graphics for Visual Basic Part 1

Published February 17, 2001 by Jack Hoxley, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
Welcome to the first of a 3 part mini-series on the usage of DirectX Graphics (the graphical component of DirectX 8). Over these 3 articles I will cover everything that you need to know to be a competent programmer in this area. While this series will not cover absolutely every aspect (that would require many more than 3 parts), by the end you will be able to do most things, and anything you can't do you should be able to work out for yourself, or read other tutorials and easily understand them.

Some people may well say that you can't write a proper 3D game in Visual Basic. I'm not here to argue about that, but I will tell you now - it is perfectly possible to write a moderate to advanced game in full 3D using pure Visual Basic - maybe not the next Quake/Half-life, but that doesn't mean you can't do any games.

In order to program with DirectX you are going to need a few things:
  1. A general knowledge of the Visual Basic language. While complicated things will be explained, I will assume that you can write a reasonably complex program.
  2. A copy of Visual Basic 5 or later. Earlier versions of Visual Basic do not support the Component Object Model (COM), and therefore will not be able to use DirectX 8.
  3. A copy of DirectX 8. The runtimes are perfectly acceptable (the ones that you get from the Microsoft site or magazine CDs). If you're serious about learning and using DirectX getting the SDK (Software Development Kit) will be a huge advantage.
While these articles are going to be in Visual Basic, the actual DirectX 8 interfaces are almost identical to those used in C/C++ (except for the obvious language differences), so if you can use DirectX 8 in C/C++ then you'll find this very easy.


[size="5"]Getting Started

DirectX Graphics all come under the name of Direct3D, which will be the term used from now on (it's shorter), but the names are interchangeable. Direct3D when it gets hard gets very, very hard indeed; but luckily the basics are very simple, and a basic application can be set up in a 100 or so lines of code. So here goes:

First we need to attach DirectX 8 to our VB program - so it knows how to use it. Open up VB and create a new "Standard EXE" project. A single form should be added to the project view. Go to the Project menu, then click on references to display the library dialog. You should see a long list of objects and libraries in the middle of the window - all of them with a small checkbox to the left. Scroll down until you see an entry called "DirectX 8 for Visual Basic Type Library", select the check box and click "Ok".

We have now referenced our project to the DirectX 8 runtime library; that is all we need to do in order to use DirectX 8 features in Visual Basic. Bear in mind that the end user will have to have DirectX 8 installed on their computer for your application to even begin execution - if it's not there your program will terminate as soon as it's started. I have a template set up for this type of application, so it appears in my "New Project" dialog box - something you may wish to do.


[size="5"]A Simple Application

Now that we can use DirectX 8 we're going to set up a very simple example - all it will do is create an instance of Direct3D and clear the screen, then terminate.

The first thing we need to do is put some variables into our (Declarations) section of the form:

Dim Dx As DirectX8 'The master Object, everything comes from here
Dim D3D As Direct3D8 'This controls all things 3D
Dim D3DDevice As Direct3DDevice8 'This actually represents the hardware doing the rendering
Dim bRunning As Boolean 'Controls whether the program is running or not...
The first 3 variables here (Dx, D3D, D3Ddevice) are all classes - we'll need to initialize them and terminate them; the fourth variable, bRunning, is just a simple Yes/No flag that states if the application is running or not - more on that one later.

Now seems like a good time to explain what these different objects do. DirectX 8 has a hierarchy of objects and interfaces, each one with a parent, and in this case a "DirectX 8" object is as far back as they go. The "Direct3D8" object deals with creating devices and enumerating their capabilities. Finally, the "Direct3DDevice8" object represents your 3D card - you tell it to do things and (within reason) it'll do it. We therefore create a "DirectX 8" object; this then helps us create a "Direct3D8" object, which in turn will setup a "Direct3DDevice8" object for us to use.

There are several other interfaces/objects that we can create, but right now we don't really need to know much about them - you'll see them as we go. It is very useful to have a copy of the DirectX 8 SDK help file when dealing with these objects. While VB's Intellisense and object browser are very useful, the SDK help file explains and lists all the functions and features of each interface/object.

Now that we have the variables defined we can start to do something with them; for this we're going to create a function called "Initialize()" which does exactly what it says it will - when it's finished execution (and assuming no errors) we'll be able to use all the objects and start making things appear on screen.

Public Function Initialize() As Boolean
On Error GoTo ErrHandler:

Initialize = True '//We succeeded
Exit Function

ErrHandler:
Debug.Print "Error Number Returned: " & Err.Number, Err.Description
Initialize = False
End Function
Above is the basic framework for the function - and you'll be seeing that most of the functions are designed like this. Technically, Initialize() does not need to be a function as it doesn't return any particular data. But I particularly like this layout because it allows me to design a good function that should never bring down the rest of the application - if it fails all it will do is return false to whoever called it. When calling this function we should use code like this:

If Not (Initialize() = True) Then GoTo Error_Handler:
Which will execute the initialization code. Then, if it succeeds, it will carry on as normal, but if it fails it will go to the "Error_Handler" for processing / correction. The above call is just a simplified (and easier to read) version of:

If Initialize() = False Then
GoTo Error_Handler:
End If
Now that we've got the basic function structure laid out we'll put something in it. The first thing we need to do is define two structures and initialize the Direct3D objects:

Dim DispMode As D3DDISPLAYMODE '//Describes our Display Mode
Dim D3DWindow As D3DPRESENT_PARAMETERS '//Describes our Viewport

Set Dx = New DirectX8 '//Create our Master Object
Set D3D = Dx.Direct3DCreate() '//Let our Master Object create the Direct3D Interface
The two structures are used in a minute to help create the final Direct3DDevice8 object, but right now all we do is define them. Next we create the DirectX 8 object - you may well know that it would have been perfectly legal to have defined the object like "Dim Dx as New DirectX 8", but this is bad for what we want to do. The method just mentioned is known as Early Binding, the method we're using is called Late Binding; the difference being that if you late bind it Visual Basic will not check to see if it is created when you try to use it (but will return errors). If you early bind it VB will compile the code with a statement around EVERY call to the object along the lines of "If the object is nothing, create it". While that may well only be true the first time around, it's still something extra for the computer to think about, and being a game we want all the speed we can get, and these objects will be used 1000's of times a second - so you can imagine the sort of speed we'll be wasting. Secondly, we make our master interface create the generic Direct3D interface. You will always be able to create a Direct3D interface - no matter what the hardware installed can do. Now we need to fill out the two structures we just defined:

D3D.GetAdapterDisplayMode D3DADAPTER_DEFAULT, DispMode '//Retrieve the current display Mode

D3DWindow.Windowed = 1 '//Tell it we're using Windowed Mode
D3DWindow.SwapEffect = D3DSWAPEFFECT_COPY_VSYNC '//We'll refresh when the monitor does
D3DWindow.BackBufferFormat = DispMode.Format '//We'll use the format we just retrieved...
Not too complicated really - but it gets more complicated if we want to use fullscreen rendering (covered later on). While it really wont matter for this sample, if you are using windowed mode it's a good idea to keep your window fairly small - 400x300 in pixels is a good size for most resolutions. The next part actually involves creating an instance of a Direct3D device. This part can be slightly dangerous. If you send parameters that the end-user's computer can't handle, then it'll fail and cause an error. This mostly tends to happen when you're using fullscreen modes and you need to choose a resolution/colour depth that suits their hardware/monitor.

Set D3DDevice = D3D.CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, _
frmMain.hWnd, _
D3DCREATE_SOFTWARE_VERTEXPROCESSING, _
D3DWindow)
The actual code isn't too complicated, but it's what goes in the parameters that is. The parameters are as follows:

Adapter As Long : While they don't seem to be making 3D cards with primary/secondary devices this is where you can change it. Almost all graphics cards will be on the default adapter (D3DADAPTER_DEFAULT). Set it to 0 or 1 otherwise

DeviceType As Const_D3DDEVTYPE : What type of device we want to use; there are 2 main types of device and an optional 3rd:
  • D3DDEVTYPE_HAL - Hardware acceleration, where the actual 3D card does the rendering
  • D3DDEVTYPE_REF - A reference device, purely for developers - you'll be lucky if you get more than 0.25 frames a second out of this. On the other hand you can do absolutely anything with it - full feature support.
  • D3DDEVTYPE_SW - This can't be used unless you register a software renderer (a plugin for DirectX), but there aren't any bundled with Direct3D, so you'll have to make your own (very hard) or get a 3rd party one. hFocusWindow As Long : This lets Direct3D know which window it needs to render to, mostly for windowed mode, so it can check if it's gone behind other windows or it's been closed and so on... always pass [lessthan]FormName>.hWnd here, but make sure that the window is visible first.

    BehaviourFlags As Long : How this device will behave, and what does what (processor and/or 3D card). This should be D3DCREATE_SOFTWARE_VERTEXPROCESSING on most computers - or computers where there is no hardware transform and lighting or better (almost every card except the GeForce cards); in which case you can put in D3DCREATE_HARDWARE_VERTEXPROCESSING, which will force the 3D card to do transform and lighting operations; alternatively you can use the D3DCREATE_PUREDEVICE option - which is new to Direct3D8, and is only available on the ?300+ GeForce2 Ultra chipsets (at time of writing anyway).

    PresentationParameters As D3DPRESENT_PARAMETERS : Just place the structure that we filled earlier in here...

    That's our initialization code complete - assuming that code runs through successfully then we'll have a fully initialized device attached to our form ready to play with. One word about DirectX errors - they always have the description "Automation Error" and a number in the negative 2 millions (-2001230 for example). Should you want to know what that means in English you'll need to check the Err.Number against a set of constants that the DirectX 8 library provides us with. If you have the SDK you can check what error numbers each function might return and only check those, otherwise you'll need to check them all - and there are a lot of them! Look for them in the object browser, they all tend to begin with "D3DERR_" or "E_"...

    One final thing that I want to cover before we move on further is the topic of enumeration; you may not have heard of this before - but it's something you'll become familiar with if you spend any length of time programming in DirectX. Enumeration is the process of analysing the hardware to see what it's capable of (or not capable of). You'll meet most of it as we go along - but there are a couple that are relevant to device creation that I need to cover here.

    In the above initialization code we specified D3DDEVTYPE_HAL, which may or may not be available on the host computer, and if we want to jump to fullscreen mode we'll need to know what resolutions and colour depths the hardware supports as well. While software vertex processing works on all computers it would be nice to take advantage of any additional hardware features available. To do this we use the following code:

    Dim DevCaps As D3DCAPS8
    On Local Error Resume Next
    D3D.GetDeviceCaps D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, DevCaps
    If Err.Number = D3DERR_INVALIDDEVICE Then
    'We couldn't get data from the hardware device - probably doesn't exist...
    D3D.GetDeviceCaps D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, DevCaps
    Err.Clear '//Remove the error value..
    End If

    '//For Hardware vertex processing:
    If (DevCaps.DevCaps And D3DDEVCAPS_HWTRANSFORMANDLIGHT) Then
    Debug.Print "Hardware Transform and lighting supported"
    Else
    Debug.Print "Hardware Transform and lighting is not supported"
    End If

    '//For Pure Device processing:
    If (DevCaps.DevCaps And D3DDEVCAPS_PUREDEVICE) Then
    Debug.Print "Pure Device is supported."
    Else
    Debug.Print "Pure device is not supported"
    End If

    '//To check the rest we use:
    If D3D.CheckDeviceType(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
    DispMode.Format, DispMode.Format, 1) = D3D_OK Then
    Debug.Print "The selected device format is acceptable"
    Else
    Debug.Print "The selected device format is not acceptable"
    End If
    At the moment the above code does nothing more than tell you, the developer, what the hardware can do - or can't do. The first two lines retrieve all the enumeration data for the device we've specified; there's a simple error handler in here that should stop us crashing if there is no hardware device present - if this is the case then the program gets data from the reference device (which will support almost everything). The D3DCAPS8 structure now holds all the information that we need to evaluate the device and it's capabilities. If you know that your application requires other features you can enumerate them now and find out if there's any point in using this device.

    So we have our structure filled with data, all we need to do now is extract the information we want. At this point it's a good idea to use the SDK help file - there are literally thousands of different flags and features we can check for, and the help file lists every one of them with a small description (which isn't always that helpful). If you are just going to be reading tutorials off the net, or not poking around much yourself then it's not too important - most tutorials will explain what enumerations you'll need to perform.

    In the above example we first check for hardware transform and lighting capabilities. The transform part is to do with vertex manipulation, and if it's done in hardware then it would imply that we can set up a device that uses the D3DCREATE_HARDWARE_VERTEXPROCESSING flag. We then check for the presence of the pure device option, which is the next step up from hardware transform and lighting (therefore use this if it's present). If this returns true then we can specify D3DCREATE_PUREDEVICE when creating the device. On the other hand if both of these return false we'll just have to use D3DCREATE_SOFTWARE_VERTEXPROCESSING. Lastly we check if the device type can be created - we specify the type of device, the format and if it's in windowed mode or not; if this call evaluates to D3D_OK then we'll be able to create a device with the same parameters, if it doesn't then we need to find some other parameters - this will usually just mean changing to the Reference device - as the call we made earlier to get the current display mode will of told us the correct format, and if we're using fullscreen modes then we'll have enumerated the possibilities (more on that later) - which only leaves the possibility that there's no hardware device present. We could avoid this completely and just remember what happened when we got the enumeration data - and which device that came from.


    [size="5"]The Main Loop

    This next part is fairly quick and simple, yet is important to any Direct3D application - how the thing runs. Anyone who has paid any attention to a commercial game will know about frame rates - how many times every second the computer is updating the game; high frame rate is good, low frame rate is bad.

    We now need to set up our application so that it runs on a loop; event based (where we only update when something has changed) simply will not cut it here - you'll be wasting valuable time either doing nothing or trying to work out if something has changed, and on top of that it's almost always going to be changing. Secondly never ever, ever, ever use a timer control or similar for this job - they are inaccurate and slow, you may well be able to set them to 1ms, but in reality they're only accurate to about 50-100ms (maximum frame rate is therefore 10-20fps).

    We're going to use a loop instead - in theory a never ending loop. This loop will execute as fast as possible, and will form the basis of our frame rate - the faster the loop goes the higher the frame rate. This loop will use a simple Boolean flag to determine if it's running; as soon as this variable goes false the loop terminates and we do something else (probably close the application down).

    Here's the code that the sample program uses:

    Private Sub Form_Load()

    Me.Show '//Make sure our window is visible

    bRunning = Initialize()
    'So you can see what happens...
    Debug.Print "Device Creation Return Code : ", bRunning

    Do While bRunning
    Render '//Update the frame...
    DoEvents '//Allow windows time to think; otherwise you'll
    '//get into a really tight (and bad) loop...

    Loop '//Begin the next frame...

    '//If we've gotten to this point the loop must have been terminated
    ' So we need to clean up after ourselves. This isn't essential, but it's
    ' good coding practise.

    On Error Resume Next 'If the objects were never created;
    ' (the initialization failed) we might get an
    ' error when freeing them... which we need to
    ' handle, but as we're closing anyway...
    Set D3DDevice = Nothing
    Set D3D = Nothing
    Set Dx = Nothing
    Debug.Print "All Objects Destroyed"

    '//Final termination:
    Unload Me
    End
    End Sub
    As you can see straight away this code is all in the Form_Load event, which isn't the optimal place to put it. Whenever I design a bigger project I always put the control loop as a Sub Main() in a separate control module and leave the form completely empty - and then all the subsequent code goes in classes (one for graphics, utilities, maths, audio, physics, AI, Input, File handling and so on).

    The first step is to make sure the form is visible, normally this won't happen until this procedure has finished (which in our case wont happen till the program terminates). As already mentioned, Direct3D wont function properly if the form isn't visible or isn't loaded.

    Next we initialize Direct3D. We place it's return value in the Boolean on/off switch (instead of the original method I showed you); the beauty of this is that if it returns false on the first pass of the loop it'll terminate itself.

    In the middle we have the main loop, a Do While ... Loop structure. At the moment it's made up of two statements; these will be the only two statements executed for the vast majority of runtime. Place any additional calls or statements that you want processed on a frame by frame basis. The key part here is to have a DoEvents call at the end of the loop, without it your program will go down the pan very quickly - and in most cases lock up the system. If this statement is not in here we won't receive any messages, no input (keyboard, mouse) and the chances are that pure VB language statements will not be executed properly. The DoEvents yields time for the system (Windows in this case) to think about things and do whatever it sees fit. If other programs are running then they'll have their time now, and if you've asked Windows to do anything it'll probably happen now.

    Lastly we have the termination code, as noted in the extract this is not necessarily required - the DirectX library will see that objects are destroyed safely - but you can never be sure from computer to computer, so it's best to do it for yourself. As with all other COM based interfaces destroy them in the reverse order from which they were created.


    [size="5"]Render()

    You should have noticed in the main loop code above that there was a call to the Render() function on every pass of the main loop. It's this code that actually presents the graphics on the screen - and processes anything relevant to how the graphics are displayed.

    For this sample this code isn't going to do anything greatly exciting - this is our first DirectX Graphics application after all. The next couple of articles will explain the more interesting parts...

    Public Sub Render()
    '//1. We need to clear the render device before we can draw anything
    ' This must always happen before you start rendering stuff...
    D3DDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET, &HCCCCFF, 1#, 0
    'the hexidecimal value in the middle is the same as when you're using
    ' colours in HTML - if you're familiar with that.

    '//2. Next we would render everything. This example doesn't do this,
    ' but if it did it'd look something like this:

    D3DDevice.BeginScene
    'All rendering calls go between these two lines
    D3DDevice.EndScene

    '//3. Update the frame to the screen...
    ' This is the same as the Primary.Flip method as used in DirectX 7
    ' These values below should work for almost all cases...
    D3DDevice.Present ByVal 0, ByVal 0, 0, ByVal 0
    End Sub
    Not too complicated, but trust me, it gets bigger and bigger as we start adding new stuff. The render procedure always follows the same pattern - Clear, Draw, Display on screen. The clear part removes whatever was left in the frame buffers, then we draw everything - all our triangles, models and whatever else we fancy. Finally we update the screen - when this call is finished whatever we just drew will appear on the monitor.

    As you learn more about DirectX Graphics, and as these articles go on this function will be adapted and altered quite dramatically - it can get very big and quite complicated. One thing to always bear in mind about this procedure (and any others that you put in the main loop) is that they have to be fine tuned and as smooth as possible - any untidy or slow code will have a massive impact on the overall speed of your application. If each call takes 6ms to process, but something you do makes it take 10ms instead your frame rate will drop from 167fps down to 100fps - it's still pretty fast, but assuming you have other things to do (AI, Audio, Physics and general gameplay) then this drop will be more significant.


    [size="5"]Drawing Something - The Theory

    Okay, so you've learnt how to initialize a Direct3D application in Visual Basic - wow. Hardly cutting edge visuals, and completely useless as well. So I'm pretty sure that you want to learn how to draw something.

    Drawing In Direct3D is extremely simple when you get your head around it, but it requires a fair amount of work and memory before you can get to this point. The first part is understanding what the different words mean - right now we'll stick to the simple definitions and elaborate on them as we get more advanced later on.

    1. Vertices (plural of vertex)
    A vertex can be thought of as a defining point - the corner of a triangle, square or other shape. Using vertices we can construct 2D and 3D shapes with various properties; a vertex will be described by a Visual Basic User Defined Type (we'll see these later) and are usually made up of a position, colour and texture coordinates.

    2. Polygon
    Polygons are what you'll have heard about by "normal" people most of the time - so and so 3D card pumps out 101 million polygons a second (or whatever). In fact, Direct3D renders all of it's primitives using triangles. But then again, a triangle is the simplest possible polygon. Lists of triangles are stored as arrays of the vertex type that you are using.

    3. Face
    A face is usually 3 vertices arranged in some sort of polygon, but it also has an orientation - you can tell which way it is facing. Direct3D interpolates vertex components across a face, in particular colour - as we'll be seeing later on. 3D models (imported from 3D modelling programs) tend to be made up of 100's or 1000's of faces.

    4. Textures
    A texture is applied to a triangle to make it look more real, Textures are just 2D bitmaps/pictures loaded from the hard drive into memory and then mapped to relevant polygons during rendering. We will cover these in more detail later on.

    5. Mesh
    A mesh is another word for a model - it is usually represented by one object in the program, and contains 100s or 1000s of vertices and faces, along with all relevant materials and textures. These will be covered later on.

    Now that you have those words floating around we can start doing some interesting things. Don't swear by the above definitions though - they are very lose and are only there to offer a basic introduction to the terms, more complex and meaningful definitions will be offered as and when they are necessary.

    The first thing I want to cover is the 3 types of vertex that you can use. While the structure of Direct3D allows for 100's of different combinations there are three types that will serve most situations - and all other situations will be adaptations or modification of these three.

    1. Untransformed and Unlit vertex
    These vertices are literally just points in 3D space with an orientation and a set of texture coordinates. Direct3D will do the lighting for you - you set up some lights and some vertices and it'll do the rest. These tend to be the most commonly used unless people opt for using lightmaps or pre-calculated lighting (as some commercial games do).

    2. Untransformed and Lit vertex
    These vertices are points in 3D space with texture coordinates like the first type, but these ones have a colour value. This allows us to make proper 3D geometry without having to worry about lighting. When we start doing 3D geometry these will be the first type to use - as they're the easiest.

    3. Transformed and Lit vertex
    These are vertices specified in two dimensions - screen coordinates. All Direct3D does is apply textures to them, clip them and draw them - you are expected to specify a colour value and a valid 2D position. Using these are the only way you will get Direct3D to do 2D graphics.

    Yet more things to remember... But before we go through a simple demonstration of how to use these vertices there is one more thing you need to know. Flexible Vertex Formats. As I mentioned earlier, Direct3D allows for 100's of possible vertex types - it is through this system of Flexible Vertex Formats (FVF) that we achieve this. A flexible vertex format description is a variable of type Long that is a combination of flags that Direct3D can use to work out what format the data you pass it is in. If you pass invalid, or incorrect for the data you use one of two things will happen: Nothing will be rendered, or something very strange will be rendered.

    The vertex formats for the 3 main types look like this:

    Const FVF_TLVERTEX = (D3DFVF_XYZRHW Or D3DFVF_TEX1 Or D3DFVF_DIFFUSE Or D3DFVF_SPECULAR)
    Const FVF_LVERTEX = (D3DFVF_XYZ Or D3DFVF_DIFFUSE Or D3DFVF_SPECULAR Or D3DFVF_TEX1)
    Const FVF_VERTEX = (D3DFVF_XYZ Or D3DFVF_NORMAL Or D3DFVF_TEX1)
    And their UDT structures will look like this:

    Private Type TLVERTEX
    X As Single
    Y As Single
    Z As Single
    rhw As Single
    color As Long
    Specular As Long
    tu As Single
    tv As Single
    End Type

    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

    Private Type VERTEX
    X As Single
    Y As Single
    Z As Single
    nx As Single
    ny As Single
    nz As Single
    tu As Single
    tv As Single
    End Type
    Right this second you don't really need to know all of these - I put them in mainly for reference purposes; those of you familiar with DirectX7 in Visual Basic, or DirectX in another language may well want to jump ahead slightly.

    In the next section we'll extend our sample program to render some basic 2D and 3D geometry in fullscreen mode. It may sound like a small task, but you've got the mountain to climb yet my friend.


    [size="5"]Drawing Something - The Practical Part

    As already mentioned we render all our geometry using triangles (I prefer to use the term triangle instead of polygon - but feel free to use whichever you prefer). These triangles will be made up of a set of vertices, and these vertices will have a specific set of properties depending on what they're for.

    It would therefore make sense that we had an array of one vertex type filled with data - which is exactly what we're going to do. As you can imagine this will get very long - one line of code per vertex, 3 per triangle - even a simple cube can take up a hundred or so lines of code... which is the reason why I'm going to keep this simple.

    Step 1: Setting things up.

    First we need to create an array of vertices:

    Dim TriVert(0 To 2) As TLVERTEX '//we require 3 vertices to make a triangle...
    Then we need to set a couple of parameters in the initialization procedure:

    D3DDevice.SetVertexShader FVF_TLVERTEX
    D3DDevice.SetRenderState D3DRS_LIGHTING, 0
    The first line tells the rendering device what type of vertex we're going to be using - pass the constant that we defined earlier here. The second parameter tells Direct3D that we don't want it to do the lighting - by default it will.

    Step 2: Making the triangle

    This is as simple as filling out the array with the required data, for clarity we'll stick it in a whole new procedure, which will be called at the end of the initialization process:

    Private Sub InitializeGeometry()
    TriVert(0) = CreateTLVertex(0, 0, 0, 1, &HFF0000, 0, 0, 0)
    TriVert(1) = CreateTLVertex(175, 0, 0, 1, &HFF00&, 0, 0, 0)
    TriVert(2) = CreateTLVertex(0, 175, 0, 1, &HFF&, 0, 0, 0)
    End Sub
    Three things to note here; firstly we have a new function here - CreateTLVertex(), this is a little helper function that I wrote to help in filling the structures with the relevant data, it looks like this:

    Private Function CreateTLVertex(X As Single, Y As Single, Z As Single, _
    rhw As Single, Color As Long, _
    Specular As Long, tu As Single, _
    tv As Single) As TLVERTEX
    CreateTLVertex.X = X
    CreateTLVertex.Y = Y
    CreateTLVertex.Z = Z
    CreateTLVertex.rhw = rhw
    CreateTLVertex.Color = Color
    CreateTLVertex.Specular = Specular
    CreateTLVertex.tu = tu
    CreateTLVertex.tv = tv
    End Function
    Secondly we have to specify the colour as a long, using the RGB( ) function wont work properly here - it is possible to reverse the values and use it - RGB(B, G, R), but if you can use hexidecimal it's the preferred method. Think of it as the same as an HTML colour code and you'll be fine.

    Thirdly, and more subtly is the order in which the vertices where created. This is actually extremely important - get this wrong and Direct3D will cull (remove) your triangles and not render them; It is often quite likely that this is the cause if you make a program and nothing is rendered (yet appears to be set up correctly). If we plot the triangle coordinates in order we will see the pattern - Clockwise:

    Image7.gif


    You can set which type of triangle Direct3D will cull (Clockwise, Counter-Clockwise or none), but by default it will cull counter-clockwise triangles, therefore rendering only those that are in a clockwise order. You may well think that it's easy just to stop it from removing any triangles - While that is true it's bad practise. If you can get into the habit of generating your vertices in the correct order from the beginning then so much the better; but it's still useful to know how you specify the culling modes:

    D3DDevice.SetRenderState D3DRS_CULLMODE, D3DCULL_NONE
    D3DDevice.SetRenderState D3DRS_CULLMODE, D3DCULL_CW
    D3DDevice.SetRenderState D3DRS_CULLMODE, D3DCULL_CCW
    Fairly simple really, whichever you specify the opposite will be rendered; for example, our triangles are clockwise ordered, therefore you should specify the above as being D3DCULL_CCW (but that's the default, so you don't need to).

    The last thing to note before we move on is to do with transformed and lit vertices. As I told you, transformed and lit vertices are 2D - X and Y, so why is there a Z coordinate? The Z-Coordinate should be on a 0.0 to 1.0 scale, and when a depth buffer is attached (more on that later) triangles will be drawn over each other based on this value, for example - a triangle with a Z of 0 will go over a triangle with a Z of 1. And a Triangle with 3 different Z values will go through any other triangles that it intersects.... If two triangles have the same Z value, whichever is rendered last will appear on top.

    Step 3: Rendering the triangle

    We'll now go back to our Render() procedure - as discussed earlier - and update it to render our new triangle. At the moment we aren't doing anything clever, no textures, no transformations - so everything can be done with one call.

    Later on we'll use a more optimised method of rendering, and some more clever tricks; but right now we'll stick to the basics:

    D3DDevice.BeginScene
    'All rendering calls go between these two lines
    D3DDevice.DrawPrimitiveUP D3DPT_TRIANGLELIST, 1, TriVert(0), Len(TriVert(0))

    D3DDevice.EndScene
    Simple as that really. We use the function "DrawPrimativeUP" to render a custom type of vertex straight from memory - it is of type TRIANGLELIST (more in a second), is made up of one triangle and uses the specified array with the specified size. In more detail:

    The first parameter is the primitive type, While Direct3D does all solid rendering as triangles, but we can also render points and lines. The full range of options are listed here:

    D3DPT_POINTLIST: Direct3D draws each vertex as an unconnected point in 3D space, just a single pixel dot on the screen. Useful for particle effects (but there are better ways).

    D3DPT_LINELIST: Every pair of entries specifies a pair of coordinates for Direct3D to draw a line between (0 & 1, 2 & 3, 4 & 5 etc...)

    D3DPT_LINESTRIP: Same as a line list, but the beginning of one line joins up with the end of the previous - creating a continuous line through all the specified points.

    D3DPT_TRIANGLELIST: Every triplet of vertices defines a new triangle - this will be filled in solid, with colour components blended across it's surface; this is the method we currently use. (0-1-2 & 3-4-5 & 6-7-8-9 etc...).

    D3DPT_TRIANGLESTRIP: Same as a triangle list, but it creates a series of triangles all joining up; from the second triangle onwards each triangle uses the last vertex of the previous triangle as it's first vertex (0-1-2 & 2-3-4 & 4-5-6 etc...). Colours are blended across the surface of these in the same way as a triangle list.

    D3DPT_TRIANGLEFAN: draws a series of triangles all connected to the first vertex - perfect for octagonal, hexagonal or circular type shapes. 0-1-2 & 0-3-4 & 0-5-6 etc... These triangles are drawn solid, and colour is blended across them.

    There are numerous advantages and disadvantages to all of these methods; some of more obvious than others. As a start you should already have guessed that the less triangles you use the faster it goes - so try to keep them to a low number (without looking nasty); more subtly, you should also keep the vertex count as low as possible when creating geometry. This is on the basis that Direct3D will send all of the data through the various cables, pipes and chips to the 3D card - and the more data you have to send the longer it takes; so the less vertices you use the faster the data can be transmitted. When drawing a continuous line it would make perfect sense to use a line strip. When dealing with triangles decide on what you need to do - often it is faster and simpler to render something using a triangle strip, other times it makes it more complicated and you should use a triangle list; then there is the option of using a triangle fan for circular type objects. Experiment and see...

    The second parameter is the primitive count, think of it as the number of triangles, or number of points (depending on the primitive type). In this example we only created one triangle, so have specified the fact that there is only 1 triangle.

    The third parameter specifies where Direct3D should look for the vertex data, this must be an entry in the array; usually the first entry - but it doesn't have to be; just remember that there needs to be the correct number of vertices left for the specified primitive count.

    The last parameter specifies how big (in memory bytes) our vertex structure is - this is for Direct3D's internal usage. You don't really need to understand how it works, but basically the third parameter points to the beginning of the memory to look for, and Direct3D knows how many entries there will be (primitive count value), using this parameter it can get the size of the structure, and therefore work out where all the individual bits of data are, and how much memory the whole lot should take up. Use Len( ) on the first element in the array for this.

    Now that you can render simple 2D geometry you should practise it - try making a square using the various methods, draw a circle-like object with a triangle fan; and try making an arch or rounded rectangle using a triangle strip. If you look at almost any basic geometric shape it will always break down into a series of triangles (sometimes not very nicely), but with some basic maths skills it is easy to write an algorithm that generates an arch (or whatever) from a series of triangles...


    [size="5"]Overview

    You may well think that very little has been done in this article - you would be very wrong thinking this. In the next articles we will cover all of the major aspects of Direct3D programming - if there was anything that you didn't follow in this article (code wise) then it's going to catch you out later. Trust me. I have written enough DirectX applications that I can write a complete DirectX 7 and DirectX 8 program similar to this off the top of my head in very little time.

    The next article will advance our knowledge of geometry into the 3[sup]rd[/sup] dimension; along the way we'll learn about vertex buffers, index buffers, fullscreen mode, depth buffers, normals and some basic lighting - sound like fun? You bet J

    Any feedback on this article is much appreciated, or if you have any questions I'd be happy to try and help you out (but remember, I'm good, but I don't know everything [yet]). Drop me a message: [email="Jollyjeffers@Greenonions.netscapeonline.co.uk"]Jollyjeffers@Greenonions.netscapeonline.co.uk[/email] or visit my site at www.vbexplorer.com/directx4vb/ for a massive collection of 55 DirectX 7 tutorials, and ongoing DirectX 8 articles (11 lessons at time of writing) and some general game programming articles for good measure!

    See you next time :)

    This article was written by Jack Hoxley, January 2001. This article is copyright (C) Jack Hoxley - all rights reserved. You may not reuse this material without prior permission from myself - please email me if you wish to discuss this.
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

The series begins with an introduction to using DirectX 8 in Visual Basic and created a simple Direct3D application.

Advertisement
Advertisement
Advertisement