Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 49 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
49
Dung lượng
177,12 KB
Nội dung
Using a Mesh Example Now that you’ve seen how a mesh is constructed and how to test the content of an X file, it’s time to see what you’ve learned in action. The example in this section performs the simple task of loading an X file and rendering it. The example code will demonstrate that some of the DirectX code is the same as you’ve used in the past but other code is different because of the managed environment. In fact, this example contains a few surprises that you might not have expected. Note A lot of developers were surprised to hear that there are some problems getting DirectX to run on some machines that have AMD processors installed. It turns out that the DrawIndexedPrimitive() function can cause DirectX to stop responding on these machines. This problem was first reported in The Inquirer (http://www.theinquirer.net/?article=4474). Later, Microsoft uploaded a Knowledge Base article to explain the problem (http://support.microsoft.com/search/preview.aspx?scid=kb;en−us;Q321178). The Knowledge Base article also includes a link where you can download a patch to fix this problem. Initializing the Application Let’s begin with application initialization. Listing 16.1 contains the constructor for the form. You’ll find this code in the \Chapter 16\C#\MeshView and \Chapter 16\VB\MeshView folders of the CD. Listing 16.1: Mesh Example Initialization // DirectX objects. private DirectX8 DX8; // DirectX 8 object. private Direct3D8 D3D8; // Direct 3D object. private Direct3DDevice8 Device; // Display device. public frmMain() { D3DDISPLAYMODE Mode; // Display mode. D3DPRESENT_PARAMETERS Params; // Display parameters. // Required for Windows Form Designer support InitializeComponent(); // Initialize the DirectX objects. DX8 = new DirectX8Class(); D3D8 = DX8.Direct3DCreate(); // Create a device to use for drawing. Begin by obtaining the // current display mode. Set the display parameters. Finally, // create the device. Mode = new D3DDISPLAYMODE(); D3D8.GetAdapterDisplayMode((int)CONST_D3DCONST.D3DADAPTER_DEFAULT, ref Mode); Params = new D3DPRESENT_PARAMETERS(); Params.Windowed = 1; Params.SwapEffect = CONST_D3DSWAPEFFECT.D3DSWAPEFFECT_COPY_VSYNC; Params.BackBufferFormat = Mode.Format; Params.BackBufferCount = 1; Params.EnableAutoDepthStencil = 1; Params.AutoDepthStencilFormat = CONST_D3DFORMAT.D3DFMT_D16; Device = D3D8.CreateDevice( (int)CONST_D3DCONST.D3DADAPTER_DEFAULT, CONST_D3DDEVTYPE.D3DDEVTYPE_HAL, pnlDisplay.Handle.ToInt32(), Using a Mesh Example 376 CONST_D3DCREATEFLAGS.D3DCREATE_SOFTWARE_VERTEXPROCESSING, ref Params); // Set the device state. // Turn off culling. Device.SetRenderState(CONST_D3DRENDERSTATETYPE.D3DRS_CULLMODE, (int)CONST_D3DCULL.D3DCULL_NONE); // Turn on the Z buffer. Device.SetRenderState(CONST_D3DRENDERSTATETYPE.D3DRS_ZENABLE, 1); // Turn on lighting. //Device.SetRenderState(CONST_D3DRENDERSTATETYPE.D3DRS_LIGHTING, 0); // Set the ambient lighting color. Device.SetRenderState(CONST_D3DRENDERSTATETYPE.D3DRS_AMBIENT, −1); } The example begins like many of the DirectX examples so far: by creating the DirectX and Direct3D objects. Working with a mesh also means creating a device—essentially a virtual display adapter and monitor combination on which you display the data. To create a device, the code needs to obtain the real display information using the GetAdapterDisplayMode() method. Once the code has this information, it can fill out a D3DPRESENT_PARAMETERS data structure (Params) that contains the information used to create the device. The Direct3D parameters control the operations the device can perform. The code uses the D3D8.CreateDevice() method to create the device. This method accepts the parameters, a device type, the handle to the current window, and some creation flags as input. We’ll use a default adapter type for the example. In addition, the example relies on hardware support. If your system only provides support for software emulation, you’ll need to change the CONST_D3DDEVTYPE.D3DDEVTYPE_HAL enumeration value to CONST_D3DDEVTYPE.D3DDEVTYPE_SW. If the code attempts to create a device for a system that doesn’t support the request device type, the call will fail, so it’s important to check for a null return value in your production code and make an alternative device creation call (or exit from the application). When the D3D8.CreateDevice() method returns, Device is in a default state that doesn’t work for anything. Microsoft is assuming that you’ll configure the device for your needs. The final four lines of code configure Device for the needs of this application by setting the render state using the Device.SetRenderState() method. The code controls the device setting using the CONST_D3DRENDERSTATETYPE enumeration members. In this case, the code sets the culling state, Z buffer, lighting, and ambient lighting color. Notice that I’ve actually commented the ambient color setting out. We’ll see later in this example why this setting can cause problems or fix them, depending on the situation. Loading a Mesh File At this point, the main DirectX objects are set up. However, we can’t do any more until the user opens the application. Listing 16.2 shows the code to open an X file and process it for use. This code demonstrates the prerequisites for preparing to render (display) the file, but we won’t display it yet. Listing 16.2: Loading the X File // These are the global file−related objects. private string File2Open; // The file we want to render. private D3DXMesh Mesh; // The Mesh Object. private D3DMATERIAL8[] Materials; // Array of materials. Loading a Mesh File 377 private Direct3DTexture8[] Textures; // Array of textures. private void mnuFileOpen_Click(object sender, System.EventArgs e) { OpenFileDialog Dlg; // File Open Dialog D3DX8 Worker; // A worker object. D3DXBuffer Adjacency; // Adjacency data buffer. D3DXBuffer MatBuffer; // Materials buffer. Int32 MatCount; // Number of materials. String TextName; // Name of the texture file. String FilePath; // Path to the texture file. // Set up the File Open Dialog Dlg = new OpenFileDialog(); Dlg.Filter = "X Format File (*.x)|*.x"; Dlg.DefaultExt = ".x"; Dlg.Title = "Open X File Dialog"; // Display the File Open Dialog and obtain the name of a file and // the file information. if (Dlg.ShowDialog() == DialogResult.OK) { // Set the name of the file. File2Open = Dlg.FileName; // Load the X file. Worker = new D3DX8(); Adjacency = Worker.CreateBuffer(0); MatBuffer = Worker.CreateBuffer(0); MatCount = 0; Mesh = Worker.LoadMeshFromX(File2Open, (int)CONST_D3DXMESH.D3DXMESH_MANAGED, Device, ref Adjacency, ref MatBuffer, ref MatCount); // Obtain a list of materials and textures. Materials = new D3DMATERIAL8[MatCount]; Textures = new Direct3DTexture8[MatCount]; FilePath = File2Open.Substring(0, File2Open.LastIndexOf("\\") + 1); for (int Counter = 0; Counter < MatCount; Counter++) { // Get a material from the buffer. Worker.BufferGetMaterial(MatBuffer, Counter, out Materials[Counter]); // Set the material ambient color. Materials[Counter].ambient = Materials[Counter].diffuse; // Determine if there is a texture to process. TextName = Worker.BufferGetTextureName(MatBuffer, Counter); if (TextName != null) { // Obtain the texture. Textures[Counter] = Worker.CreateTextureFromFile(Device, FilePath + TextName); Loading a Mesh File 378 } } // Tell DirectX that we’re done with the materials buffer. MatBuffer = null; // Render the object. Render(); // Start the timer. RenderTimer.Start(); // Enable the timer setting menu. mnuTimerSet.Enabled = true; mnuTimerSet.Text = "Stop"; } } The first task the code performs is to create more fields that the entire class can use. Unlike the DirectX objects the code created earlier, these objects are file specific and enable other parts of the application to interact with the file. The File2Open variable contains the path and name of the file. You should always create such a variable because your code is likely to need it. The Mesh variable contains the mesh object, which includes the display data. The Materials and Textures arrays contain the colors and textures used to render the image on screen. The last three variables are all that application requires in the form of file objects to render the image on screen. The mnuFileOpen_Click() method begins as any file opening code would: by creating the dialog box and waiting for user input. If Dlg returns DialogResult.OK, the application begins processing the resulting filename. Otherwise, the application exits to the main form and waits for the user to stop clicking Cancel (or exit the application). The code begins by creating an instance of the D3DX8 worker class (Worker). The Worker object provides access to a wealth of methods that you can use to manipulate graphics files of all types, including mesh files. The code then creates two temporary buffers, Adjacency and MatBuffer, that the application will use to hold data from the X file. The LoadMeshFromX() method loads data from the X file that the user selected into the two buffers and the device. The MatCount variable holds the number of materials retrieved from the X file and stored in MatBuffer. Device now contains the data required to render the image, but we still need to perform other tasks on the buffers. Tip Listing 16.2 contains the Adjacency buffer for the LoadMeshFromX() method even though the application will never use the data contained within the buffer. C# doesn’t provide a method to leave out optional parameters, so we’re forced to use this technique to keep the compiler happy even when the application doesn’t need the data. You can get around the optional parameter problem, in some cases, by using either the Type.Missing or Missing.Value value. Of course, other options include using null or IntPtr.Zero as needed. The next step in the process is to create Materials and Textures arrays. The code sizes these two arrays to hold the number of items specified by MatCount. Given the way that an X file stores the texture information, the Textures array will never include more items than the Materials array, so using this technique is safe. The code uses a for loop for processing. It begins by obtaining the current material from MatBuffer using the BufferGetMaterial() method. The code must call this method once for each material in the buffer. We’ve set the device to display the ambient color, so the next step is to transfer the diffuse color value to the ambient Loading a Mesh File 379 color value in the Materials array. Getting the texture comes next. The code checks for a texture attached to the current MatBuffer entry using the BufferGetTextureName() method. If TextName is null on return, then the material doesn’t include a texture. Otherwise, the code combines the FilePath contents with TextName to create a filename and path. It uses the CreateTextureFromFile() method to place the texture within the Textures array. The code has to perform some cleanup at this point. Because we don’t need MatBuffer anymore, the code sets this object to null. Remember that we’re still working with unmanaged code. You’ll create a memory leak if you don’t perform this step. The code calls Render() to render the image on screen, starts the timer used to update the image, and enables the Timer Ø Stop menu item. At this point, the first image is displayed on screen and the display will receive regular updates. Displaying the Image On Screen It’s finally time to discuss the rendering process. Listing 16.3 shows one way to render an image on screen. This is probably the least−code−intensive method for displaying a rotating image on screen. Once you get past these basics, the coding can become quite intense because you start working with the mathematics required to render images precisely. Listing 16.3: Rendering the Image // Define the D3DXMatrixRotationY() function. [DllImport("DX8VB.DLL", CharSet=CharSet.Auto, SetLastError=true, EntryPoint="VB_D3DXMatrixRotationY")] public static extern void D3DXMatrixRotationY(out D3DMATRIX MOut, float angle); // Define the D3DXMatrixLookAtLH() function. [DllImport("DX8VB.DLL", CharSet=CharSet.Auto, SetLastError=true, EntryPoint="VB_D3DXMatrixLookAtLH")] public static extern void D3DXMatrixLookAtLH(out D3DMATRIX MOut, ref D3DVECTOR VEye, ref D3DVECTOR VAt, ref D3DVECTOR VUp); // Define the D3DXMatrixPerspectiveFovLH() function. [DllImport("DX8VB.DLL", CharSet=CharSet.Auto, SetLastError=true, EntryPoint="VB_D3DXMatrixPerspectiveFovLH")] public static extern void D3DXMatrixPerspectiveFovLH( out D3DMATRIX MOut, float fovy, float aspect, float zn, float zf); // We need to use pi for some of the values. public const float pi = 3.1415F; private void Render() { D3DMATRIX WorldView; // Drawing matrices world view. D3DMATRIX Camera; // Drawing matrices camera view. D3DMATRIX Projection; // Drawing matrices projection. D3DVECTOR Vect1; // Matrix vectors. D3DVECTOR Vect2; // Matrix vectors. Displaying the Image On Screen 380 D3DVECTOR Vect3; // Matrix vectors. // Clear the display area. Device.Clear(0, IntPtr.Zero, CONST_D3DCLEARFLAGS.D3DCLEAR_TARGET | CONST_D3DCLEARFLAGS.D3DCLEAR_ZBUFFER, 0xFFFFFF, (float)1.0, 0); // Begin the scene. Device.BeginScene(); // Set up the world view. D3DXMatrixRotationY(out WorldView, DateTime.Now.Millisecond); Device.SetTransform(CONST_D3DTRANSFORMSTATETYPE.D3DTS_WORLD, ref WorldView); // Set up the camera view. Vect1 = new D3DVECTOR(); Vect2 = new D3DVECTOR(); Vect3 = new D3DVECTOR(); Vect1.x = 0; Vect1.y = 3; Vect1.z = −5; Vect2.x = 0; Vect2.y = 0; Vect2.z = 0; Vect3.x = 0; Vect3.y = 1; Vect3.z = 0; D3DXMatrixLookAtLH(out Camera, ref Vect1, ref Vect2, ref Vect3); Device.SetTransform(CONST_D3DTRANSFORMSTATETYPE.D3DTS_VIEW, ref Camera); // Set up the projection view. D3DXMatrixPerspectiveFovLH(out Projection, pi / 4, 1, 1, 1000); Device.SetTransform(CONST_D3DTRANSFORMSTATETYPE.D3DTS_PROJECTION, ref Projection); // Render the mesh. Set the materials and textures, and then draw // the mesh subset. for (int Counter = 0; Counter < Materials.Length; Counter ++) { Device.SetMaterial(ref Materials[Counter]); Device.SetTexture(0, (Direct3DBaseTexture8)Textures[Counter]); Mesh.DrawSubset(Counter); } // End the scene. Device.EndScene(); // Present the scene on screen. Device.Present(IntPtr.Zero, IntPtr.Zero, 0, IntPtr.Zero); } Notice that the code begins by using [DllImport] to access some functions in the DX8VB.DLL, which is surprising considering that this is the same DLL used for the COM library. It turns out that the .NET Displaying the Image On Screen 381 interoperability layer doesn’t always do a good job of importing everything you need into the IDE. DirectX provides an extensive math library (among other functions) that’s totally inaccessible because the math functions aren’t imported. You should see another surprise in the code at this point. This is the first time we’ve had to use the EntryPoint property. As shown in Figure 16.17, the function names listed in the DirectX help files don’t match the function names that actually appear in the DLL. Microsoft had to write a Visual Basic compatibility layer to make DirectX work properly with this language. Consequently, the developers at Microsoft had to give the Visual Basic specific functions special names, with the result shown in Figure 16.17. This use of alternative names is another problem to look out for when working with DLLs, especially those used to support COM. There’s one additional surprise in this DLL. Notice that the function declarations define MOut as an out value rather than a ref. In addition, the other arguments are defined as ref values. The reason that this is a surprising turn of events is that the Visual Basic would lead you to believe that you can pass these arguments by value. Even the Visual C++ documentation only passes the MOut argument as a pointer (by reference), so there’s apparently no reason to set the declarations up as we have. This is one of those odd declarations that you discover by trial and error rather than by observation. Try changing the function calls to the Visual Basic or Visual C++ documented format and you’ll find that they no longer work properly (or perhaps at all). Figure 16.17: Exercise care when importing COM libraries because you might leave the functions behind. It’s time to look at the Render() method. The code begins as you might expect. It clears the display. Make sure you clear both the target area and the Z buffer as shown in the code or you’ll have unpleasant results (mainly data corruption). The Clear() method also accepts a background color (white, in this case), the Z buffer depth (where 1.0 is farthest away and 0.0 is closest), and a stencil buffer setting. Drawing the image begins by a call to the BeginScene() method. The code then sets various matrices for display purposes. Notice that this is where we use the math functions declared earlier in Listing 16.3. This book isn’t about the math behind DirectX, so I’ll leave a complete discussion of the intricacies of matrix code to someone else. The important issue is that you need to set up a world view, camera view, and projection view in order to see the image. One item of note is that you need to provide some value in radians for the D3DXMatrixRotationY() function. The example uses the current time in milliseconds (DateTime.Now.Millisecond), which produces acceptable results if you synchronize this value with the timer interval. After the code sets up the various matrices, it begins to render the image. Direct3D requires that the code Displaying the Image On Screen 382 render the image on a material−by−material basis. Consequently, the code uses a for loop to process each material and texture in turn. The application relies on the SetMaterial() and SetTexture() methods to perform the processing. Once the code has applied the current color and texture, it uses the Mesh.DrawSubset() to display image components that use that material on screen. The rendering process ends with two steps. First, the code tells the device that the scene has ended using the EndScene() method. Second, the Direct3D device presents the rendered image to the real display using the Present() method. At this point, the user sees the image. The user experience with Direct3D probably won’t be the same without some form of animation. To add an animated effect to your display, you need a timer. The example application uses a simple timer to render the image each time the timer expires, which hopefully produces smooth animation. Here’s the code used to implement the timer: private void timer1_Tick(object sender, System.EventArgs e) { // Render the object. Render(); } As with every other DirectX example in the book, you need to perform some cleanup when you complete the application. Here’s the cleanup code for this example. Notice that we need to verify that the Mesh object exists. Otherwise, the application could exit with an error: private void frmMain_Closing(object sender, System.ComponentModel.CancelEventArgs e) { // Clean up the DirectX objects. if (Mesh != null) Mesh = null; Device = null; D3D8 = null; DX8 = null; } A Few Words about Output Let’s look at the output of the application as it exists now. Figure 16.18 shows the standard output with color. Notice that the colors interact with the texture just as they did in the MeshView Tool. A Few Words about Output 383 Figure 16.18: The standard output from the application allows color and texture to interact. I mentioned earlier in the section that you need to exercise care in setting up your device. In some cases, you might discover to your horror that the X file loads and displays but the image lacks color. This normally occurs because you’ve turned off lighting effects. Likewise, if you add lighting when all you need is a texture, the image might not appear as anticipated—it might have odd interactions in the texture. Let’s see an example of a device setup change. Remove the comments from this line of code in Listing 16.1: Device.SetRenderState(CONST_D3DRENDERSTATETYPE.D3DRS_LIGHTING, 0); Recompile the code and run it. Load the Box4.X file and your output should look similar to that shown in Figure 16.19. This is the texture−only version of that X file. If you load any of the other X files that we created with the MeshView Tool, you’ll notice that they all display without color (you can only see them if you change the background color of the application). A Few Words about Output 384 Figure 16.19: A texture−only form of the Box4.X file The moral behind this particular piece of coding is that you need to verify that your DirectX settings are correct before you blame the managed environment for some oddity in your code. The fact is that DirectX is complex even without the vagaries of managed code to worry about—using managed code only adds to the developer’s burden. Where Do You Go from Here? This chapter has shown you how to use three new DirectX tools. You’ve also seen a coding example that shows how to use the MeshView utility with your application. What you’ve received is a general overview of the extended capabilities of DirectX—we haven’t really explored the depths of this complex API. You should leave this chapter knowing that the tools do indeed work, that they reduce the effort required to code certain types of DirectX applications, and that everything works with the managed environment—at least partially. At this point, you know that DirectX does provide full functionality in the managed environment as long as you’re willing to work around some problem areas. From the examples, you should also know that DirectX doesn’t work nearly as fast in the managed environment as it does in a native executable. Finally, you should understand that the amount of work you need to perform to use DirectX will be reduced by the introduction of DirectX 9. However, at the time of this writing, DirectX 9 is still in beta and Microsoft hasn’t provided a release date for it. If you plan on working with DirectX in your applications, it’s time to explore further. This book hasn’t covered a lot of areas that you’ll need to learn before you can write professional−quality DirectX applications. For example, we haven’t discussed much of the math behind DirectX even though DirectX is a math−intensive development environment. We also haven’t discussed the art that goes into DirectX development—three chapters in a book aren’t enough to cover such broad issues. You need to learn more about the general use of DirectX. Congratulations, you’ve finished the chapters in the book. However, you’re not finished yet. There are still Where Do You Go from Here? 385 [...]... creating a StringBuilder object In addition, CLR has to perform less marshaling of the data value 42 Always use a StringBuilder object when the Win32 API call is expecting a text buffer into which it will write data A StringBuilder is more resource and processor intensive, but CLR can change the value of a StringBuilder, while the value in a String remains constant 43 At least a few of the Win32 API. .. function to obtain the graphical presentation for the object in question and then use the GetHdc() function to obtain a handle to the device context 46 One reason to use a Win32 API call instead of the built in NET Framework functionality is that the Win32 API provides extended functionality For example, the NET Framework emulates the functionality provided by the MessageBox() function Using the MessageBoxEx()... cleans the stack) and CDECL when the application runs on Windows CE (the caller cleans the stack) Even though the enumeration contains an option for the FastCall convention, this version of the NET Framework doesn’t support it In other words, you might be out of luck calling certain types of DLLs using the [DllImport] attribute for now, but Microsoft is planning on resolving this issue in the future In. .. learning about the System.Runtime.InteropServices namespace This namespace contains a wealth of classes and attributes you can use to make access to both the Win32 API and COM much easier 19 You can use the Win32 API FormatMessage() function to interpret the numeric output of the Marshal.GetHRForLastWin32Error() method, the Marshal.GetLastWin32Error() method, or the GetLastError() function 20 Use the. .. requires a portion of the entire enumeration The use of custom enumerations leads to confusion on the part of other developers and increases the work required to use the Win32 API, without much benefit to the originator 2 Verify the type of variable that a handle or other pointer requires In most cases, you’ll use an IntPtr to represent a handle or other pointer However, some pointers (especially handles)... classes that the Win32 API provides 40 Most developers consider the GetLastError() function unsafe for use in the managed environment because CLR could have made another Win32 API call in the interim Always use the Marshal.GetHRForLastWin32Error() method or the Marshal.GetLastWin32Error() method for maximum compatibility 41 Always use a String for one−way text data transfers Using a String is more efficient... build the bridge between the two technologies, there are going to be problems in translating calls and data from one technology to the other For this reason, there is some risk in using the interoperability techniques found in this book No matter how closely you follow the examples, there are going to be times where the Win32 API throws a monkey wrench in your plans and makes it difficult to obtain a... Works Improperly The [DllImport] Attribute Works Improperly Most of the Win32 API calls that you’ll make use standard calling conventions In many situations, you can use the [DllImport] attribute with just the name of the DLL you want to use the compiler will find the rest of the required information based on your function declaration However, as you’ve seen in many of the examples in the book, you sometimes... a lot in a short space, especially when working with complex ideas or concepts These tips are especially worthwhile because they often represent hours of work They will help you create a better environment in which to access the Win32 API Many of them are significant and show the hours of work required for discovery; others are subtle suggestions exposed in a moment of clear thinking A few of the tips... requires use of LocalFree(), then you’ll need to gain access to that function through the Win32 API because the NET Framework doesn’t provide support for it The same holds true for any resources that you allocate using a Win32 API call Make sure you use the proper Win32 API call to free the resource when the application is done using it Windows Doesn’t Appear to Handle Messages Correctly Above all, Windows . reason to use a Win32 API call instead of the built in .NET Framework functionality is that the Win32 API provides extended functionality. For example, the .NET Framework emulates the functionality. required .NET Framework function is arcane. For example, many developers will forget that they can access all of the features of other .NET languages using the Interaction class. Making Win32 API calls. portion of the entire enumeration. The use of custom enumerations leads to confusion on the part of other developers and increases the work required to use the Win32 API, without much benefit to the originator. 1.