DirectX12 三角形的绘制
一、Dx12三角形的绘制
上一章说完了Dx12的绘制总流程,但只有一个背景板,因此我们在这章要绘制一个三角形。
在这一阶段中,主要就是补充完上一次总流程的一个核心步骤,但对此步骤,我们是要前面有些准备的,如下:
1、准备资产;
2、核心绘制;
二、准备资产(InitAsset)
我们想要绘制一个三角形,就要去向dx12要,而dx12会提出两个问题:
1、想要一个什么样的三角形
2、三角形需要什么加工
这两个问题的回答,就是 准备网格体(Mesh)和 准备渲染管线(PipeLineStateObject)。
void cDx12Rendering::InitAsset()
{
//准备网格体
BuildMeshData();
//准备渲染管线
BuildPSO();
}
1、准备网格体(BuildMeshData)
(1)定义三角形
首先,需要我们先去定义一个三角形的基础数据结构,也就是确定具有那些基础属性,比如位置,比如颜色
struct cVertex
{
cVertex(const XMFLOAT3& InPos, const XMFLOAT4& InColor)
{
m_mPosition = InPos;
m_mColor = InColor;
}
XMFLOAT3 m_mPosition;
XMFLOAT4 m_mColor;
};
然后,创建根据数据结构,创建出这个三角形,如下:
cVertex triangleVertices[] =
{
{ { 0.0f, 0.5f, 0.0f }, XMFLOAT4(Colors::Red) },
{ { 0.5f, -0.5f, 0.0f }, XMFLOAT4(Colors::Green)},
{ { -0.5f, -0.5f, 0.0f }, XMFLOAT4(Colors::Blue) }
};
诚然,我们是不能直接将vertex buffer绑定到pipeline上,而需要使用descriptor,即在Dx12中使用 D3D12_VERTEX_BUFFER_VIEW 结构体表示:
const UINT triangleVerticesSize = sizeof(triangleVertices);
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.SizeInBytes = triangleVerticesSize;
m_vertexBufferView.StrideInBytes = sizeof(cVertex);
(2)创建内存存放区
这里涉及到了CPU和GPU的内存交互,如下图:
理论上上传资源应该是使用upload heap,但由于我们很多资源是不会改变的,而GPU每帧都要读取上传堆里面的资源,就会造成性能问题。
因此针对于这类数据来自于CPU,且属于 静态数据 的情况,我们往往是先创建一个resource在default heap,然后在创建一个resource在upload heap作媒介,通过upload heap的resource将CPU数据传输到default heap的resource上,后续GPU只需要读取default heap上的resource即可,达到最优性能。
//默认堆(default heap),处在显存中的
CD3DX12_RESOURCE_DESC BufferResourceDESC = CD3DX12_RESOURCE_DESC::Buffer(triangleVerticesSize);
CD3DX12_HEAP_PROPERTIES BufferProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
m_spD3dDevice->CreateCommittedResource(
&BufferProperties,
D3D12_HEAP_FLAG_NONE,
&BufferResourceDESC,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer));
//上传堆(upload heap),处在共享内存中的
ComPtr<ID3D12Resource> vertexUploadBuffer;
CD3DX12_HEAP_PROPERTIES UpdateBufferProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
m_spD3dDevice->CreateCommittedResource(
&UpdateBufferProperties,
D3D12_HEAP_FLAG_NONE,
&BufferResourceDESC,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&vertexUploadBuffer));
可以通过 ID3D12Device::CreateCommittedResource 函数,创建一个resource以及一个隐式的heap(此heap指的并不是用来存放descriptor的descriptor heap)。
resource会被映射到这个heap上,该heap有足够大的空间包含整个resource。
(3)资源合入默认堆
这一步骤,就是将我们确定的三角形资源合并到我们的提交列表里面,这就要涉及到部分提交列表的部分,鉴于第三章已经讲过,就不再赘述。
// 设置要传输的CPU数据
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = triangleVertices;
subResourceData.RowPitch = triangleVerticesSize;
subResourceData.SlicePitch = subResourceData.RowPitch;
// 之前close了command list,这里要使用到,需要reset操作才可记录command
m_spGraphicsCommandList->Reset(m_spCommandAllocator.Get(), nullptr);
CD3DX12_RESOURCE_BARRIER CopyDestBarrier = CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_COPY_DEST);
//传输前要更改resource的state
m_spGraphicsCommandList->ResourceBarrier(1, &CopyDestBarrier);
//会先将CPU内存数据拷贝到upload heap中,然后再通过ID3D12CommandList::CopySubresourceRegion从upload heap中拷贝到default buffer中
UpdateSubresources<1>(
m_spGraphicsCommandList.Get(),
m_vertexBuffer.Get(),
vertexUploadBuffer.Get(),
0,
0,
1,
&subResourceData);
CD3DX12_RESOURCE_BARRIER ReadDestBarrier = CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_GENERIC_READ);
m_spGraphicsCommandList->ResourceBarrier(1, &ReadDestBarrier);
m_spGraphicsCommandList->Close();
ID3D12CommandList* cmdsLists[] = { m_spGraphicsCommandList.Get() };
m_spCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
WaitGPUComplete();
这一部分重点就在于,我们要如何通过upload heap将resource传输到default heap里;
dx12为我们提供了 UpdateSubresources 函数来实现这样的操作,在一次完整的提交操作中插入这个指令即可。
2、准备渲染管线(BuildPSO)
在准备渲染管线的阶段,其主要就是围绕 PSO(渲染管线状态对象,Pipeline State Object)的构建而进行。
ComPtr<ID3D12PipelineState> m_pipelineState;
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
...
...
...
m_spD3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState));
(1)定义输入布局
由于是我们自己定义的结构体,必然是要提供一份“使用说明”给Dx12的。这个使用说明就是结构体D3D12_INPUT_LAYOUT_DESC 。
注意,这部分会与后面的shader部分的 hlsl文件 强关联。
//InputLayout
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
D3D12_INPUT_LAYOUT_DESC inputLayout = { inputElementDescs, _countof(inputElementDescs) };
psoDesc.InputLayout = inputLayout;
(2)定义跟签名
Root Signature 用来配置 Shader 需要的 Resource ,例如 constant buffer。
本例子暂时用不到,利用下面代码创建一个空的 root signature:
//Root Signature
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(
0,
nullptr,
0,
nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> signature, error;
D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
ComPtr<ID3D12RootSignature> rootSignature;
m_spD3dDevice->CreateRootSignature(
0,
signature->GetBufferPointer(),
signature->GetBufferSize(),
IID_PPV_ARGS(&rootSignature));
psoDesc.pRootSignature = rootSignature.Get();
(3)定义shader
Shader(着色器)在可编程渲染流水线中,所处的位置是顶点着色器(VS)和片元着色器(PS),这两个部分是高度可编程的。
因此,此阶段我们的目标主要是把这两个部分定义好,具体就是把指定操作从 hlsl 文件中读出来,做成 ID3DBlob ,最后绑定在管线上。
首先是定义标志,毕竟hlsl里面不一定不会错,DEBUG帮助调试。
#if defined(_DEBUG)
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
后续就是固定流程,定义ID3DBlob,读取指定hlsl文件,绑定到管线的三步走。
ComPtr<ID3DBlob> vsByteCode;
D3DCompileFromFile(
L"E:/Code/Dx12Test0302/Dx12Text/Debug/shaders.hlsl",
nullptr,
nullptr,
"VSMain", "vs_5_0",
compileFlags,
0,
&vsByteCode,
nullptr);
//绑定顶点着色器代码
psoDesc.VS.pShaderBytecode = reinterpret_cast<BYTE*>(vsByteCode->GetBufferPointer());
psoDesc.VS.BytecodeLength = vsByteCode->GetBufferSize();
ComPtr<ID3DBlob> psByteCode;
D3DCompileFromFile(
L"E:/Code/Dx12Test0302/Dx12Text/Debug/shaders.hlsl",
nullptr,
nullptr,
"PSMain", "ps_5_0",
compileFlags,
0,
&psByteCode,
nullptr);
//绑定像素着色器
psoDesc.PS.pShaderBytecode = psByteCode->GetBufferPointer();
psoDesc.PS.BytecodeLength = psByteCode->GetBufferSize();
最后,别忘了新建一个hlsl文件,里面具体的解释,就很有内容了,还是先放到后面仔细去说,大框架如下:
a、定义输入(PSInput)
b、定义顶点如何处理的函数(VSMain):
c、定义像素如何处理的函数(PSMain)
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
PSInput VSMain(float3 position : POSITION, float4 color : COLOR)
{
PSInput result;
result.position = float4(position, 1.0f);
result.color = color;
return result;
}
float4 PSMain(PSInput input) : SV_TARGET
{
return input.color;
}
(4)定义其他设置
rendering pipeline 中很多阶段是可编程的(例如vertex shader,pixel shader),但是有些阶段我们只能修改它们的设置。我们可以通过 D3D12_RASTERIZER_DESC 来对 rasterization 阶段进行设置。
d3dx12.h中为我们提供了CD3DX12_RASTERIZER_DESC类,继承于D3D12_RASTERIZER_DESC,我们可以用其快速的生成一个全是默认值的D3D12_RASTERIZER_DESC对象:
//Other Setting
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
//混合状态,深度测试设置
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
//模板测试关闭
psoDesc.DepthStencilState.StencilEnable = FALSE;
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
//绑定RTV和DSV
psoDesc.RTVFormats[0] = m_dfBackBufferFormat;
psoDesc.DSVFormat = m_dfDepthStencilFormat;
//确定交换链个数
psoDesc.SampleDesc.Count = 2;
三、核心绘制(SubmitDrawingTaskCore)
上次,我们说 提交绘制任务 是 绘制 的绝对核心,但当时只是画了一个背景板,现在我们要做核心的核心,也就是用准备好的资源画一个三角形出来。
1、提交单绑定PSO
void cDx12Rendering::ResetCMDListAlloctor()
{
//重置录制相关的内存,为下一帧做准备
...
m_spGraphicsCommandList->Reset(m_spCommandAllocator.Get(), m_pipelineState.Get());
}
2、提交单绘制资源命令添加
当GPU拥有了vertex buffer后,却还是不知道应该如何绘制,其原因是vertex顶点有很多,但GPU不知道画成点就可以,还是每两个vertex画一条线,亦或者每三个vertex画一个三角形?
因此我们需要通过 IASetPrimitiveTopology 函数来告诉GPU该如何使用vertex buffer进行绘制。
m_spGraphicsCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
设置好Primitive Topology后,就可以通过 IASetVertexBuffers 函数将vertex buffer绑定到pipeline的input assembler阶段的input slot上了。
输入装配(Input Assembler,简称 IA)阶段从内存读取几何数据(顶点和索引)并将这些数据组合为几何图元(例如,三角形、直线)。
m_spGraphicsCommandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
通过IASetVertexBuffers函数我们仅仅是将vertex buffer准备好进入pipeline,最后我们需要通过 DrawInstanced 函数来进行实际上的绘制。
m_spGraphicsCommandList->DrawInstanced(3, 1, 0, 0);
三、整体代码
BOOL cDx12Rendering::Init(cDx12RenderConfig* io_cDx12RenderConfig)
{
//第二章初始化步骤的函数
...
//补充 准备资产的函数
InitAsset();
return TRUE;
}
void cDx12Rendering::InitAsset()
{
//准备网格体
BuildMeshData();
//准备渲染管线
BuildPSO();
}
oid cDx12Rendering::BuildMeshData()
{
cVertex triangleVertices[] =
{
{ { 0.0f, 0.5f, 0.0f }, XMFLOAT4(Colors::Red) },
{ { 0.5f, -0.5f, 0.0f }, XMFLOAT4(Colors::Green)},
{ { -0.5f, -0.5f, 0.0f }, XMFLOAT4(Colors::Blue) }
};
ComPtr<ID3D12Resource> vertexUploadBuffer;
const UINT triangleVerticesSize = sizeof(triangleVertices);
CD3DX12_RESOURCE_DESC BufferResourceDESC = CD3DX12_RESOURCE_DESC::Buffer(triangleVerticesSize);
CD3DX12_HEAP_PROPERTIES BufferProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
m_spD3dDevice->CreateCommittedResource(
&BufferProperties,
D3D12_HEAP_FLAG_NONE,
&BufferResourceDESC,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer));
CD3DX12_HEAP_PROPERTIES UpdateBufferProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
m_spD3dDevice->CreateCommittedResource(
&UpdateBufferProperties,
D3D12_HEAP_FLAG_NONE,
&BufferResourceDESC,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&vertexUploadBuffer));
// 设置要传输的CPU数据
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = triangleVertices;
subResourceData.RowPitch = triangleVerticesSize;
subResourceData.SlicePitch = subResourceData.RowPitch;
m_spGraphicsCommandList->Reset(m_spCommandAllocator.Get(), nullptr); // 之前close了command list,这里要使用到,需要reset操作才可记录command
CD3DX12_RESOURCE_BARRIER CopyDestBarrier = CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_COPY_DEST);
//传输前要更改resource的state
m_spGraphicsCommandList->ResourceBarrier(1, &CopyDestBarrier);
//会先将CPU内存数据拷贝到upload heap中,然后再通过ID3D12CommandList::CopySubresourceRegion从upload heap中拷贝到default buffer中
UpdateSubresources<1>(
m_spGraphicsCommandList.Get(),
m_vertexBuffer.Get(),
vertexUploadBuffer.Get(),
0,
0,
1,
&subResourceData);
CD3DX12_RESOURCE_BARRIER ReadDestBarrier = CD3DX12_RESOURCE_BARRIER::Transition(m_vertexBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_GENERIC_READ);
//m_spGraphicsCommandList->ResourceBarrier(1, &ReadDestBarrier);
m_spGraphicsCommandList->Close();
ID3D12CommandList* cmdsLists[] = { m_spGraphicsCommandList.Get() };
m_spCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
WaitGPUComplete();
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.SizeInBytes = triangleVerticesSize;
m_vertexBufferView.StrideInBytes = sizeof(cVertex);
}
void cDx12Rendering::BuildPSO()
{
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
//InputLayout
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
D3D12_INPUT_LAYOUT_DESC inputLayout = { inputElementDescs, _countof(inputElementDescs) };
psoDesc.InputLayout = inputLayout;
//Root Signature
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> signature, error;
D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
ComPtr<ID3D12RootSignature> rootSignature;
m_spD3dDevice->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature));
psoDesc.pRootSignature = rootSignature.Get();
//Shader Compiler
ComPtr<ID3DBlob> vsByteCode, psByteCode;
#if defined(_DEBUG)
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
D3DCompileFromFile(L"E:/Code/Dx12Test0302/Dx12Text/Debug/shaders.hlsl", nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vsByteCode, nullptr);
D3DCompileFromFile(L"E:/Code/Dx12Test0302/Dx12Text/Debug/shaders.hlsl", nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &psByteCode, nullptr);
//绑定顶点着色器代码
psoDesc.VS.pShaderBytecode = reinterpret_cast<BYTE*>(vsByteCode->GetBufferPointer());
psoDesc.VS.BytecodeLength = vsByteCode->GetBufferSize();
//绑定像素着色器
psoDesc.PS.pShaderBytecode = psByteCode->GetBufferPointer();
psoDesc.PS.BytecodeLength = psByteCode->GetBufferSize();
//Other Setting
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] = m_dfBackBufferFormat;
psoDesc.DSVFormat = m_dfDepthStencilFormat;
psoDesc.SampleDesc.Count = 2;
m_spD3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState));
}
void cDx12Rendering::SubmitDrawingTask()
{
//把后缓冲区的资源状态切换成Render Target
...
//设置视口和裁剪区域。
...
//清空后缓存和深度缓存。
...
//输出的合并阶段
...
SubmitDrawingTaskCore();
//把后缓冲区切换成PRESENT状态
...
//录入完成
...
//提交命令
...
}
void cDx12Rendering::SubmitDrawingTaskCore()
{ m_spGraphicsCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_spGraphicsCommandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_spGraphicsCommandList->DrawInstanced(3, 1, 0, 0);
}
四、效果展示
PS:留个问题,蓝色背景可能太喧宾夺主了,那怎么把这蓝色背景改成灰色呢?