[DirectX12學習筆記] 用Direct3D繪圖 Part.1
用DX12畫一個BOX
頂點與輸入
可以這樣定義頂點
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
需要定義D3D12_INPUT_LAYOUT_DESC來把D3D12_INPUT_ELEMENT_DESC繫結到PSO。
typedef struct D3D12_INPUT_LAYOUT_DESC
{
_Field_size_full_(NumElements) const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
UINT NumElements;
} D3D12_INPUT_LAYOUT_DESC;
然後就是為shader宣告輸入變數的D3D12_INPUT_ELEMENT_DESC
typedef struct D3D12_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName;//變數名字
UINT SemanticIndex;//如果有多個重名的話用這個來區分,如TEXCOORD0和TEXCOORD1,名字都是TEXCOORD,這個index分別是0和1
DXGI_FORMAT Format;
UINT InputSlot;
UINT AlignedByteOffset; //起始點從0開始算,如R32G32B32算12,則下一個變數這個值加12
D3D12_INPUT_CLASSIFICATION InputSlotClass;
UINT InstanceDataStepRate;
} D3D12_INPUT_ELEMENT_DESC;
舉個例子,D3D12_INPUT_ELEMENT_DESC陣列可以這樣定義:
std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
注意這裡用vector的話繫結到PSO的時候要用vector::data()返回首地址。
一般頂點有兩種存法,對於靜態的mesh,我們把頂點存在default heap裡,然後還要一個upload heap來上傳頂點,這倆heap都是gpu資源,用CreateCommitedResource建立,但是cpu只對upload heap有寫入許可權,再提交命令讓gpu從upload buffer寫入到default buffer,default buffer之後就不再去改動,這樣效率更高,但不能改的話就必須是靜態的了。對於能動的物體我們只用一個upload buffer來存頂點和indices。書上在d3dUtil裡面封裝好了一個CreateDefaultBuffer來做這件事。
一個使用例如下:
std::array<Vertex, 8> vertices =
{
Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) })
};
mBoxGeo = std::make_unique<MeshGeometry>();
mBoxGeo->Name = "boxGeo";
//mBoxGeo的VertexBufferGPU和VertexBufferUploader在構造的時候都是nullptr
mBoxGeo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices.data(), vbByteSize, mBoxGeo->VertexBufferUploader);
現在有了VertexBuffer,還要定義VertexBuferView,注意vbv和ibv比較特殊,不需要存在heap裡,建立好view之後,要用ID3D12GraphicsCommandList::IASetVertexBuffers來繫結到渲染管線。示例如下:
const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
mBoxGeo->VertexByteStride = sizeof(Vertex);
mBoxGeo->VertexBufferByteSize = vbByteSize;
/** This part is wrapped in mBoxGeo->VertexBufferView();
***
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = VertexByteStride;
vbv.SizeInBytes = VertexBufferByteSize;
return vbv;
**/
mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView());
現在有了vertices了,還需要indices來構成三角形,需要建立ibv再繫結到渲染管線,原理基本同上,示例程式碼如下:
std::array<std::uint16_t, 36> indices =
{
// front face
0, 1, 2,
0, 2, 3,
// back face
4, 6, 5,
4, 7, 6,
// left face
4, 5, 1,
4, 1, 0,
// right face
3, 2, 6,
3, 6, 7,
// top face
1, 5, 6,
1, 6, 2,
// bottom face
4, 0, 3,
4, 3, 7
};
const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);
mBoxGeo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(), ibByteSize, mBoxGeo->IndexBufferUploader);
mBoxGeo->IndexFormat = DXGI_FORMAT_R16_UINT;
mBoxGeo->IndexBufferByteSize = ibByteSize;
/** wrapped in mBoxGeo->IndexBufferView()
***
D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
ibv.Format = IndexFormat;
ibv.SizeInBytes = IndexBufferByteSize;
return ibv;
**/
mCommandList->IASetIndexBuffer(&mBoxGeo->IndexBufferView());
需要注意的是,如果頂點數有點多(超過65536個),那應該用DXGI_FORMAT_R32_UINT,indices陣列也要用uint32_t型別,否則存不下。
用來繪圖的介面是
virtual void STDMETHODCALLTYPE DrawIndexedInstanced(
_In_ UINT IndexCountPerInstance,
_In_ UINT InstanceCount,
_In_ UINT StartIndexLocation,
_In_ INT BaseVertexLocation,
_In_ UINT StartInstanceLocation) = 0;
這裡面考慮了IndexCount,StartIndex,BaseVertex是因為把多個物體的VertexBuffer和IndexBuffer合併成一個共用了,注意IndexBuffer在合併的時候不要把後面的index數值增加,也就是說每個物體的indices依然是從0開始,只要在draw call的這一步標明StartIndexLocation和BaseVertexLocation和IndexCountPerInstance,DX就能知道這個物體上的點如何構成三角形,不需要我們去手動累加了。VertexBuffer也是直接拼就行。
頂點著色器和畫素著色器
畫方盒子用到的hlsl程式碼如下
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// Just pass vertex color into the pixel shader.
vout.Color = vin.Color;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
頂點著色器輸出可以用一個struct(如下),也可以用out引數,如out float4 oPosH : SV_POSITION。
注意輸入引數的名字是任意的,冒號後面的也可以隨便取名,但是必須和c++中D3D12_INPUT_ELEMENT_DESC中宣告的一致,此外,SV_開頭的也是不能隨便起名的,這些是System Value, 比如VS必須輸出SV_POSITION給後面的裁剪等步驟,PS後面必須加冒號和SV_Target。
輸入引數列表的順序可以換,因為只看冒號後面。c++裡面也可以換順序,因為規定了初始位移。
VS的輸出引數必須整整好對上PS的輸入引數,否則會報錯。
Constant Buffer 和 Root Signature
簡單說一下用constant buffer往shader裡傳資料要建立的資源和要做的事:
首先要多個constant buffer,即建立多個gpu資源,存在一個upload buffer裡(可以用書本提供的UploadBuffer.h裡面的UploadBuffer來同時建立一個Upload Buffer和多個Constant Buffer,但要知道他做了什麼),然後每個constant buffer要一個cbv,這些cbv都存在一個cbv的DescriptorHeap裡面,然後每一幀還要在DrawCall之前SetDescriptHeap才能把這一幀的資料傳進去。
上述步驟是把cb繫結到渲染管線,還要一個RootSignature來把cb繫結到暫存器b0,RootSignature是一個RootParameter陣列,RootParameter可以是RootConstant、RootDescriptor或者DescriptorTable,這章用的是DescriptTable來裝一個cbv,把這個對應的cb綁到b0。
constant buffer是一個可以被shader引用的gpu資源,constant buffer可以有很多個(每個物體一個),存在一個大的upload buffer裡,constant buffer的大小必須256位對齊,用書本提供的d3dUtil::CalcConstantBufferByteSize(UINT bytesize)來計算256位對齊的大小。
如果要向constant buffer中寫入資料,可以用mUploadBuffer->Map(0,nullptr,reinterpret_Cast<void**>(&mMappedData))來把gpu的緩衝區map到cpu,然後memcpy(mMappedData,&data,dataSizeInBytes)就可以了,資料就會傳到gpu了。如果要釋放cpu上的這一塊,要mUploadBuffer->Unmap(0,nullptr);
不過書上也封裝好了一個UploadBuffer類可以用,就不用做這些步驟了,用UploadBuffer::CopyData(int elementIndex, const T& data)即可。建立UploadBuffer則用
std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB =
std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 1, true);
注意示例程式中每幀往UploadBuffer裡傳worldViewProj的時候轉置了一下,是因為在shader裡寫的是點乘矩陣而不是矩陣乘點。
建立cbv和存cbv的堆的程式碼(因為這一章只要畫一個盒子所以只建了一個cbv,以後會建多個):
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
IID_PPV_ARGS(&mCbvHeap)));
mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 1, true);
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
// Offset to the ith object constant buffer in the buffer.
int boxCBufIndex = 0;
cbAddress += boxCBufIndex*objCBByteSize;
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
md3dDevice->CreateConstantBufferView(
&cbvDesc,
mCbvHeap->GetCPUDescriptorHandleForHeapStart());
接下來要用一個DescriptorTable組成的RootSignature把建立好的ConstantBuffer繫結到暫存器b0。
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
// Create a single descriptor table of CBVs.
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);
// A root signature is an array of root parameters.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// create a root signature with a single slot which points to a descriptor range consisting of a single constant buffer
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf());
if(errorBlob != nullptr)
{
::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(&mRootSignature)));
編譯shader
為了方便改書中使用的是即時編譯shader的方法,實際上游戲中一般還是提前編譯好shader,用DX提供的FXC可以編譯,VS把shader加到專案工程中也可以編譯。
如果要線上編譯的話,用書本提供的d3dUtil::CompileShader即可。
ComPtr<ID3DBlob> mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS", "vs_5_0");
ComPtr<ID3DBlob> mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS", "ps_5_0");
如果要用已經離線編譯好的.cso檔案,也可以用封裝好的d3dUtil::LoadBinary。
ComPtr<ID3DBlob> mvsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_vs.cso");
ComPtr<ID3DBlob> mpsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_ps.cso");
Rasterizer State和PSO
最後設定好渲染需要的引數就準備好渲染了。
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
psoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() };
psoDesc.pRootSignature = mRootSignature.Get();
psoDesc.VS =
{
reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()),
mvsByteCode->GetBufferSize()
};
psoDesc.PS =
{
reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()),
mpsByteCode->GetBufferSize()
};
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = mBackBufferFormat;
psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
psoDesc.DSVFormat = mDepthStencilFormat;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));
GeometryHelper
封裝好的MeshGeometry類和SubmeshGeometry類在d3dUtil.h中,可以大概看下實現。
Draw部分
做好以上工作後就可以畫盒子了,過載的Update和Draw程式碼如下
void BoxApp::Update(const GameTimer& gt)
{
// Convert Spherical to Cartesian coordinates.
float x = mRadius*sinf(mPhi)*cosf(mTheta);
float z = mRadius*sinf(mPhi)*sinf(mTheta);
float y = mRadius*cosf(mPhi);
// Build the view matrix.
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
XMStoreFloat4x4(&mView, view);
XMMATRIX world = XMLoadFloat4x4(&mWorld);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world*view*proj;
// Update the constant buffer with the latest worldViewProj matrix.
ObjectConstants objConstants;
XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
mObjectCB->CopyData(0, objConstants);
}
void BoxApp::Draw(const GameTimer& gt)
{
// Reuse the memory associated with command recording.
// We can only reset when the associated command lists have finished execution on the GPU.
ThrowIfFailed(mDirectCmdListAlloc->Reset());
// A command list can be reset after it has been added to the command queue via ExecuteCommandList.
// Reusing the command list reuses memory.
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO.Get()));
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
// Indicate a state transition on the resource usage.
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
// Clear the back buffer and depth buffer.
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr);
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
// Specify the buffers we are going to render to.
mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView());
mCommandList->IASetIndexBuffer(&mBoxGeo->IndexBufferView