利用Direct3D繪制幾何體
學習目標
- 探索用於定義、存儲和繪制幾何體數據的Direct接口和方法
- 學習編寫簡單的頂點着色器和像素着色器
- 了解如何用渲染流水線狀態對象來配置渲染流水線
- 理解怎樣創建常量緩沖區數據。並將其綁定到渲染流水線上
- 掌握根簽名的用法
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結構體的定義如下:
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這兩個頂點結構體的對應的輸入布局描述:
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方法的三個參數的意義分別是:
- 指定了欲映射的子資源的索引,對於緩沖區來說,它自身便是唯一的子資源,所以我們可以把這個參數設置為0;
- 第二個參數是一個可選項,用於指定內存的映射范圍,如果該參數指定為空,則對整個資源進行映射;
- 返回待映射資源數據的目標內存塊
當常量緩沖區更新完成之后,我們應該在釋放映射內存之前對其進行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
};
上面的結構體中大部分對我們而言都是不怎么使用的成員,這里主要介紹三個:
- FileMode:用於指定是使用實體模式渲染還是使用線框模式進行渲染
- CullMode:用於指定剔除模式,是使用背面剔除、正面剔除還是不剔除
- 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;
}
};