So if you read my review of Wendy Jones’ book, you know my feelings on the state of DX10 tutorials and books, I want to try and maybe help some people out with tutorials in getting started with DX10, I am by no means an expert and the tutorials will basically cover everything that I’ve learnt so far. They will not be rehashes of the SDK tutorials nor Wendy Jones’ book. I’m hoping to slowly build up a dxManager wrapper class that can be easily used for some basic D3D apps. So let’s get started with the most basic topic: setting up the D3D device for drawing.
Note: The DX10 SDK tutorials are excellent, they are a must read and my early tutorials will be a concatenation of the information found in them!
The entry point of most 3D tutorials is win32 and so I’m not going to rewrite all those tutorials. If you want an excellent win32 tutorial Google forger’s win32 tutorials, they are the best I’ve found. Basically a simple win32 window consists of two things: the window and the window processing function (acts as an event handler). Inside of the tutorial project I’ve attached you’ll see three functions that create a window: the winMain function, the initialize window function and the wndProc, the functions are very basic and so I’m not going to go into any more detail. There are plenty of tutorials available online if you want more detail on win32.
Before I start with the directX initialization stuff, I just want to cover something: the dxManager class. Most tutorials you’ll find will have the directX variables as global with a few global functions in a single file to render a simple triangle. Even Wendy Jones does so in her book, but I’m not, simply because it’s a terrible practice, its messy, inextensible and just plain silly. I’m going to instead create a dxManager class to wrap all my directX functions and just neaten things up.
In later tutorials you’ll see the justification for this more clearly. Enough of me prattling on, let’s dig in.
Okay let just cover some very basic concepts about 3D graphics, the graphics API (directX/openGL) is what you’ll use to draw your objects out to the screen. The API is basically a layer that sits between your graphics card and you, you tell the layer what to do and the layer then tells the card.
The card makes use of a pipeline structure to display objects, I’m not going to go into the justifications about this since Google is your friend. Basically there a few key stages (their names differ across APIs), below is a diagram of the stages in DX10 followed by a brief description (taken from the SDK docs):
- Input-Assembler Stage – The input-assembler stage is responsible for supplying data (triangles, lines and points) to the pipeline.
- Vertex-Shader Stage – The vertex-shader stage processes vertices, typically performing operations such as transformations, skinning, and lighting. A vertex shader always takes a single input vertex and produces a single output vertex.
- Geometry-Shader Stage – The geometry-shader stage processes entire primitives. Its input is a full primitive (which is three vertices for a triangle, two vertices for a line, or a single vertex for a point). In addition, each primitive can also include the vertex data for any edge-adjacent primitives. This could include at most an additional three vertices for a triangle or an additional two vertices for a line. The Geometry Shader also supports limited geometry amplification and de-amplification. Given an input primitive, the Geometry Shader can discard the primitive, or emit one or more new primitives.
- Stream-Output Stage – The stream-output stage is designed for streaming primitive data from the pipeline to memory on its way to the rasterizer. Data can be streamed out and/or passed into the rasterizer. Data streamed out to memory can be recirculated back into the pipeline as input data or read-back from the CPU.
- Rasterizer Stage – The rasterizer is responsible for clipping primitives, preparing primitives for the pixel shader and determining how to invoke pixel shaders.
- Pixel-Shader Stage – The pixel-shader stage receives interpolated data for a primitive and generates per-pixel data such as color.
- Output-Merger Stage – The output-merger stage is responsible for combining various types of output data (pixel shader values, depth and stencil information) with the contents of the render target and depth/stencil buffers to generate the final pipeline result.
The aim of this tutorial is to set up the D3D device to be ready for later tutorials. We will set up the swap chain, the D3D device, the render target, the viewport and finally just set up a view and projection matrix for the future.
The SwapChain and the D3D device
This is what is written in the SDK regarding the swapchain: “An IDXGISwapChain interface implements one or more surfaces for storing rendered data before presenting it to an output.” What this basically means is that you will set up a few “frames” or “buffers” to which you will draw and then swap out when you’re finished drawing. This is how you enable double or triple buffering in DX10.
Once we’ve set up our swapchain we create a D3D device with that swapchain, pretty simple.
To achieve this we need to fill out a DXGI_SWAP_CHAIN_DESC structure and then call D3D10CreateDeviceAndSwapChain to create our swap chain and our D3D device. The function parameters are in the SDK docs and I’m not going to describe what each one does since I’m pretty sure you can read it yourself. You will need pointers to both a swap chain interface and a D3D device interface. Here’s the code to set up and create the swap chain and D3D device.
//Set up DX swap chain //-------------------------------------------------------------- DXGI_SWAP_CHAIN_DESC swapChainDesc; ZeroMemory(&swapChainDesc, sizeof(swapChainDesc)); //set buffer dimensions and format swapChainDesc.BufferCount = 2; swapChainDesc.BufferDesc.Width = width; swapChainDesc.BufferDesc.Height = height; swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;; //set refresh rate swapChainDesc.BufferDesc.RefreshRate.Numerator = 60; swapChainDesc.BufferDesc.RefreshRate.Denominator = 1; //sampling settings swapChainDesc.SampleDesc.Quality = 0; swapChainDesc.SampleDesc.Count = 1; //output window handle swapChainDesc.OutputWindow = *hWnd; swapChainDesc.Windowed = true; //Create the D3D device //-------------------------------------------------------------- if ( FAILED( D3D10CreateDeviceAndSwapChain( NULL, D3D10_DRIVER_TYPE_HARDWARE, NULL, 0, D3D10_SDK_VERSION, &swapChainDesc, &pSwapChain, &pD3DDevice ) ) ) return fatalError("D3D device creation failed");
The render target describes what the format of output of the output merger stage will be. This output format is the same the format of the swapchain buffers. So what we need to do is get all the details from the swapchain about its buffer format and use that to create a render target.
//Create render target view //-------------------------------------------------------------- //try to get the back buffer ID3D10Texture2D* pBackBuffer; if ( FAILED( pSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), (LPVOID*)&pBackBuffer) ) ) return fatalError("Could not get back buffer"); //try to create render target view if ( FAILED( pD3DDevice->CreateRenderTargetView(pBackBuffer, NULL, &pRenderTargetView) ) ) return fatalError("Could not create render target view"); //release the back buffer pBackBuffer->Release(); //set the render target pD3DDevice->OMSetRenderTargets(1, &pRenderTargetView, NULL);
Note: Something that should be mentioned here is the “Release” call, basically all resources are semi-managed, basically resources are only freed once nothing is using them, whenever a resource is used an internal counter is incremented (semaphore) , once something doesn’t need the resource any more the counter is decremented. Once this counter reaches 0, the resource is freed. Here we don’t need the pBackBuffer resource any longer so we release it, if nothing else is using it then the resource is freed.
The viewport defines an area in the render target that you can draw to, in some cases it is necessary to have multiple viewports but for now we will be setting the viewport to take up our entire render target. Creating this structure is really simple, it basically takes the height and width of your window and the starting positions of the window (usually 0 and 0). The depth variables should be set to 0 and 1 for min and max respectively.
//create viewport structure viewPort.Width = width; viewPort.Height = height; viewPort.MinDepth = 0.0f; viewPort.MaxDepth = 1.0f; viewPort.TopLeftX = 0; viewPort.TopLeftY = 0; //set the viewport pD3DDevice->RSSetViewports(1, &viewPort);
The View and Projection Matrices
Okay now we’re nearly done, the last thing we need to set up is the view and projection matrices. The view matrix basically positions and orients the camera in the scene. The matrix usage and transformations will be covered in the next tutorial.
The projection matrix basically defines the range of the view from the camera. This is explained extremely well in the 4th tutorial in the SDK docs! I really suggest reading it. It can also apply perspective to the scene; this basically allows near objects to be large and far objects to be small. In our case we’re going to set up a basic perspective projection with a view angle of 45 degrees and a max depth of 100 units. The near depth must not be set to 0, I will explain why in a later tutorial regarding picking and unprojecting co-ordinates.
// Set up the view matrix //-------------------------------------------------------------- D3DXVECTOR3 eye(0.0f, 0.0f, -5.0f); D3DXVECTOR3 view(0.0f, 0.0f, 1.0f); D3DXVECTOR3 up(0.0f, 1.0f, 0.0f)); D3DXMatrixLookAtLH( &viewMatrix, eye, view, up ); //Set up projection matrix //-------------------------------------------------------------- D3DXMatrixPerspectiveFovLH(&projectionMatrix, (float)D3DX_PI * 0.5f, (float)width/height, 0.1f, 100.0f);
Rendering of the Scene
Okay so we’ve set up our D3D device, let just output something. Unfortuneatly in DX10 doing that isnt all that simple and there are several more stage we need to do before we can proceed. So for now lets just clear the screen and flip the buffers.
//clear scene pD3DDevice->ClearRenderTargetView( pRenderTargetView, D3DXCOLOR(0,0,0,0) ); //SCENE RENDERING GOES HERE!!! //flip buffers pSwapChain->Present(0,0);
So that’s it for my first tutorial! I didn’t cover a lot of material but I’ve set up the base for the rest of the tutorials, and time-permitting I’ll put up several more in the coming weeks.
Tutorial 1 VS2k8 project and Source Code: GitHub