DirectX Tutorial For C
DirectX Tutorial For C
This part of the site focuses on programming DirectX using C#. This tutorial is aimed at people who haven't done any 3D programming so far and would like to see some results in the shortest possible time. To this end, C# is an ideal programming language. C# looks very much like Java, so anyone having some notions of Java should be able to start right away. Even more, this tutorial is written in such a way that anyone who has any programming experience should be able to understand and complete it. The C# tutorial gives you a general introduction to DirectX. It is divided in several chapters, which you can find listed below. In every chapter youll find a basic DirectX feature: Opening a window: setting up and using the Development Environment Linking to the device: Creating the most basic DirectX element, the device Drawing a triangle: defining points, displaying them using DirectX Camera: defining points in 3D space, defining camera position Rotation & translation: rotating and translating the scene Indices: removing redundant vertex information to decrease AGP/PCIX bandwidth Terrain/Landscape: using indices to display data read from a file Keyboard: read user input on the keyboard through DirectInput Importing bmp files: change your terrain from within Paint! Colored vertices: add simple color to you terrain DirectX light basics: lighting can be complex to fully understand it, a whole chapter Mesh creation: putting your buffers together into a powerful new format Mesh lighting: using the Mesh format to compute complex data needed for lighting
(= this.Text) to "DirectX Tutorial" or whatever you like. Next we are going to change the Main() method a bit: static void Main() { using (WinForm our_directx_form = new WinForm()) { Application.Run(our_directx_form); } } You are now ready to start programming with DirectX! Compiling this code should give you a small form.
After each chapter I will list the whole code so far, so this is what you should have by now : using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace DirectX_Tutorial { public class WinForm : System.Windows.Forms.Form
{ private Device device; private System.ComponentModel.Container components = null; public WinForm() { InitializeComponent(); } protected override void Dispose (bool disposing) { if (disposing) { if (components != null) { components.Dispose(); } } base.Dispose(disposing); } private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.Size = new System.Drawing.Size(500,500); this.Text = "DirectX Tutorial"; } static void Main() { using (WinForm our_dx_form = new WinForm()) { Application.Run(our_dx_form); } } } }
The first line creates the Presentation Parameters, which we will need to tell the device how to behave. We will tell the device that we don't want a fullscreen application, and to discard the Swapeffect, so you write to the device immediately, and not to an extra back buffer that will be presented (= swapped) at runtime. Then, the device is created. The 0 selects the first graphical adapter in your PC. We want to render the graphics using the hardware. If you don't have a hardware card, you can use DeviceType.Reference, which supports all possible features, but is a lot slower. Next we bind 'this' window to the device and we for now we want all 'vertex processing' to happen on the CPU. More on vertices in the next chapter. Finally we pass our presentParams, and our device has been created! Of course, we have to call this method, so add this line to your Main method : our_dx_form.InitializeDevice(); When you try to compile your program now, youll probably get some errors like The type or namespace name 'DirectX' does not exist in the namespace. Make sure you add references to Microsoft.DirectX and Microsoft.Direct3D by clicking Project -> Add References to correct these errors. If you have multiple versions installed of these references, make sure you select references of the same version. If these 2 references arent included in your list, check if you have installed the full (free) Microsoft DirectX SDK. If theyre still missing, check out the first paragraphs of Linking to the Device in the C++ part of this tutorial. Running this code will still give you an empty form, but in the background the device has been initialized! Next we are going to overwrite the OnPaint method, so we can control what to draw on the screen. Add the following code below the InitializeDevice method: protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Clear(ClearFlags.Target, Color.DarkSlateBlue , 1.0f, 0); device.Present(); } This method will be called every time something is drawn to the screen. The Clear method will fill the window with a solid color, darkslateblue in our case. The ClearFlags indicate what we actually want to clear, in our case the target window. To actually update our display, we have to Present the updates to the device. Running this code will give you a blueish window. For even more stunning results, read on!
device.Present(); } This method will be called every time something is drawn to the screen. The Clear method will fill the window with a solid color, darkslateblue in our case. The ClearFlags indicate what we actually want to clear, in our case the target window. To actually update our display, we have to Present the updates to the device. Running this code will give you a blueish window. For even more stunning results, read on!
Here is the complete code: using using using using using using using using System; System.Drawing; System.Collections; System.ComponentModel; System.Windows.Forms; System.Data; Microsoft.DirectX; Microsoft.DirectX.Direct3D;
namespace DirectX_Tutorial { public class WinForm : System.Windows.Forms.Form { private Device device; private System.ComponentModel.Container components = null;
public WinForm() { InitializeComponent(); } public void InitializeDevice() { PresentParameters presentParams = new PresentParameters(); presentParams.Windowed = true; presentParams.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams); } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Clear(ClearFlags.Target, Color.DarkSlateBlue , 1.0f, 0); device.Present(); } protected override void Dispose (bool disposing) { if (disposing) { if (components != null) { components.Dispose(); } } base.Dispose(disposing); } private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.Size = new System.Drawing.Size(500,500); this.Text = "DirectX Tutotial"; } static void Main() { using (WinForm our_dx_form = new WinForm()) { our_dx_form.InitializeDevice(); Application.Run(our_dx_form); } } } }
whole window. To solve the problem, simply add the following line to the bottom of your OnPaint method: this.Invalidate(); You should also add the following line to the constructor of your form: this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true);
Much better. I've listed the total code below : using using using using using using using using System; System.Drawing; System.Collections; System.ComponentModel; System.Windows.Forms; System.Data; Microsoft.DirectX; Microsoft.DirectX.Direct3D;
namespace DirectX_Tutorial { public class WinForm : System.Windows.Forms.Form { private Device device; private System.ComponentModel.Container components = null; public WinForm() { InitializeComponent(); this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true); } public void InitializeDevice() { PresentParameters presentParams = new PresentParameters(); presentParams.Windowed = true; presentParams.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams); } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { CustomVertex.TransformedColored[] vertices = new CustomVertex.TransformedColored[3]; vertices[0].Position = new Vector4(150f, 100f, 0f, 1f); vertices[0].Color = Color.Red.ToArgb(); vertices[1].Position = new Vector4(this.Width/2+100f, 100f, vertices[1].Color = Color.Green.ToArgb(); vertices[2].Position = new Vector4(250f, 300f, 0f, 1f); vertices[2].Color = Color.Yellow.ToArgb(); 0); device.Clear(ClearFlags.Target, Color.DarkSlateBlue , 1.0f, device.BeginScene(); device.VertexFormat = CustomVertex.TransformedColored.Format; device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, vertices); device.EndScene(); device.Present(); this.Invalidate(); } protected override void Dispose (bool disposing) { if (disposing) { if (components != null) { components.Dispose(); } }
0f, 1f);
base.Dispose(disposing); } private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.Size = new System.Drawing.Size(500,500); this.Text = "DirectX Tutorial"; } static void Main() { using (WinForm our_directx_form = new WinForm()) { our_directx_form.InitializeDevice(); Application.Run(our_directx_form); } }
} }
device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,30), new Vector3(0,0,0), new Vector3(0,1,0)); The first line tells the device what and how the camera should look at the scene. The first parameter sets the view angle, 90 in our case. The we set the view aspect ratio, which is 1 in our case, but will be different if our window is a rectangle instead of a square. The last parameters define the view range. Any objects closer to the camera than 1f will not be shown. Any object farther than 50f won't be shown either. These distances are called the near and the far clipping planes, since all objects between these planes will be clipped (=not drawn). The second line actually positions the camera. The first parameter defines the position. We position it 30 units above our (0,0,0) point, the origin. The next parameter sets the target point the camera is looking at. We will be looking at our origin. At this point, we have defined the viewing axis of our camera, but we can still rotate our camera around this axe. So we still need to define which vector will be considered as 'up'. Now run the code again. This time, we see a triangle, but it's all black! This is because world space is a little more advanced than our previous chapter. Here we are also required to place some lights. However, these will be handled in a following chapter, thus here we will simply tell our device not to expect any lights. Add the following line underneath your camera definition and you'll see our colored triangle again: device.RenderState.Lighting = false; Now everything has been set to use world space coordinates. One thing you should notice: you'll see the green corner of the triangle on the LEFT side of the window, while you defined it on the POSITIVE x-axis. This is because DirectX uses left-handed coordinates!! So, if you would position your camera on the negative z-axis: device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,-30), new Vector3(0,0,0), new Vector3(0,1,0)); you would expect to see the green point in the right half of the window. Try to run this now. This might again not be exactly what you expected. Something very important has happened. DirectX only draws triangles that are facing the camera. DirectX defines that triangles facing the camera should be drawn clockwise relative to the camera. If you position the camera on the negative z-axis, the triangle will be defined counter-clockwise relative to the camera, and thus will not be drawn! One way to remove this problem is simply redefining the vertices clockwise (this time clockwise relative to our camera on the negative part of the Z axis) :
vertices[2].Position = new Vector3(0f, 0f, 0f); vertices[2].Color = Color.Red.ToArgb(); vertices[0].Position = new Vector3(5f, 10f, 0f); vertices[0].Color = Color.Yellow.ToArgb(); vertices[1].Position = new Vector3(10f, 0f, 0f); vertices[1].Color = Color.Green.ToArgb();
This will indeed draw the triangle with the green point to the right. The other way is to add the following line after you camera definition :
device.RenderState.CullMode = Cull.None;
This will simply draw all triangles, even those not facing the camera. You should note that this should never be done in a final product, because it slows down the drawing process, as all triangles will be drawn, even those not facing the camera! However, while designing, it is wise to turn off culling, so you'll always see everything you draw. Also, I chose the background color to be non-black, again because if your triangle might be wrongly defined and drawn black, you'ld still see it. So, while designing, you should turn culling off and set a non-black background color.
You can find some more information on culling in the Culling chapter of the C++ part of this tutorial, where I have made a small animation to visualize the culling process. Here's the complete code again : using using using using using using using System; System.Drawing; System.Collections; System.ComponentModel; System.Windows.Forms; System.Data; Microsoft.DirectX;
using Microsoft.DirectX.Direct3D; namespace DirectX_Tutorial { public class WinForm : System.Windows.Forms.Form { private Device device; private System.ComponentModel.Container components = null; public WinForm() { InitializeComponent(); this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true); } public void InitializeDevice() { PresentParameters presentParams = new PresentParameters(); presentParams.Windowed = true; presentParams.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams); } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4, this.Width/this.Height, 1f, 50f); device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,-30), new Vector3(0,0,0), new Vector3(0,1,0)); device.RenderState.Lighting = false; device.RenderState.CullMode = Cull.None; CustomVertex.PositionColored[] vertices = new CustomVertex.PositionColored[3]; vertices[0].Position = new Vector3(0f, 0f, 0f); vertices[0].Color = Color.Red.ToArgb(); vertices[1].Position = new Vector3(10f, 0f, 0f); vertices[1].Color = Color.Green.ToArgb(); vertices[2].Position = new Vector3(5f, 10f, 0f); vertices[2].Color = Color.Yellow.ToArgb(); device.Clear(ClearFlags.Target, Color.DarkSlateBlue , 1.0f, 0); device.BeginScene(); device.VertexFormat = CustomVertex.PositionColored.Format; device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, device.EndScene(); device.Present(); } this.Invalidate();
vertices);
{ if (disposing) { if (components != null) { components.Dispose(); } } base.Dispose(disposing); } private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.Size = new System.Drawing.Size(500,500); this.Text = "DirectX Tutorial"; } static void Main() { using (WinForm our_directx_form = new WinForm()) { our_directx_form.InitializeDevice(); Application.Run(our_directx_form); } }
} }
your World matrix with a translation matrix : device.Transform.World = Matrix.Translation(-5,10*1/3,0)*Matrix.RotationZ(angle); This will move the triangle so the (0,0,0) point is positioned in the gravity point of the triangle. Then our triangle is rotated around this point, giving us the desired result. Please note the order of transformations. Go ahead and place the translation AFTER the rotation. You will see a triangle rotation around one point, moved to the left and below. You can easily change the code to make the triangle rotate around the Y or Z axis. Make sure to try one of them, to get the first feeling of 3D. A bit more complex is the Matrix.RotateAxis, where you first specify your own custom rotation axis : device.Transform.World = Matrix.Translation(-5,10*1/3,0)*Matrix.RotationAxis(new Vector3(angle*4,angle*2,angle*3), angle); This will make our triangle spin around an every changing axe. Before starting next chapter, let's clear things up a bit. Put the camera positioning in a new method CameraPositioning() and the vertex declaration in VertexDeclaration(), as you can find in the code at the bottom of the page. Since both of them need to be declared only once, we'll only call them from our Main method, so the OnPaint method will go faster. Don't forget to make vertices a variable in your class by adjusting its definition and placing this line in the top of your class : private CustomVertex.PositionColored[] vertices; Here's the code: using using using using using using using using System; System.Drawing; System.Collections; System.ComponentModel; System.Windows.Forms; System.Data; Microsoft.DirectX; Microsoft.DirectX.Direct3D;
namespace DirectX_Tutorial { public class WinForm : System.Windows.Forms.Form { private Device device; private System.ComponentModel.Container components = null; private float angle = 0f; private CustomVertex.PositionColored[] vertices; public WinForm() { InitializeComponent(); this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true); } public void InitializeDevice() { PresentParameters presentParams = new PresentParameters(); presentParams.Windowed = true; presentParams.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams); }
private void CameraPositioning() { device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4, this.Width/this.Height, 1f, 50f); device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,-30), new Vector3(0,0,0), new Vector3(0,1,0)); device.RenderState.Lighting = false; device.RenderState.CullMode = Cull.None; } private void VertexDeclaration() { vertices = new CustomVertex.PositionColored[3]; vertices[0].Position = new Vector3(0f, 0f, 0f); vertices[0].Color = Color.Red.ToArgb(); vertices[1].Position = new Vector3(10f, 0f, 0f); vertices[1].Color = Color.Green.ToArgb(); vertices[2].Position = new Vector3(5f, 10f, 0f); vertices[2].Color = Color.Yellow.ToArgb(); } protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) { device.Clear(ClearFlags.Target, Color.DarkSlateBlue , 1.0f, 0); device.BeginScene(); device.VertexFormat = CustomVertex.PositionColored.Format; device.Transform.World = Matrix.Translation(-5,10*1/3,0)*Matrix.RotationAxis(new Vector3(angle*4,angle*2,angle*3), angle); device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, vertices); device.EndScene(); device.Present(); this.Invalidate(); angle += 0.05f; } protected override void Dispose (bool disposing) { if (disposing) { if (components != null) { components.Dispose(); } } base.Dispose(disposing); } private void InitializeComponent() { this.components = new System.ComponentModel.Container(); this.Size = new System.Drawing.Size(500,500); this.Text = "DirectX Tutorial"; }
static void Main() { using (WinForm our_directx_form = new WinForm()) { our_directx_form.InitializeDevice(); our_directx_form.CameraPositioning(); our_directx_form.VertexDeclaration(); Application.Run(our_directx_form); } } } }
Only 4 out of 6 vertices are unique. So the other 2 are simply a waste of bandwidth to your graphics card! It would be better to define the 4 vertices in an array from 0 to 3, and to define triangle 1 as vertices 1,2 and 3 and triangle 2 as vertices 2,3 and 4. This way, the complex vertex data is not duplicated. This is exactly the idea behind IndexBuffers. Suppose we would like to draw these 2 triangles :
Normally we would have to define 6 vertices, now only 5. So change our VertexDeclaration method as follows: private void VertexDeclaration() { vb = new VertexBuffer(typeof(CustomVertex.PositionColored), 5, device, Usage.Dynamic | Usage.WriteOnly, CustomVertex.PositionColored.Format, Pool.Default); vertices = new CustomVertex.PositionColored[5]; vertices[0].Position = new Vector3(0f, 0f, 0f); vertices[0].Color = Color.White.ToArgb(); vertices[1].Position = new Vector3(5f, 0f, 0f); vertices[1].Color = Color.White.ToArgb(); vertices[2].Position = new Vector3(10f, 0f, 0f); vertices[2].Color = Color.White.ToArgb(); vertices[3].Position = new Vector3(5f, 5f, 0f); vertices[3].Color = Color.White.ToArgb(); vertices[4].Position = new Vector3(10f, 5f, 0f); vertices[4].Color = Color.White.ToArgb(); } vb.SetData(vertices, 0 ,LockFlags.None);
The middle part of this method is easy: we simply define our 5 needed vertices to draw the 2 triangles. However, the first and last lines are new. The first one creates a new VertexBuffer, which will later be needed to link our indices to. The first argument tells the buffer what vertex format to expect. Then the numbers of vertices, our device and some default settings. The last line links our vertex array to the vertex buffer. Of course, you first have to declare vb before this will compile : private VertexBuffer vb; You can already declare our array of indices we are going to fill next, togerther with its IndexBuffer, ib : private int[] indices; private IndexBuffer ib; Next, create this IndicesDeclaration method: private void IndicesDeclaration() { ib = new IndexBuffer(typeof(int), 6, device, Usage.WriteOnly, Pool.Default); indices = new int[6]; indices[0]=3; indices[1]=1; indices[2]=0;
indices[3]=4; indices[4]=2; indices[5]=1; ib.SetData(indices, 0, LockFlags.None); } As with our VertexDefinition method, the first line declares the buffer that will be used to draw triangles from. As you can see, we now will need 6 indices, since 1 triangle is defined by 3 indices. Next, our indices array is initiated. As you can see, vertex number 1 is used twice, this was our initial goal. In this case, the profit is rather small, but in real-life application (as you will see soon ;)this is the way to go. Also note that the triangles have been defined in a clockwise order again, so DirectX will see them as facing the camera. The last line attaches the array to the buffer. -- NOTE: Andy Beatty pointed to me this chapter wouldnt run on his PC. After some digging, he found this might be a limitation of his graphics card, which has a maximum indexbuffer size of 65.543 indices. Instead of using an index buffer consisting of integers (int), he used the type short, making this chapter run on his computer! Thanks Andy for this info! -Make sure to call this method from our Main method : our_directx_form.IndicesDeclaration(); All that's left for this chapter is to draw the triangles from our buffer! Make the following changes to your OnPaint method : device.BeginScene(); device.VertexFormat = CustomVertex.PositionColored.Format; device.SetStreamSource(0, vb, 0); device.Indices = ib; device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 5, 0, 2); device.EndScene(); You still have to indicate what format is to be expected. Then you set your sources, the VertexBuffer and the IndexBuffer. Finally, you call the DrawIndexedPrimitives method. We still offer a list of separate triangles. The first zero indicates at which index to start counting in your indexbuffer. Then you indicate the minimum amount of used indices. We give 0, which will bring no speed optimization. Then the amount of used vertices and the starting point in our vertexbuffer. Finally, we have to indicate how many primitives (=triangles) we want to be drawn. That's it! When you run the program, you'll see 2 white triangles next to each other. Try putting this line directly after your device creation : device.RenderState.FillMode = FillMode.WireFrame;
This will only draw the lines of our triangles, instead of solid triangles.