-
Notifications
You must be signed in to change notification settings - Fork 3
Rotating Pyramid Demo
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
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;
}
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;
}
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;
}
We create a device which represents a GPU.
DXR::GraphicsDevice device(1);
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");
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);
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};
We create the fence that we will need for frame synchronization
DXR::Fence fence = device.CreateFence(0);
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());
We create the swapchain that will handle the buffers we draw to.
DXR::Swapchain swapchain = device.CreateSwapchain(window, 60, commandList);
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
});
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});
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});
}
The prepare
method transitions the render targets from present mode to render target mode
swapchain.Prepare(commandList);
We set the root signature we need for the draw call we will execute
commandList->SetGraphicsRootSignature(root_signature.GetRootSignature());
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);
We set the buffer we will be rendering into
commandList->OMSetRenderTargets(1, &swapchain.GetCurrentBackBufferDescriptor(), FALSE, &swapchain.GetDepthStencilBufferDescriptor());
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);
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());
We bind the constant buffer descriptor to the pipeline for usage
commandList->SetGraphicsRootDescriptorTable(0, constant_buffer.GetDescriptorHeap()->Get(0));
We issue a draw call
commandList->DrawIndexedInstanced(6*3, 1, 0, 0, 0);
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);
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);
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());
#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;
}
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;
}