Create your own game
eng   рус

Direct3D 11 Initialization. First Triangle

With every new version of DirectX the creation of the first application, which uses 3d graphics, is becoming harder. In this tutorial, we'll go through all steps of Direct3D 11 initialization. But first, we need to understand what is Direct3D.

Direct3D

Direct3D is used for rendering 3d graphics. Rendering is the process of creating of 2d image from a virtual 3d scene.

For development we'll need to use DirectX 11 SDK. SDK is a Software Development Kit. Some time ago we needed to download it, but now DirectX SDK is part of Windows SDK (since Windows 8).

Direct3D 11.3 (the latest version of DirectX 11) consists of following parts: Direct3D Graphics, DXGI, HLSL, DDS. DDS (it's a graphics format) is not used anymore, HLSL (shaders) we'll discuss in the second part of this tutorial. Let's start with two first parts.

Direct3D Graphics

This is the main Direct3D part. It includes interfaces and resources for 3d rendering and you use them to configure the graphics pipeline. These interfaces starts with ID3D11 prefix, for example: ID3D11Device, ID3D11DeviceContext.

This part is just the buildup on the next part - DXGI.

DXGI

DXGI - DirectX Graphics Infrastructure. DXGI are low-level commands. All Direct3D stuff communicates with a video adapter through DXGI. A coder can use directly just some parts of DXGI, and others are covered by high-level interfaces. These interfaces start with the IDXGI prefix. DXGI has an important interface - IDXGISwapChain. This interface allows showing the rendered image in the window.

Important interfaces of Direct3D 11.3

D3D11 device is a virtual representation of a video adapter in our program. D3D11 devices represented by the interface ID3D11Device. D3D11 devices can create different resources.

Device Context - interface ID3D11DeviceContext. Device context is responsible for rendering. This interface has commands for creation 3d scene.

Swap Chain - interface IDXGISwapChain. Contains buffers in which 3d scene is rendered. The swap chain is a very important concept so we need to understand it better.

IDXGISwapChain

When swap chain is created it is bound to the window (HWND) and d3d11 device (ID3D11Device). Every swap chain consists of one and more buffers. Buffer in that case is just a rectangle picture (and in the memory, it's just a chunk/array). The size of the buffers should be the same as the size of a client area of the window. Every swap chain has one front buffer and some back buffers - zero and more.

Rendering of a 3d scene is happening several times per second. Created 2d image is copied in one of the back buffers of a swap chain. When the back buffer is filled it is swapped with the front buffer. It's called presenting. After that, the back buffer's content becomes visible in the application window. The main goal of the swap chain is to present a rendered image to the screen.

Now we can start initialization of Direct3D application.

Direct3D 11 initialization

1. Firstly, we must create a d3d11 device and bind a device context and swap chain to the device.
2. Secondly, create a render-target view. The render-target view is a picture where the 3d scene is rendered.

After that we can start loading resources of our program.

Creation of ID3D11Device

In DirectX application all actions are made by interfaces. Except one. The first interface is created through function. The main interface in Direct3D 11 is ID3D11Device. To get this interface you can call one of these functions: D3D11CreateDevice, D3D11CreateDeviceAndSwapChain. The first function creates just a device. The second creates both the d3d11 device and the swap chain. We'll use the second function - that's just simpler. Let's look at the prototype:

D3D11CreateDeviceAndSwapChain function

HRESULT D3D11CreateDeviceAndSwapChain( _In_ IDXGIAdapter *pAdapter, _In_ D3D_DRIVER_TYPE DriverType, _In_ HMODULE Software, _In_ UINT Flags, _In_ const D3D_FEATURE_LEVEL *pFeatureLevels, _In_ UINT FeatureLevels, _In_ UINT SDKVersion, _In_ const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, _Out_ IDXGISwapChain **ppSwapChain, _Out_ ID3D11Device **ppDevice, _Out_ D3D_FEATURE_LEVEL *pFeatureLevel, _Out_ ID3D11DeviceContext **ppImmediateContext );

This function gets 12 arguments. It creates three interfaces and one variable. Let's look at arguments more closely:

1. IDXGIAdapter *pAdapter - pointer to a video adapter. A video adapter by default will be used when this argument is NULL. This argument has sense when you have not just one video adapter plugged to monitors. We will set NULL in all future programs.

2. D3D_DRIVER_TYPE DriverType - driver type. One of the values of enumeration D3D_DRIVER_TYPE. We will use D3D_DRIVER_TYPE_HARDWARE - all commands are happening in hardware. Other values make sense if your video adapter doesn't support DirectX 11.

3. HMODULE Software - a handle to a DLL library that is used as a software rasterizer. You should have value in this argument when the previous argument is D3D_DRIVER_TYPE_SOFTWARE. We will set it to NULL.

4. UINT Flags is a combination of flags from the enumeration D3D11_CREATE_DEVICE_FLAG. The main flag - D3D11_CREATE_DEVICE_DEBUG. This value starts the debug layer - additional conditions of resource binding and shaders and also has error descriptions. Direct3D 11 in runtime consists of two layers: core layer and debug layer. Core layer - is a Direct3D API. When you use the debug layer you can get additional information. We will learn the debug layer in a separate tutorial, and before that, we will use only the core layer. Thus this argument will be 0.

5. const D3D_FEATURE_LEVEL *pFeatureLevels - an array of feature levels. By now the highest feature level - D3D_FEATURE_LEVEL_11_1. We will use this level. This argument takes a pointer to the array. In this array, different feature levels are enumerated - from the highest to the lowest. If a video adapter doesn't support the highest feature level, DirectX will try to create a d3d11 device with the next feature level in the array. If this argument is NULL, then the next element in the array will be used:

{ D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1, };

In the sample code I'll use a variable instead of an array. It's equivalent to having an array with one element.

6. UINT FeatureLevels - number of elements in the array from the previous argument. If you will pass one element (that's I do in attached example), then in the previous argument you can pass a pointer to variable D3D_FEATURE_LEVEL, not to array.

7. UINT SDKVersion - version of the software development kit. For Direct3D 11 you need to pass D3D11_SDK_VERSION.

8. const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc - pointer to structure that describes swap chain.

9. IDXGISwapChain **ppSwapChain - pointer to interface of swap chain.

10. ID3D11Device **ppDevice - pointer to interface of D3D11 device.

11. D3D_FEATURE_LEVEL *pFeatureLevel - in this variable feature level of the video adapter will be stored. In all tutorials, I will assume that this level is D3D_FEATURE_LEVEL_11_1. This value depends on the 5th argument.

12. ID3D11DeviceContext **ppImmediateContext - pointer to d3d11 device context.

After calling D3D11CreateDeviceAndSwapChain we will get a variable with feature level and three interfaces: IDXGISwapChain, ID3D11Device, ID3D11DeviceContext. 8th argument - pointer to the structure DXGI_SWAP_CHAIN_DESC. This structure we must fill before calling function D3D11CreateDeviceAndSwapChain. Let's see the description of this structure:

DXGI_SWAP_CHAIN_DESC structure

typedef struct DXGI_SWAP_CHAIN_DESC { DXGI_MODE_DESC BufferDesc; DXGI_SAMPLE_DESC SampleDesc; DXGI_USAGE BufferUsage; UINT BufferCount; HWND OutputWindow; BOOL Windowed; DXGI_SWAP_EFFECT SwapEffect; UINT Flags; } DXGI_SWAP_CHAIN_DESC;

This structure says which properties swap chain should have.

1. DXGI_MODE_DESC BufferDesc - structure which describes buffers: BufferDesc.Width - width, BufferDesc.Height - height, BufferDesc.Format - buffer pixel format, BufferDesc.ScanlineOrdering - rasterization method - not important now, BufferDesc.Scaling - scaling method - not important now, BufferDesc.RefreshRate - refresh rate in hertz, is set by two fields: BufferDesc.RefreshRate.Numerator - numerator and BufferDesc.RefreshRate.Denominator - denominator.

2. DXGI_SAMPLE_DESC SampleDesc - multisampling parameters. This structure consists of two fields: Count and Quality. By default 0 is used for quality, and 1 for count.

3. DXGI_USAGE BufferUsage - this field sets how to use buffer, and also access of central processor to this buffer. We will use the buffer for the rendering of 3d scene to the window, so we'll use the value DXGI_USAGE_RENDER_TARGET_OUTPUT.

4. UINT BufferCount - number of buffers.

5. HWND OutputWindow - window where will be final 2d image.

6. BOOL Windowed - this field sets the windowed mode.

7. DXGI_SWAP_EFFECT SwapEffect - this field sets what will happen with the buffer's content after the Present command. Value by default DXGI_SWAP_EFFECT_DISCARD. We'll learn more about this field in the next tutorials.

8. UINT Flags - set of flags which are described the behavior of a swap chain. We will set 0.

Creation of D3D11 device and swap chain

Let's look at the code:

ID3D11Device* dev; // d3d11 device ID3D11DeviceContext* devContext; // device context IDXGISwapChain* sc; // swap chain DXGI_SWAP_CHAIN_DESC sсd; ZeroMemory(&sсd, sizeof(sсd)); sсd.BufferCount = 2; sсd.BufferDesc.Width = 500; sсd.BufferDesc.Height = 500; sсd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sсd.BufferDesc.RefreshRate.Numerator = 60; sсd.BufferDesc.RefreshRate.Denominator = 1; sсd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sсd.OutputWindow = hWnd; sсd.SampleDesc.Count = 1; sсd.SampleDesc.Quality = 0; sсd.Windowed = TRUE; D3D_FEATURE_LEVEL FeatureLevels = D3D_FEATURE_LEVEL_11_1; UINT numLevels = 1; D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, &FeatureLevels, numLevels, D3D11_SDK_VERSION, &sсd, &sc, &dev, NULL, &devContext);

Here we are filling DXGI_SWAP_CHAIN_DESC description and calling D3D11CreateDeviceAndSwapChain function.

Creation of render-target view

Now we need to understand the responsibility of every interface and why we need a render-target-view. D3D11 device creates different resources. The swap chain presents the content of the back buffer to the screen. The device context does all rendering. The device context "draws" graphics to the buffer, and the swap chain shows graphics in the window. But there is one problem. The device context can't work with swap chain buffers directly. We need a render-target view for this task. The device context uses interface ID3D11RenderTargetView for rendering. So, we need to bind this interface to the back buffer of a swap chain. Firstly, we need to get the address of a back buffer:

HRESULT IDXGISwapChain::GetBuffer( [in] UINT Buffer, [in] REFIID riid, [in, out] void **ppSurface );

UINT Buffer - buffer index. If we set for swap chain DXGI_SWAP_EFFECT_DISCARD, then this argument is always 0.

REFIID riid - here we are setting the type of interface that we want to get. We need to get an ID3D11Texture2D (2d image) interface. The type of this argument is COM stuff. We'll learn COM in a separate tutorial. By then we set this argument: __uuidof(ID3D11Texture2D).

void **ppSurface - pointer to interface (in our case - ID3D11Texture2D).

After calling the GetBuffer method we have interface ID3D11Texture2D, which has the back buffer of the swap chain. Now, using this interface we can create a render-target view:

HRESULT ID3D11Device::CreateRenderTargetView( [in] ID3D11Resource *pResource, [in] const D3D11_RENDER_TARGET_VIEW_DESC *pDesc, [out] ID3D11RenderTargetView **ppRTView );

ID3D11Resource *pResource - we pass here interface that we have got in the previous method.

const D3D11_RENDER_TARGET_VIEW_DESC *pDesc - description of the render-target view. Just pass NULL.

ID3D11RenderTargetView **ppRTView - address of the ID3D11RenderTargetView. This interface will be used for rendering by d3d11 device context.

Let's look at the code of creation of render-target view:

ID3D11RenderTargetView* view; ID3D11Texture2D* backBuffer; sc->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBuffer); dev->CreateRenderTargetView(backBuffer, NULL, &view);

We have prepared device, device context, and swap chain for rendering. Now we need to learn what shaders are:

HLSL - Shaders in DirectX

Shader is a crucial part of modern video graphics. DirectX uses HLSL (High-Level Shader Language) for shaders. The shader is just a program that is executed by GPU. To add a shader to our program we need to create separate files with shader's source code, read it in our program, and compile.

There are different kinds of shaders. The mandatory shaders are vertex and pixel.

Shaders are stored in separate files, which has .hlsl extension. For this tutorial, I've added in the project file shaders.hlsl. It stores both shaders. We'll store both vertex and pixel shaders in the same file and will compile their code manually, so we need to exclude the file from the build. Click the right mouse button on the shaders.hlsl file and choose properties. Then change the value for "Excluded From Build" to "Yes".

Now let's look at the code of shaders:

float4 VS(float4 position : POSITION) : SV_Position { return position; } float4 PS() : SV_Target { return float4(0.0f, 0.0f, 0.0f, 1.0f); }

HLSL code is similar to the C language. As we can see, there are two functions: VS (vertex shader) and PS (pixel shader).

VS takes one argument - position of type float4. float4 - is the array of four floats. There are many types in HLSL that help to simplify the math. POSITION, SV_Position, SV_Target are examples of semantics. The data that is passed between different kinds of graphics pipeline stages is generic. Semantics gives data meaning. There are input and output variables. Semantics for output data we write after the argument list, i.e. POSITION is related to input data position and SV_Position is related to output data (what we'll return).

POSITION says that the input parameter represents the position of the vertex in the object space. Vertex shader receives one vertex at a time, makes some transformations ("moves", "rotates", "scales"), and passes the transformed vertex to the next stage in the pipeline. At the end of the vertex shader, coordinates should be in homogeneous form. We don't do yet any manipulations with the position - just pass input data next.

Somewhere in between vertex and pixel shader GPU makes rasterization. So pixel shader receives pixel data, not a vertex.

Pixel shaders can output data only with SV_Target or SV_Depth semantics. SV_Target says that this data will be put in render target (back buffer). We fill each pixel with black color. Pay attention that only pixels inside our triangle will be filled. I.e. Pixel shader is called only for triangle pixels.

Now we can return to our program.

Shader compiling

In DirectX 11 under Windows 10 shader could be compiled in three different ways. Two of them use the D3D11 device. The third way - D3DCompileFromFile function. We will use this function for shader compiling. This is the quickest and simplest way.

For your notion, the documentation says that applications that use function D3DCompileFromFile could not be submitted to Windows Store.

To use shader compiler you need to include header file D3DCompiler.h and add library d3dcompiler.lib:

#include <D3Dcompiler.h> #pragma comment(lib,"D3dcompiler.lib")

D3DCompileFromFile function

D3DCompileFromFile function compiles HLSL shaders. It takes a file with source code of shader and returns (last but one argument) interface object ID3DBlob (blob - Binary Large Object, this is just a large array of data):

HRESULT WINAPI D3DCompileFromFile( LPCWSTR pFileName, const D3D_SHADER_MACRO *pDefines, ID3DInclude *pInclude, LPCSTR pEntrypoint, LPCSTR pTarget, UINT Flags1, UINT Flags2, ID3DBlob **ppCode, ID3DBlob **ppErrorMsgs );

1. pFileName - the name of the file with source code of a shader.

2. pDefines - an array of macros of the shader. Just set it to NULL for now.

3. pInclude - this argument is set when in shader file there is #include directive. Set it to NULL.

4. pEntrypoint - shader's entry point - the name of the function with shader's code. By default Visual Studio uses main. We use VS and PS.

5. pTarget - version and type of the shader: compute, domain, geometry, hull, pixel, vertex. We need by now the last two: pixel and vertex. In DirectX 11 you need to use the 5th shader version. Set this value to: vs_5_0 - vertex, ps_5_0 - pixel.

6-7. Flags. Set it to NULL for now.

8. ID3DBlob object where compiled shader will be stored.

9. ppErrorMsgs - error messages will be stored in this object. Set it to NULL.

So, let's compile vertex and pixel shaders:

ID3DBlob* vsBlob = nullptr; D3DCompileFromFile(L"shaders.hlsl", NULL, NULL, "VS", "vs_5_0", 0, 0, &vsBlob, NULL); ID3DBlob* psBlob = nullptr; D3DCompileFromFile(L"shaders.hlsl", NULL, NULL, "PS", "ps_5_0", 0, 0, &psBlob, NULL);

Vertex shader we compile in vsBlob, and pixel shader in psBlob.

Creation of shader objects in DirectX 11

After having compiled shader with D3D11 device you can create shader objects - it is the representation of shader in the application. For the creation of different shader types, the ID3D11Device interface uses different methods. We need two methods: ID3D11Device::CreateVertexShader and ID3D11Device::CreatePixelShader. They are alike (only the last arguments are different). Let's look at the prototype of the first:

HRESULT CreateVertexShader( const void *pShaderBytecode, SIZE_T BytecodeLength, ID3D11ClassLinkage *pClassLinkage, ID3D11VertexShader **ppVertexShader );

1. pShaderBytecode - address of compiled shader in memory.

2. BytecodeLength - size of the compiled shader.

ID3DBlob interface has two methods which store two values: GetBufferPointer - address of the compiled shader, GetBufferSize - size. These methods don't take arguments.

3. pClassLinkage - this argument used for linking. Set it to NULL.

4. ppVertexShader - shader object will be stored in this variable.

The code:

ID3D11VertexShader* vs; ID3D11PixelShader* ps; dev->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), NULL, &vs); dev->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), NULL, &ps);

Now we need to make created shaders active:

devContext->VSSetShader(vs, NULL, NULL); devContext->PSSetShader(ps, NULL, NULL);

Here device context sets current vertex and pixel shaders. 2nd and 3rd arguments don't matter now.

Preparing triangle vertices

We've prepared Direct3D for rendering. Now we need to send data to the graphics pipeline. We'll create geometry for a triangle and send it to GPU.

Direct3D can render different kinds of primitives. Now we need only triangles. Each primitive consists of vertices and a triangle consists of 3 vertices. Each vertex consists of components - (x, y, z, w). The last component we'll set for now to 1. We need the fourth component as it will make the math simpler.

When we send vertex array to GPU, vertices go through the rendering pipeline. First, they go to the vertex shader. In our case vertex shader doesn't do anything with vertices - it just passes the position to the next stage. At the end of this stage, coordinates should be in the range between -1 to 1. Vertex shader receives vertices one by one.

So, let's create geometry. We need to define the structure for vertices first:

struct Vertex { XMFLOAT4 position; }; Vertex vertices[] = { XMFLOAT4(0.0f, 0.5f, 0.0f,1.0f), XMFLOAT4(0.5f, -0.5f, 0.0f,1.0f), XMFLOAT4(-0.5f, -0.5f, 0.0f,1.0f), };

Each vertex has a field of type XMFLOAT4. XMFLOAT4 defined in directxmath.h. It's a structure and it has four fields: x, y, z, w. It's important to use DirectXMath data types as it has many defined structures and conversion functions to data that's fast on GPU - we'll talk about it later.

Here we just have the array vertices with three elements. The triangle is defined in xy plane - z components are zeroes. We set here perspective projected coordinates - the final form of coordinates - they should be this way after vertex shader. The Center of the screen is (0,0). The range of values is from -1 to +1. We'll talk about different kinds of coordinates later.

Now we need to bind our geometry to the graphics pipeline. To represent our data in GPU we'll use ID3D11Buffer interface:

ID3D11Buffer* vertexBuffer = nullptr; D3D11_BUFFER_DESC bd = {}; bd.ByteWidth = sizeof(Vertex) * 3; bd.Usage = D3D11_USAGE_DEFAULT; bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; bd.CPUAccessFlags = 0; D3D11_SUBRESOURCE_DATA InitData = {}; InitData.pSysMem = vertices; dev->CreateBuffer(&bd, &InitData, &vertexBuffer);

First we'll fill D3D11_BUFFER_DESC structure variable. ByteWidth is the size of our buffer. The usage field tells how buffer will be used by GPU and CPU. We'll pass D3D11_USAGE_DEFAULT for now. BindFlags tells how the buffer will be used. We want to create a vertex buffer. CPUAccessFlags - how CPU can access the buffer. We don't want to access buffer from CPU so we pass 0.

Next we create D3D11_SUBRESOURCE_DATA structure variable. This structure is used to create buffers. We set pSysMem to point of the beginning of our vertices array.

ID3D11Device::CreateBuffer creates a buffer. In our case - vertex buffer. This method will save the buffer address in the pointer of the last argument.

We are almost done. Next, we need to bind the buffer to the graphics pipeline:

UINT stride = sizeof(Vertex); UINT offset = 0; devContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);

The first argument in ID3D11DeviceContext::IASetVertexBuffers is the first input slot for binding. Pass 0 for now. The second argument tells how many vertex buffers in the array we want to bind. The third argument is the address to the array of vertex buffers. The fourth argument is stride - offset between different elements of elements in the vertex buffer. The last argument is the offset to the element that will be used.

The last thing we need to do before the main loop is to set what geometry we want to render:

devContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

Here we'll render a triangle list. We can render other types of primitives (lines, points) or triangles in a different format, we'll talk about that later.

Main loop with Direct3d

const float greyColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; while (msg.message != WM_QUIT) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } devContext->ClearRenderTargetView(rtv, greyColor); devContext->Draw(3, 0); sc->Present(0, 0); }

After dealing with the message, we clear the render target view with grey color. Then we make a Draw call. The first argument is the number of vertices we want to draw, second - from what vertex to start.

And lastly we presenting the back buffer with swap chain. For back buffer presenting (when the content of back buffer shown in the window) you just need to call the Present method:

sc->Present(0, 0);

First argument - synchronization. Set it to zero.
Second argument - flags. Zero too.

Conclusion

In the attached project shaders contains code which is created by Visual Studio by default. These pixel and vertex shaders don't do anything with data. When you'll start the application, you don't see any differences with the program from the previous lesson. Just make sure (in the debugger) that vsBlob, psBlob, vs, and ps have real addresses (not null).

In this tutorial we learned how to set up two important stages of the graphics pipeline of DirectX 11: vertex and pixel shaders. Now we need to start the graphics pipeline with data.

We've learned initialization of Direct3D application. By now you must understand the logic of initialization. I hope I've made a clear explanation of this process.

Comments:

No comments yet