【DirectX12从零渲染04】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:留个问题,蓝色背景可能太喧宾夺主了,那怎么把这蓝色背景改成灰色呢?

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值