1. 程式人生 > >[DirectX12學習筆記] 用Direct3D繪圖 Part.1

[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