Skip to content

Rotating Pyramid Demo

Tiago Jose Sousa Magalhães edited this page Dec 22, 2019 · 5 revisions

On this page you will see a complete demo on creating a rotating pyramid on screen.

To achieve that we need to do the following things:

  • Create a window
  • Create a thread for the DX12 code
  • Create a window update loop
  • Create a device
  • Compile the Vertex and Pixel shaders
  • Create the corresponding root signatures
  • Create the pipeline state object
  • Create a fence
  • Create a command list
  • Create the swapchain
  • Create the Vertex and Index buffers
  • Create a constant buffer that will contain the model view projection matrix
  • Create the rendering loop
    • Update Model View Projection Matrix
    • Set the render targets into rendering mode
    • Set the root signature
    • Clear render targets and depth buffers
    • Set render targets
    • Set descriptor heaps to be used in shaders
    • Set index and vertex buffers
    • Set constant buffers
    • Issue draw call
    • Set render targets into present mode from render mode
    • Wait for frame to be rendered
    • Reset command list and clear command allocators

Create a window

We need to create a window to display our rendered images to, for this we use the class Window, we give it the resolution of the window and the name of the window. This needs to be done inside a WinMain which is a Windows API main function.

int WINAPI CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
							LPSTR lpCmdLine, int nCmdShow)
{
	DXR::Window window{hInstance,nCmdShow,{1280,720},"DX Renderer"};

	...

	return 0;
}

Create a thread for the DX12 code

The render update loop and the window update loop can sometimes cause issues when they are in the same thread due to race conditions that lead to a deadlock, to avoid this we run the dx12 render loop on a separate thread from the window update thread. The window update thread must always be ran in the same thread as the window was created in, that is why it is kept in the main thread. To create the thread we simply rely on the STL's implementation of threads, which is easy enough and powerful enough for our needs in this case.

void MainDirectXThread(DXR::Window& window){...}

int WINAPI CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
							LPSTR lpCmdLine, int nCmdShow)
{
	...
	std::thread main_dx12_thread(MainDirectXThread, std::ref(window));
	...
	return 0;
}

Create a window update loop

To keep the window needs to keep being updated so that it can process all events it needs to, we do this with the UpdateWindow method. When we close the window the variable ShouldContinue will be set to false causing us to exit from the loop and end the program.

int WINAPI CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
							LPSTR lpCmdLine, int nCmdShow)
{
	...

	while(window.ShouldContinue)
	{
		window.UpdateWindow();
	}

	...
	return 0;
}

Create a device

We create a device which represents a GPU.

DXR::GraphicsDevice device(1);

Compile the Vertex and Pixel shaders

We need to create shaders that will handle where our vertices will be positioned as well as their color

DXR::VertexShader vs = DXR::VertexShader::CompileShaderFromFile(L"/Resources/Shaders/VertexShader.hlsl", "VSMain");
DXR::PixelShader ps = DXR::PixelShader::CompileShaderFromFile(L"/Resources/Shaders/VertexShader.hlsl", "PSMain");

Create the corresponding root signatures

We need to create a root signature for the shaders we specified, the root signature specifies the types of data the shader expect to receive from the CPU. Here we add a descriptor table with a constant buffer as that is what our shaders require.

DXR::RootSignature root_signature;
DXR::DescriptorTableRootParameter desc_table;
desc_table.AddCBVEntry(1);
root_signature.AddDescriptorTableRootParameter(desc_table);
root_signature.CreateRootSignature(device);

Create the pipeline state object

The pipeline state object control's the configuration of the graphics pipeline, to create it we need to supply the shaders we will use, the root signature, the vertex buffer data layout, the backbuffer layout and the depth stencil buffer layout. There are other ways to configure the pipeline however they are currently not available without modifying the pipeline state object code directly.

DXR::PipelineStateObject pso = {
	device,
	vs.GetShaderBytecode(),
	ps.GetShaderBytecode(),
	root_signature,
	DXR::Vertex::GetInputLayout(),
	DXR::Swapchain::m_backbuffer_format,
	DXR::DepthStencilBuffer::DepthStencilBufferFormat};

Create a fence

We create the fence that we will need for frame synchronization

DXR::Fence fence = device.CreateFence(0);

Create a command list

We create the command list that we will use to issue commands and then reset it, in the proccess setting the pipeline state object we will use.

DXR::GraphicsCommandList commandList = device.CreateGraphicsCommandList();
commandList.GetCommandAllocator()->Reset();
commandList->Reset(commandList.GetCommandAllocator(), pso.GetPipelineStateObject());

Create the swapchain

We create the swapchain that will handle the buffers we draw to.

DXR::Swapchain swapchain = device.CreateSwapchain(window, 60, commandList);

Create the Vertex and Index buffers

We create a vertex buffer with the positions and colors of the vertices we need for a pyramid. We also create an index buffer so that the vertices will be used to draw the various faces of the pyramid, vertices are being defined in clockwise order, however this can be changed in the Renderer configuration portion of the pipeline state object.

DXR::VertexBuffer<DXR::Vertex> vertex_buffer(device, commandList,
											 {
												 {{-1.0f, -1.0f,  1.0f},{1.0f,1.0f,1.0f,1.0f}},
												{{1.0f, -1.0f,  -1.0f},{0.0f,1.0f,0.0f,1.0f}},
												{{1.0f, -1.0f,  1.0f},{1.0f,0.0f,0.0f,1.0f}},
												{{-1.0f, -1.0f,  -1.0f},{0.0f,0.0f,1.0f,1.0f}},
												{{0.0f, 1.0f,  0.0f},{0.0f,0.0f,0.0f,1.0f}},
											 });
DXR::IndexBuffer index_buffer(device, commandList, 
								{   0,2,1,
									0,1,3,
									0,2,4,
									2,1,4,
									1,3,4,
									3,0,4
							  });

Create a constant buffer that will contain the model view projection matrix

We create a constant buffer that we will contain the model view projection matrix that we use to transfer the vertices coordinates to a homogeneous coordinate space.

DirectX::XMMATRIX projection = DirectX::XMMatrixPerspectiveFovLH(0.25f * DirectX::XM_PI, 1280.0f / 720.0f, 0.1f, 1000.0f);
DirectX::XMMATRIX view = DirectX::XMMatrixLookAtLH({0.0f,0.0f,-10.0f,1.0f}, {0.0f,0.0f,0.0f,1.0f}, {0.0f,1.0f,0.0f,0.0f});
DirectX::XMMATRIX model = DirectX::XMMatrixScaling(1, 1, 1);
DirectX::XMMATRIX mvp = model * view * projection;
DXR::ConstantBuffer<DirectX::XMMATRIX> constant_buffer(device, {mvp});

Create the rendering loop

Update Model View Projection Matrix

Each frame we update the model view projection matrix, here we rotate te pyramid a bit every frame

{
	scale_factor += scale_step;
	model = DirectX::XMMatrixRotationAxis({0.0f,1.0f,0.0f},scale_factor);
	mvp = model * view * projection;
	constant_buffer.UpdateData({mvp});
}

Set the render targets into rendering mode

The prepare method transitions the render targets from present mode to render target mode

swapchain.Prepare(commandList);

Set the root signature

We set the root signature we need for the draw call we will execute

commandList->SetGraphicsRootSignature(root_signature.GetRootSignature());

Clear render targets and depth buffers

We clear the various buffers so that data from the previous frame will not interfere in the current frame

commandList->ClearRenderTargetView(swapchain.GetCurrentBackBufferDescriptor(), color, 0, nullptr);
commandList->ClearDepthStencilView(swapchain.GetDepthStencilBufferDescriptor(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f,  0, nullptr);

Set render targets

We set the buffer we will be rendering into

commandList->OMSetRenderTargets(1, &swapchain.GetCurrentBackBufferDescriptor(), FALSE, &swapchain.GetDepthStencilBufferDescriptor());

Set descriptor heaps to be used in shaders

We set descriptor heaps that we will need to access during the rendering proccess

ID3D12DescriptorHeap* heaps[] = {constant_buffer.GetDescriptorHeap()->GetRAWInterface()};
commandList->SetDescriptorHeaps(_countof(heaps), heaps);

Set index and vertex buffers

We need to bind the vertex and index buffer to the pipeline as well as tell the pipeline how we want our fragments to be displayed, here we are using triangles but we could also use square or lines, among many other options.

commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertex_buffer.GetVertexBufferDescriptor());
commandList->IASetIndexBuffer(&index_buffer.GetIndexBufferDescriptor());

Set constant buffers

We bind the constant buffer descriptor to the pipeline for usage

commandList->SetGraphicsRootDescriptorTable(0, constant_buffer.GetDescriptorHeap()->Get(0));

Issue draw call

We issue a draw call

commandList->DrawIndexedInstanced(6*3, 1, 0, 0, 0);

Set render targets into present mode from render mode

We transition the render targets from render target mode to present mode, then we close the command list and order the command queue to execute it.

swapchain.PrepareBackbufferForPresentation(commandList);
commandList->Close();
(*device.GetGraphicsCommandQueue())->ExecuteCommandLists(1, commandLists)
swapchain.Present(commandList);

Wait for frame to be rendered

We wait for the frame to finish rendering so that we can safely reset the command list and its command allocator

device.GetGraphicsCommandQueue()->Flush(fence);

Reset command list and clear command allocators

We reset the comamnd list and its command allocator so that we do not need to create a new command list for every frame

commandList.GetCommandAllocator()->Reset();
commandList->Reset(commandList.GetCommandAllocator(), pso.GetPipelineStateObject());

Full Sample Code

#include <Windows.h>
#include <thread>
#include "Tooling/Log.hpp"
#include "Core/Windows Abstractions/Window.hpp"
#include "Core/Components/GraphicsDevice.hpp"
#include "Core/Components/Fence.hpp"
#include "Core/Components/Command List/GraphicsCommandList.hpp"
#include "Core/Components/Swapchain.hpp"
#include "Core/Components/Vertices/VertexBuffer.hpp"
#include "Core/Components/Vertices/IndexBuffer.hpp"
#include "Core/Components/Pipeline/PipelineStateObject.hpp"
#include "Core/Components/Shader/VertexShader.hpp"
#include "Core/Components/Shader/PixelShader.hpp"
#include "Core/Components/Resource/ConstantBuffer.hpp"

void MainDirectXThread(DXR::Window& window)
{
	SUCCESS_LOG(L"Main DirectX12 Thread Started");
	DXR::GraphicsDevice device(1);

	DXR::VertexShader vs = DXR::VertexShader::CompileShaderFromFile(L"/Resources/Shaders/VertexShader.hlsl", "VSMain");
	DXR::PixelShader ps = DXR::PixelShader::CompileShaderFromFile(L"/Resources/Shaders/VertexShader.hlsl", "PSMain");

	DXR::RootSignature root_signature;
	DXR::DescriptorTableRootParameter desc_table;
	desc_table.AddCBVEntry(1);
	root_signature.AddDescriptorTableRootParameter(desc_table);
	root_signature.CreateRootSignature(device);

	DXR::PipelineStateObject pso = {
		device,
		vs.GetShaderBytecode(),
		ps.GetShaderBytecode(),
		root_signature,
		DXR::Vertex::GetInputLayout(),
		DXR::Swapchain::m_backbuffer_format,
		DXR::DepthStencilBuffer::DepthStencilBufferFormat};

	DXR::Fence fence = device.CreateFence(0);
	DXR::GraphicsCommandList commandList = device.CreateGraphicsCommandList();

	commandList.GetCommandAllocator()->Reset();
	commandList->Reset(commandList.GetCommandAllocator(), pso.GetPipelineStateObject());

	DXR::Swapchain swapchain = device.CreateSwapchain(window, 60, commandList);
	DXR::VertexBuffer<DXR::Vertex> vertex_buffer(device, commandList,
												 {
													 {{-1.0f, -1.0f,  1.0f},{1.0f,1.0f,1.0f,1.0f}},
													{{1.0f, -1.0f,  -1.0f},{0.0f,1.0f,0.0f,1.0f}},
													{{1.0f, -1.0f,  1.0f},{1.0f,0.0f,0.0f,1.0f}},
													{{-1.0f, -1.0f,  -1.0f},{0.0f,0.0f,1.0f,1.0f}},
													{{0.0f, 1.0f,  0.0f},{0.0f,0.0f,0.0f,1.0f}},
												 });
	DXR::IndexBuffer index_buffer(device, commandList, 
									{   0,2,1,
										0,1,3,
										0,2,4,
										2,1,4,
										1,3,4,
										3,0,4
								  });

	commandList->Close();
	ID3D12CommandList* commandLists[] = {commandList.GetRAWInterface()};
	(*device.GetGraphicsCommandQueue())->ExecuteCommandLists(1, commandLists);
	device.GetGraphicsCommandQueue()->Flush(fence);

	DirectX::XMMATRIX projection = DirectX::XMMatrixPerspectiveFovLH(0.25f * DirectX::XM_PI, 1280.0f / 720.0f, 0.1f, 1000.0f);
	DirectX::XMMATRIX view = DirectX::XMMatrixLookAtLH({0.0f,0.0f,-10.0f,1.0f}, {0.0f,0.0f,0.0f,1.0f}, {0.0f,1.0f,0.0f,0.0f});
	DirectX::XMMATRIX model = DirectX::XMMatrixScaling(1, 1, 1);

	DirectX::XMMATRIX mvp = model * view * projection;
	DXR::ConstantBuffer<DirectX::XMMATRIX> constant_buffer(device, {mvp});

	commandList.GetCommandAllocator()->Reset();
	commandList->Reset(commandList.GetCommandAllocator(), pso.GetPipelineStateObject());

	FLOAT color[4] = {0.4f, 0.6f, 0.9f, 1.0f};

	float scale_factor = 1;
	float scale_step = 0.1f;

	while(window.ShouldContinue)
	{
		{
			scale_factor += scale_step;
			model = DirectX::XMMatrixRotationAxis({0.0f,1.0f,0.0f},scale_factor);
			mvp = model * view * projection;
			constant_buffer.UpdateData({mvp});
		}

		commandList->SetGraphicsRootSignature(root_signature.GetRootSignature());
		swapchain.Prepare(commandList);

		commandList->ClearRenderTargetView(swapchain.GetCurrentBackBufferDescriptor(), color, 0, nullptr);
		commandList->ClearDepthStencilView(swapchain.GetDepthStencilBufferDescriptor(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
		commandList->OMSetRenderTargets(1, &swapchain.GetCurrentBackBufferDescriptor(), FALSE, &swapchain.GetDepthStencilBufferDescriptor());
		ID3D12DescriptorHeap* heaps[] = {constant_buffer.GetDescriptorHeap()->GetRAWInterface()};
		commandList->SetDescriptorHeaps(_countof(heaps), heaps);

		commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
		commandList->IASetVertexBuffers(0, 1, &vertex_buffer.GetVertexBufferDescriptor());
		commandList->IASetIndexBuffer(&index_buffer.GetIndexBufferDescriptor());
		commandList->SetGraphicsRootDescriptorTable(0, constant_buffer.GetDescriptorHeap()->Get(0));

		commandList->DrawIndexedInstanced(6*3, 1, 0, 0, 0);
		swapchain.PrepareBackbufferForPresentation(commandList);

		commandList->Close();
		(*device.GetGraphicsCommandQueue())->ExecuteCommandLists(1, commandLists);

		swapchain.Present(commandList);

		device.GetGraphicsCommandQueue()->Flush(fence);

		commandList.GetCommandAllocator()->Reset();
		commandList->Reset(commandList.GetCommandAllocator(), pso.GetPipelineStateObject());
	}

}

int WINAPI CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
							LPSTR lpCmdLine, int nCmdShow)
{
	DXR::Window window{hInstance,nCmdShow,{1280,720},"DX Renderer"};

	std::thread main_dx12_thread(MainDirectXThread, std::ref(window));

	while(window.ShouldContinue)
	{
		window.UpdateWindow();
	}

	main_dx12_thread.join();
	return 0;
}

Shader Code

cbuffer MVPBuffer : register(b0)
{
	float4x4 MVP;
};

struct VS_OUTPUT
{
	float4 position:SV_POSITION;
	float4 col:COLOR;
};

struct VS_INPUT
{
	float3 pos:POSITION;
	float4 col:COLOR;
};

VS_OUTPUT VSMain(VS_INPUT input)
{
	VS_OUTPUT output;

	output.position = mul(MVP,float4(input.pos, 1.0f));
	output.col = input.col;

	return output;
}

struct PS_OUTPUT
{
	float4 color:SV_TARGET;
};

PS_OUTPUT PSMain(VS_OUTPUT input)
{
	PS_OUTPUT output;

	output.color = input.col;

	return output;
}