d3d12龍書閱讀----繪製幾何體(上)
本節主要介紹了構建一個簡單的彩色立方體所需流程與重要的api
下面主要結合立方體程式碼分析本節相關知識
頂點
輸入裝配器階段的輸入
首先,我們需要定義立方體的八個頂點
頂點結構體:
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
當然,對於更復雜的情況,我們不僅要定義頂點的位置與顏色,還要包括法線向量、紋理x座標、紋理y座標等等
但在這裡情形比較簡單
之後,我們還需要定義一個頂點結構體描述子陣列,被稱為輸入佈局描述
陣列中的每個成員與頂點結構體的成員一一對應,同時也與頂點著色器中的引數對應:
std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout;
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 }
};
//頂點著色器
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
D3D12_INPUT_ELEMENT_DESC的定義與引數說明可見:
https://learn.microsoft.com/zh-cn/windows/win32/api/d3d12/ns-d3d12-d3d12_input_element_desc
接著,我們還需要為頂點建立頂點緩衝區,與第四章內容建立深度緩衝區的步驟相似,我們首先要填寫D3D12_RESOURCE_DESC結構體描述緩衝區資源,然後使用CreateCommittedResource 方法,建立資源與一個堆,並把資源上傳到堆中。
CreateCommittedResource 方法的引數說明可見:
https://learn.microsoft.com/zh-cn/windows/win32/api/d3d12/nf-d3d12-id3d12device-createcommittedresource
其中有三個引數在本節中很重要
一個是D3D12_HEAP_PROPERTIES *pHeapProperties
一個是D3D12_RESOURCE_DESC *pDesc
一個是D3D12_RESOURCE_STATES
D3D12_RESOURCE_STATES代表著資源狀態
在d3d的初始化中我們提到這樣可以防止資源冒險 比如在讀的狀態在寫資源等等
詳細的資源種類可見:
https://learn.microsoft.com/zh-cn/windows/win32/api/d3d12/ne-d3d12-d3d12_resource_states
D3D12_HEAP_PROPERTIES是一個結構體:
其中D3D12_HEAP_TYPE的型別主要有以下幾種:
D3D12_RESOURCE_DESC 與 D3D12_HEAP_PROPERTIES的建立 這裡分別借用了CD3DX12_HEAP_PROPERTIES 與 CD3DX12_RESOURCE_DESC兩種變體方法來簡化緩衝區的建立過程:
ThrowIfFailed(device->CreateCommittedResource(
//預設堆
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
//bytesize 代表緩衝區所佔位元組數
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
//common狀態
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(defaultBuffer.GetAddressOf())));
讓我們回到建立頂點緩衝區上來,當我們想要為樹木、地形等預設幾何體(每一幀都不會發生變化的結合體)來建立頂點緩衝區時,常常選擇預設堆來最佳化效能,當頂點緩衝區初始化完畢後,只有gpu需要從中讀取資料來繪製幾何體。但是在初始化緩衝區時,需要cpu向預設堆中的頂點緩衝區寫入資料,這是我們就需要一個上傳堆作為中介,為此本節編寫了CreateDefaultBuffer函式:
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_COPY_DEST));
UpdateSubresources<1>(cmdList, defaultBuffer.Get(), uploadBuffer.Get(), 0, 0, 1, &subResourceData);
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ));
return defaultBuffer;
}
整個建立頂點緩衝區的流程如下:
然後我們還需要為其建立檢視(無需為其建立描述符堆) 以及將其繫結到渲染流水線上的輸入槽,這樣就可以向輸入裝配器傳入頂點資料:
D3D12_VERTEX_BUFFER_VIEW VertexBufferView()const
{
D3D12_VERTEX_BUFFER_VIEW vbv;
//虛擬地址 使用函式即可獲得
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
//頂點緩衝區所佔位元組大小
vbv.StrideInBytes = VertexByteStride;
//每個頂點資料所佔位元組大小
vbv.SizeInBytes = VertexBufferByteSize;
return vbv;
}
//0 代表繫結第0個輸入槽 共有16個
//1 代表頂點緩衝區的數量為1
mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView());
最後繪製頂點:
定義圖元拓撲型別
mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
索引
索引緩衝區的建立過程和頂點的過程很類似:
定義索引
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);
//定義預設堆 與 上傳堆
Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferUploader = nullptr;
//初始化索引緩衝區
mBoxGeo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(), ibByteSize, mBoxGeo->IndexBufferUploader);
//建立檢視 繫結到渲染流水線
D3D12_INDEX_BUFFER_VIEW IndexBufferView()const
{
D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
ibv.Format = IndexFormat;
ibv.SizeInBytes = IndexBufferByteSize;
return ibv;
}
mCommandList->IASetIndexBuffer(&mBoxGeo->IndexBufferView());
//繪製頂點
mCommandList->DrawIndexedInstanced(
mBoxGeo->DrawArgs["box"].IndexCount,
1, 0, 0, 0);
注意在上述過程中我們採用索引來繪製頂點 而不是像上一部分那樣使用DrawInstanced 引數解釋如下:
頂點著色器
頂點著色器程式碼如下
//cbuffer 代表常量緩衝區 b0儲存資源的暫存器
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;
//轉換到齊次裁剪空間
//mul 有向量矩陣 或者矩陣矩陣乘法的多個過載版本
//透視除法步驟是交由硬體處理 人為無需編寫程式碼
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// 直接將輸入顏色傳遞給畫素著色器
vout.Color = vin.Color;
return vout;
}
不同暫存器儲存不同型別資源如下:
由於使用的著色器語言 HLSL沒有 引用或者指標 所以返回多條資料 可以使用結構體的形式 在HLSL中所有函式都是內聯的
注意上述程式碼的語義都是特定的 比如SV_POSITION就代表著儲存著齊次裁剪空間的頂點位置資訊 其餘語義說明可見:
https://learn.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
還有一個地方注意的是 頂點著色器中使用的資料必須要都在之前的頂點結構體中定義(當然還有輸入佈局描述)但是我們定義的頂點結構體資料可以更多 必須是一個包含關係
畫素著色器
對頂點著色器輸出的資料 進行插值 在不使用幾何著色器的情況下 插值的結果作為畫素著色器的輸入
這裡還強調了一下pixel fragment 與 pixel的區別 畫素著色器的輸入是畫素片段 而畫素是已經透過深度測試 模版測試等等 最終繪製到螢幕上去的畫素
d3d還提到 由於硬體最佳化的原因 有些畫素片段 進行early-z之後就已經被篩除 但是有可能畫素著色器中對畫素片段的深度值進行了改變 此時就不能進行early-z 因為畫素片段的最終深度值尚未確定
本節的畫素著色器的程式碼很簡單,直接輸出顏色:
函式引數列表之後的SV_Target語義表示 輸出的格式應該與渲染目標的格式相匹配
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
著色器編譯
ComPtr<ID3DBlob> mvsByteCode = nullptr;
ComPtr<ID3DBlob> mpsByteCode = nullptr;
mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS", "vs_5_0");
mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS", "ps_5_0");
ComPtr<ID3DBlob> d3dUtil::CompileShader(
const std::wstring& filename,
const D3D_SHADER_MACRO* defines,
const std::string& entrypoint,
const std::string& target)
{
UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
HRESULT hr = S_OK;
ComPtr<ID3DBlob> byteCode = nullptr;
ComPtr<ID3DBlob> errors;
hr = D3DCompileFromFile(filename.c_str(), defines, D3D_COMPILE_STANDARD_FILE_INCLUDE,
entrypoint.c_str(), target.c_str(), compileFlags, 0, &byteCode, &errors);
if(errors != nullptr)
OutputDebugStringA((char*)errors->GetBufferPointer());
ThrowIfFailed(hr);
return byteCode;
}
其中比較重要的引數有
檔名 比如:L"Shaders\color.hlsl" 這裡的型別是wstring 因此要使用L
著色器的入口點 VS/PS
著色器版本 vs_5_0等等
這裡簡要介紹了一下ID3DBlob這個型別:
我在知乎看到一個回答介紹的更為詳細:
https://zhuanlan.zhihu.com/p/304352552
下面引用如下
Blob(binary large object),二進位制大物件。ID3DBlob則是DX12內建的一種存放較為龐大的二進位制物件。在GPU上面,我們對於大部分資源的描述一般都是用地址起點(address starting point)加上物件記憶體容量(object memory)來描述並且確定某一物件資源
因為其資源記憶體容量較為龐大的特點,這些資源大多數都不能直接上傳到GPU,而是首先在CPU預處理成Blob,然後再上傳繫結到GPU上面,才能供GPU使用
上傳的物件包括但不限於頂點資料(Vertex data),索引資料(Index data),材質(Texture)等,還包括我們著色器程式(shader)。即我們寫的HLSL(high level shader language)程式,需要在CPU端透過預處理和編譯才能上傳到GPU端供GPU讀取並且執行
常量緩衝區
常量緩衝區也是一種GPU資源(ID3D12Resource),但是常量緩衝區是CPU每幀都要更新一次,比如攝像機如果每幀都在移動,那麼常量緩衝區每幀都需要更新其中的檢視矩陣,所以我們需要將常量緩衝區建立到一個上傳堆而非預設堆,這樣我們就可以從cpu端更新常量。
下面讓我們來看看示例程式中是如何建立常量緩衝區的
首先,定義常量緩衝區結構體:
struct ObjectConstants
{
XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};
我們可以看到目前裡面只定義了檢視矩陣
其次,定義了上傳緩衝區的輔助類UploadBuffer.h
注意該輔助類主要用於需要提交到上傳堆的gpu資源,而我們之前有一個用於建立預設堆的輔助函式:
template<typename T>
class UploadBuffer
{
public:
//引數說明
//elementCount表示ObjectConstants的數量
//isConstantBuffer表示是否為要建立常量緩衝區
UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) :
mIsConstantBuffer(isConstantBuffer)
{
mElementByteSize = sizeof(T);
//如果為常量緩衝區,重新計算ObjectConstants結構體的大小
if(isConstantBuffer)
mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
//建立gpu資源(常量緩衝區) 與 一個上傳堆 並把資源提交到堆上
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)));
//使用map方法,在cpu端分配一塊虛擬地址範圍,用來對映gpu的資源
ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
}
UploadBuffer(const UploadBuffer& rhs) = delete;
UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
~UploadBuffer()
{
//呼叫unmap取消對gpu資源的對映
if(mUploadBuffer != nullptr)
mUploadBuffer->Unmap(0, nullptr);
mMappedData = nullptr;
}
//獲取gpu資源
ID3D12Resource* Resource()const
{
return mUploadBuffer.Get();
}
//從cpu端更新常量緩衝區中的內容
void CopyData(int elementIndex, const T& data)
{
memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
}
private:
Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer;
BYTE* mMappedData = nullptr;
UINT mElementByteSize = 0;
bool mIsConstantBuffer = false;
};
建立常量緩衝區 我們可以使用如下程式碼:
std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB = nullptr;
定義常量緩衝區儲存的是ObjectConstants型別資料 數量為1
mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 1, true);
上述程式碼中 我們可以看到UploadBuffer這個類是使用了模版 這意味著該方法不僅可以建立常量緩衝區資源 也可以建立其它使用上傳堆的gpu資源
同時上述程式碼中在獲取ObjectConstants的大小時,我們可以看到使用了d3dUtil::CalcConstantBufferByteSize的方法,該方法程式碼如下:
static UINT CalcConstantBufferByteSize(UINT byteSize)
{
// Example: Suppose byteSize = 300.
// (300 + 255) & ~255
// 555 & ~255
// 0x022B & ~0x00ff
// 0x022B & 0xff00
// 0x0200
// 512
return (byteSize + 255) & ~255;
}
這是因為常量緩衝區的大小必須是硬體最小分配空間的整數倍(通常是256b) 這是因為硬體只能按照這樣的規格來檢視常量資料,所以要對常量緩衝區的陣列進行填充位元組
然後,我們還需要建立相應的描述符來將資源繫結到渲染流水線上,和之前頂點緩衝區描述符以及索引不同,我們要為常量緩衝區描述符建立描述堆,然後再建立描述符:
//建立cbv描述符堆
void BoxApp::BuildDescriptorHeaps()
{
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)));
}
//計算第i個物體ObjectConstants的起始記憶體位置 與大小
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
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());
根簽名與描述符表
根簽名的作用是,定義繫結到渲染流水線上的資源,與對應的著色器的輸入暫存器的對映關係,從而可以被著色器程式訪問。
不同的繪製呼叫可能用到一組不同的著色器程式,這就意味著用到不同的根簽名。
在d3d中,根簽名使用ID3DRootSignature介面來表示,並且由一組描述繪製呼叫過程中著色器所需資源的根引數定義而成
根引數可以是根常量、根描述符或者描述符表。在本章中,我們只是簡要了解根簽名,詳細的介紹將在下一章中展開,本章只使用了描述符表,即描述符堆中存有描述符的一塊連續區域
下面根據程式碼簡要分析:
void BoxApp::BuildRootSignature()
{
// 根引數
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
// 建立一個cbv的描述符表
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
1, //描述符數量
0 //繫結到b0暫存器);
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);
// 根簽名由一組根引數構成
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// 建立根簽名 必須要先將根簽名的描述佈局透過ID3DBlob序列化才能傳入建立根簽名的方法
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)));
}
然後還要透過命令列表設定cbv堆與根簽名,再透過設定描述符表繫結資源:
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
mCommandList->SetGraphicsRootDescriptorTable(0, mCbvHeap->GetGPUDescriptorHandleForHeapStart());
一些關於根簽名的注意事項:
配置光柵器狀態與流水線狀態物件
大多數控制圖形流水線狀態物件被統稱為流水線狀態物件PSO,用介面ID3D12PipelineState表示
建立其的程式碼如下:
void BoxApp::BuildPSO()
{
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)));
}
描述符的詳細屬性可檢視微軟文件