DirectX12 3D 遊戲開發與實戰第六章內容

#利用Direct3D繪製幾何體c++

學習目標

  1. 探索用於定義、存儲和繪製幾何體數據的Direct接口和方法
  2. 學習編寫簡單的頂點着色器和像素着色器
  3. 瞭解如何用渲染流水線狀態對象來配置渲染流水線
  4. 理解怎樣建立常量緩衝區數據。並將其綁定到渲染流水線上
  5. 掌握根簽名的用法

##6.1 頂點與輸入佈局 由5.5.1節可知,除了空間位置,Direct3D的頂點還能夠存儲不少其餘的屬性數據。爲了構建自定義的頂點格式,咱們首先要建立一個結構體來容納選定的頂點數據。好比:編程

//由位置和顏色信息組成的頂點結構體
typedef struct Vertex1
{
	XMFLOAT3 Pos;
	XMFLOAT4 Color;
};

//由位置、法向量以及兩組2D紋理座標組成的頂點結構體
typedef struct Vertex2
{
	XMFLOAT3 Pos;
	XMFLOAT3 Normal;
	XMFLOAT2 Tex0;
	XMFLOAT2 Tex1;
};

定義完頂點結構體以後,咱們還須要向Direct3D提供該頂點結構體的描述,使他了解應該要怎樣處理頂點結構體中的每個成員。這種描述稱爲輸入佈局描述,咱們能夠用結構體D3D12_INPUT_LAYOUT_DESC來表示輸入佈局描述:數組

typedef struct D3D12_INPUT_LAYOUT_DESC
{
	const D3D12_INPUT_ELEMENT_DESC * pInputElementDesc;		//D3D12_INPUT_ELEMENT_DESC元素構成的數組
	UINT NumElements;										//數組元素數量
}D3D12_INPUT_LAYOUT_DESC;

D3D12_INPUT_ELEMENT_DESC數組中的元素依次描述了頂點結構體中對應的成員,若是某一個頂點結構體有兩個成員,那麼與之對應的D3D12_INPUT_ELEMETN_DESC數組也將會有兩個元素。D3D12_INPUT_ELEMENT_DESC結構體的定義以下:app

typedef struct D3D12_INPUT_ELEMENT_DESC {
	LPCSTR SemanticName;								//語義,傳達該元素的用途
	UINT SemanticIndex;									//附加到語義上的索引
	DXGI_FORMAT Format;									//指定頂點元素的格式(即數據類型)
	UINT InputSlot;										//指定傳遞元素所使用的輸入槽
	UINT AlignedByteOffset;								//從C++頂點結構體的首地址到其中某點元素起始地址的偏移量
	D3D12_INPUT_CLASSIFICATION InputSlotClass;			//暫時指定爲D3D12_INPUT_CALSSIFICATION_PER_VERTEX_DATA
	UINT InstanceDataSetpRate;							//暫時指定爲0
}D3D12_INPUT_ELEMETN_DESC;

下面是以本節開頭的Vertex1和Vertex2這兩個頂點結構體的對應的輸入佈局描述:ide

D3D12_INPUT_ELEMETN_DESC desc1[] = {
		{"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}
	};

	D3D12_INPUT_ELEMENT_DESC desc2[] = {
		{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
		{"NORMAL",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
		{"TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,24,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0},
		{"TEXCOORD",1,DXGI_FORMAT_R32G32_FLOAT,0,32,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,0}
	};

##6.2頂點緩衝區 爲了使GPU能夠訪問頂點數組,咱們須要把頂點數組放置在稱爲緩衝區的GPU資源裏,咱們把存儲頂點的緩衝區稱爲頂點緩衝區。函數

咱們要先經過填寫D3D12_RESOURCE_DESC結構體來描述緩衝區資源,接着在調用ID3D12Device::CreateCommittedResource方法來建立ID3D12Resource對象。固然,咱們也可使用D3D12_Resource_Desc的派生類CD3DX12_RESOURCE_DESC來建立ID3d12Resource對象工具

對於靜態幾何體(每一幀都不會發生改變的幾何體)而言,咱們會將它的頂點緩衝區放置在默認堆中來優化性能。由於靜態幾何體的頂點緩衝區初始化完成以後,只有GPU須要從頂點緩衝區中讀取數據,因此能夠直接將該頂點緩衝區放在默認堆中。可是,若是CPU不能向默認堆中的頂點緩衝區寫入數據,那麼咱們要怎樣才能夠初始化該頂點緩衝區呢?佈局

解答:咱們須要使用D3D12_HEAP_TYPE_UPLOAD這種堆類型來建立一個處於中介位置的上傳緩衝區資源,而後咱們就能夠把頂點數據從系統內存複製到上傳緩衝區中,而後把頂點數據從上傳緩衝區複製到真正的頂點緩衝區中性能

咱們在d3dUtil文件中構建了相關的工具函數,以免在每次使用默認緩衝區時要重複的工做學習

Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
	ID3D12Device * device,
	ID3D12GraphicsCommandList * cmdList,
	const void * initData,
	UINT64 byteSize,
	Microsoft::WRL::ComPtr<ID3D12Resource> uploadBuffer
)
{
	ComPtr<ID3D12Resource> defaultBuffer;

	//建立實際的默認緩衝區資源
	ThrowIfFailed(device->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
		D3D12_RESOURCE_STATE_COMMON,
		nullptr,
		IID_PPV_ARGS(defaultBuffer.GetAddressOf())));

	//建立一個處於中介位置的上傳堆
	ThrowIfFailed(device->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(uploadBuffer.GetAddressOf())));

	//描述咱們但願複製到默認緩衝區中的數據
	D3D12_SUBRESOURCE_DATA subResourceData = {};
	subResourceData.pData = initData;
	subResourceData.RowPitch = byteSize;
	subResourceData.SlicePitch = subResourceData.RowPitch;

	//轉換默認緩衝區的狀態
	cmdList->ResourceBarrier(1,
		&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
			D3D12_RESOURCE_STATE_COMMON,
			D3D12_RESOURCE_STATE_GENERIC_READ));

	//將上傳堆的數據複製到默認緩衝區中
	UpdateSubresources(cmdList, defaultBuffer.Get(), uploadBuffer.Get(),
		0, 0, 1, &subResourceData);
	//將默認緩衝區的狀態轉變爲普通狀態
	cmdList->ResourceBarrier(1,
		&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
			D3D12_RESOURCE_STATE_GENERIC_READ,
			D3D12_RESOURCE_STATE_COMMON));
	
	//返回默認緩衝區
	return defaultBuffer;

}

下面的代碼展現瞭如何建立存有立方體八個頂點的默認緩衝區,併爲每個頂點都分別賦予了不一樣的顏色

Vertex 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) }),
	Vertex({ XMFLOAT3(0, 0, +1.0f), XMFLOAT4(Colors::Red) })
};

const UINT64 vbByteSize = 8 * sizeof(Vertex);
ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(), mCommandList.Get(),
	vertices, vbByteSize, VertexBufferUploader);

爲了將頂點緩衝區綁定到渲染流水線上,咱們還要爲頂點緩衝區建立一個頂點緩衝區視圖,不過咱們沒必要爲頂點緩衝區視圖建立描述符堆,頂點緩衝區視圖是由結構體D3D12_BUFFER_VIEW表示的:

typedef struct D3D12_VERTEX_BUFFER_VIEW {
	D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;	//頂點緩衝區的虛擬地址
	UINT SizeInByte;							//頂點緩衝區的大小
	UINT StrideInByte;							//每一個頂點元素佔用的字節數
}D3D12_VERTEX_BUFFER_VIEW;

在頂點緩衝區以及其對應的視圖建立完成以後,咱們就能夠將它和渲染流水線上的一個輸入槽綁定了。這樣咱們就能夠向流水線中的輸入裝配階段傳遞頂點數據了。此操做能夠有如下函數實現:

void ID3D12GraphicsCommandList::IASetVertexBuffers(
	UINT StartSlot,
	UINT NumViews,
	const D3D12_VERTEX_BUFFER_VIEW * pViews
);

將頂點緩衝區設置到輸入槽上並不會對其執行真正的繪製操做,而是僅僅爲頂點數據傳送到渲染流水線上作準備,咱們經過ID3D12GraphicsCommanList::DrawInstanced方法才能夠真正地繪製頂點:

void ID3D12GraphicsCommandList::DrawInstanced(
	UINT VertexCountPerInstance,		//每一個實例要繪製的頂點數量
	UINT InstanceCount,					//暫時設置爲1
	UINT StartVertexLocation,			//指定頂點緩衝區內第一個被繪製的頂點的索引		
	UINT StartInstanceLoaction			//暫時設置爲0
);

##6.3 索引和索引緩衝區 和頂點類似,爲了使GPU能夠訪問索引數組,咱們須要把索引反之放置在GPU的緩衝區資源你(ID3D12Resource)中,存儲索引的緩衝區成爲索引緩衝區,咱們也可使用d3dUtil::CreateDefaultBuffer函數來建立索引緩衝區。

爲了使索引緩衝區和渲染流水線相互綁定,咱們須要爲索引緩衝區建立索引緩衝區視圖,和頂點緩衝區視圖同樣,咱們不須要爲索引緩衝區視圖建立描述符堆,索引緩衝區視圖由結構體D3D12_INDEX_BUFFER_VIEW表示:

typedef struct D3D12_INDEX_BUFFER_VIEW {
	D3D12_GPU_VIRTUAL_ADDRESS BufferLoaction;	//索引緩衝區的虛擬地址
	UINT SizeInByte;							//索引緩衝區的大小
	DXGI_FORMAT Format;							//索引的格式
}D3D12_INDEX_BUFFER_VIEW;

和頂點緩衝區類似,在使用以前,咱們要使用ID3D12GraphicsCommandList::IASetIndexBuffer函數來將索引緩衝區綁定到輸入裝配階段。最後,咱們要使用ID3D12GraphicsCommandList::DrawIndexedInstanced方法來繪製。

void ID3D12GraphicsCommandList::DrawIndexedInstanced(
	UINT IndexCountPerInstance,		//每一個實例須要繪製的頂點數量
	UINT InstanceCount,				//暫時設置爲1
	UINT StartIndexLoaction,		//指向索引緩衝區中的某一個元素,該元素爲起始索引
	int BaseVertexLoaction,			//爲每個索引加上這個整數值
	UINT StartInstanceLoaction		//暫時設置爲0
);

##6.4 頂點着色器示例 如下代碼實現的是一個簡單的頂點着色器(vertex shader):

cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
};

void VS(float3 iPosL : POSITION,
    float4 iColor:COLOR,
    out float4 oPosH : SV_POSITION,
    out float4 oColor : COLOR
)
{
    //把頂點變換到齊次裁剪空間
    oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
    //直接將頂點的顏色信息輸出到像素着色器中
    oColor = iColor;

}

在Driect3D中,編寫着色器的語言爲高級着色語言(High Level Shading Language),其語法和c++十分類似。通常狀況下,着色器一般要編寫在以.hlsl爲擴展名的文本文件中。

頂點着色器就是上面那個名爲VS的函數,上述頂點着色器有四個參數,前面兩個爲輸入參數,後面兩個爲輸出參數,由於HLSL沒有引用和指針的概念,因此須要藉助結構體或是多個輸出參數才能夠返回多個數值。

前兩個輸入參數分別對應繪製立方體時自定義的頂點結構體中的兩個數據成員,也構成了頂點着色器的輸入簽名,參數語義「POSITION」和「COLOR」用於將頂點結構體的元素映射到頂點着色器的輸入簽名中。輸出參數也有各自的語義,輸出參數會根據語義,將頂點着色器的輸出映射到下一處理階段(幾何着色器或者像素着色器)中,這裏有個「SV_POSITION」語義比較特殊,由於它所修飾頂點着色器輸出元素存有齊次裁剪空間中的位置信息。

補充:內置函數mul用於計算向量和矩陣之間的乘法,也能夠用於矩陣和矩陣之間的乘法

下面咱們將把頂點着色器的的返回類型和輸入簽名用結構體替換,以免出現過程的參數列表。即把上述頂點着色器改寫成另外一種等價實現:

cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
};

struct VertexIn
{
    float3 PosL : POSITION;
    float4 Color : COLOR;
};

struct VertexOut
{
    float4 PosH : POSITION;
    float4 Color : COLOR;
};

VertexOut VS(VertexIn vIn)
{
    VertexOut vOut;
    //將頂點數據從局部空間變換到齊次裁剪空間
    vOut.PosH = mul(float4(vIn, 1.0f), gWorldViewProj);
    //直接把頂點顏色做爲輸出
    vOut.Color = vIn.Color;

    return vOut;
}

注意:若是沒有使用幾何着色器(十二章介紹),那麼頂點着色器必須使用SV_POSITION語義輸出頂點在齊次裁剪空間中的位置,由於硬件但願得到頂點在齊次裁剪空間中的座標,若是使用了幾何着色器,那麼能夠把輸出頂點在齊次裁剪空間中的座標的任務交給幾何着色器來處理

鏈接輸入佈局描述符和輸入簽名

略(該小節主要介紹輸入的頂點數據和頂點着色器指望的輸入不符合的狀況)

像素着色器示例

爲了計算出三角形內的每個像素的屬性,咱們會再光柵化階段對頂點着色器(或是幾何着色器)輸出的頂點屬性進行插值,而後這些插值數據會做爲像素着色器的輸入。

像素着色器和頂點着色器類似,後者是針對每個頂點而運行的函數,而前者是針對每個像素片斷而運行的函數。只要爲像素着色器指定了輸入數,它就會爲像素片斷計算出一個對應的顏色。不過輸入像素着色器的片斷那不必定會被傳入或留存在後臺緩衝區中,可能會在進入後臺緩衝區以前被裁剪掉了,或者是沒有經過深度/模板測試而被丟棄。

下面是一段像素着色器代碼,由於要和上一節的頂點着色器相呼應,因此這裏也會把頂點着色器的代碼一塊兒給出來

cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
}

void VS(float3 iPosL:POSITION,
float4 iColor:COLOR,
out float4 oPosL:SV_POSITION,
out float4 oColor : COLOR
)
{
    //將頂點變換到齊次裁剪空間
    oPosL = mul(float4(iPosL, 1.0f), gWorldViewProj);
    //直接將頂點顏色傳遞到像素着色器
    oColor = iColor;
}

float4 PS(flaot4 posH : SV_POSITION, float4 color : COLOR) : SV_Target
{
    return color;
}

在上面的示例中,像素着色器只是簡單的返回了插值顏色數據,能夠發現,像素着色器的輸入和頂點着色器的輸出是精確匹配的,這是必需要知足的一點。而位於像素着色器參數列表後面的語義SV_TARGE則表示該返回值的類型和渲染目標格式相互匹配(該輸出會被存到渲染目標之中)

和頂點着色器同樣,咱們能夠利用輸入/輸出結構體重寫像素着色器,以下:

cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
}

struct VertexIn
{
    float3 Pos : POSITION;
    flaot4 Color : COLOR;
};

struct VertexOut
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};

VertexOut VS(VertexIn vIn)
{
    VertexOut vOut;
    //將頂點座標從局部空間轉換到齊次裁剪空間
    vOut.PosH = mul(flaot4(vIn.Pos, 1.0f), gWorldViewProj);
    //直接將輸入顏色輸出到像素着色器中
    vOut.Color = vIn.Color;
    return vOut;
}

flaot4 PS(VertexIn vIn):SV_Target
{
    return vIn.Color;
}

##6.6 常量緩衝區 常量緩衝區也是一種GPU資源(ID3D12Resource),其數據內容能夠給着色器程序使用,就像咱們即將學習到的紋理等其餘資源同樣,他們均可以被着色器程序使用。

和頂點緩衝區不一樣的是,常量緩衝區由CPU每幀更新一次,因此咱們會把常量緩衝區建立到一個上傳堆中而不是默認堆(只有GPU能訪問的堆,CPU沒法對其進行寫入)中。同時,常量緩衝區對硬件也有特別的要求,即常量緩衝區的大小必須是硬件最小分配空間(256B)的整數倍

因爲咱們常常要使用多個相同類型的常量緩衝區,因此下面的代碼將展現如何建立一個緩衝區資源,並利用該緩衝區來存儲NumElements個常量緩衝區:

struct ObjectConstants
{
	DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};

UINT mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

ComPtr<ID3D12Resource> mUploadCBuffer;

md3dDevice->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
	D3D12_HEAP_FLAG_NONE,
	&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*NumElement),
	D3D12_RESOURCE_STATE_GENERIC_READ,
	nullptr,
	IID_PPV_ARGS(&mUploadCBuffer)
	)

工具函數d3dUtil::CalcConstantBufferByteSize()會進行適當的計算,使緩衝區的大小湊整爲硬件的最小分配空間的整數倍。(函數內部具體實現不解釋)

隨着Direct3D一塊兒推出的是着色器模型(Shader Model)5.1,其中新引進了一條能夠用於定義常量緩衝區的HLSL語法,它的使用方法以下:

struct ObjectConstances
{
    flaot4x4 gWorldViewProj;
};

ConstantBuffer<ObjectConstances> gObjectConstants : register(b0);

咱們在前面的實例中使用的都是着色器模型5.0的標準,接下來咱們會盡量的使用着色器模型5.1的標準,(5.1暫時不支持Driect11)

##6.6.2 更新常量緩衝區 因爲常量緩衝區是使用D3D12_HEAP_TYPE_UPLOAD這種類型建立的,因此咱們能夠經過CPU來更新數據,爲此,咱們須要獲取指向欲更新數據的指針,可使用Map方法獲取:

ComPtr<ID3D12Resource> mUploadBuffer;
BYTE* mMappedData = nullptr;
mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));

Map方法的三個參數的意義分別是:

  1. 指定了欲映射的子資源的索引,對於緩衝區來講,它自身即是惟一的子資源,因此咱們能夠把這個參數設置爲0;
  2. 第二個參數是一個可選項,用於指定內存的映射範圍,若是該參數指定爲空,則對整個資源進行映射;
  3. 返回待映射資源數據的目標內存塊

當常量緩衝區更新完成以後,咱們應該在釋放映射內存以前對其進行Unmap(取消映射)操做。

if(mUploadBuffer != nullptr)
{
	mUploadBuffer->Unmap(0,nullptr);
}

##6.6.3 上傳緩衝區輔助函數 爲了使上傳緩衝區的相關處理工做更加輕鬆,咱們在UploadBuffer.h文件中定義了下面這個類,它會替咱們實現上傳緩衝區資源的構造和析構函數,處理資源的映射和取消映射操做,還提供了CopyData方法來更新緩衝區中的特定元素。(這個類不是僅僅針對常量緩衝區,也能夠用來管理各類類型的上傳緩衝區)。

template<typename T>
class UploadBuffer
{
public:
	UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) :mIsConStantBuffer(isConstantBuffer)
	{
		mElementByteSize = sizeof(T);

		//若是是常量緩衝區。將緩衝區的大小設置爲硬件最小分配空間的整數倍
		if (isConstantBuffer)
		{
			mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
		}

		ThrowIfFailed(device->CreateCommittedResource(&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
			D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(
				mElementByteSize*elementCount), D3D12_RESOURCE_STATE_GENERIC_READ,
				nullptr,
				IID_PPV_ARGS(&mUploadBuffer)
			));
		ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
	}

	UploadBuffer(const UploadBuffer& rhs) = delete;
	UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
	~UploadBuffer()
	{
		if (mUploadBuffer != nullptr)
		{
			mUploadBuffer->Unmap(0, nullptr);
		}

		mMappedData = nullptr;
	}

	ID3D12Resource* Resource()const
	{
		return mUploadBuffer.Get();
	}

	void CopyData(int elemetnIndex, const T& data)
	{
		memcpy(&mMappedData[elemetnIndex*mElementByteSize], &data, sizeof(T));
	}


private:
	Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer;
	BYTE* mMappedData = nullptr;

	UINT mElementByteSize = 0;
	bool mIsConStantBuffer = false;
};

題外話:通常來講,物體的世界矩陣會根據移動/旋轉/縮放而改變,觀察矩陣會根據虛擬攝像機的移動/旋轉而改變,投影矩陣會根據窗口大小的調整而改變。

##6.6.4 常量緩衝區描述符 到目前爲止,咱們已經介紹了渲染目標,深度/模板緩衝區,頂點緩衝區以及索引緩衝區這幾種資源視圖(描述符)的使用方法,接下來咱們將介紹如何利用描述符將常量緩衝區綁定到渲染流水線中,

由於常量緩衝區須要使用D3D12_DESCRIPTOR_HEAP_CBV_SRV_UAV類型所建立的描述符堆,這種堆內能夠存儲常量緩衝區視圖,着色器資源視圖以及無序訪問視圖(unordered access),爲了存放這些描述符,咱們須要建立如下類型的描述符堆

D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.NodeMask = 0;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;

ComPtr<ID3D12DescriptorHeap> mCbvHeap;
md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mCbvHeap));

而後經過填寫D3D12_CONSTANT_BUFFER_VIEW_DESC實例,再調用ID3D12Device::CreateConstantBufferView方法即可以建立常量緩衝區視圖:

//繪製物體所用對象的常量數據
struct ObjectConstant
{
	XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};
//建立一個存儲繪製n個物體所需常量數據的常量緩衝區
std::unique_ptr<UploadBuffer<ObjectConstant>> mObjectCB = nullptr;
mObjectCB = std::make_unique<UploadBuffer<ObjectConstant>>(md3dDevice.Get(), n, true);

UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstant));

//緩衝區的起始地址(索引爲0的常量緩衝區地址)
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();

//偏移到常量緩衝區中繪製第i個物體所須要的常量數據
int boxCBufferIndex = i;
cbAddress += objCBByteSize * boxCBufferIndex;

//綁定到HLSL常量緩衝區結構體的常量緩衝區資源子集
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstant));
md3dDevice->CreateConstantBufferView(&cbvDesc, mCbvHeap->GetCPUDescriptorHandleForHeapStart());

根簽名和描述符表

在繪製調用開始以前,咱們要把不一樣類型的資源綁定到特定的寄存器槽上,以供着色器程序訪問。好比說,前文的頂點着色器和像素着色器須要的就是一個綁定到寄存器b0的常量緩衝區,在後續的章節中,咱們會使用到這兩種着色器更高級的配置方法,以使多個常量緩衝區、紋理和採樣器均可以和各自的寄存器槽相互綁定

根簽名:在執行繪製命令以前,根簽名必定要爲着色器提供其執行期間所須要綁定到渲染流水線的全部資源,在建立流水線狀態對象(pipeline state object)時會對此進行驗證,不一樣的繪製調用可能須要不一樣的着色器程序,這樣意味着要使用不一樣的根簽名。

在Direct3D中,根簽名由ID3DRootSignature接口表示,並經過一組根參數(用以描述繪製調用過程當中着色器所需的資源)定義而成,根參數能夠是根常量、根描述符、或者描述符表。下面的代碼將建立一個根簽名,他的根參數爲描述符表(描述符表是描述符堆一塊連續區域):

CD3DX12_ROOT_PARAMETER slotRootParameter[1];

//建立一個只存有一個CBV的描述符表
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
	D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
	1,			//表中描述符數量
	0			//這段描述符區域綁定的目標寄存器槽編號
);

slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);

//根簽名由一組根參數組成
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc(1, slotRootParameter, 0, nullptr,
	D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

//建立一個僅含有一個槽位的根簽名
ComPtr<ID3D12RootSignature> mRootSignature = nullptr;
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlod = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSignatureDesc,
	D3D_ROOT_SIGNATURE_VERSION_1,
	serializedRootSig.GetAddressOf(),
	errorBlod.GetAddressOf());

ThrowIfFailed(md3dDevice->CreateRootSignature(
	0,
	serializedRootSig->GetBufferPointer(),
	serializedRootSig->GetBufferSize(),
	IID_PPV_ARGS(&mRootSignature)));

咱們將在第七章對CD3DX12_ROOT_PARAMETER和CD3DX12_DESCRIPTOR_RANGE這兩個結構體進行詳細的說明,在這裏只須要理解如下代碼便可:

CD3DX12_ROOT_PARAMETER slotRootParameter[1];

//建立一個只存有一個CBV的描述符表
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
	D3D12·_DESCRIPTOR_RANGE_TYPE_CBV,
	1,			//表中描述符數量
	0			//這段描述符區域綁定的目標寄存器槽編號
);

slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);

這段代碼建立了一個根參數,目的是將含有一個CBV的描述符表綁定到常量緩衝區寄存器0

根簽名只定義了應用程序要綁定的渲染流水線的資源,不過沒有真正執行任何資源綁定操做,只有率先經過命令列表設置好根簽名,而後使用ID3D12GraphicsCommandList::SetGraphicRootDescriptorTable方法令描述符表和渲染流水線相互綁定

下列代碼先將根簽名和CBV設置到命令列表中,而後經過設置描述符表來指定咱們但願綁定到渲染流水線的資源:

mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);

//偏移到這次繪製調用所需的CBV處
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv.Offset(cbvIndex, mCbvSruUavDescriptorSize);

mCommandList->SetGraphicsRootDescriptorTable(0, cbv);

##6.7 編譯着色器 在Direct3D中,着色器程序必需要先被編譯爲一種可移植的字節碼,接下來,圖形驅動程序將獲取這些字節碼,並將這些字節碼從新編譯爲針對當前系統GPU所優化的本地指令,咱們在運行期間可使用如下函數對着色器程序進行編譯:

HRESULT D3DCompileFormFile(
	LPCWSTR pFlieName,
	const D3D_SHADER_MACRO * pDefines,
	ID3DInclude * pInclude,
	LPCSTR pEntrypoint,
	LPCSTR pTarget,
	UINT Falgs1,
	UINT Flags2,
	ID3DBlob ** ppCode,
	ID3DBlob ** ppErrorMsgs
);

爲了可以輸出編譯着色器的錯誤信息,咱們在d3dUtil文件中實現了下列輔助函數在運行時編譯着色器:

ComPtr<ID3DBlob> d3dUtil::CompileShader(
	const std::wstring& filename,
	const D3D_SHADER_MACRO* defines,
	const std::string& entrypoint,
	const std::string& target
)
{
	//若是處於調試狀態,則使用調試標誌
	UINT compileFalgs = 0;

#if defined(DEBUG) || defined(_DEBUG)
	compileFalgs = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
	HRESULT hr = S_OK;

	ComPtr<ID3DBlob> byteCode = nullptr;
	ComPtr<ID3DBlob> errors = nullptr;

	hr = D3DCompileFromFile(filename.c_str(), defines,
		D3D_COMPILE_STANDARD_FILE_INCLUDE,
		entrypoint.c_str(), target.c_str(), compileFalgs, 0, &byteCode, &errors);

	//將錯誤信息輸出到調試窗口
	if (errors != nullptr)
	{
		OutputDebugStringA((char*)errors->GetBufferPointer());
	}

	ThrowIfFailed(hr);

	return byteCode;
}

如下是調用此函數的實例:

ComPtr<ID3DBlob> mvsByteCode = nullptr;
ComPtr<ID3DBlob> mpsByteCode = nullptr;

mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color,hlsl", nullptr, "VS", "vs_5_1");
mpsByteCode = d3dUtil::CompileShader(L"Shadres\\color.hlsl", nullptr, "PS", "ps_5_1");

##6.7.1離線編譯 略

##6.7.2 生成着色器代碼 略

##6.7.3 利用Visual Studio離線編譯着色器 Visual Studio 2015 集成了一些對着色器程序進行編譯工做的支持,咱們能夠向工程內添加hlsl文件,而Visual Studio會識別他們並提供編譯的選項。可是,使用Visual Studio集成的HLSL工具備一個缺點,即它只容許每個文件中只能用一個着色器程序。所以,這個限制將致使頂點着色器和像素着色器不能同時放置在一個文件中,不然必有一個不會被編譯。

##6.8 光柵器狀態 在DriectX3D12的渲染流水線中,大多階段都是能夠編程的,可是有些特定階段只接受配置,好比用於配置渲染流水線中光柵化階段的光柵器狀態組則由結構體D3D12_RASTERIZER_DESC表示

typedef struct D3D12_RASTERIZER_DESC
{
	D3D12_FILL_MODE FillMode;		//默認值爲:D3D12_FILL_SOLID
	D3D12_CULL_MODE CullMode;		//默認值爲:D3D12_CULL_BACK
	BOOL FrontCounterClockwise;		//默認值爲:false
	INT DepthBias;					//默認值爲:0
	FLOAT DepthBiasClamp;			//默認值爲:0.0f
	FLOAT SlopeScaleDepthBias;		//默認值爲:0.0f
	BOOL DepthClipEnable;			//默認值爲:true
	BOOL MultisampleEnable;			//默認值爲:false
	BOOL AntialiasedLineEnable;		//默認值爲:false
	UINT ForcedSampleCount;			//默認值爲:0
};

上面的結構體中大部分對咱們而言都是不怎麼使用的成員,這裏主要介紹三個:

  1. FileMode:用於指定是使用實體模式渲染仍是使用線框模式進行渲染
  2. CullMode:用於指定剔除模式,是使用背面剔除、正面剔除仍是不剔除
  3. FrontCounterClockwise:若是指定爲false,則根據攝像機的觀察視角,將頂點順序爲順時針的視爲正面朝向,若是爲true,則根據將頂點順序爲逆時針的視爲正面朝向

下列代碼展現如何建立一個開啓線框模式並且禁用剔除操做的光柵器狀態:

CD3DX12_RASTERIZER_DESC rsDesc(D3D12_DEFAULT);
rsDesc.FillMode = D3D12_FILL_MODE_WIREFRAME;
rsDesc.CullMode = D3D12_CULL_MODE_NONE;

CD3DX12_RASTERIZER_DESC是擴展自D3D12_RASTERIZER_DESC結構體的基礎上又添加了一些輔助構造函數的工具類,

##6.9 流水線狀態對象 到目前爲止,咱們已經展現了編寫輸入佈局描述,建立頂點着色器和像素着色器,以及配置光柵器狀態組這3個步驟,可是咱們尚未講解如何將這些對象綁定到推向流水線上,用於繪製圖形。流水線狀態對象(Pipeline State Object,PSO)是控制大多數流水線狀態對象的統稱,ID3D12PipelineState接口表示,要建立PSO,首先咱們要填寫一份描述其中細節的D3D12_GRAPHICS_PIPELINE_STATE_DESC結構體實例:

typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC {
	ID3D12RootSignature * pRootSignature;					//指向一個與該PSO綁定的根簽名的指針
	D3D12_SHADER_BYTECODE VS;								//待綁定的頂點着色器
	D3D12_SHADER_BYTECODE PS;								//待綁定的像素着色器
	D3D12_SHADER_BYTECODE DS;								//待綁定的域着色器
	D3D12_SHADER_BYTECODE HS;								//待綁定的外殼着色器
	D3D12_SHADER_BYTECODE GS;								//待綁定的幾何着色器
	D3D12_STREAM_OUTPUT_DESC StreamOutput;					//用於實現一種稱爲流輸出的高級技術
	D3D12_BLEND_DESC BlendState;							//指定混合操做時所使用的混合狀態
	UINT SmapleMask;										//設置每一個採樣點的採集狀況(採集或者禁止採集)
	D3D12_RASTERIZER_DESC RasterizerState;					//指定用來配置光柵器的光柵器狀態
	D3D12_DEPTH_STENCIL_DESC DepthStencilState;				//指定用於配置深度/模板測試的深度/模板狀態
	D3D12_INPUT_LAYOUT_DESC InputLayout;					//輸入佈局描述
	D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;	//指定圖元的拓撲類型
	UINT NumRenderTargets;									//同時所用的渲染目標數量
	DXGI_FORMAT RTVFormats[8];								//渲染目標的格式
	DXGI_FORMAT DSVForamt;									//深度/模板緩衝區的格式
	DXGI_SAMPLE_DESC SmapleDesc;							//描述多重採樣對每個像素的採樣數量以及質量級別
}D3D12_GRAPHICS_PIPELINE_STATE_DESC;

在D3D12_GRAPHICS_PIPELINE_DESC實例填寫完畢以後,咱們即可以使用ID3D12Device::CreateGraphicsPipelineState方法來建立ID3D12PipelineState對象:

D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc;
ZeroMemory(&PSODesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
PSODesc.InputLayout = { mInputLayout.data(),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.RTVFormats[0] = mBackBufferFormat;
PSODesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
PSODesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
PSODesc.DSVFormat = mDepthStencilFormat;

ComPtr<ID3D12PipelineState> mPSO;
md3dDevice->CreateComputePipelineState(&PSODesc, IID_PPV_ARGS(&mPSO));

並不是全部的渲染狀態都封裝在PSO內,好比視口和裁剪矩形等屬性就獨立於PSO。Direct3D實質上就是一種狀態機,裏面的事物會保持它們各自的狀態,直到咱們將他們改變。

##6.10 幾何圖形輔助結構體 通常來講,咱們都會經過建立一個同時存有頂點緩衝區和索引緩衝區的結構體來方便的定義多個結構體,當須要定義多個結構體時,咱們就可使用定義在d3dUtil文件中的MeshGeometry結構體:

//先利用SubMeshGeometry來定義MeshGeometry中存儲的單個結合體
//此結構體適用於將多個幾何體數據存於一個頂點緩衝區和一個索引緩衝區的狀況
struct SubmeshGeometry
{
	UINT IndexCount = 0;
	UINT StartIndexLocaltion = 0;
	INT BaseVertexLoaction = 0;

	//經過此子網格來定義當前SubmeshGeometry結構體中所存結合體的包圍盒(bounding box)
	DirectX::BoundingBox Bounds;
};

struct MeshGeometry
{
	//指定此幾何體網格集合的名稱,這樣咱們就能根據名稱找到它
	std::string Name;

	//系統內存的副本,因爲頂點/索引能夠是泛型格式,因此用Blod類型表示
	//待用戶使用時再將他轉換爲適當的類型
	Microsoft::WRL::ComPtr<ID3DBlob> VertexBufferCPU = nullptr;
	Microsoft::WRL::ComPtr<ID3DBlob> IndexBufferCPU = nullptr;

	Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
	Microsoft::WRL::ComPtr<ID3D12Resource> IndexBUfferGPU = nullptr;

	Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
	Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferUploader = nullptr;

	//與緩衝區相關的數據
	UINT VertexByteStride = 0;
	UINT VertexBufferByteSize = 0;
	DXGI_FORMAT IndexForamt = DXGI_FORMAT_R16_UINT;
	UINT IndexBufferByteSize = 0;

	//一個MeshGeometry結構體可以存儲一組頂點/索引緩衝區的多個幾何體
	//若利用下列容器阿里定義子網格幾何體,咱們就能單獨地繪製出其中的幾何體
	std::unordered_map<std::string, SubmeshGeometry> DrawArgs;

	//返回頂點緩衝區視圖的方法
	D3D12_VERTEX_BUFFER_VIEW VertexBufferView()const
	{
		D3D12_VERTEX_BUFFER_VIEW vbv;
		vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
		vbv.StrideInBytes = VertexByteStride;

		return vbv;
	}

	//返回索引緩衝區視圖的方法
	D3D12_INDEX_BUFFER_VIEW IndexBufferView()const
	{
		D3D12_INDEX_BUFFER_VIEW ibv;
		ibv.BufferLocation = IndexBUfferGPU->GetGPUVirtualAddress();
		ibv.Format = IndexForamt;
		ibv.SizeInBytes = IndexBufferByteSize;

		return ibv;
	}

	//待數據上傳到GPU後,咱們就能夠釋放這些內存了
	void DisposeUploaders()
	{
		VertexBufferUploader = nullptr;
		IndexBufferUploader = nullptr;
	}
};
相關文章
相關標籤/搜索