|
上一篇文章中我们学习了怎么在Windows中启动一个D3D12应用,今天这篇文章我们再来学习下怎么使用D3D12来画一个三角形。
我们还是打开上一篇文章中提到的/Samples/Desktop/D3D12HelloWorld/src/D3D12HelloWorld.sln工程,然后选择D3D12HelloTriangle作为启动项目,然后就可以启动就可以看到下图了。

HelloTriangle
下面我们看下代码,主循环还是一样的,主要修改了初始化和渲染中的部分内容:
初始化资产
具体代码如下:
// Load the sample assets.
void D3D12HelloTriangle::LoadAssets()
{
// Create an empty 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;
ComPtr<ID3DBlob> error;
ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature)));
}
// Create the pipeline state, which includes compiling and loading shaders.
{
ComPtr<ID3DBlob> vertexShader;
ComPtr<ID3DBlob> pixelShader;
#if defined(_DEBUG)
// Enable better shader debugging with the graphics debugging tools.
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L&#34;shaders.hlsl&#34;).c_str(), nullptr, nullptr, &#34;VSMain&#34;, &#34;vs_5_0&#34;, compileFlags, 0, &vertexShader, nullptr));
ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L&#34;shaders.hlsl&#34;).c_str(), nullptr, nullptr, &#34;PSMain&#34;, &#34;ps_5_0&#34;, compileFlags, 0, &pixelShader, nullptr));
// Define the vertex input layout.
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
{ &#34;POSITION&#34;, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ &#34;COLOR&#34;, 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
// Describe and create the graphics pipeline state object (PSO).
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
psoDesc.pRootSignature = m_rootSignature.Get();
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());
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;
ThrowIfFailed(m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState)));
}
// Create the command list.
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&m_commandList)));
// Command lists are created in the recording state, but there is nothing
// to record yet. The main loop expects it to be closed, so close it now.
ThrowIfFailed(m_commandList->Close());
// Create the vertex buffer.
{
// Define the geometry for a triangle.
Vertex triangleVertices[] =
{
{ { 0.0f, 0.25f * m_aspectRatio, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { 0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { -0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
};
const UINT vertexBufferSize = sizeof(triangleVertices);
// Note: using upload heaps to transfer static data like vert buffers is not
// recommended. Every time the GPU needs it, the upload heap will be marshalled
// over. Please read up on Default Heap usage. An upload heap is used here for
// code simplicity and because there are very few verts to actually transfer.
ThrowIfFailed(m_device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer)));
// Copy the triangle data to the vertex buffer.
UINT8* pVertexDataBegin;
CD3DX12_RANGE readRange(0, 0); // We do not intend to read from this resource on the CPU.
ThrowIfFailed(m_vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
m_vertexBuffer->Unmap(0, nullptr);
// Initialize the vertex buffer view.
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.StrideInBytes = sizeof(Vertex);
m_vertexBufferView.SizeInBytes = vertexBufferSize;
}
// Create synchronization objects and wait until assets have been uploaded to the GPU.
{
ThrowIfFailed(m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
m_fenceValue = 1;
// Create an event handle to use for frame synchronization.
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (m_fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
// Wait for the command list to execute; we are reusing the same command
// list in our main loop but for now, we just want to wait for setup to
// complete before continuing.
WaitForPreviousFrame();
}
}主要分为以下步骤:
根签名是什么?根签名是为了整体上统一集中管理之前在D3D11中分散在各个资源创建函数参数中的存储Slot和对应寄存器序号的对象。也就是说在D3D12中我们不用在创建某个资源时单独在其参数中指定对应哪个Slot和寄存器及序号了,而是统一在D3D12中用一个根签名就将这些描述清楚。这里还参考了下面这篇文章:
// Shader programs typically require resources as input (constant buffers,
// textures, samplers). The root signature defines the resources the shader
// programs expect. If we think of the shader programs as a function, and
// the input resources as function parameters, then the root signature can be
// thought of as defining the function signature.
// Root parameter can be a table, root descriptor or root constants.上面说shader程序是需要资源作为输入的,常量缓存,纹理,采样器等等,根签名就是定义了shader程序期望的那些资源,如果把shader看作一个函数,而输入的资源看作是函数的参数,那么根签名就是定义这个函数的签名。
假设我们已经写好了shader程序,现在需要一张纹理作为输入,那么首先我们需要把资源从硬盘中加载到内存中,然后利用ID3D12Resource、ID3D12Device的CreateCommittedResource等D3D12 API,在GPU Memory上开辟一段大小合适的存储空间,并把这段开辟好的GPU Memory地址空间映射成为BYTE*指针,再写一段C++代码把内存中的图片上传到GPU Memory上(即拷贝内存中的图片数据到这个BYTE*指针所指的地址空间上)。这时GPU Memory上就存在了shader需要的纹理图片了。但是此时此刻shader还不知道这张纹理存储在GPU Memory的哪个位置(地址),也不知道图片的格式,更不知道用哪个采样器去采样这张图片。
这时候就需要根签名了,说明这个shader需要那些输入数据资源,这些资源的格式,类型,存储在哪里(通过指定资源占用的register slot实现)等。在shader代码里只需要利用语义register为图片纹理指定相同的register就行了,将来shader运行时需要的纹理资源就能从指定的register slot访问到,同时shader中纹理的格式也需要和根签名中指定的格式一致。
D3D12中shader(即HLSL代码)要访问的资源都被绑定到虚拟寄存器(virtual register)上,这些虚拟寄存器又被分成不同的逻辑寄存器空间(logical register space)。每个虚拟寄存器对应shader要使用的某个资源。每个register被标记为不同的类型(一个字母,可以是t,s,u,b中的某一个)和编号(一个数字,0,1,2,3...),它们共同标志该register,比如: t0可以合法地代表一个寄存器,它可以绑定shader resource view(比如,纹理图片)。t,s,u,b的对应关系如下:
t – for shader resource views (SRV)
s – for samplers
u – for unordered access views (UAV)
b – for constant buffer views (CBV)
同时,寄存器空间的名字是这样的space0, space1, ...,等等。一个寄存器空间(register space)可以有各种类型的registers,且space0中的t0和space1中的t0是不同的register,不会有冲突(相当于C++中两个namespace下存在相同名字的类名,但是这两个类是不同的类,且编译器不会报任务错误)。
其实这些registers相当于是一堆“资源绑定点”,shader要使用的每个资源都要绑定到某个“资源绑定点”上,这些“资源绑定点”有特定的命名规则,且被分成不同的空间管理,方便命名,防止某些情况下命名冲突。
声明了根签名后,还没完。还需要在发送绘制命令前,写一段C++代码调用ID3D12GraphicsCommandList的SetGraphicsRootShaderResourceView()等接口把对应的资源指定到Root Parameter上。这样Shader就可以正常访问到资源了。
使用CD3DX12_ROOT_SIGNATURE_DESC来定义一个根签名的结构。
使用D3D12SerializeRootSignature来编译根签名描述结构。
最后使用CreateRootSignature来创建根签名。
我们使用D3DCompileFromFile来加载和编译Shader文件。
这里我们写了一个最简单的Shader,代码如下:
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
PSInput VSMain(float4 position : POSITION, float4 color : COLOR)
{
PSInput result;
result.position = position;
result.color = color;
return result;
}
float4 PSMain(PSInput input) : SV_TARGET
{
return input.color;
}顶点着色器中每个顶点元素都有一个由D3D12_INPUT_ELEMENT_DESC数组指定的相关语义,顶点着色器的每个参数也具有附加的语义, 语义用于将顶点元素与顶点着色器参数进行匹配,这里我们指定了位置和颜色。然后输出。
像素着色器中输入是顶点着色器的输出,这里我们直接返回了顶点的颜色值。
D3D12_INPUT_ELEMENT_DESC这个来定义顶点数据的布局。
初始化由语义,语义索引,格式,槽,偏移值,数据类型,是否实例化等参数来确定。
- 填写管道状态说明,然后使用可用的帮助器结构创建图形管道状态。
这里我们使用D3D12_GRAPHICS_PIPELINE_STATE_DESC结构体来初始化管道状态。然后使用CreateGraphicsPipelineState来创建一个PSO对象及其代表接口ID3D12PipelineState。
这里我们先定义了一个三角形,带顶点的位置和每个顶点的颜色(分别是红绿蓝)
使用CreateCommittedResource来创建一个缓冲区,并且把上面定义的三角形数据copy进顶点缓冲区这样就完成了数据从CPU向显存的转换。
D3D12_VERTEX_BUFFER_VIEW我们使用这个结构体来描述整个顶点缓冲区视图,目的是告诉GPU被描述的资源实际是Vertex Buffer这个类型。
- 创建并初始化围栏。
- 创建事件句柄用于帧同步。
- 等待 GPU 完成。
渲染
主要是修改了分配指令的部分,具体代码如下:
void D3D12HelloTriangle::PopulateCommandList()
{
// Command list allocators can only be reset when the associated
// command lists have finished execution on the GPU; apps should use
// fences to determine GPU execution progress.
ThrowIfFailed(m_commandAllocator->Reset());
// However, when ExecuteCommandList() is called on a particular command
// list, that command list can then be reset at any time and must be before
// re-recording.
ThrowIfFailed(m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get()));
// Set necessary state.
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, &m_scissorRect);
// Indicate that the back buffer will be used as a render target.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
// Record commands.
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_commandList->DrawInstanced(3, 1, 0, 0);
// Indicate that the back buffer will now be used to present.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
ThrowIfFailed(m_commandList->Close());
}主要分为以下步骤:
- 重置命令分配器和命令列表。
- 设置根签名、视口和裁切矩形。
什么是视口?假设你站在一个密封的房子里,这个房子只有一个很小的窗口,你站在窗口前面,通过这个窗口你可以观察到外面的世界,那么这个窗口就相当于一个视口,而外面的世界就是3D中的场景。视口有以下几个属性,长度和宽度,为了确定窗口的位置,还需要一个左上角坐标。为了支持Z-Buffer,还需要两个深度值,分别是zMin, zMax,表示最小深度和最大深度。
什么是裁切矩形?在这个矩形之外的像素都不会被光栅化到后台缓冲区(被剔除),这个方法可以优化程序的性能。比如我们在游戏界面放置了一个UI,我们可以通过设置裁剪矩形使程序不必对3D空间中那些被它遮挡的像素进行处理了。
ResourceBarrier的这段代码确切含义就是说我们判定并等待完成渲染目标的资源是否完成了从Present(提交)状态切换到Render Target(渲染目标)状态。我们现在把back buffer的状态改成Render Target,然后使用OMSetRenderTargets绑定到管线上。
ClearRenderTargetView是把整个Render Target刷成某种颜色。
IASetPrimitiveTopology是设置我们顶点之间的拓扑结构是怎么样的,相同的点集不同的拓扑方式最后的图形是不一样的,这里我们设置的是Triangle List, 每三个点会形成一个三角形。
IASetVertexBuffers设置顶点缓存数据。
DrawInstanced绘制非索引的几何体。第一个参数是每个实例化顶点的个数,第二个参数是实例化的个数,第三个参数是实例化顶点开始的索引,第四个参数是从顶点缓冲区读取每个实例数据之前加到索引的值。
这里我们把back buffer的状态再改成Present。
|
|