C++ and DirectX 11
Triangle
In this tutorial you will learn to render your first triangle. I will also show you a way to load and use custom shader code without the need of additional libraries. Because the last tutorial only covered the necessary parts to initialize DirectX, some additional code is needed to make the DXManager ready to render the triangle. Start by opening the dXManager header and add the following lines.
dXManager.h
ID3D11Device *GetDevice();
ID3D11DeviceContext *GetDeviceContext();
All classes that want to interact with DirectX have to use the device and the device context.
ID3D11Texture2D *_depthStencilBuffer;
ID3D11DepthStencilView *_depthStencilView;
D3D11_VIEWPORT _viewport;
This additional class members are the missing pieces to complete the DXManager class. The depth stencil buffer is used as a depth stencil texture. The depth stencil view will be bind together with the depth stencil buffer and the render target view to complete the swap chain. With the viewport the clip space coordinates are mapped to the render target space.
dXManager.cpp
_depthStencilBuffer(nullptr),
_depthStencilView(nullptr)
Add the new variables to the constructor initialization list. The viewport is just a struct and will be set later.
D3D11_TEXTURE2D_DESC depthBufferDesc;
ZeroMemory(&depthBufferDesc, sizeof(depthBufferDesc));
depthBufferDesc.Width = screenWidth;
depthBufferDesc.Height = screenHeight;
depthBufferDesc.MipLevels = 1;
depthBufferDesc.ArraySize = 1;
depthBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthBufferDesc.SampleDesc.Count = 1;
depthBufferDesc.SampleDesc.Quality = 0;
depthBufferDesc.Usage = D3D11_USAGE_DEFAULT;
depthBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthBufferDesc.CPUAccessFlags = 0;
depthBufferDesc.MiscFlags = 0;
if (FAILED(_device->CreateTexture2D(&depthBufferDesc , NULL, & _depthStencilBuffer ))) {
return Error("failed to create depth stencil buffer");
}
Now the variables will be initialized inside the DXManager::Initialize method. Start with the depth stencil buffer. Because this is just a 2D Texture a D3D11_TEXTURE2D_DESC is used to describe it. In the last tutorial you already used a (different) description to setup your swap chain. These are just structs which specify all properties for the corresponding create method. Because DirectX uses a lot of these descriptions i will always add the MSDN link with the detailed information of the specific struct members. This one can be found here. With a filled out description the CreateTexture2D method is used to actually create your depth stencil buffer. Following the style of the last tutorial an error dialog is displayed if the method fails.
D3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc;
ZeroMemory(&depthStencilViewDesc , sizeof(depthStencilViewDesc));
depthStencilViewDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthStencilViewDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
depthStencilViewDesc.Texture2D.MipSlice = 0;
if (FAILED(_device->CreateDepthStencilView(_depthStencilBuffer, &depthStencilViewDesc, &_depthStencilView ))) {
return Error("failed to create depth stencil view");
}
Next on the list is the depth stencil view. Information about the description on MSDN. With the description and the previously generated depth stencil buffer the depth stencil view is created.
_deviceContext->OMSetRenderTargets(1, &_renderTargetView , _depthStencilView);
OMSetRenderTargets binds the render target view and the depth stencil view to the output render pipeline. Graphic will now be rendered on the back buffer to be later switched with the front buffer and therefore displayed to the user.
ZeroMemory(&_viewport , sizeof(_viewport));
_viewport.Width = static_cast <float>(screenWidth);
_viewport.Height = static_cast <float>(screenHeight);
_viewport.MinDepth = 0.0f;
_viewport.MaxDepth = 1.0f;
_viewport.TopLeftX = 0.0f;
_viewport.TopLeftY = 0.0f;
The last new member is the viewport. As mentioned before it is used to map the coordinates between the clip and render target space. Usually you set this to the size of your application screen.
_deviceContext->RSSetViewports(1, &_viewport);
To use the defined viewport set it with the RSSetViewports method. Now you have everything that is required for proper rendering!
SAFE_RELEASE(_depthStencilBuffer);
SAFE_RELEASE(_depthStencilView);
Don't forget to add the release calls for the two new pointers. As usual this is done in the DXManager::Shutdown method.
_deviceContext->ClearDepthStencilView(_depthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0);
Complete DXManager::StartFrameRender() by adding the clear method for the newly created depth stencil view.
ID3D11Device *DXManager::GetDevice()
{
return _device;
}
ID3D11DeviceContext *DXManager::GetDeviceContext()
{
return _deviceContext;
}
Finally write the two getter methods for the device and device context.
Now continue by creating the shader files. First add a new filter to your project: Right click on project -> Add -> New Filter and name it Shader Files. For the triangle rendering a vertex shader (colorShader_VS.hlsl) and a pixel shader (colorShader_PS) is used. Within the shader files you can also use a shader include file (colorShader.hlsli) to organize and reuse structures and variables. More information about the general shader pipeline and where the vertex and pixel shader come into action is available here: MSDN - Pipeline. Create the three mentioned files with Right click on Shader Files -> Add -> New item.... Switch to HLSL and make sure to use the correct file ending and template for the different files. I will write the selection in brackets next to the filename.
colorShader.hlsli (HLSL Header File)
struct VS_INPUT
{
float4 position : POSITION;
float4 color : COLOR;
};
struct PS_INPUT
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
The HLSL header file is similar to your normal header file (.h). Here it is used to define the input structures for the pixel and vertex shader. Because the shaders are in different files and they both need those structs it is nice to encapsulate them in a single file. For the triangle it is enough to specify position and color.
colorShader_VS.hlsl (Vertex Shader File)
#include "colorShader.hlsli"
PS_INPUT main(VS_INPUT input)
{
PS_INPUT output;
output.position = input.position;
output.color = input.color;
return output;
}
If you selected the correct layout the basic structure should already be there. Delete it to add the above code. To use the VS_INPUT struct simply include the hlsli file like you would include a header file. Usually this is the place to change the position of the vertex by multiplying it with the world view projection matrix which comes from a camera. But this is a topic which will be explained in one of the next tutorials. For now the position and color is just passed to PS_INPUT and then the struct is returned.
colorShader_PS.hlsl (Pixel Shader File)
#include "colorShader.hlsli"
float4 main(PS_INPUT input) : SV_TARGET
{
return input.color;
}
For now the pixel shader is also very simple. It just uses the color from the vertex shader to return it to the next stage (Output Merger). Visual Studio automatically selects the right compile method if you create the shader files via the template. If you want to change some of the default values you can do so with: Right click on shader file -> Properties -> switch to HLSL Compiler.
What I also like to show you is how you can automatically copy files (like your compiled shader files) into the debug or release folder while building the project. First go to the Post-Build Event with: Right click on project -> Properties -> switch to Configuration Properties -> Build Events -> Post-Build Event. Make sure you switch to Configuration: All Configurations on the top drop down menu. Now enter following line into the Command Line field: xcopy "$(ProjectDir)*.cso" "$(OutDir)" /Y /C and hit apply. This will copy all compiled shader files to your output folder. Newer versions (Visual Studio 2015) have the reverse problem. They automatically build the shader file in the correct output folder and then can not run the project because the relative path does not match. This can be easily solved by adding an additional Line with xcopy "$(OutDir)*.cso" "$(MSBuildProjectDirectory)" /Y /C. To utilize the shader code you need a class which can load and apply a compiled shader file to an object.
colorShaderRenderer.h
#pragma once
#include <d3d11.h>
#include "macros.h"
#include <fstream>
class ColorShaderRenderer
{
public:
ColorShaderRenderer();
bool Initialize(ID3D11Device*);
void Shutdown();
void OnFrameRender(ID3D11DeviceContext*, int);
private:
ID3D11VertexShader *_vertexShader;
ID3D11PixelShader *_pixelShader;
ID3D11InputLayout *_inputLayout;
};
[4] This include is used in the method which will read the compiled shader files.
[12] Similar to the OnFrameRender method in the Core class this one is called each frame and handles everything which is related to rendering. Later the method is called to render objects with this specific shader. indexCount is needed for the draw call. It tells the method how much of the buffer should be drawn.
[14-16] Vertex and pixel shader are pointers to the DirectX representation of the compiled shader files. The input layout specifies the expected shader input that was declared in the colorShader.hlsli file.
Also add the following method to the header file. If you have more shaders which can use the method it would be a good idea to place it somewhere more accessible.
static char *ReadShaderFile(const char *filename, size_t &size)
{
std::ifstream inFile;
inFile.open(filename, std::ios::in | std::ios::binary | std::ios::ate);
if (inFile.is_open()) {
inFile.seekg(0, std::ios::end);
size = (size_t)inFile.tellg();
inFile.seekg(0, std::ios::beg);
char *byteArray = new char[size];
inFile.read(byteArray, size);
return byteArray;
}
return nullptr;
}
This static method takes filename and size as parameters. size is used to set the file size which is needed the actual creation. The rest is straight forward. If the file was successfully opened the size is determined and then the content is read to a char array which then is returned.
colorShaderRenderer.cpp
#include "colorShaderRenderer.h"
ColorShaderRenderer::ColorShaderRenderer() :
_vertexShader(nullptr),
_pixelShader(nullptr),
_inputLayout(nullptr)
{
}
Business as usual.
bool ColorShaderRenderer::Initialize(ID3D11Device *device)
{
size_t VSSize, PSSize;
char *VS = nullptr, *PS = nullptr;
const D3D11_INPUT_ELEMENT_DESC colorShaderVertexInputLayout[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};
if ((VS = ReadShaderFile("ColorShader_VS.cso", VSSize)) == nullptr) {
return false;
}
if ((PS = ReadShaderFile("ColorShader_PS.cso", PSSize)) == nullptr) {
return false;
}
device->CreateVertexShader(VS, VSSize, nullptr, &_vertexShader);
device->CreatePixelShader(PS, PSSize, nullptr, &_pixelShader);
device->CreateInputLayout(colorShaderVertexInputLayout, 2, VS, VSSize, &_inputLayout);
SAFE_DELETE_A(VS);
SAFE_DELETE_A(PS);
return true;
}
Now this is a very interesting part. This code shows how you can load and use compiled shader files in your project.
[15-18] The D3D11_INPUT_ELEMENT_DESC array is a quick method to define the description of the shader input. It is important that those parameters matches the specified struct in your shader file. Here is the MSDN link.
[20-25] For the shaders you need the size and corresponding char array. With the just created method this is a piece of cake.
[27-29] The create methods then creates the required shaders and sets the pointers so you can use them later.
[31,32] Because the arrays aren't needed after the creation method is called delete them to avoid memory leaks. This is done by a new macro function which is added to the macros.h file.
macros.h
#define SAFE_DELETE_A(x) if(x!=nullptr) { delete [] x; x = nullptr; }
If the pointer is valid delete it and set it to null.
colorShaderRenderer.cpp
void ColorShaderRenderer::Shutdown()
{
SAFE_RELEASE(_vertexShader);
SAFE_RELEASE(_pixelShader);
SAFE_RELEASE(_inputLayout);
}
As a rule of thumb: if the variable name contains ID3DXX it should probably be released. Therefore safe release is called for the variables.
void ColorShaderRenderer::OnFrameRender(ID3D11DeviceContext *deviceContext, int indexCount)
{
deviceContext->IASetInputLayout(_inputLayout);
deviceContext->VSSetShader(_vertexShader, NULL, 0);
deviceContext->PSSetShader(_pixelShader, NULL, 0);
deviceContext->DrawIndexed(indexCount, 0, 0);
}
The last method in this class is responsible to draw whatever is in the current buffer. Each frame the input layout and the vertex &pixel shaders are set. Then a draw call, here DrawIndexed (MSDN) is used to finally render the buffer. The only thing missing now is something to apply the created shader on.
triangleObject.h
#pragma once
#include <d3d11.h>
#include <DirectXMath.h>
#include "macros.h"
class TriangleObject
{
public:
TriangleObject();
bool Initialize(ID3D11Device* device);
void Shutdown();
int GetIndexCount();
void OnFrameRender(ID3D11DeviceContext* deviceContext);
private:
ID3D11Buffer* _vertexBuffer;
ID3D11Buffer* _indexBuffer;
int _vertexCount;
int _indexCount;
struct ColorShaderVertex {
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT4 color;
};
};
[3] DirectXMath includes all the necessary methods and variables to operate and handle most DirectX applications. Right now only XMFLAOT3 and 4 are used.
[12] This method provides the index count for the render method in the ColorShaderRenderer class.
[15-18] To save the triangle structure on the graphic card the correct buffers are needed. The count variables are used for clarity.
[20-23] ColorShaderVertex represents the input structure of the shader file. It is important that the variables matches. floatX in the shader code corresponds to XMFLOATX in the C++ code.
triangleObject.cpp
#include "triangleObject.h"
TriangleObject::TriangleObject() :
_vertexBuffer(nullptr),
_indexBuffer(nullptr)
{
_vertexCount = 3;
_indexCount = 3;
}
For a triangle only three vertices are used. The index count is set accordingly.
bool TriangleObject::Initialize(ID3D11Device *device)
{
ColorShaderVertex *vertices = new ColorShaderVertex[_vertexCount];
unsigned long *indices = new unsigned long[_indexCount];
vertices[0].position = DirectX::XMFLOAT3(-1.0f, -1.0f, 0.0f);
vertices[1].position = DirectX::XMFLOAT3(0.0f, 1.0f, 0.0f);
vertices[2].position = DirectX::XMFLOAT3(1.0f, -1.0f, 0.0f);
vertices[0].color = DirectX::XMFLOAT4(0.0f, 0.15f, 0.55f, 1.0f);
vertices[1].color = DirectX::XMFLOAT4(0.40f, 0.75f, 0.90f, 1.0f);
vertices[2].color = DirectX::XMFLOAT4(0.0f, 0.15f, 0.55f, 1.0f);
indices[0] = 0;
indices[1] = 1;
indices[2] = 2;
D3D11_BUFFER_DESC vertexBufferDesc;
ZeroMemory(&vertexBufferDesc , sizeof(vertexBufferDesc));
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.ByteWidth = sizeof(ColorShaderVertex) * _vertexCount;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = 0;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vertexData;
ZeroMemory(&vertexData , sizeof(vertexData));
vertexData.pSysMem = vertices;
vertexData.SysMemPitch = 0;
vertexData.SysMemSlicePitch = 0;
if (FAILED(device->CreateBuffer(&vertexBufferDesc, &vertexData, &_vertexBuffer ))) {
return false;
}
D3D11_BUFFER_DESC indexBufferDesc;
ZeroMemory(&indexBufferDesc , sizeof(indexBufferDesc));
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
indexBufferDesc.ByteWidth = sizeof(unsigned long) * _indexCount;
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
indexBufferDesc.CPUAccessFlags = 0;
indexBufferDesc.MiscFlags = 0;
indexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA indexData;
ZeroMemory(&indexData , sizeof(indexData));
indexData.pSysMem = indices;
indexData.SysMemPitch = 0;
indexData.SysMemSlicePitch = 0;
if (FAILED(device->CreateBuffer(&indexBufferDesc, &indexData, &_indexBuffer ))) {
return false;
}
SAFE_DELETE_A(vertices);
SAFE_DELETE_A(indices);
return true;
}
This is a lot of code for initialization. Most of it is filling out the correct descriptions which are required to create the buffers.
[16-24] First the vertex positions are defined. Because no additional transform matrices are used the whole application screen is mapped to -1.0 to 1.0. The positions are define in this order: x y z. So the code above defines a triangle which stretches from the bottom left (first position) to the middle top (second position) and finally to the bottom right. Then the color for each vertex is defined. The two on the bottom have a dark blue and the top is a lighter blue. The cool thing is that DirectX in its standard configuration will automatically interpolate between those colors and mix them accordingly. The indices are just the numbers 0, 1 and 2.
[26-62] Vertex and index buffer uses the same buffer description which you can lookup on MSDN.
[41,60] The CreateBuffer method also requires a second description for the subresource data (also on MSDN). For each buffer the descriptions are filled and then the create method is called to create the buffer.
[64,65] After that the arrays are deleted to prevent memory leaks.
void TriangleObject::Shutdown()
{
SAFE_RELEASE(_vertexBuffer);
SAFE_RELEASE(_indexBuffer);
}
Remember the rule of thumb.
void TriangleObject::OnFrameRender(ID3D11DeviceContext *deviceContext)
{
unsigned int stride = sizeof(ColorShaderVertex);
unsigned int offset = 0;
deviceContext->IASetVertexBuffers(0, 1, &_vertexBuffer, &stride, &offset );
deviceContext->IASetIndexBuffer(_indexBuffer, DXGI_FORMAT_R32_UINT, 0);
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
}
In the OnFrameRender method you just have to set the vertex and index buffer which where created before. With the SetPrimitiveTopology method you tell the GPU to draw the buffers as triangles.
int TriangleObject::GetIndexCount()
{
return _indexCount;
}
This method is used to get the index count for the draw call of the OnFrameRender method within the ColorShaderRenderer class.
If you made it this far you have created everything to draw your first triangle in DirectX11 with custom shader files. For the result to be rendered you just have to setup the new classes within the Core class.
core.h
#include "TriangleObject.h"
#include "ColorShaderRenderer.h"
Add the header files of the newly created classes.
TriangleObject *_triangle;
ColorShaderRenderer *_colorShaderRenderer;
Add additional pointers for the classes.
core.cpp
_triangle(nullptr),
_colorShaderRenderer(nullptr)
Extend the Core::Core constructor with the pointers.
_triangle = new TriangleObject();
if (!_triangle->Initialize(_DXManager->GetDevice())) {
return false;
}
_colorShaderRenderer = new ColorShaderRenderer();
if (!_colorShaderRenderer->Initialize(_DXManager->GetDevice())) {
return false;
}
Initialize both objects in the Core::Initialize method.
_triangle->OnFrameRender(_DXManager->GetDeviceContext());
_colorShaderRenderer->OnFrameRender(_DXManager->GetDeviceContext(), _triangle->GetIndexCount());
And finally add the two OnFrameRender calls to Core::OnFrameRender (between Start and EndFrameRender). If you start the application you see your first triangle which also happens to be blue.
Download Executable and Source
Back to Initialize or continue with [tell me what you want to see next!]