Visual Basic Explorer
Visual Basic Explorer
 Navigation
 Home


 Coding
 Source Code

 FAQ Center

 VB Tips

 Downloads

 ToolBox

 Tutorials

 VB Games

 VB News

 VB Award

 VB Forums



 Affiliates
 Planet Source Code

 Rent a Coder

 DirectX4VB


 Misc
 Search

 Feedback

 Advertise

 About


Need to hire
a VB coder?

Please support our sponsor:

 Home 
 Site Map 
 Forums 
 News 
 Feedback 

Using Direct3D with VB

By Rod Stephens

Direct3D: A First Program

This article shows how to build a simple Direct3D application that displays a single triangle.

This program draws the single triangle shown in Figure 1. It's not much, but it shows how to prepare Direct3D for drawing. Once you know how to draw one triangle, you can draw lots of others. By drawing enough triangles, appropriately colored, shaded, and textured, you can draw any three-dimension scene.


Figure 1. A Direct3D Triangle.

Click here to download this program's Visual Basic source code. The following sections describe the program's routines.

Routine Purpose
Declarations Module-level variable declarations
Form_Load Calls other routines to get Direct3D ready
Form_Unload Stops the program's render loop so ends nicely
InitializeDirectDraw Initializes some DirectDraw variables
InitializeDirect3D Initializes Direct3D variables
InitializeScene Initialize the scene (camera position, lighting, etc.)
InitializeObjects Create the objects we will draw
RenderLoop Repeatedly draw the objects
RenderObjects Draw the objects
Summary Summary

The exact division of this work is a bit arbitrary. I try to put the DirectDraw and Direct3D initialization code in separate routines. InitializeScene and InitializeObjects together define the scene. Depending on the program, some code may slide from one routine to another. For example, if the camera position changes every time the program draws the scene, it must be defined in RenderLoop so there's no point defining a camera position in InitializeScene.

Don't worry if you don't catch every nuance. This is a lot of code to draw a crummy triangle, but most of the code in this example just gets Direct3D running. You can use similar initialization code in most other programs. If it seems a bit mysterious, that's okay for now. For now the most important routines are InitializeObjects which defines the triangle the program will draw and RenderLoop which draws it. If you want to experiment with the program, make changes in InitializeObjects. You can come back to study the other routines later.

Back to page index


Declarations

The program begins by declaring several variables and constants it will need later. The variables here begin with m_ to remind you that they are declared Private at the module level. That means they are available in all of the code in the form module but not outside the module.

    PI
    This value (3.14159365...) is used to create a projection matrix. I'll say more about that later. In future programs that rotate objects, this value plays a more important role.
    m_dx
    The DirectX7 class provides methods to create other DirectX objects including the main DirectDraw object we will use. It also provides utility functions to create transformation matrices (more on them later), and routines for creating colors in DirectDraw formats.
    m_dd
    The DirectDraw class provides methods that control the DirectDraw system, create drawing surfaces, set the display cooperative level, etc. It also gives access to the Direct3D object.
    m_ddPrimarySurface
    This surface represents what the user sees.
    m_ddRenderSurface
    This is the surface we draw on. We draw on this surface, and then copy the results to m_ddPrimarySurface.
    m_d3d
    This is the Direct3D object we use to create a device.
    m_d3dDevice
    The device represents the computer's graphics hardware and software. DirectDraw uses the characteristics of the device you create to determine how to draw things.
    m_ViewportRect(0)
    This entry describes the viewport area we will display. Even though we only need one entry, this is an array because m_d3dDevice.Clear takes an array as a parameter.
    m_Running
    This variable controls the rendering loop. The program continues drawing the triangle as long as m_Running is true.
    m_NumVertices
    The number of points we will use. This program has three points.
    m_Vertex
    Array of D3DVERTEX structures defining the points' coordinates.
    m_PictureRect
    Stores the drawing area dimensions.
    m_RenderRect
    Stores the render surface's dimensions.
Option Explicit

Private Const PI As Single = 3.14159265

Private m_dx As DirectX7
Private m_dd As DirectDraw7
Private m_ddPrimarySurface As DirectDrawSurface7
Private m_ddRenderSurface As DirectDrawSurface7
Private m_d3d As Direct3D7
Private m_d3dDevice As Direct3DDevice7

' This is an array because we need to pass an array to
' m_d3dDevice.Clear.
Private m_ViewportRect(0) As D3DRECT

' True while the program should redraw the triangle.
Private m_Running As Boolean

' The vertices we will draw.
Private m_NumVertices As Integer
Private m_Vertex() As D3DVERTEX

' Picture dimensions.
Private m_PictureRect As RECT

' Rendering surface dimensions.
Private m_RenderRect As RECT

Back to page index


Form_Load

When the program begins, the Form_Load event handler runs. It calls other routines to do all the interesting work. InitializeDirectDraw and InitializeDirect3D prepare the Direct3D system for use. InitializeScene initializes all the program-specific scene stuff except for the actual drawing coordinates. InitializeObjects creates the coordinate data describing the triangle.

From_Load then uses the Show method to make the form visible. It calls routine RenderLoop to display the triangle. That routine does not return until the program should exit. When it does return, Form_Load unloads the form so the program ends.
Private Sub Form_Load()
    ' Initialize DirectDraw.
    InitializeDirectDraw

    ' Initialize Direct3D.
    InitializeDirect3D

    ' Initialize the scene.
    InitializeScene

    ' Initialize the objects we will display.
    InitializeObjects

    Show

    ' Display the triangle.
    RenderLoop

    ' End.
    Unload Me
End Sub

Back to page index


Form_Unload

When the user clicks the form's close button (the little X in the upper right corner), the form unloads and triggers the Form_Unload event handler. This unloads the form's visible component, but the code is still running. If you do nothing else, subroutine RenderLoop continues running and the program doesn't end.

To prevent this, Form_Unload sets m_Running to False so RenderLoop exits and the program can end properly. Comment out this line and see what happens.

Private Sub Form_Unload(Cancel As Integer)
    m_Running = False
End Sub

Back to page index


InitializeDirectDraw

This routine initializes DirectDraw and assigns some variables. It first creates a DirectX7 object (m_dx) and a DirectDraw object (m_dd). The program uses these to access DirectX and DirectDraw features.

SetCooperativeLevel determines the program's top-level behavior. It can indicate things like exclusive of the entire screen or, in this case, normal windows behavior.

The routine then creates the primary surface that the user will see. The surf_desc structure defines the surface.

The DirectX object's GetWindowRect routines fills in a RECT structure with the Left, Right, Top, and Bottom coordinate values for the PictureBox that will contain the drawing. The program uses these coordinates later.

InitializeDirectDraw then creates a render surface. This is the surface the program will draw on. The program will then copy the results to the primary surface so the user can see them. Notice that the program uses the dimensions of the PictureBox here to tell DirectDraw how big to make the render surface.

The routine saves the dimensions of the render surface in a RECT structure. Unlike the PictureBox's coordinate values, the render surface's coordinates start with Left = 0 and Top = 0.

Finally, the routine saves a reference to the Direct3D object provided by DirectDraw.

' Initalize DirectDraw.
Private Sub InitializeDirectDraw()
Dim surf_desc As DDSURFACEDESC2

    ' Create the DirectDraw object and set cooperative level.
    Set m_dx = New DirectX7
    Set m_dd = m_dx.DirectDrawCreate("")
    m_dd.SetCooperativeLevel Picture1.hWnd, DDSCL_NORMAL

    ' Create the primary drawing surface.
    surf_desc.lFlags = DDSD_CAPS
    surf_desc.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE
    Set m_ddPrimarySurface = m_dd.CreateSurface(surf_desc)

    ' Save the picture's size for later use.
    m_dx.GetWindowRect Picture1.hWnd, m_PictureRect

    ' Create the render surface making it fit Picture1.
    ' Specify system memory because we may use the RGB rasterizer.
    surf_desc.lFlags = DDSD_HEIGHT Or DDSD_WIDTH Or DDSD_CAPS
    surf_desc.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN Or DDSCAPS_3DDEVICE Or _
        DDSCAPS_SYSTEMMEMORY
    surf_desc.lWidth = m_PictureRect.Right - m_PictureRect.Left
    surf_desc.lHeight = m_PictureRect.Bottom - m_PictureRect.Top
    Set m_ddRenderSurface = m_dd.CreateSurface(surf_desc)

    ' Save the size of the render surface for later use.
    With m_RenderRect
        .Left = 0
        .Top = 0
        .Bottom = surf_desc.lHeight
        .Right = surf_desc.lWidth
    End With

    ' Save a reference to the Direct3D object.
    Set m_d3d = m_dd.GetDirect3D
End Sub

Back to page index


InitializeDirect3D

This routine begins by verifying that the system is using more than 8-bit color (256 colors). Color modes with 256 colors or fewer use color palettes to define their colors. Color palettes add another level of complexity to problem and are generally a hassle. These days most computers have enough graphic memory that they can easily use the higher color modes like 24- and 32-bit color. 24-bit color provides photo-realistic images with more colors than most people can distinguish so that's the mode I usually use.

InitializeDirect3D then tries to create a Direct3D device with the name IID_IDirect3DHALDevice. If it fails, it tries to create an IID_IDirect3DRGBDevice device. If neither of these work on your system, consult the CreateDevice function's help to see what other values you can try.

Next the routine users SetViewport to tell the device on which part of the render surface to draw. This area is called the viewport. Usually you will set the lHeight and lWidth parameters to give the entire size of the surface, and you leave lX and lY equal to zero so the image begins in the upper left corner.

The routine finishes by saving the viewport's position in the first entry in the m_ViewportRect array. Subroutine DrawObjects uses this entry to clear the viewport before drawing on it. Although this program only has one viewport, this information is stored in an array because the Direct3D device's Clear method takes an array as a parameter.

' Initalize Direct3D.
Private Sub InitializeDirect3D()
Dim surf_desc As DDSURFACEDESC2
Dim viewport_desc As D3DVIEWPORT7

    ' Ensure that the display mode uses greater than 8-bit color.
    m_dd.GetDisplayMode surf_desc

    If surf_desc.ddpfPixelFormat.lRGBBitCount <= 8 Then
        MsgBox "This program requires a color mode higher than 8-bit."
        End
    End If

    ' Create the Direct3D device. Try for IID_IDirect3DHALDevice
    ' first and IID_IDirect3DRGBDevice if it isn't available.
    On Error Resume Next
    Set m_d3dDevice = m_d3d.CreateDevice("IID_IDirect3DHALDevice", m_ddRenderSurface)
    If m_d3dDevice Is Nothing Then
        Set m_d3dDevice = m_d3d.CreateDevice("IID_IDirect3DRGBDevice", m_ddRenderSurface)
    End If
    If m_d3dDevice Is Nothing Then
        ' We failed to create a device.
        MsgBox "Could not create a Direct3D device."
        End
    End If

    ' Define the viewport rectangle.
    With viewport_desc
        .lWidth = m_PictureRect.Right - m_PictureRect.Left
        .lHeight = m_PictureRect.Bottom - m_PictureRect.Top
        .minz = 0#
        .maxz = 1#
    End With
    m_d3dDevice.SetViewport viewport_desc

    ' Save the viewport rectangle for later use.
    With m_ViewportRect(0)
        .X1 = 0
        .Y1 = 0
        .X2 = viewport_desc.lWidth
        .Y2 = viewport_desc.lHeight
    End With
End Sub

Back to page index


InitializeScene

InitializeScene defines scene-related values that do not change as the program runs. This program doesn't change much each time it redraws its triangle so almost all of the scene definition can happen here.

The routine starts by creating a material. The R, G, and B components in the material definition set the amounts of red, green, and blue light the material reflects. In this example, the material reflects all light. A red material, for example, would reflect most of the red light and little of the green and blue light. This material might set R = 1.0, G = 0.0, and B = 0.0.

The call to SetMaterial makes Direct3D use that material for drawing all objects until further notice. Because this example draws only one triangle, it does not need to ever change the material.

The routine then creates a projection matrix. This determines how the three-dimensional image is projected from three dimensions onto the viewport. First the routine uses the DirectX object's ProjectionMatrix method to create a projection matrix. It uses the device's SetTransform method to make the device use the projection matrix.

InitializeScene uses the ViewMatrix to initialize a matrix to represent a viewing position. Its three parameters are a "from" position, a "to" position, the world's "up" direction, and a roll value measuring clockwise rotation around the viewing direction. You can think of "from" as the position where you are standing and "to" as the object you are looking at as shown in Figure 2.


Figure 2. Specifying a View Matrix.

The routine uses the SetTransform method again, this time to specify the viewing position.

Finally InitializeScene uses the SetRenderState method to create an ambient light. Ambient light is light without any source. It shines on all surfaces from all directions. Calling SetRenderState makes Direct3D use this ambient light until it is changed later. This program never changes the ambient light so it is always yellow.

' Initalize the scene (lighting, material, etc).
Private Sub InitializeScene()
Dim matrix_projection As D3DMATRIX
Dim matrix_camera As D3DMATRIX
Dim material As D3DMATERIAL7

    ' Set the device's material so it reflects all light.
    With material
        .Ambient.r = 1#
        .Ambient.g = 1#
        .Ambient.B = 1#
    End With
    m_d3dDevice.SetMaterial material

    ' Define the projection's clipping planes.
    m_dx.ProjectionMatrix matrix_projection, 1, 10000, PI / 2
    m_d3dDevice.SetTransform D3DTRANSFORMSTATE_PROJECTION, matrix_projection

    ' Set the viewing position to (0, 0, -20).
    m_dx.ViewMatrix matrix_camera, MakeVector(0, 0, -20), _
        MakeVector(0, 0, 0), MakeVector(0, 1, 0), 0
    m_d3dDevice.SetTransform D3DTRANSFORMSTATE_VIEW, matrix_camera

    ' Set the ambient color to yellow.
    m_d3dDevice.SetRenderState D3DRENDERSTATE_AMBIENT, _
        m_dx.CreateColorRGB(1#, 1#, 0#)
End Sub

Back to page index


InitializeObjects

Subroutine InitializeObjects creates the data that describes the objects that will be drawn. In this program, that is a single triangle.

The routine stored the vertices of the triangle in the m_Vertex array of the D3DVERTEX type. This data structure's x, y, and z members give the vertex's coordinates in three-dimensional space. In this example, the triangle connects the points (-10, -10, 0), (0, 10, 0), and (10, -10, 0).

The ordering of the three points in each triangle is very important. Direct3D uses the ordering to help determine whether it needs to draw a triangle.

Position the wrist of your left hand over the triangle's first point and point your fingers toward the second point. Now curl your fingers toward the third point. Your thumb gives the direction of a vector perpendicular to the triangle. This vector is called the triangle's normal or surface normal. (Technically the normal should be normalized meaning it should be stretched or shrunk to have length 1.0, but any perpendicular vector works for many Direct3D operations.)

When Direct3D must draw a triangle, it examines the normal. If the normal points generally toward the viewing position, Direct3D draws the triangle. If the normal points generally away from the viewing position, Direct3D does not draw the triangle.

This all makes good sense if you consider the case of a closed solid. Suppose you are drawing a tetrahedron or other solid made up of triangular faces. Also suppose the points that make up each triangle are ordered so the triangle's normal given by the left-hand rule points out of the solid. This is called the outward orientation of the triangles.

Now when Direct3D starts to draw a triangle, it determines whether the normal points toward or away from the viewing position. If the normal points toward the viewing position, the face is on the near side of the solid so Direct3D draws it. If the normal points away from the viewing position, the face is on the far side of the solid so its view is blocked by the faces on the near side. In that case, Direct3D does not need to draw the triangle.

Now you can probably see why the order of the points is important. If you get it backwards, the triangle will not be drawn when it should be. For a solid, the triangle will be invisible when it is in front and it will be drawn when it should be hidden.

If you want a triangle to be visible from both sides, you should draw it twice: once with each orientation.

In this example, the viewing position and the triangle never move so the triangle is either always drawn or always omitted. Try modifying InitializeObjects so the triangle's points are listed in reverse order and see what happens.

' Initalize the objects we will display.
Private Sub InitializeObjects()
    ' Make three vertices for a triangle.
    m_NumVertices = 3
    ReDim m_Vertex(1 To m_NumVertices)
    
    With m_Vertex(1)
        .x = -10    ' (-10, -10, 0)
        .y = -10
        .z = 0
        .nx = 0     ' Normal points towards the camera.
        .ny = 0
        .nz = -1
    End With
    With m_Vertex(2)
        .x = 0      ' (0, 10, 0)
        .y = 10
        .z = 0
        .nx = 0     ' Normal points towards the camera.
        .ny = 0
        .nz = -1
    End With
    With m_Vertex(3)
        .x = 10     ' (10, -10, 0)
        .y = -10
        .z = 0
        .nx = 0     ' Normal points towards the camera.
        .ny = 0
        .nz = -1
    End With
End Sub

Back to page index


RenderLoop

RenderLoop enters a loop. As long as the variable m_Running is True, the routine calls RenderObjects to draw the objects on the render surface. It then use the primary surface's Blt method to copy the results from the render surface to the primary surface that the user sees.

When the user closes the form by clicking the X in the upper right corner, the Form_Unload event handler sets m_Running to False so this loop ends.

' Display the scene.
Private Sub RenderLoop()
Dim status As Long

    m_Running = True
    Do While m_Running
        ' Draw the objects.
        RenderObjects

        ' Display the results.
        status = m_ddPrimarySurface.Blt(m_PictureRect, m_ddRenderSurface, _
            m_RenderRect, DDBLT_WAIT)
        If status <> DD_OK Then
            MsgBox "Error " & Format$(status) & " displaying the scene."
            m_Running = False
        End If
        DoEvents
    Loop
End Sub

Back to page index


RenderObjects

RenderObjects is where the program actually draws. It begins by clearing the viewport to erase the previous scene. Parameters tell the Clear method which viewports to clear, where the viewports are located, and the color to use.

The routine then calls the device's BeginScene method, draws the triangle, and calls the device's EndScene method. That's all there is to it.

' Draw the objects.
Private Sub RenderObjects()
    ' Clear the viewport.
    m_d3dDevice.Clear 1, m_ViewportRect(), D3DCLEAR_TARGET, _
        m_dx.CreateColorRGB(0#, 0#, 0.5), 1, 0

    ' Begin the scene.
    m_d3dDevice.BeginScene

    ' Draw the triangle.
    m_d3dDevice.DrawPrimitive D3DPT_TRIANGLELIST, _
        D3DFVF_VERTEX, m_Vertex(1), 3, D3DDP_DEFAULT

    ' End the scene.
    On Error Resume Next
    m_d3dDevice.EndScene
End Sub

Back to page index


Summary

This is an awful lot of work to draw a single triangle. It hardly seems worth it. Fortunately most of this code can stay the same when you draw more complicated scenes. Most of the changes come in the InitializeObjects and RenderObjects routines. That's where they should be since those routines determine what is drawn and how. For the most part, you can ignore the other routines and concentrate on building the scene you want to display.

Back to page index

Back to Direct3D Introduction





Home | About | What's New | Source Code | FAQ | Tips & Tricks | Downloads | ToolBox | Tutorials | Game Programming | VB Award | Search | VB Forums | Feedback | VBNews | Copyright & Disclaimer | Advertise | Privacy Policy |

Quick searches: Site Search | Advanced Site Search 

Copyright 2002 by Exhedra Solutions, Inc.
By using this site you agree to its terms and conditions
VB Explorer and VBExplorer.com are trademarks of Exhedra Solutions, Inc.