Of Shaders and Triangles
Hell… it’s about time.
— Tychus Findlay
In this tutorial, we will finally learn how to render triangles. We won’t go into every detail yet, but I thought it was about time that we saw our hard work pay off.
Rendering a triangle is a six steps process:
- Create three vertices to define a triangle.
- Store these vertices in video memory.
- Tell the GPU how to read these vertices.
- Tell the GPU how to translate those vertices into a flat image.
- Tell the GPU where on the back buffer we want the image to appear.
- Render the triangle.
None of these steps are very difficult, but a substantial amount of theory is required to really understand what is happening. Don’t worry, though, we won’t delve too deeply into the theoretical stuff just yet. In this tutorial, we simply want to see something rendered on the screen.
Creating a Triangle
Obviously, to define a triangle, three points, or vectors
Here is a trivial example for a vertex structure, holding only information about the position of the vertex:
For example, to create a single triangle, we could do something like:
GPU Memory
Obviously the newly created C++-structure is stored in system memory. To not suffer from an extreme slowdown, it is at the utmost importance, however, that the data be stored in video memory once needed by the GPU.
In order to get access to the video memory, Direct3D provides a specific COM object to maintain buffers both in system and video memory. This COM object is called a vertex buffer, the COM interface is ID3D11Buffer. When rendering calls for specific data, Direct3D will automatically copy it over to the GPU and take care of all the details for us. If the video card becomes low on memory, Direct3D will delete buffers that haven’t been used in a while, or are considered low priority, in order to make room for newer resources. Once again, thank you, Microsoft!
To create the vertex buffer, a description structure, the D3D11_BUFFER_DESC structure, must be filled out:
UINT ByteWidth
The size of the buffer in bytes.
D3D11_USAGE Usage
This parameter identifies how the buffer is expected to be read from and written to. The most common value is typically D3D11_USAGE_DEFAULT, which tells Direct3D that the GPU, and only the GPU, will read from and write to the buffer.
UINT BindFlags
This member identifies how the buffer will be bound to the graphics pipeline (there will be a chapter about the theory behind the graphics or rendering pipeline later on). Obviously, to create a vertex buffer, we use the D3D11_BIND_VERTEX_BUFFER flag.
UINT CPUAccessFlags
This parameter specifies how the CPU may access the buffer data. We will use zero, as we don’t want the CPU to access the buffer at all.
UINT MiscFlags
We don’t need those flags for now, so we set this member to zero.
UINT StructureByteStride
This parameter defines the size of each element in the buffer structure (in bytes) when the buffer represents a structured buffer. Since we are not interested in using structured buffers at the moment, we can simply set this to zero as well.
Now that the vertex buffer is well-defined, it can be given data to be initialized with using the D3D11_SUBRESOURCE_DATA structure:
const void *pSysMem
This parameter is a pointer to the system memory which contains the data to be copied to the GPU.
UINT SysMemPitch
This member defines the distance (in bytes) from the beginning of one line of a texture to the next line. We do not need this for a vertex buffer, so we can simply set this to zero.
UINT SysMemSlicePitch
This member defines the distance (in bytes) from the beginning of one depth level to the next. We do not need this for a vertex buffer, so we can safely set it to zero.
Now to finally obtain the COM interface, the ID3D11Buffer, a simple call to the ID3D11Device::CreateBuffer method is sufficient:
The first parameter is a pointer to a struct describing the desired buffer.
The second parameter is a pointer to a struct describing the data to initialize the buffer with.
The third parameter is the address of a pointer to the ID3D11Buffer interface for the buffer object that was just created.
CreateBuffer returns E_OUTOFMEMORY if there is insufficient memory to create the buffer.
Here is an example of the above theory in action:
Shaders
To actually render vertices, or pixels to the screen, DirectX uses shaders. The process of rendering is controlled by the (graphics) rendering pipeline. It is a series of steps which take vertices as input and result in a fully rendered image. The pipeline must be programmed by the above-mentioned shaders. A shader is actually a small program that controls one step of the pipeline. Here is an image of a shader 5.0 pipeline:
As we can see, there are several different types of shaders, and each one is run many times during rendering. The vertex shader, for example, is run once for each vertex rendered, while a pixel shader is a program that is run for each pixel drawn. We won’t get into any details yet, but we obviously do have to write a simple vertex and pixel shader. This will take four relatively easy steps:
- Create seperate vertex and pixel shader files.
- Load the two shaders from a .cso file into the program.
- Encapsulate both shaders into shader objects.
- Set both shaders to be the active shaders.
Creating shaders
Shaders are written in High Level Shader Language, or HLSL, for short. Visual Studio is capable of creating HLSL programs. Fortunately for us, the pixel and vertex shaders that Visual Studio creates automatically (right-click -> new file -> HLSL -> vertex/pixel shader) are good enough for the purpose of this tutorial; only minimal changes are needed. Later tutorials will cover more details about HLSL.
Here is the vertex shader:
As input, the vertex shader takes the position of a vertex, defined by three floats, just as defined by our VERTEX structure. For now, the shader does nothing but to return the input position in homogeneous coordinates (defined by the SV_POSITION semantic). There will be more theoretical tutorials later on, including one tutorial about projective geometry and homogeneous coordinates, but for now, we will focus on getting that triangle on the screen.
Thus, without further ado, here is the pixel shader:
The SV_TARGET semantic indicates that the return value of the pixel shader should match the render target format. For now, we simply set the colour of each pixel to yellow.
Loading Shader Files
When compiling the project, the HLSL files automatically compile as well and are transformed into Compiled Shader Objects (CSO), which the running program must load in to be able to use.
To achieve this, a little helper function is needed:
This function simply dumps all the data from the .cso file into an array; this is sophisticated enough for now.
To load and initialize shaders, the Direct3D class is given a new function, called initPipeline:
COM Interfaces
A special COM object exists for every shader type. The Direct3D interfaces to the vertex and pixel shaders are ID3D11VertexShader and ID3D11PixelShader.
Encapsulating both shaders into their respective shader objects is quite easy.
To get an interface to the vertex shader, a simple call to the ID3D11Device::CreateVertexShader method is sufficient:
const void *pShaderBytecode
The first parameter is a pointer to the compiled shader.
SIZE_T BytecodeLength
The second parameter is the size of the compiled vertex shader.
ID3D11ClassLinkage *pClassLinkage
The third parameter is a pointer to a class linkage interface, this is rather advanced and we will set it to NULL.
ID3D11VertexShader **ppVertexShader
The fourth parameter holds the address of a pointer to the returned ID3D11VertexShader interface.
To create the pixel shader, the ID3D11Device::CreatePixelShader method, which is isomorphic to the ID3D11Device::CreateVertexShader method, can be used.
And here is the code to create the actual objects and to get the pointers to the interfaces:
Activating the Shaders
To set the newly created shaders as the active shaders to use, a simple call to the ID3D11DeviceContext::VSSetShader and ID3D11DeviceContext::PSSetShader methods is sufficient. Both methods are isomorphical, here is the VSSetShader function:
The first parameter is the address of the shader object to be set as active. The other two parameters are advanced and are not needed at the moment.
Here is the final code to activate the vertex and pixel shaders:
That is all about shaders that we need to know for now, but we will speak a lot more about shaders in upcoming tutorials.
Input Layouts
To tell the GPU how to handle the vertices that we defined in the VERTEX structure, Direct3D uses a technique called input layouts. An input layout is an interface to a COM object (what a surprise!) that holds a definition of how to feed vertex data that is laid out in memory into the input-assembler stage of the graphics rendering pipeline (yes, we will talk about all of that, in detail, in a later tutorial).
Anyway, an input layout is defined by a D3D11_INPUT_ELEMENT_DESC. Basically, by filling out an input layout description, the GPU is being taught how to read a custom vertex structure. It is possible to select what information gets stored with each vertex to improve the rendering speed by helping the GPU to organize the data appropriately and efficiently.
By now we are experts at filling out those descriptions, so here is the definition:
LPCSTR SemanticName
As seen above, a semantic) is a string which tells the GPU what a certain value is used for. There are numerous semantics possible, but for now, we only need to know that POSITION represents a three-dimensional position using float values, which is what we want. Semantics are used to map elements in the vertex structure to elements in the vertex shader input signature, which we will discuss soon.
UINT SemanticIndex
The semantic index for the element. A semantic index modifies a semantic with an integer index number. A semantic index is only needed when there are multiple elements with the same semantic. A semantic name without an index defaults to index 0, for example, POSITION without an index is equivalent to POSITION0. We won’t use this, as we only have one input element, but we must keep this in mind for later.
DXGI_FORMAT Format
We have come across this one before: a DXGI_FORMAT specifies the data format of the vertices. We will use DXGI_FORMAT_R32G32B32_FLOAT, that is, 32 bit for each x,y and z value.
UINT InputSlot
An integer value that identifies the input-assembler, or input slot, that this vertex element will come from. Direct3D supports sixteen input slots (0-15) through which we can feed vertex data to the GPU. For now, as we only have one input data, we will also only use one input slot.
UINT AlignedByteOffset
The optional offset (in bytes) between each element in the structure. We can use D3D11_APPEND_ALIGNED_ELEMENT for convenience to define the current element directly after the previous one, including any packing if necessary. We will come back to this, later, when we add colour to our vertices.
D3D11_INPUT_CLASSIFICATION InputSlotClass
This member identifies the input data class for a single input slot. We will use D3D11_INPUT_PER_VERTEX_DATA.
UINT InstanceDataStepRate
We set this to zero, it is used with the other option in D3D11_INPUT_CLASSIFICATION.
Now to create the desired input layout, or more specifically, to obtain a pointer to an ID3D11InputLayout interface which represents an input layout, it is sufficient to call the ID3D11Device::CreateInputLayout method:
The first parameter holds the address of the input element description array and the second parameter is the number of elements in the specified input element description array.
The third and fourth parameters are the data and length of the vertex shader files. When CreateInputLayout is called, Direct3D will check the input layout against the vertex shader to make sure they match up correctly.
The final parameter holds the address of the returned input layout interface.
Just like with shader objects and render targets, once created, the input layout object must be activated using the ID3D11DeviceContext::IASetInputLayout method, which only takes one parameter, the input layout to set.
Here is the code to define and create the input layout:
Drawing Primitives
OK, that was a lot of work and a lot of information to process, but we have created the triangle, loaded the triangle to the GPU memory, written, compilted, read and activated the shaders and created the input layout. Now it is time to actually draw the triangle!
We are three steps away from drawing the triangle:
- Set which vertex buffer we intend to use.
- Set the primitive type.
- Draw the triangle!
Setting the Vertex Buffer
Setting the vertex buffer is done by calling the ID3D11DeviceContext::IASetVertexBuffers method, which binds an array of vertex buffers to the input-assembler stage. This will tell the GPU which vertices to read from when rendering. It has a couple of easy parameters, so let us look at the protoype:
UINT StartSlot
The first parameter defines the first input slot for binding. This is advanced and not needed at the moment, and thus we set it to 0 for now.
UINT NumBuffers
The second parameter is the number of vertex buffers in the array. We only have one buffer, so we set this to 1.
ID3D11Buffer *const *ppVertexBuffer
The third parameter is a pointer to a constant array of vertex buffers. The vertex buffers must have been created with the D3D11_BIND_VERTEX_BUFFER flag. As we only have one vertex buffer, we can simply use vertexbuffer.GetAddressOf().
const UINT *pStrides,
The fourth parameter is a constant array of stride values; one stride value for each buffer in the vertex-buffer array. Each stride is the size (in bytes) of the elements that are to be used from that vertex buffer. To fill this parameter, we create a UINT, fill it with the size of our vertex structure and put the address of that variable into this parameter.
const UINT *pOffsets
The fifth parameter is a constant array of offset values; one offset value for each buffer in the vertex-buffer array. Each offset is the number of bytes between the first element of a vertex buffer and the first element that will be used. This will usually be 0.
Primitive Topology
Direct3D has no idea about the mathematical conventions we use, and thus we have to tell Direct3D what exactly we mean when we define vectors in
Draw
Now that Direct3D knows what kind of primitives to render, and what vertex buffer to read from, the contents of that buffer can finally be rendered to the screen by the ID3D11DeviceContext::Draw method. The Draw function has two parameters, the first parameter is the number of vertices that should be drawn, and the second parameter defines the first vertex in the vertex buffer to be drawn.
Here is the code to draw a triangle:
Putting It All Together
Note that the above function to initialize the rendering pipeline must be called each time the game must resize its graphics.
There was also a slight oversight when we created the present function a few tutorials ago. Since we are using the flip mode, the render targets are reset after each call to present, thus we have to rebind them each time as well:
You can download the source code from here.
And here is the log file:
That’s it! I admit it was a bit more difficult than just printing „Hello World“, but it was still rather straightforward. I don’t know about you, but I definitely need a beer now. Cheers! In the next tutorial, we will learn how to add colour to the scene.
References
Literature
- Tricks of the Windows Game Programming Gurus, by André LaMothe
- Microsoft Developer Network (MSDN)