Tutorial 4: Resources, Shaders, and HLSL

泄露秘密 提交于 2020-01-11 02:33:08

Continued from Rastertek.com: DirectX 12 Tutorials

Tutorial 4: Resources, Shaders, and HLSL

http://www.rastertek.com/pic1001.gif

This tutorial will be the introduction to writing vertex and pixel shaders in DirectX 12. It will also be the introduction to using resources like vertex and index buffers in DirectX 12. These are the most fundamental concepts that you need to understand and utilize to render 3D graphics.

 

Vertex Buffers

The first concept to understand is vertex buffers. To illustrate this concept let us take the example of a 3D model of a sphere:

http://www.rastertek.com/pic0163.gif

The 3D sphere model is actually composed of hundreds of triangles:

http://www.rastertek.com/pic0164.gif

Each of the triangles in the sphere model has three points to it, we call each point a vertex. So for us to render the sphere model we need to put all the vertices that form the sphere into a special data array that we call a vertex buffer. Once all the points of the sphere model are in the vertex buffer we can then send the vertex buffer to the GPU so that it can render the model.

 

Index Buffers

Index buffers are related to vertex buffers. Their purpose is to record the location of each vertex that is in the vertex buffer. The GPU then uses the index buffer to quickly find specific vertices in the vertex buffer. The concept of an index buffer is similar to the concept using an index in a book, it helps find the topic you are looking for at a much higher speed. The DirectX SDK documentation says that using index buffers can also increase the possibility of caching the vertex data in faster locations in video memory. So it is highly advised to use these for performance reasons as well.

 

Vertex Shaders

Vertex shaders are small programs that are written mainly for transforming the vertices from the vertex buffer into 3D space. There are other calculations that can be done such as calculating normals for each vertex. The vertex shader program will be called by the GPU for each vertex it needs to process. For example a 5,000 polygon model will run your vertex shader program 15,000 times each frame just to draw that single model. So if you lock your graphics program to 60 fps it will call your vertex shader 900,000 times a second to draw just 5,000 triangles. As you can tell writing efficient vertex shaders is important.

 

Pixel Shaders

Pixel shaders are small programs that are written for doing the coloring of the polygons that we draw. They are run by the GPU for every visible pixel that will be drawn to the screen. Coloring, texturing, lighting, and most other effects you plan to do to your polygon faces are handled by the pixel shader program. Pixel shaders must be efficiently written due to the number of times they will be called by the GPU.

 

HLSL

HLSL is the language we use in DirectX 12 to code these small vertex and pixel shader programs. The syntax is pretty much identical to the C language with some pre-defined types. HLSL program files are composed of global variables, type defines, vertex shaders, pixel shaders, and geometry shaders. As this is the first HLSL tutorial we will do a very simple HLSL program using DirectX 12 to get started.

 

Updated Framework

http://www.rastertek.com/pic0008.gif

The framework has been updated for this tutorial. Under GraphicsClass we have added three new classes called CameraClass, ModelClass, and ColorShaderClass. CameraClass will take care of our view matrix we talked about previously. It will handle the location of the camera in the world and pass it to shaders when they need to draw and figure out where we are looking at the scene from. The ModelClass will handle the geometry of our 3D models, in this tutorial the 3D model will just be a single triangle for simplicity reasons. And finally ColorShaderClass will be responsible for rendering the model to the screen invoking our HLSL shader.

We will begin the tutorial code by looking at the HLSL shader programs first.

 

Color.vs

These will be our first shader programs. Shaders are small programs that do the actual rendering of models. These shaders are written in HLSL and stored in source files called color.vs and color.ps. I placed the files with the .cpp and .h files in the engine for now. The purpose of this shader is just to draw colored triangles as I am keeping things simple as possible in this first HLSL tutorial. Here is the code for the vertex shader first:

////////////////////////////////////////////////////////////////////////////////

// Filename: color.hlsl

////////////////////////////////////////////////////////////////////////////////

In shader programs you begin with the global variables. These globals can be modified externally from your C++ code. You can use many types of variables such as int or float and then set them externally for the shader program to use. Generally you will put most globals in buffer object types called "cbuffer" even if it is just a single global variable. Logically organizing these buffers is important for efficient execution of shaders as well as how the graphics card will store the buffers. In this example I've put three matrices in the same buffer since I will update them each frame at the same time.

/////////////
// GLOBALS //
/////////////
cbuffer MatrixBuffer: register(b0)
{
    matrix worldMatrix;
    matrix viewMatrix;
    matrix projectionMatrix;
};

Similar to C we can create our own type definitions. We will use different types such as float4 that are available to HLSL which make programming shaders easier and readable. In this example we are creating types that have x, y, z, w position vectors and red, green, blue, alpha colors. The POSITION, COLOR, and SV_POSITION are semantics that convey to the GPU the use of the variable. I have to create two different structures here since the semantics are different for vertex and pixel shaders even though the structures are the same otherwise. POSITION works for vertex shaders and SV_POSITION works for pixel shaders while COLOR works for both. If you want more than one of the same type then you have to add a number to the end such as COLOR0, COLOR1, and so forth.
 

//////////////
// TYPEDEFS //
//////////////
struct VertexInputType
{
    float4 position : POSITION;
    float4 color : COLOR;
};

struct PixelInputType
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};

The vertex shader is called by the GPU when it is processing data from the vertex buffers that have been sent to it. This vertex shader which I named ColorVertexShader will be called for every single vertex in the vertex buffer. The input to the vertex shader must match the data format in the vertex buffer as well as the type definition in the shader source file which in this case is VertexInputType. The output of the vertex shader will be sent to the pixel shader. In this case the output type is called PixelInputType which is defined above as well.

With that in mind you see that the vertex shader creates an output variable that is of the PixelInputType type. It then takes the position of the input vertex and multiplies it by the world, view, and then projection matrices. This will place the vertex in the correct location for rendering in 3D space according to our view and then onto the 2D screen. After that the output variable takes a copy of the input color and then returns the output which will be used as input to the pixel shader. Also note that I do set the W value of the input position to 1.0 otherwise it is undefined since we only read in a XYZ vector for position.

////////////////////////////////////////////////////////////////////////////////
// Shaders
////////////////////////////////////////////////////////////////////////////////
PixelInputType ColorVertexShader(VertexInputType input)
{
    PixelInputType output;
    

    // Change the position vector to be 4 units for proper matrix calculations.
    input.position.w = 1.0f;

    // Calculate the position of the vertex against the world, view, and projection matrices.
    output.position = mul(input.position, worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);
    
    // Store the input color for the pixel shader to use.
    output.color = input.color;
    
    return output;
}

 

 

Color.ps

The pixel shader draws each pixel on the polygons that will be rendered to the screen. In this pixel shader it uses PixelInputType as input and returns a float4 as output which represents the final pixel color. This pixel shader program is very simple as we just tell it to color the pixel the same as the input value of the color. Note that the pixel shader gets its input from the vertex shader output. 

////////////////////////////////////////////////////////////////////////////////
// Filename: color.hlsl
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
float4 ColorPixelShader(PixelInputType input) : SV_TARGET
{
    return input.color;
}

 

////////////////////////////////////////////////////////////////////////////////
// Filename: modelclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _MODELCLASS_H_
#define _MODELCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <d3d12.h>
#include <DirectXMath.h>

using namespace DirectX;

////////////////////////////////////////////////////////////////////////////////
// Class name: ModelClass
////////////////////////////////////////////////////////////////////////////////
class ModelClass
{
private:

 

Here is the definition of our vertex type that will be used with the vertex buffer in this ModelClass. Also take note that this typedef must match the layout in the ColorShaderClass that will be looked at later in the tutorial.

        struct VertexType
	{
		XMFLOAT3 position;
		XMFLOAT4 color;
	};

public:
	ModelClass();
	ModelClass(const ModelClass&);
	~ModelClass();

 

The functions here handle initializing and shutdown of the model's vertex and index buffers. The Render function puts the model geometry on the video card to prepare it for drawing by the color shader. 

	bool Initialize(ID3D12Device*, ID3D12GraphicsCommandList*);
	void Shutdown();
	void Render();

	int GetIndexCount();

private:
	bool InitializeBuffers(ID3D12Device*, ID3D12GraphicsCommandList*);
	void ShutdownBuffers();
	void RenderBuffers(ID3D12GraphicsCommandList*);

 

The private variables in the ModelClass are the vertex and index buffer as well as two integers to keep track of the size of each buffer. We also need a command list variable to record the commands for future use because in D3D12, all the operations are not directly executed on the GPU side but recorded to be uploaded to GPU, which will determine how the command list will be executed. At the same time, the CPU will do something else on other threads instead of waiting for the GPU. Note that all DirectX 12 buffers generally use the generic ID3D12Resource type created on a ID3D12Heap, so we also need heap variables.

private:
	ID3D12Heap* m_heapUpload, * m_heapDefault;
	ID3D12Resource *m_vertexBuffer, *m_indexBuffer, *m_vertexUpload, *m_indexUpload;
	int m_vertexCount, m_indexCount;

};

#endif

 

 

Modelclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: modelclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "modelclass.h"

The class constructor initializes the vertex and index buffer pointers to null.

ModelClass::ModelClass()
{
	m_vertexBuffer = 0;
	m_indexBuffer = 0;
	m_vertexUpload = 0;
	m_indexUpload = 0;
	m_vertexCount = 0;
	m_indexCount = 0;

}
ModelClass::ModelClass(const ModelClass& other)
{
}


ModelClass::~ModelClass()
{
}

The Initialize function will call the initialization functions for the vertex and index buffers.

bool ModelClass::Initialize(ID3D12Device* device, ID3D12GraphicsCommandList* cmd)
{
	bool result;
	

	// Initialize the vertex and index buffer that hold the geometry for the triangle.
	result = InitializeBuffers(device, cmd);
	if(!result)
	{
		return false;
	}

	return true;
}

The Shutdown function will call the shutdown functions for the vertex and index buffers.

void ModelClass::Shutdown()
{
	// Release the vertex and index buffers.
	ShutdownBuffers();

	return;
}

 

Render is called from the GraphicsClass::Render function. This function calls RenderBuffers to put the vertex and index buffers on the graphics pipeline so the color shader will be able to render them.

void ModelClass::Render(ID3D12GraphicsCommandList* cmd)
{
	// Put the vertex and index buffers on the graphics pipeline to prepare them for drawing.
	RenderBuffers(cmd);

	return;
}

GetIndexCount returns the number of indexes in the model. The color shader will need this information to draw this model.

int ModelClass::GetIndexCount()
{
	return m_indexCount;
}

 

The InitializeBuffers function is where we handle creating the vertex and index buffers. Usually you would read in a model and create the buffers from that data file. For this tutorial we will just set the points in the vertex and index buffer manually since it is only a single triangle.

 

bool ModelClass::InitializeBuffers(ID3D12Device* device, ID3D12GraphicsCommandList* cmd)
{
	VertexType* vertices;
	unsigned long* indices;
	D3D12_HEAP_DESC heapDesc;
	D3D12_RESOURCE_DESC bufferDesc;
	DXGI_SAMPLE_DESC sDesc;
	HRESULT result;
	D3D12_RESOURCE_BARRIER barrier = {};

 

First create two temporary arrays to hold the vertex and index data that we will use later to populate the final buffers with.               

        // Set the number of vertices in the vertex array.
	m_vertexCount = 3;

	// Set the number of indices in the index array.
	m_indexCount = 3;
	//Set buffer size
	m_nBuffSize = sizeof(VertexType) * m_vertexCount;
	// Create the vertex array.
	vertices = new VertexType[m_vertexCount];
	if(!vertices)
	{
		return false;
	}

	// Create the index array.
	indices = new unsigned long[m_indexCount];
	if(!indices)
	{
		return false;
	}

Now fill both the vertex and index array with the three points of the triangle as well as the index to each of the points. Please note that I create the points in the clockwise order of drawing them. If you do this counter clockwise it will think the triangle is facing the opposite direction and not draw it due to back face culling. Always remember that the order in which you send your vertices to the GPU is very important. The color is set here as well since it is part of the vertex description. I set the color to green.

              

        // Load the vertex array with data.
	vertices[0].position = XMFLOAT3(-1.0f, -1.0f, 0.0f);  // Bottom left.
	vertices[0].color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[1].position = XMFLOAT3 (0.0f, 1.0f, 0.0f);  // Top middle.
	vertices[1].color = XMFLOAT4 (0.0f, 1.0f, 0.0f, 1.0f);

	vertices[2].position = XMFLOAT3 (1.0f, -1.0f, 0.0f);  // Bottom right.
	vertices[2].color = XMFLOAT4 (0.0f, 1.0f, 0.0f, 1.0f);

	// Load the index array with data.
	indices[0] = 0;  // Bottom left.
	indices[1] = 1;  // Top middle.
	indices[2] = 2;  // Bottom right.

 

With the vertex array and index array filled out we can now use those to create the vertex buffer and index buffer. Creating both buffers is done in the same fashion. We need to create a heap for both buffers. There are 2 types of heaps, one of which is the upload heap as a cache for uploading data to the GPU, and then we copy the data to the default heap with the copy engine. First fill out a descriptor of the heap. In this case we are going to create a placed heap for both vertex and index buffers. Note that if we want 2 different resources on the same heap, we need to align the resources to 64k. In the descriptor the SizeInBytes (size of the buffer) and the Flags (type of buffer) are what you need to ensure to be filled out correctly.

Using the D3D device and it will return a pointer to your new buffer.

               

        // Set up the description of the upload heap. Here we use custom heap so we can freely specify the usage of memory
	heapDesc.SizeInBytes = ((sizeof(VertexType) * m_vertexCount)/65536+1)*65536 + sizeof(unsigned long)* m_indexCount;//we will put both vertex and index buffer together on the upload heap, so we need to align the data to 64k bytes
	heapDesc.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT;
	heapDesc.Properties.Type = D3D12_HEAP_TYPE_CUSTOM;//We need a customized heap for the resources
	heapDesc.Properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_WRITE_COMBINE;
	heapDesc.Properties.CreationNodeMask = 0;
	heapDesc.Properties.VisibleNodeMask = 0;
	heapDesc.Properties.MemoryPoolPreference = D3D12_MEMORY_POOL_L0;//Use system memory
	heapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS;
	//Create the heap for our vertex buffer
	result = device->CreateHeap(&heapDesc, __uuidof(ID3D12Heap), (void**)&m_heapUpload);
	if (FAILED(result))
	{
		return false;
	}
	//Setup the description for the sample desc
	sDesc.Count = 1;
	sDesc.Quality = 0;
	//Setup the description for the vertex buffer
	bufferDesc.Alignment = 65536;//alignment to 64k for more than one resource on the heap
	bufferDesc.DepthOrArraySize = 1;
	bufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
	bufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
	bufferDesc.Format = DXGI_FORMAT_UNKNOWN;
	bufferDesc.Height = 1;
	bufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
	bufferDesc.MipLevels = 1;
	bufferDesc.SampleDesc = sDesc;
	bufferDesc.Width = sizeof(VertexType) * m_vertexCount;
	
        //Once the heap is ready, we can allocate memory on it. First, it’s the vertex buffer.
	result = device->CreatePlacedResource(m_heapUpload, 0, &bufferDesc
		, D3D12_RESOURCE_STATE_GENERIC_READ
		, nullptr
		, IID_PPV_ARGS(&(m_vertexUpload)));
	if (FAILED(result))
	{
		return false;
	}
	bufferDesc.Alignment = 0;
	bufferDesc.Width = sizeof(unsigned long) * m_indexCount;//Set size to index buffer size

 

Then comes the index buffer. Note that when creating both vertex and index buffer on the same heap, a warning on the debug layer will be triggered.

              

        result = device->CreatePlacedResource(m_heapUpload
		, 65536//offset for 64k alignment
		, &bufferDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr
		, IID_PPV_ARGS(&(m_indexUpload)));
	if (FAILED(result))
	{
		return false;
	}

 

When the 2 resources are ready, it is just the beginning. Now we have the buffers in system memory but not in the video memory. To get the buffer in the video memory, we need to copy them to a default heap, which resides in the video memory. But before we start, we need to create a default heap.

For the default heap, we only need to change 2 fields in D3D12_RESOURCE_DESC structure.

        //Change the description for a default heap on GPU side
	heapDesc.Properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_NOT_AVAILABLE;//CPU cannot get access to the VRAM
	heapDesc.Properties.MemoryPoolPreference = D3D12_MEMORY_POOL_L1;//L1 for VRam

	//Now create the default heap and buffers
	result = device->CreateHeap(&heapDesc, __uuidof(ID3D12Heap), (void**)&m_heapDefault);
	if (FAILED(result))
	{
		return false;
	}
	//Now create the resources
	bufferDesc.Alignment = 0;
	bufferDesc.Width = sizeof(VertexType) * m_vertexCount;//Set size to index buffer size
	result = device->CreatePlacedResource(m_heapDefault, 0, &bufferDesc
		, D3D12_RESOURCE_STATE_COPY_DEST
		, nullptr
		, IID_PPV_ARGS(&(m_vertexBuffer)));
	if (FAILED(result))
	{
		return false;
	}
	bufferDesc.Alignment = 0;//This is the last resource, then set alignment to 0
	bufferDesc.Width = sizeof(unsigned long) * m_indexCount;//Set size to index buffer size
	result = device->CreatePlacedResource(m_heapDefault
		, 65536//offset for 64k alignment
		, &bufferDesc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr
		, IID_PPV_ARGS(&(m_indexBuffer)));
	if (FAILED(result))
	{
		return false;
	}

 

 

All we need to pay attention is the size of each resource. GPU doesn’t accept a memory/buffer/resource size which is not aligned. For 2 or more resources on the same heap, the data should be aligned to 64k, which is to say, each resource should be at least 64KB and its times. Here is what we will do:

UINT size = (m_nBuffSize / _64k + 1) * _64k;

But in this tutorial, there are only three vertices, which cover much less than 64k bytes. We can simply set the buffer size to 64kB.

When the 2 heaps are done, we will start uploading data. This is done by copying the vertices and indices into the upload resources.

        //Copy the vertex and index data to the buffers
	//vertices can only go into upload resource
	//[begin] copy vertices
	void* pvBegin = nullptr;
	m_vertexUpload->Map(0, nullptr, &pvBegin);
	memcpy(pvBegin, vertices, sizeof(VertexType) * m_vertexCount);
	m_vertexUpload->Unmap(0, nullptr);
	//[end] copy vertices

	//[begin] copy indices
	m_indexUpload->Map(0, nullptr, &pvBegin);
	memcpy(pvBegin, indices, sizeof(unsigned long) * m_indexCount);
	m_indexUpload->Unmap(0, nullptr);
	//[end] copy indices

 

When we have the data ready in the upload resource, we can copy it into video memory with copy engine. Before doing so, I’m going to introduce the resource barrier. You may have noticed that in the process of creating a resource, there is a flag for the current status of the resource like generic read or copy dest. This is crucial when the application is running in a multi-thread environment. The resource barrier can tell the threads which chunk of memory is protected to prevent covering data. To use a barrier, we need to fill in a form but we can also employ a D3DX helper method to get this done.

    

        barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
	barrier.Transition.pResource = m_vertexBuffer;
	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER;
	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;

	cmd->CopyBufferRegion(m_vertexBuffer, 0, m_vertexUpload, 0, sizeof(VertexType) * m_vertexCount);
	cmd->ResourceBarrier(1, &barrier);
	cmd->CopyBufferRegion(m_indexBuffer, 0, m_indexUpload, 0, sizeof(unsigned long) * m_indexCount);
	barrier.Transition.pResource = m_indexBuffer;
	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_INDEX_BUFFER;
	cmd->ResourceBarrier(1, &barrier);

 

Now you can see that after this, the default resource turns from copy dest to vertex/index buffer.

After the vertex buffer and index buffer have been created you can delete the vertex and index arrays as they are no longer needed since the data was copied into the buffers.

              

// Release the arrays now that the vertex and index buffers have been created and loaded.
	delete [] vertices;
	vertices = 0;

	delete [] indices;
	indices = 0;

	return true;
}

 

But remember! Do not release upload resource before executing the command list because the method is NOT actually executed at THIS MOMENT!!!! In DirectX 12, all the rendering commands are asynchronic methods. They will not work until the command list is executed. If the upload resource is released now, the copy engine can’t find its resource.

The ShutdownBuffers function just releases the heaps and resources that were created in the InitializeBuffers function.

void ModelClass::ShutdownBuffers()

void ModelClass::ShutdownBuffers()
{
	// Release the index buffer.
	if(m_indexBuffer)
	{
		m_indexBuffer->Release();
		m_indexBuffer = 0;
	}

	// Release the vertex buffer.
	if(m_vertexBuffer)
	{
		m_vertexBuffer->Release();
		m_vertexBuffer = 0;
	}
	if(m_vertexUpload)
	{
		m_vertexUpload->Release();
		m_vertexUpload = 0;
	}
	if(m_indexUpload)
	{
		m_indexUpload->Release();
		m_indexUpload = 0;
	}
	//Release the heap
	if (m_heapUpload)
	{
		m_heapUpload->Release();
		m_heapUpload = 0;
	}
	//Release the heap
	if (m_heapDefault)
	{
		m_heapDefault->Release();
		m_heapDefault = 0;
	}
	return;
}

 

RenderBuffers is called from the Render function. The purpose of this function is to set the vertex buffer and index buffer as active on the input assembler in the GPU. Once the GPU has an active vertex buffer it can then use the shader to render that buffer. This function also defines how those buffers should be drawn such as triangles, lines, fans, and so forth. In this tutorial we set the vertex buffer and index buffer as active in the command list and tell the GPU that the buffers should be drawn as triangles using the IASetPrimitiveTopology DirectX function.

void ModelClass::RenderBuffers(ID3D12GraphicsCommandList* cmd)
{
	D3D12_VERTEX_BUFFER_VIEW vbView;
	D3D12_INDEX_BUFFER_VIEW idView;
	// Set vertex buffer stride and offset.
	vbView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
	vbView.SizeInBytes = sizeof(VertexType) * m_vertexCount;
	vbView.StrideInBytes = sizeof(VertexType);
	// Set vertex buffer stride and offset.
	idView.BufferLocation = m_indexBuffer->GetGPUVirtualAddress();
	idView.Format = DXGI_FORMAT_R32_UINT;
	idView.SizeInBytes = sizeof(unsigned long) * m_indexCount;
	// Set the vertex buffer to active in the input assembler so it can be rendered.
	cmd->IASetVertexBuffers(0, 1, &vbView);

        // Set the index buffer to active in the input assembler so it can be rendered.
	cmd->IASetIndexBuffer(&idView);

        // Set the type of primitive that should be rendered from this vertex buffer, in this case triangles.
	cmd->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	return;
}

 

Colorshaderclass.h

The ColorShaderClass is what we will use to invoke our HLSL shaders for drawing the 3D models that are on the GPU.

////////////////////////////////////////////////////////////////////////////////
// Filename: colorshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _COLORSHADERCLASS_H_
#define _COLORSHADERCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <d3d12.h>
#include <d3dx12.h>
#include <directxmath.h>
#include <fstream>
#include <d3dcompiler.h>

#pragma comment(lib, "d3dcompiler.lib")
using namespace std;
using namespace DirectX;


////////////////////////////////////////////////////////////////////////////////
// Class name: ColorShaderClass
////////////////////////////////////////////////////////////////////////////////
class ColorShaderClass
{
private:

 

Here is the definition of the cBuffer type that will be used with the vertex shader. This typedef must be exactly the same as the one in the vertex shader as the model data needs to match the typedefs in the shader for proper rendering.

struct MatrixBufferType
	{
		XMMATRIX world;
		XMMATRIX view;
		XMMATRIX projection;
	};

public:
	ColorShaderClass();
	ColorShaderClass(const ColorShaderClass&);
	~ColorShaderClass();

The functions here handle initializing and shutdown of the shader. The render function sets the shader parameters and then draws the prepared model vertices using the shader. When the signature and the PSO are ready, we also need methods to send them out.
 

	bool Initialize(ID3D12Device*, HWND);
	void Shutdown();
	bool Render(ID3D12GraphicsCommandList*, int, XMMATRIX, XMMATRIX, XMMATRIX);
	ID3D12RootSignature* GetSignature();
	ID3D12PipelineState* GetPSO();

private:
        bool InitializeShader(ID3D12Device*, HWND, const WCHAR*);
        void ShutdownShader();
        void OutputShaderErrorMessage(ID3DBlob*, HWND, const WCHAR*);

        bool SetShaderParameters(ID3D12GraphicsCommandList*, XMMATRIX, XMMATRIX, XMMATRIX);
	void RenderShader(ID3D12GraphicsCommandList*, int);

private:
        ID3D12Resource* m_matrixBuffer;
        ID3D12DescriptorHeap* m_cbvHeap;
        ID3D12RootSignature* m_prtSignature;
        ID3D12PipelineState* m_pipelineState;
	void* m_pDataPtr;
};

#endif

 

Colorshaderclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: colorshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "colorshaderclass.h"

As usual the class constructor initializes all the private pointers in the class to null.
 

ColorShaderClass::ColorShaderClass()
{
	m_matrixBuffer = 0;
	m_cbvHeap = 0;
	m_prtSignature = 0;
	m_pipelineState = 0;
}

ColorShaderClass::ColorShaderClass(const ColorShaderClass& other)
{
}

ColorShaderClass::~ColorShaderClass()
{
}

The Initialize function will call the initialization function for the shaders. We pass in the name of the HLSL shader file, in this tutorial it is cbuffer,hlsl.

bool ColorShaderClass::Initialize(ID3D12Device* device, HWND hwnd)
{
	bool result;

	// Initialize the vertex and pixel shaders.
	result = InitializeShader(device, hwnd, L"color.hlsl");
	if(!result)
	{
		return false;
	}

	return true;
}

The Shutdown function will call the shutdown of the shader.

void ColorShaderClass::Shutdown()
{
	// Shutdown the vertex and pixel shaders as well as the related objects.
	ShutdownShader();

	return;
}

We need to pass the signature and pso to other classes to tell Direct3D how the scene is rendered.

ID3D12RootSignature* ColorShaderClass::GetSignature()
{
	return m_prtSignature;
}

ID3D12PipelineState* ColorShaderClass::GetPSO()
{
	return m_pipelineState;
}

Render will first set the parameters inside the shader using the SetShaderParameters function. Once the parameters are set it then calls RenderShader to draw the green triangle using the HLSL shader.

bool ColorShaderClass::Render(ID3D12GraphicsCommandList* cmd, int indexCount, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
							  XMMATRIX projectionMatrix)
{
	bool result;


	// Set the shader parameters that it will use for rendering.
	result = SetShaderParameters(cmd, worldMatrix, viewMatrix, projectionMatrix);
	if(!result)
	{
		return false;
	}

	// Now render the prepared buffers with the shader.
	RenderShader(cmd, indexCount);

	return true;
}

Now we will

bool ColorShaderClass::InitializeShader(ID3D12Device * device, HWND hwnd, const WCHAR* filename)
{
	HRESULT result;
	ID3DBlob* errorMessage;
	ID3DBlob* vertexShaderBuffer;
	ID3DBlob* pixelShaderBuffer;
	ID3DBlob* pSignatureBlob;
	D3D12_INPUT_ELEMENT_DESC polygonLayout[2];
	unsigned int numElements;
	D3D12_HEAP_PROPERTIES heapp = {};
	D3D12_RESOURCE_DESC bufferDesc;
	DXGI_SAMPLE_DESC sDesc;
	D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
	D3D12_DESCRIPTOR_RANGE1 cbvRange;
	D3D12_ROOT_DESCRIPTOR_TABLE1 descTable;
	D3D12_ROOT_PARAMETER1  rootParameters;
	D3D12_CONSTANT_BUFFER_VIEW_DESC matrixBufferDesc = {};

	D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {};//decide version
	D3D12_VERSIONED_ROOT_SIGNATURE_DESC vrtSignatureDesc = {};
	D3D12_ROOT_SIGNATURE_DESC1 rtSignatureDesc = {};
	D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc = {};

	// Initialize the pointers this function will use to null.
	errorMessage = 0;
	vertexShaderBuffer = 0;
	pixelShaderBuffer = 0;
	pSignatureBlob = 0;

Here is where we compile the shader programs into buffers. We give it the name of the shader file, the name of the shader, the shader version (5.0 in DirectX 12), and the buffer to compile the shader into. If it fails compiling the shader it will put an error message inside the errorMessage string which we send to another function to write out the error. If it still fails and there is no errorMessage string then it means it could not find the shader file in which case we pop up a dialog box saying so.
      

        // Compile the vertex shader code.
	result = D3DCompileFromFile(filename, NULL, NULL, "ColorVertexShader", "vs_5_0", 0, 0, &vertexShaderBuffer, &errorMessage);
	if(FAILED(result))
	{
		// If the shader failed to compile it should have writen something to the error message.
		if(errorMessage)
		{
			OutputShaderErrorMessage(errorMessage, hwnd, filename);
		}
		// If there was  nothing in the error message then it simply could not find the shader file itself.
		else
		{
			MessageBox(hwnd, filename, L"Missing Shader File", MB_ICONSTOP);
		}

		return false;
	}

        // Compile the pixel shader code.
	result = D3DCompileFromFile(filename, NULL, NULL, "ColorPixelShader", "ps_5_0", 0, 0, &pixelShaderBuffer, &errorMessage);
	if(FAILED(result))
	{
		// If the shader failed to compile it should have writen something to the error message.
		if(errorMessage)
		{
			OutputShaderErrorMessage(errorMessage, hwnd, filename);
		}
		// If there was nothing in the error message then it simply could not find the file itself.
		else
		{
			MessageBox(hwnd, filename, L"Missing Shader File", MB_ICONSTOP);
		}

		return false;
	}

Once the vertex shader and pixel shader code has successfully compiled into buffers we then pass the shader bytecode to the pipeline state object, PSO. We will fill up D3D12_GRAPHICS_PIPELINE_STATE_DESC structure with the vertex and pixel shader later.

The next step is to create the layout of the vertex data that will be processed by the shader. As this shader uses a position and color vector we need to create both in the layout specifying the size of both. The semantic name is the first thing to fill out in the layout, this allows the shader to determine the usage of this element of the layout. As we have two different elements we use POSITION for the first one and COLOR for the second. The next important part of the layout is the Format. For the position vector we use DXGI_FORMAT_R32G32B32_FLOAT and for the color we use DXGI_FORMAT_R32G32B32A32_FLOAT. The final thing you need to pay attention to is the AlignedByteOffset which indicates how the data is spaced in the buffer. For this layout we are telling it the first 12 bytes are position and the next 16 bytes will be color, AlignedByteOffset shows where each element begins. You can use D3D12_APPEND_ALIGNED_ELEMENT instead of placing your own values in AlignedByteOffset and it will figure out the spacing for you. The other settings I've made default for now as they are not needed in this tutorial.               

        // Now setup the layout of the data that goes into the shader.
	// This setup needs to match the VertexType stucture in the ModelClass and in the shader.
	polygonLayout[0].SemanticName = "POSITION";
	polygonLayout[0].SemanticIndex = 0;
	polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[0].InputSlot = 0;
	polygonLayout[0].AlignedByteOffset = 0;
	polygonLayout[0].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
	polygonLayout[0].InstanceDataStepRate = 0;

	polygonLayout[1].SemanticName = "COLOR";
	polygonLayout[1].SemanticIndex = 0;
	polygonLayout[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
	polygonLayout[1].InputSlot = 0;
	polygonLayout[1].AlignedByteOffset = D3D12_APPEND_ALIGNED_ELEMENT;
	polygonLayout[1].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
	polygonLayout[1].InstanceDataStepRate = 0;

Now that we have defined the format of our vertices, we can create a buffer to hold the matrices. In this case, we will create it in a way different from how we created the vertex and index buffer. And this is much easier than before. In DirectX12, we have 3 ways of creating buffers/resources. The previous way was the “Placed” resource, and now we will pick up the “Committed” way.

As a routine, we fill in the structure for a committed resource.

        //Fill up a description for the constant buffer
	heapp.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
	heapp.CreationNodeMask = 1;
	heapp.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
	heapp.Type = D3D12_HEAP_TYPE_UPLOAD;
	heapp.VisibleNodeMask = 1;
	
	//Setup the description for the sample desc
	sDesc.Count = 1;
	sDesc.Quality = 0;
	//Setup the description for the vertex buffer
	bufferDesc.Alignment = 0;//for constant buffer we set it to 0
	bufferDesc.DepthOrArraySize = 1;
	bufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
	bufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
	bufferDesc.Format = DXGI_FORMAT_UNKNOWN;
	bufferDesc.Height = 1;
	bufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
	bufferDesc.MipLevels = 1;
	bufferDesc.SampleDesc = sDesc;
	bufferDesc.Width = (sizeof(MatrixBufferType)/256+1)*256;

The buffer width is the length, which should be aligned to 256 bytes for a constant buffer.

        // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
	result = device->CreateCommittedResource(&heapp, D3D12_HEAP_FLAG_NONE,
		&bufferDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_matrixBuffer));
	if (FAILED(result))
	{
		return false;
	}

In this “Committed” way, we don’t care too much about the heap as both the heap and the resource are created at once. We simply need to tell DirectX12 the heap type and the buffer size, and the rest will be handled. This is because we don’t need to copy the resource for a constant buffer on the GPU side. Constant buffer will be modified almost every frame we render, so it will stay in the system memory to be uploaded via the PCIE interface.

Then at this moment we can get the address for the matrix buffer and then use that to access the internal variables in the shader using the function SetShaderParameters. Note that we don’t unmap this resource until we shut down this class.

    // Lock the constant buffer so it can be written to.
    m_matrixBuffer->Map(0, nullptr, &m_pDataPtr);

We will fill up the D3D12_GRAPHICS_PIPELINE_STATE_DESC structure with the input layout information to create PSO later. Note that do not release the shader blobs since they are not loaded into the PSO.

Next, we need to setup the constant buffer. As you saw in the vertex shader we currently have just one constant buffer so we only need to setup one here so we can interface with the shader. We need to set the heap for holding constant buffer by setting Type to CBV. The flags need to be set to shader visible. Once we fill out the description we can then create the descriptor heap for constant buffer and then use that to create a buffer view.               

        // Setup the description of the dynamic matrix constant buffer that is in the vertex shader.
        cbvHeapDesc.NumDescriptors = 1;//only for cbv, so there is only 1
	cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
	cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	cbvHeapDesc.NodeMask = 0;
	
	// Create the descriptor heap
	result = device->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&m_cbvHeap));
	if (FAILED(result))
	{
		return false;
	}

After the heap is ready, we will create a constant buffer view on this heap. The buffer view structure stores the information for a constant buffer, which is considered as a ”sampler” in DirectX12. In this structure, we need the address and the size of the matrix buffer.

        // Setup the description of the dynamic matrix constant buffer that is in the vertex shader.
	matrixBufferDesc.BufferLocation = m_matrixBuffer->GetGPUVirtualAddress();
	matrixBufferDesc.SizeInBytes = (sizeof(MatrixBufferType) / 256 + 1) * 256;//Must be rounded up to 256 or the device will be removed!
	device->CreateConstantBufferView(&matrixBufferDesc, m_cbvHeap->GetCPUDescriptorHandleForHeapStart());

We need a range to specify the type of data in it. This is done by filling up the range structure, or we can do it with a d3dx helper. In this way, DirectX knows how we will use this constant buffer.
 

        //Set up the cbv range
	cbvRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
	cbvRange.NumDescriptors = 1;
	cbvRange.BaseShaderRegister = 0;
	cbvRange.RegisterSpace = 0;
	cbvRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
	cbvRange.Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;

Then set the root parameter with the range we have just specified. We no longer use the empty root signature.
   

        //Fill desc table
	descTable.NumDescriptorRanges = 1;
	descTable.pDescriptorRanges = &cbvRange;

	//Fill in root parameters
	rootParameters.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; // this is a descriptor table type, not a constant
	rootParameters.DescriptorTable = descTable; // this is our descriptor table for this root parameter
	rootParameters.ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX; // our VERTEX shader will be the only shader accessing this parameter for now

 

When all the above is done, we can build the root signature for the pipeline, which plays a role as an argument list for the pipeline.

        //Decide the highest version
	featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
	result = device->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &featureData, sizeof(featureData));
	if (FAILED(result))
	{
		featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
	}

	//Fill up root signature desc
	rtSignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
	rtSignatureDesc.NumParameters = 1;
	rtSignatureDesc.NumStaticSamplers = 0;
	rtSignatureDesc.pParameters = &rootParameters;
	rtSignatureDesc.pStaticSamplers = nullptr;

	vrtSignatureDesc.Desc_1_1 = rtSignatureDesc;
	vrtSignatureDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;

    Here we build the signature and then send it to the PSO description.
   

        //Compile root signature
	result = D3DX12SerializeVersionedRootSignature(&vrtSignatureDesc, featureData.HighestVersion, &pSignatureBlob, &errorMessage);
	if (FAILED(result))
	{
		return false;
	}
	result = device->CreateRootSignature(0, pSignatureBlob->GetBufferPointer(), pSignatureBlob->GetBufferSize(), IID_PPV_ARGS(&m_prtSignature));
	if (FAILED(result))
	{
		return false;
	}

When the root signature is done, we can get ready to create the PSO, which depicts all the things we are going to do for the frame. The command list needs this to reset in every frame, so it’s the key object we expect. The pipeline state description contains many important information we need. Vertex and pixel shaders are also set here.
   

        //Build PSO
	PSODesc.InputLayout = { polygonLayout, numElements };
	PSODesc.pRootSignature = m_prtSignature;
	PSODesc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBuffer);
	PSODesc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBuffer);
	PSODesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
	PSODesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
	PSODesc.DepthStencilState.DepthEnable = FALSE;
	PSODesc.DepthStencilState.StencilEnable = FALSE;
	PSODesc.SampleMask = UINT_MAX;
	PSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
	PSODesc.NumRenderTargets = 1;
	PSODesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
	PSODesc.SampleDesc.Count = 1;
	result = device->CreateGraphicsPipelineState(&PSODesc, IID_PPV_ARGS(&m_pipelineState));
	if (FAILED(result))
	{
		return false;
	}

We no longer use the compile buffers, so release them.

        if (errorMessage)
	{
		errorMessage->Release();
		errorMessage = 0;
	}
	if (vertexShaderBuffer)
	{
		vertexShaderBuffer->Release();
		vertexShaderBuffer = 0;
	}
	if (pixelShaderBuffer)
	{
		pixelShaderBuffer->Release();
		pixelShaderBuffer = 0;
	}
	if (pSignatureBlob)
	{
		pSignatureBlob->Release();
		pSignatureBlob = 0;
	}

ShutdownShader releases the four interfaces that were setup in the InitializeShader function. At the same time, it also closes the constant buffer.

void ColorShaderClass::ShutdownShader()
{
	// Release the matrix constant buffer.
	if (m_matrixBuffer)
	{
		// Unlock the constant buffer.
		m_matrixBuffer->Unmap(0, nullptr);
		m_matrixBuffer->Release();
		m_matrixBuffer = 0;
	}

	// Release the layout.
	if(m_cbvHeap)
	{
		m_cbvHeap->Release();
		m_cbvHeap = 0;
	}

	// Release the pixel shader.
	if(m_prtSignature)
	{
		m_prtSignature->Release();
		m_prtSignature = 0;
	}

	// Release the vertex shader.
	if(m_pipelineState)
	{
		m_pipelineState->Release();
		m_pipelineState = 0;
	}
	return;
}

The OutputShaderErrorMessage writes out error messages that are generating when compiling either vertex shaders or pixel shaders.
 

void ColorShaderClass::OutputShaderErrorMessage(ID3DBlob* errorMessage, HWND hwnd, const WCHAR* shaderFilename)
{
	char* compileErrors;
	unsigned long bufferSize, i;
	ofstream fout;


	// Get a pointer to the error message text buffer.
	compileErrors = (char*)(errorMessage->GetBufferPointer());

	// Get the length of the message.
	bufferSize = errorMessage->GetBufferSize();

	// Open a file to write the error message to.
	fout.open("shader-error.txt");

	// Write out the error message.
	for(i=0; i<bufferSize; i++)
	{
		fout << compileErrors[i];
	}

	// Close the file.
	fout.close();

	// Release the error message.
	errorMessage->Release();
	errorMessage = 0;

	// Pop a message up on the screen to notify the user to check the text file for compile errors.
	MessageBox(hwnd, L"Error compiling shader.  Check shader-error.txt for message.", shaderFilename, MB_ICONSTOP);

	return;
}

The SetShaderParameters function exists to make setting the global variables in the shader easier. The matrices used in this function are created inside the GraphicsClass, after which this function is called to send them from there into the vertex shader during the Render function call.

bool ColorShaderClass::SetShaderParameters(ID3D12GraphicsCommandList* cmd, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
										   XMMATRIX projectionMatrix)
{
	MatrixBufferType cbData;

Make sure to transpose matrices before sending them into the shader, this is a requirement for DirectX 12.
   

        // Transpose the matrices to prepare them for the shader.
	cbData.world = XMMatrixTranspose(worldMatrix);
	cbData.view = XMMatrixTranspose(viewMatrix);
	cbData.projection = XMMatrixTranspose(projectionMatrix);

As the m_matrixBuffer has already been locked, set the new matrices inside it but do not unlock it.
      

        // Copy the matrices into the constant buffer.
	memcpy(m_pDataPtr, &cbData, sizeof(MatrixBufferType));
	return true;
}

}

RenderShader is the second function called in the Render function. SetShaderParameters is called before this to ensure the shader parameters are setup correctly.

The first step in this function is to record setting descriptor list in the command list. We specify the constant buffer view heap which contains all the information for the constant buffer. The second step is to set the root descriptor table. For now, we only have one table, but if there are more, the index should follow the element in the table even if the view descriptors are on one heap. Once the descriptor table is set we render the triangle by calling the DrawIndexedInstanced DirectX 12 function using the D3D command list. Once this function is called it will render the green triangle.

void ColorShaderClass::RenderShader(ID3D12GraphicsCommandList* cmd, int indexCount)
{
	// Set the vertex input layout.
	cmd->SetDescriptorHeaps(1, &m_cbvHeap);
	cmd->SetGraphicsRootDescriptorTable(0, m_cbvHeap->GetGPUDescriptorHandleForHeapStart());
    
	// Render the triangle.
	cmd->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);

	return;
}

Cameraclass.h

We have examined how to code HLSL shaders, how to setup vertex and index buffers, and how to invoke the HLSL shaders to draw those buffers using the ColorShaderClass. The one thing we are missing however is the view point to draw them from. For this we will require a camera class to let DirectX 12 know from where and also how we are viewing the scene. The camera class will keep track of where the camera is and its current rotation. It will use the position and rotation information to generate a view matrix which will be passed into the HLSL shader for rendering.

////////////////////////////////////////////////////////////////////////////////
// Filename: cameraclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _CAMERACLASS_H_
#define _CAMERACLASS_H_


//////////////
// INCLUDES //
//////////////
#include <directxmath.h>
using namespace DirectX;


////////////////////////////////////////////////////////////////////////////////
// Class name: CameraClass
////////////////////////////////////////////////////////////////////////////////
class CameraClass
{
public:
	CameraClass();
	CameraClass(const CameraClass&);
	~CameraClass();

	void SetPosition(float, float, float);
	void SetRotation(float, float, float);

	XMVECTOR GetPosition();
	XMVECTOR GetRotation();

	void Render();
	void GetViewMatrix(XMMATRIX&);

private:
	float m_positionX, m_positionY, m_positionZ;
	float m_rotationX, m_rotationY, m_rotationZ;
	XMMATRIX m_viewMatrix;
};

#endif

The CameraClass header is quite simple with just four functions that will be used. The SetPosition and SetRotation functions will be used to set the position and rotation of the camera object. Render will be used to create the view matrix based on the position and rotation of the camera. And finally GetViewMatrix will be used to retrieve the view matrix from the camera object so that the shaders can use it for rendering.

 

Cameraclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: cameraclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "cameraclass.h"

The class constructor will initialize the position and rotation of the camera to be at the origin of the scene.

CameraClass::CameraClass()
{
	m_positionX = 0.0f;
	m_positionY = 0.0f;
	m_positionZ = 0.0f;

	m_rotationX = 0.0f;
	m_rotationY = 0.0f;
	m_rotationZ = 0.0f;
}


CameraClass::CameraClass(const CameraClass& other)
{
}


CameraClass::~CameraClass()
{
}

The SetPosition and SetRotation functions are used for setting up the position and rotation of the camera.

void CameraClass::SetPosition(float x, float y, float z)
{
	m_positionX = x;
	m_positionY = y;
	m_positionZ = z;
	return;
}

void CameraClass::SetRotation(float x, float y, float z)
{
	m_rotationX = x;
	m_rotationY = y;
	m_rotationZ = z;
	return;
}

The GetPosition and GetRotation functions return the location and rotation of the camera to calling functions.

XMVECTOR CameraClass::GetPosition()
{
	return XMVectorSet(m_positionX, m_positionY, m_positionZ, 1.0f);
}


XMVECTOR CameraClass::GetRotation()
{
	return XMVectorSet(m_rotationX, m_rotationY, m_rotationZ, 1.0f);
}

The Render function uses the position and rotation of the camera to build and update the view matrix. We first setup our variables for up, position, rotation, and so forth. Then at the origin of the scene we first rotate the camera based on the x, y, and z rotation of the camera. Once it is properly rotated when then translate the camera to the position in 3D space. With the correct values in the position, lookAt, and up we can then use the D3DXMatrixLookAtLH function to create the view matrix to represent the current camera rotation and translation.

void CameraClass::Render()
{
	XMVECTOR up, position, lookAt;
	float yaw, pitch, roll;
	XMMATRIX rotationMatrix;


	// Setup the vector that points upwards.
	up = XMVectorSet(0.0f, 1.0f, 0.0f, 1.0f);

	// Setup the position of the camera in the world.
	position = XMVectorSet(m_positionX, m_positionY, m_positionZ, 1.0f);

	// Setup where the camera is looking by default.
	lookAt = XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f);

	// Set the yaw (Y axis), pitch (X axis), and roll (Z axis) rotations in radians.
	pitch = m_rotationX * 0.0174532925f;
	yaw   = m_rotationY * 0.0174532925f;
	roll  = m_rotationZ * 0.0174532925f;

	// Create the rotation matrix from the yaw, pitch, and roll values.
	rotationMatrix = XMMatrixRotationRollPitchYaw(pitch, yaw, roll);

	// Transform the lookAt and up vector by the rotation matrix so the view is correctly rotated at the origin.
	lookAt = XMVector3TransformCoord(lookAt, rotationMatrix);
	up = XMVector3TransformCoord(up, rotationMatrix);

	// Translate the rotated camera position to the location of the viewer.
	lookAt = position + lookAt;

	// Finally create the view matrix from the three updated vectors.
	m_viewMatrix = XMMatrixLookAtLH(position, lookAt, up);

	return;
}

After the Render function has been called to create the view matrix we can provide the updated view matrix to calling functions using this GetViewMatrix function. The view matrix will be one of the three main matrices used in the HLSL vertex shader.

void CameraClass::GetViewMatrix(XMMATRIX & viewMatrix)
{
	viewMatrix = m_viewMatrix;
	return;
}

D3DClass.h

D3DClass now starts to handle the command list in two steps. Before we start to render a frame, we need to reset it to prepare for a new cycle. When the scene is rendered, we also need to close the command list and execute it.

////////////////////////////////////////////////////////////////////////////////
// Filename: d3dclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _D3DCLASS_H_
#define _D3DCLASS_H_


/////////////
// LINKING //
/////////////
#pragma once
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d12.lib")


//////////////
// INCLUDES //
//////////////
#include <dxgi1_6.h>
#include <d3dcommon.h>
#include <d3d12.h>
#include <directxmath.h>
using namespace DirectX;

////////////////////////////////////////////////////////////////////////////////
// Class name: D3DClass
////////////////////////////////////////////////////////////////////////////////
class D3DClass
{
public:
	D3DClass();
	D3DClass(const D3DClass&);
	~D3DClass();

	bool Initialize(int, int, bool, HWND, bool, float, float);
	void Shutdown();
	
	bool BeginScene(float, float, float, float);
	bool EndScene();

	void SetRootSignature(ID3D12RootSignature*);
	void SetRenderState(ID3D12PipelineState*);
	bool FinishUp();

	ID3D12Device* GetDevice();
	ID3D12GraphicsCommandList* GetCommandList();

	void GetProjectionMatrix(XMMATRIX&);
	void GetWorldMatrix(XMMATRIX&);
	void GetOrthoMatrix(XMMATRIX&);
	void GetVideoCardInfo(char*, int&);

private:
	bool m_vsync_enabled;
	ID3D12Device* m_device;
	ID3D12CommandQueue* m_commandQueue;
	int m_videoCardMemory;
	char m_videoCardDescription[128];
	IDXGISwapChain3* m_swapChain;
	ID3D12DescriptorHeap* m_renderTargetViewHeap;
	ID3D12Resource* m_backBufferRenderTarget[2];
	unsigned int m_bufferIndex;
	ID3D12CommandAllocator* m_commandAllocator;
	ID3D12CommandList* m_ppCommandLists[1];
	ID3D12GraphicsCommandList* m_commandList;
	ID3D12RootSignature* m_prtSignature;
	ID3D12PipelineState* m_pipelineState;
	ID3D12Fence* m_fence;
	HANDLE m_fenceEvent;
	unsigned long long m_fenceValue;
#if defined(_DEBUG)
	ID3D12Debug* debugController;
#endif
 
	D3D12_VIEWPORT m_vp;
	D3D12_RECT m_sr;

	XMMATRIX m_projectionMatrix;
	XMMATRIX m_worldMatrix;
	XMMATRIX m_orthoMatrix;
};

D3DClass.cpp

I have changed the render sequence, so we can render the scene in several steps with other class. In the BeginScene method we do preparation.

bool D3DClass::BeginScene(float red, float green, float blue, float alpha)
{
	// Reset (re-use) the memory associated command allocator.
	D3D12_RESOURCE_BARRIER barrier;
	D3D12_CPU_DESCRIPTOR_HANDLE renderTargetViewHandle;
	unsigned int renderTargetViewDescriptorSize;
	float color[4];

	HRESULT result = m_commandAllocator->Reset();
	if (FAILED(result))
	{
		return false;
	}

	// Reset the command list with the PSO passed from the shader class to tell Direct3D what buffers we have and how they should be rendered to the scene.
	result = m_commandList->Reset(m_commandAllocator, m_pipelineState);
	if (FAILED(result))
	{
		return false;
	}
	// Record commands in the command list now.
	m_commandList->SetGraphicsRootSignature(m_prtSignature);
	// Create the viewport.
	m_commandList->RSSetViewports(1, &m_vp);
	m_commandList->RSSetScissorRects(1, &m_sr);
	// Start by setting the resource barrier.
	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
	barrier.Transition.pResource = m_backBufferRenderTarget[m_bufferIndex];
	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
	m_commandList->ResourceBarrier(1, &barrier);

	// Get the render target view handle for the current back buffer.
	renderTargetViewHandle = m_renderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
	renderTargetViewDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	if (m_bufferIndex == 1)
	{
		renderTargetViewHandle.ptr += renderTargetViewDescriptorSize;
	}

	// Set the back buffer as the render target.
	m_commandList->OMSetRenderTargets(1, &renderTargetViewHandle, FALSE, NULL);
	// Then set the color to clear the window to.
	color[0] = red;
	color[1] = green;
	color[2] = blue;
	color[3] = alpha;
	m_commandList->ClearRenderTargetView(renderTargetViewHandle, color, 0, NULL);
	return true;
}

 

And in EndScene we execute the command list. This is where the draw call actually executes. The Render method in the previous tutorial is removed, and for multi-thread application we still need to modify EndScene method in future tutorials.

bool D3DClass::EndScene()
{
	// Indicate that the back buffer will now be used to present.
	D3D12_RESOURCE_BARRIER barrier;
	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
	barrier.Transition.pResource = m_backBufferRenderTarget[m_bufferIndex];
	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
	m_commandList->ResourceBarrier(1, &barrier);
	// Close the list of commands.
	HRESULT result = m_commandList->Close();
	if (FAILED(result))
	{
		return false;
	}

	// Load the command list array (only one command list for now).
	m_ppCommandLists[0] = m_commandList;

	// Execute the list of commands.
	m_commandQueue->ExecuteCommandLists(1, m_ppCommandLists);
	// Finally present the back buffer to the screen since rendering is complete.
	if (m_vsync_enabled)
	{
		// Lock to screen refresh rate.
		result = m_swapChain->Present(1, 0);
		if (FAILED(result))
		{
			return false;
		}
	}
	else
	{
		// Present as fast as possible.
		result = m_swapChain->Present(0, 0);
		if (FAILED(result))
		{
			return false;
		}
	}
	// Signal and increment the fence value.
	UINT64 fenceToWaitFor = m_fenceValue;
	result = m_commandQueue->Signal(m_fence, fenceToWaitFor);
	if (FAILED(result))
	{
		return false;
	}
	m_fenceValue++;

	// Wait until the GPU is done rendering.
	if (m_fence->GetCompletedValue() < fenceToWaitFor)
	{
		result = m_fence->SetEventOnCompletion(fenceToWaitFor, m_fenceEvent);
		if (FAILED(result))
		{
			return false;
		}
		WaitForSingleObject(m_fenceEvent, INFINITE);//replace this with other tasks
	}
	// Alternate the back buffer index back and forth between 0 and 1 each frame.
	m_bufferIndex == 0 ? m_bufferIndex = 1 : m_bufferIndex = 0;
	
	return true;
}

Graphicsclass.h

GraphicsClass now has the three new classes added to it. CameraClass, ModelClass, and ColorShaderClass have headers added here as well as private member variables. Remember that GraphicsClass is the main class that is used to render the scene by invoking all the needed class objects for the project.

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "d3dclass.h"
#include "cameraclass.h"
#include "modelclass.h"
#include "colorshaderclass.h"


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = true;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;


////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
	GraphicsClass();
	GraphicsClass(const GraphicsClass&);
	~GraphicsClass();

	bool Initialize(int, int, HWND);
	void Shutdown();
	bool Frame();

private:
	bool Render();

private:
	D3DClass* m_D3D;
	CameraClass* m_Camera;
	ModelClass* m_Model;
	ColorShaderClass* m_ColorShader;
};

#endif

 

Graphicsclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"

The first change to GraphicsClass is initializing the camera, model, and color shader objects in the class constructor to null.

GraphicsClass::GraphicsClass()
{
	m_D3D = 0;
	m_Camera = 0;
	m_Model = 0;
	m_ColorShader = 0;
}

The Initialize function has also been updated to create and initialize the three new objects.

bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
	bool result;


	// Create the Direct3D object.
	m_D3D = new D3DClass;
	if(!m_D3D)
	{
		return false;
	}

	// Initialize the Direct3D object.
	result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize Direct3D.", L"Error", MB_ICONSTOP);
		return false;
	}

	// Create the camera object.
	m_Camera = new CameraClass;
	if(!m_Camera)
	{
		return false;
	}

	// Set the initial position of the camera.
	m_Camera->SetPosition(0.0f, 0.0f, -10.0f);
	
	// Create the model object.
	m_Model = new ModelClass;
	if(!m_Model)
	{
		return false;
	}

	// Initialize the model object.
	result = m_Model->Initialize(m_D3D->GetDevice(), m_D3D->GetCommandList());
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_ICONSTOP);
		return false;
	}

	// Create the color shader object.
	m_ColorShader = new ColorShaderClass;
	if(!m_ColorShader)
	{
		return false;
	}

	// Initialize the color shader object.
	result = m_ColorShader->Initialize(m_D3D->GetDevice(), hwnd);
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize the color shader object.", L"Error", MB_ICONSTOP);
		return false;
	}

	// Update signature and PSO
	m_D3D->SetRootSignature(m_ColorShader->GetSignature());
	m_D3D->SetRenderState(m_ColorShader->GetPSO());

	//Finish the initialization stage
	result = m_D3D->FinishUp();
	if (!result)
	{
		MessageBox(hwnd, L"Could not finish initialization D3D.", L"Error", MB_ICONSTOP);
		return false;
	}
	return true;
}

Shutdown is also updated to shutdown and release the three new objects.

void GraphicsClass::Shutdown()
{
	// Release the color shader object.
	if(m_ColorShader)
	{
		m_ColorShader->Shutdown();
		delete m_ColorShader;
		m_ColorShader = 0;
	}

	// Release the model object.
	if(m_Model)
	{
		m_Model->Shutdown();
		delete m_Model;
		m_Model = 0;
	}

	// Release the camera object.
	if(m_Camera)
	{
		delete m_Camera;
		m_Camera = 0;
	}

	// Release the Direct3D object.
	if(m_D3D)
	{
		m_D3D->Shutdown();
		delete m_D3D;
		m_D3D = 0;
	}

	return;
}

The Frame function has remained the same as the previous tutorial.

bool GraphicsClass::Frame()
{
	bool result;


	// Render the graphics scene.
	result = Render();
	if(!result)
	{
		return false;
	}

	return true;
}

As you would expect the Render function had the most changes to it. It begins with clearing the scene except that it is cleared to black. After that it calls the Render function for the camera object to create a view matrix based on the camera's location that was set in the Initialize function. Once the view matrix is created we get a copy of it from the camera class. We also get copies of the world and projection matrix from the D3DClass object. We then call the ModelClass::Render function to put the green triangle model geometry on the graphics pipeline. With the vertices now prepared we call the color shader to draw the vertices using the model information and the three matrices for positioning each vertex. The green triangle is now drawn to the back buffer. With that the scene is complete and we call EndScene to display it to the screen.

bool GraphicsClass::Render()
{
	XMMATRIX viewMatrix, projectionMatrix, worldMatrix;
	bool result;


	// Clear the buffers to begin the scene.
	m_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

	// Generate the view matrix based on the camera's position.
	m_Camera->Render();

	// Get the world, view, and projection matrices from the camera and d3d objects.
	m_Camera->GetViewMatrix(viewMatrix);
	m_D3D->GetWorldMatrix(worldMatrix);
	m_D3D->GetProjectionMatrix(projectionMatrix);

	// Put the model vertex and index buffers on the graphics pipeline to prepare them for drawing.
	m_Model->Render(m_D3D->GetDeviceContext());

	// Render the model using the color shader.
	result = m_ColorShader->Render(m_D3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix);
	if(!result)
	{
		return false;
	}

	// Present the rendered scene to the screen.
	m_D3D->EndScene();

	return true;
}

 

Summary

So in summary you should have learned the basics about how vertex and index buffers work. You should have also learned the basics of vertex and pixel shaders and how to write them using HLSL. And finally you should understand how we've incorporated these new concepts into our frame work to produce a green triangle that renders to the screen. I also want to mention that I realize the code is fairly long just to draw a single triangle and it could have all been stuck inside a single main() function. However I did it this way with a proper frame work so that the coming tutorials require very few changes in the code to do far more complex graphics.

 

To Do Exercises

1. Compile and run the tutorial. Ensure it draws a green triangle to the screen. Press escape to quit once it does.

2. Change the color of the triangle to red.

3. Change the triangle to a square.

4. Move the camera back 10 more units.

5. Change the pixel shader to output the color half as bright. (huge hint: multiply something in ColorPixelShader by 0.5f)

 

Source Code

Source Only: dx12src04.zip

Executable Only: dx12exe04.zip

http://www.rastertek.com/pic1002.gif

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!