【DirectX12】1.基本组件创建和绘图流程
目錄
基本組件的創建和繪圖流程
1.頭文件
2.類型名字
3.檢測XMath支持
4.開啟調試層
4.創建DXGI的Factory接口
5.獲取硬件適配器
6.輸出顯卡信息(可選)
7.創建Device
8.創建命令隊列(command queue)
9.創建交換鏈(swap chain)
10.禁止alt+enter的功能
11.記錄當前后背緩沖索引
12.創建繪制表面視圖(rtv)堆描述符
?13.創建深度模板視圖(dsv)堆描述符
14.獲取堆描述符偏移大小
15.創建rtv
16.創建命令分配器(command allocator)
17.創建dsv
18.創建命令列表(command list)
19.創建用于同步的資源
20.創建根簽名(root signature)
21.創建輸入布局(input element)
22.編譯shader
23.創建管道狀態對象(pipeline state object)
光柵(rasterizer)狀態
混合(blend)狀態
深度模板(depth stencil)狀態
最終創建PSO
24.創建采樣器
25.創建索引緩沖(index buffer)
填充內存
創建默認堆(heap default)
?上載堆(heap upload)的創建
轉換資源狀態
?創建索引緩沖視圖(ibv)
26.執行命令列表
27.繪制流水線
重置命令分配器和命令列表
rtv、stv清屏
繪制
轉換rtv狀態
關閉命令列表并執行
呈現并進行下一幀
28.繪圖流程
29.代碼倉庫
基本組件的創建和繪圖流程
1.頭文件
| #include <d3d12.h>? ? ? | 最基本的dx12頭文件 |
| #include "d3dx12.h"? ? | d3dx12與以往的不同,現在只有一個頭文件,去官方示例里復制一份即可 |
| #include <dxgi1_6.h>? | ?dxgi相關 |
| #include <D3DCompiler.h> | 著色器編譯相關 |
| #include <DirectXMath.h> | 數學庫 |
2.類型名字
? ? ? ? 使用DirectX命名空間,然后typedef一些DXGI的類型,方便以后替換。
using namespace DirectX;typedef IDXGIFactory7 _IDXGIFactory; typedef IDXGIAdapter4 _IDXGIAdapter; typedef IDXGISwapChain4 _IDXGISwapChain;3.檢測XMath支持
if (!XMVerifyCPUSupport()) {dnd_debug(DL::DAMAGE, L"DirectXMath不支持!");return; }4.開啟調試層
? ? ? ? 開啟調試層后,在出現錯誤時,被DX庫檢測到,就會輸出原因。
#if defined(_DEBUG)// Enable the D3D12 debug layer.com_ptr<ID3D12Debug> debugController;hr = D3D12GetDebugInterface(IID_PPV_ARGS(&debugController));if (FAILED(hr)){dnd_debug(DL::DAMAGE, L"創建調試層失敗!");return;}debugController->EnableDebugLayer(); #endif4.創建DXGI的Factory接口
? ? ? ?IDXGIFactory是DXGI的基礎接口,我們需要用com_ptr保存它的指針。可能你還看到其他人用ComPtr,這是偏舊一些的代碼。
//.h com_ptr<_IDXGIFactory> _factory; //.cpp hr = CreateDXGIFactory1(__uuidof(_IDXGIFactory), (void**)(&_factory)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"F創建DXGI接口失敗!");return; }5.獲取硬件適配器
? ? ? ? IDXGIAdapter是顯卡的抽象,但不一定是硬件的。通過GetHardwareAdapter函數返回硬件適配器,并attach賦予指針所有權給com_ptr<_IDXGIAdapter>。
//.h com_ptr<_IDXGIAdapter> _adapter; //.cpp _IDXGIAdapter* adapter; GetHardwareAdapter(_factory.get(), &adapter); if (adapter == nullptr) {dnd_debug(DL::DAMAGE, L"獲取硬件適配器失敗!");return; } _adapter.attach(adapter); void GetHardwareAdapter(_IDXGIFactory* pFactory, _IDXGIAdapter** ppAdapter) {*ppAdapter = nullptr;for (UINT adapterIndex = 0; ; ++adapterIndex){_IDXGIAdapter* pAdapter = nullptr;if (DXGI_ERROR_NOT_FOUND == pFactory->EnumAdapterByGpuPreference(adapterIndex,DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, _uuidof(_IDXGIAdapter), (void**)&pAdapter)){// No more adapters to enumerate.break;}// Check to see if the adapter supports Direct3D 12, but don't create the// actual device yet.if (SUCCEEDED(D3D12CreateDevice(pAdapter, D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr))){*ppAdapter = pAdapter;return;}pAdapter->Release();} }6.輸出顯卡信息(可選)
//輸出顯卡信息 DXGI_ADAPTER_DESC adapter_desc; _adapter->GetDesc(&adapter_desc);dnd_debug(DL::MSG, adapter_desc.Description); dnd_debug(DL::MSG, L"顯卡內存:" + to_wstring(int(adapter_desc.DedicatedVideoMemory / 1024 / 1024))); dnd_debug(DL::MSG, L"獨占內存:" + to_wstring(int(adapter_desc.DedicatedSystemMemory / 1024 / 1024))); dnd_debug(DL::MSG, L"共享內存:" + to_wstring(int(adapter_desc.SharedSystemMemory / 1024 / 1024)));7.創建Device
? ? ? ? 其中D3D_FEATURE_LEVEL_11_0是最低要求顯卡支持的特性等級(feature level),注意在GetHardwareAdapter函數里,我們也是填的這個值。這是官方文檔的方法,應該不能填再低于它的值。
hr = D3D12CreateDevice(_adapter.get(),D3D_FEATURE_LEVEL_11_0,IID_PPV_ARGS(&_device)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建Device失敗!");return; }8.創建命令隊列(command queue)
? ? ? ? 命令隊列用于執行命令列表(command list),簡單情況只需要創建一個。
D3D12_COMMAND_QUEUE_DESC desc; desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; desc.NodeMask = 0; desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;hr = _device->CreateCommandQueue(&desc, IID_PPV_ARGS(&_commandQueue)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建命令隊列失敗!");return; }9.創建交換鏈(swap chain)
| SwapEffect | 交換模型 | DirectX12應該使用DXGI_SWAP_EFFECT_FLIP_DISCARD交換模型 |
| BufferCount | 緩沖數量 | 翻轉丟棄模型,此值必須大于2 |
| BufferUsage | 緩沖標記 | 作為繪制表面(render target)應該填DXGI_USAGE_RENDER_TARGET_OUTPUT |
| OutputWindow | 窗口句柄 | 填入窗口句柄hwnd |
| Windowed | 窗口模式 | 這個值最好為TRUE,切換全屏是之后的事 |
| Flag | 標記 | 填入DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH讓DXGI可以自行修改緩沖大小,由于不需要響應alt+enter操作,直接填0 |
| Format | 顏色格式 | 統一使用DXGI_FORMAT_R8G8B8A8_UNORM |
| Width | 緩沖寬度 | 比如800 |
| Height | 緩沖高度 | 比如600 |
????????BufferDesc其余的刷新率、掃描方式字段如下填默認值即可,因為全屏模式,還需要匹配這些信息。
? ? ? ? 而SamleDesc多重采樣(MSAA)字段,Count填1,Quality填0即可。因為翻轉丟棄模型不支持多重采樣。
com_ptr<IDXGISwapChain> swapChain;DXGI_SWAP_CHAIN_DESC descSwapChain; descSwapChain.BufferCount = SWAP_CHAIN_BUFFER_COUNT; descSwapChain.BufferDesc.Format = DXGI_FORMAT_TYPE; descSwapChain.BufferDesc.Height = h; descSwapChain.BufferDesc.RefreshRate.Denominator = 0; descSwapChain.BufferDesc.RefreshRate.Numerator = 0; descSwapChain.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; descSwapChain.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; descSwapChain.BufferDesc.Width = w; descSwapChain.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; descSwapChain.Flags = 0;// DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; descSwapChain.OutputWindow = g_system.GetHwnd(); descSwapChain.SampleDesc.Count = 1; descSwapChain.SampleDesc.Quality = 0; descSwapChain.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; descSwapChain.Windowed = TRUE;hr = _factory->CreateSwapChain(_commandQueue.get(),&descSwapChain,swapChain.put() ); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建交換鏈失敗!");return; } swapChain.as(_swapChain);10.禁止alt+enter的功能
? ? ? ? 創建交換鏈關聯窗口后調用才有效。
_factory->MakeWindowAssociation(g_system.GetHwnd(), DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);11.記錄當前后背緩沖索引
UINT _frameIndex = _swapChain->GetCurrentBackBufferIndex();12.創建繪制表面視圖(rtv)堆描述符
? ? ? ? 繪制表面視圖(render target view),縮寫rtv。
? ? ? ? 堆描述符(heap descriptor),用于描述多個資源的抽象。所以我們創建了一個_rtvHeap來表示對rtv的引用,其中NumDescriptors字段和創建交換鏈時填入的緩沖數量一致。
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; rtvHeapDesc.NodeMask = 0; rtvHeapDesc.NumDescriptors = SWAP_CHAIN_BUFFER_COUNT;// rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;hr = _device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&_rtvHeap));if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建RTV堆描述符失敗!");return; }?13.創建深度模板視圖(dsv)堆描述符
? ? ? ? 深度緩沖用作深度測試,而模板緩沖可作為鏡子效果。為了創建深度模板視圖(depth stencil view),首先需要創建堆描述符。這里NumDescriptors字段填1,因為只需要1個。
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {}; dsvHeapDesc.NumDescriptors = 1; dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; hr = _device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&_dsvHeap));if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建深度模板緩存堆描述符失敗!");return; }14.獲取堆描述符偏移大小
? ? ? ? 其中常量緩沖視圖(constant buffer view)、無序訪問視圖(unordered access view)和dsv的堆描述符偏移大小是一樣的。
_sizeDescriptorRtv = _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); _sizeDescriptorCbvSrv = _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);? ? ? ? 一個堆描述符描述了多個資源,這個偏移就是用于確定某個資源所在的位置。比如我們創建了包含N個紋理的著色器資源視圖(shader resource view)堆描述符,通過以下方式切換紋理:
CD3DX12_GPU_DESCRIPTOR_HANDLE srvHandleTex(_srvHeap->GetGPUDescriptorHandleForHeapStart(), index, g_dx._sizeDescriptorCbvSrv); _commandList->SetGraphicsRootDescriptorTable(0, srvHandleTex);15.創建rtv
? ? ? ? 前面創建了rtv的堆描述符,那個時候就確定了描述資源的數量(開始填入的SWAP_CHAIN_BUFFER_COUNT)。這時實際的創建rtv,由于它需要動態修改(需要響應窗口大小發生改變),我們寫成可反復調用的形式:
void DirectX::_create_rtv(LONG w, LONG h) {CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeap->GetCPUDescriptorHandleForHeapStart());// Create a RTV for each frame.for (UINT n = 0; n < SWAP_CHAIN_BUFFER_COUNT; n++){HRESULT hr = _swapChain->GetBuffer(n, IID_PPV_ARGS(&_renderTargets[n]));if (FAILED(hr)){dnd_debug(DL::ERR, L"從交換鏈獲取后背緩沖失敗!");continue;}_device->CreateRenderTargetView(_renderTargets[n].get(), nullptr, rtvHandle);rtvHandle.Offset(1, _sizeDescriptorRtv);} }? ? ? ? 這里構造了一個rtvHandle變量,然后每個緩沖創建一個RTV,循環的末尾調用了rtvHandle.Offset(1, _sizeDescriptorRtv);進行偏移。
16.創建命令分配器(command allocator)
? ? ? ? 命令分配器用于記錄命令列表執行過的操作。它的數量等于交換鏈的緩沖數量。
for (UINT n = 0; n < SWAP_CHAIN_BUFFER_COUNT; n++) {hr = _device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&_commandAllocator[n]));if (FAILED(hr)){dnd_debug(DL::DAMAGE, L"創建命令分配器失敗!");return;} }17.創建dsv
? ? ? ? 先通過CreateCommittedResource在顯存上創建資源,再調用CreateDepthStencilView和前面dsvHeap綁定關系。
? ? ? ? 因為我暫時只使用了深度測試,所以DepthStencil.Stencil字段為0,而DepthStencil.Depth字段我設為的是1.0f,這個值只能在[0, 1]的范圍。在構建投影矩陣時,所有頂點的z值都會被變換到這個范圍。而2d繪制使用的是正交投影矩陣,我們手動設置z值在此范圍即可,這也是精靈的遮擋關系。
? ? ? ? 并且每幀都需要重置dsv的緩沖值,類似于清屏。
? ? ? ? 它依賴于交換鏈緩沖的大小,所以需要多次調用,寫成函數的形式。
void DirectX::_create_dsv(LONG w, LONG h) {D3D12_DEPTH_STENCIL_VIEW_DESC depthStencilDesc = {};depthStencilDesc.Format = DXGI_FORMAT_D32_FLOAT;depthStencilDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;depthStencilDesc.Flags = D3D12_DSV_FLAG_NONE;D3D12_CLEAR_VALUE depthOptimizedClearValue = {};depthOptimizedClearValue.Format = DXGI_FORMAT_D32_FLOAT;depthOptimizedClearValue.DepthStencil.Depth = DEPTH_VALUE_START;depthOptimizedClearValue.DepthStencil.Stencil = 0;HRESULT hr = _device->CreateCommittedResource(pointer(CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)),D3D12_HEAP_FLAG_NONE,pointer(CD3DX12_RESOURCE_DESC::Tex2D(DXGI_FORMAT_D32_FLOAT, w, h, 1, 0, 1, 0, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)),D3D12_RESOURCE_STATE_DEPTH_WRITE,&depthOptimizedClearValue,IID_PPV_ARGS(&_depthStencil));if (FAILED(hr)){dnd_debug(DL::DAMAGE, L"創建dsv失敗!");return;}_device->CreateDepthStencilView(_depthStencil.get(), &depthStencilDesc, _dsvHeap->GetCPUDescriptorHandleForHeapStart());}18.創建命令列表(command list)
? ? ? ? 在命令分配器的基礎上創建命令列表,通過_frameIndex只需要創建1個。
hr = _device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, _commandAllocator[_frameIndex].get(), nullptr, IID_PPV_ARGS(&_commandList)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建命令列表失敗!");return; }19.創建用于同步的資源
? ? ? ? 當命令列表的操作被命令隊列執行時,這時從內存向顯卡傳輸資源,但這是異步的,需要時間來完成這些操作。通過同步資源我們可以知道,何時顯卡完成了操作,此時就可以釋放內存資源等等。
//.h HANDLE _fenceEvent; com_ptr<ID3D12Fence> _fence; UINT64 _fenceValues[SWAP_CHAIN_BUFFER_COUNT] = {};//.cpp // Create synchronization objects and wait until assets have been uploaded to the GPU. hr = _device->CreateFence(_fenceValues[_frameIndex], D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&_fence)); _fenceValues[_frameIndex]++;// Create an event handle to use for frame synchronization. _fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); if (_fenceEvent == nullptr) {//hr = HRESULT_FROM_WIN32(GetLastError()));dnd_debug(DL::DAMAGE, L"創建同步事件失敗!");return; }? ? ? ? 等待GPU完成操作的函數。
void DirectX::WaitForGpu() {HRESULT hr = E_FAIL;// Schedule a Signal command in the queue.hr = _commandQueue->Signal(_fence.get(), _fenceValues[_frameIndex]);if (FAILED(hr)){dnd_debug(DL::ERR, L"命令隊列Signal失敗!");return;}// Wait until the fence has been processed.hr = _fence->SetEventOnCompletion(_fenceValues[_frameIndex], _fenceEvent);if (FAILED(hr)){dnd_debug(DL::ERR, L"圍欄設置同步對象失敗!");return;}WaitForSingleObjectEx(_fenceEvent, INFINITE, FALSE);// Increment the fence value for the current frame._fenceValues[_frameIndex]++; }20.創建根簽名(root signature)
? ? ? ? 根簽名簡單理解,需要與shader的資源一致。最常見的需要紋理、矩陣這兩種資源,紋理歸于srv,而矩陣屬于cbv。還需要創建一個動態的采樣器(sampler)資源,采樣器也可以是靜態的。
// Create the root signature. D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {};// This is the highest version the sample supports. If CheckFeatureSupport succeeds, the HighestVersion returned will not be greater than this. featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;if (FAILED(g_dx._device->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &featureData, sizeof(featureData)))) {featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0; }CD3DX12_DESCRIPTOR_RANGE1 ranges[3]; ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);//紋理資源 ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);//采樣器 ranges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);//常量區CD3DX12_ROOT_PARAMETER1 rootParameters[3]; rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_PIXEL); rootParameters[1].InitAsDescriptorTable(1, &ranges[1], D3D12_SHADER_VISIBILITY_PIXEL); rootParameters[2].InitAsDescriptorTable(1, &ranges[2], D3D12_SHADER_VISIBILITY_ALL);//所有可見CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init_1_1(_countof(rootParameters), rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);com_ptr<ID3DBlob> signature; com_ptr<ID3DBlob> error; hr = D3DX12SerializeVersionedRootSignature(&rootSignatureDesc, featureData.HighestVersion, signature.put(), error.put()); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"序列化根簽名失敗:" + String::Mbtowc((char*)error->GetBufferPointer(), false));return; } hr = g_dx._device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&_rootSignature)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建根簽名失敗!");return; }? ? ? ? 可以看到,0為srv,1為sampler,2為cbv。這與shader文件一致:
//2d.hlsl cbuffer cb0 : register(b0) {float4x4 g_mWorldViewProj; };Texture2D g_txDiffuse : register(t0); SamplerState g_sampler : register(s0);????????只有cbv標記了全部可見(D3D12_SHADER_VISIBILITY_ALL ),因為頂點著色器(vertex shader)需要進行頂點變換:
//2d.hlsl PSInput VSMain(VSInput input) {PSInput result;result.position = mul(float4(input.position, 1.0f), g_mWorldViewProj);result.color = input.color;result.uv = input.uv;return result; }? ? ? ? 而像素著色器(pixel shader)使用了紋理和采樣器:
//2d.hlsl float4 PSMain(PSInput input) : SV_TARGET {float4 Color = g_txDiffuse.Sample(g_sampler, input.uv) * input.color;clip(Color.a - 0.004f);return Color; }21.創建輸入布局(input element)
? ? ? ? 輸入布局描述了與頂點的結構,分別是位置、顏色、UV。
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = {{ "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 },{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 28, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, };? ? ? ? 這是2d繪圖需要的頂點,3d繪圖則可能不需要頂點的顏色,但需要加上法線、切線等屬性通過光照混合紋理來計算它的顏色。
struct Vertex {XMFLOAT3 _pos;XMFLOAT4 _color;XMFLOAT2 _t; };22.編譯shader
com_ptr<ID3DBlob> vertexShader;com_ptr<ID3DBlob> pixelShader;#if defined(_DEBUG)// Enable better shader debugging with the graphics debugging tools.UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #elseUINT compileFlags = 0; #endifID3DBlob* errors = nullptr;hr = D3DCompileFromFile(L"2d.hlsl", nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, vertexShader.put(), &errors);if (errors != nullptr){dnd_debug(Debug::Level::INFO, String::Mbtowc((char*)errors->GetBufferPointer(), false));errors->Release();}if (FAILED(hr)){dnd_debug(DL::DAMAGE, L"編譯vs失敗!");return;}hr = D3DCompileFromFile(L"2d.hlsl", nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, pixelShader.put(), &errors);if (errors != nullptr){dnd_debug(Debug::Level::INFO, String::Mbtowc((char*)errors->GetBufferPointer(), false));errors->Release();}if (FAILED(hr)){dnd_debug(DL::DAMAGE, L"編譯ps失敗!");return;}? ? ? ? 簡單的2d.hlsl文件如下:
struct VSInput {float3 position : POSITION;float4 color : COLOR;float2 uv : TEXCOORD0; };struct PSInput {float4 position : SV_POSITION;float4 color : COLOR;float2 uv : TEXCOORD0; };cbuffer cb0 : register(b0) {float4x4 g_mWorldViewProj; };Texture2D g_txDiffuse : register(t0); SamplerState g_sampler : register(s0);PSInput VSMain(VSInput input) {PSInput result;result.position = mul(float4(input.position, 1.0f), g_mWorldViewProj);result.color = input.color;result.uv = input.uv;return result; }float4 PSMain(PSInput input) : SV_TARGET {float4 Color = g_txDiffuse.Sample(g_sampler, input.uv) * input.color;clip(Color.a - 0.004f);return Color; }23.創建管道狀態對象(pipeline state object)
光柵(rasterizer)狀態
? ? ? ? 由于2d繪圖不需要通過頂點繞序判斷正面與背面,CullMode設置為NONE或許能減少GPU的消耗。
CD3DX12_RASTERIZER_DESC rasterizerStateDesc(D3D12_DEFAULT); rasterizerStateDesc.CullMode = D3D12_CULL_MODE_NONE;混合(blend)狀態
? ? ? ? 使用如下的混合方式可以進行半透明物體的正常繪制,但BlendEnable字段現在設置的暫時是false,即未開啟顏色混合。
D3D12_BLEND_DESC blend_desc; blend_desc.AlphaToCoverageEnable = FALSE; blend_desc.IndependentBlendEnable = FALSE;//false只使用RenderTarget[0]blend_desc.RenderTarget[0].BlendEnable = FALSE; blend_desc.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; blend_desc.RenderTarget[0].SrcBlend = D3D12_BLEND_SRC_ALPHA; blend_desc.RenderTarget[0].DestBlend = D3D12_BLEND_INV_SRC_ALPHA;//兩個透明物體混合,其透明度應該是較不透明者 blend_desc.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_MAX; blend_desc.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_SRC_ALPHA; blend_desc.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_DEST_ALPHA;blend_desc.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; blend_desc.RenderTarget[0].LogicOpEnable = FALSE;深度模板(depth stencil)狀態
? ? ? ? 這里打開了深度測試,但關閉了模板測試。D3D12_COMPARISON_FUNC_LESS標記會使繪制時,z值小于當前像素z值的才被寫入。
D3D12_DEPTH_STENCIL_DESC depth_stencil_desc; //小于時成功 depth_stencil_desc.DepthEnable = TRUE; depth_stencil_desc.DepthFunc = D3D12_COMPARISON_FUNC_LESS; depth_stencil_desc.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;depth_stencil_desc.StencilEnable = FALSE;最終創建PSO
? ? ? ? 創建PSO依賴從20節(創建根簽名)到前面所創建的資源,通過修改一些字段,我們創建了兩個PSO。一個用于非透明精靈的繪制、一個用于半透明精靈的繪制。非透明精靈的繪制關閉了顏色混合,半透明精靈的繪制打開了顏色混合,并且深度測試函數改為了小于等于時成功(D3D12_COMPARISON_FUNC_LESS_EQUAL),因為半透明精靈必須后繪制,遇到z值相同的非透明像素依舊需要寫入。
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };psoDesc.pRootSignature = _rootSignature.get();psoDesc.VS = { reinterpret_cast<UINT8*>(vertexShader->GetBufferPointer()), vertexShader->GetBufferSize() };psoDesc.PS = { reinterpret_cast<UINT8*>(pixelShader->GetBufferPointer()), pixelShader->GetBufferSize() };psoDesc.RasterizerState = rasterizerStateDesc;psoDesc.BlendState = blend_desc;//CD3DX12_BLEND_DESC(D3D12_DEFAULT);psoDesc.DepthStencilState = depth_stencil_desc;psoDesc.SampleMask = UINT_MAX;psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;psoDesc.NumRenderTargets = 1;psoDesc.RTVFormats[0] = DXGI_FORMAT_TYPE;psoDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT;psoDesc.SampleDesc.Count = 1;//非透明任然需要深度測試,但不需要alpha混合hr = g_dx._device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&_pipelineState[PsoType::NORMAL]));if (FAILED(hr)){dnd_debug(DL::DAMAGE, L"創建PSO(normal)失敗!");return;}psoDesc.BlendState.RenderTarget[0].BlendEnable = TRUE;//相等也必須混合,因為背景可能是不透明物體,在DrawCall里,一定是后畫的psoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;hr = g_dx._device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&_pipelineState[PsoType::TRANSLUCENT]));if (FAILED(hr)){dnd_debug(DL::DAMAGE, L"創建PSO(translucent)失敗!");return;}24.創建采樣器
? ? ? ? 先創建堆描述符,再創建采樣器資源:
// Describe and create a sampler descriptor heap. D3D12_DESCRIPTOR_HEAP_DESC samplerHeapDesc = {}; samplerHeapDesc.NumDescriptors = 1; samplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER; samplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; hr = g_dx._device->CreateDescriptorHeap(&samplerHeapDesc, IID_PPV_ARGS(&_samplerHeap)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建采樣器堆描述符失敗!");return; }// Describe and create a sampler. D3D12_SAMPLER_DESC samplerDesc = {}; samplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP; samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP; samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP; samplerDesc.MinLOD = 0; samplerDesc.MaxLOD = D3D12_FLOAT32_MAX; samplerDesc.MipLODBias = 0.0f; samplerDesc.MaxAnisotropy = 1; samplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS; g_dx._device->CreateSampler(&samplerDesc, _samplerHeap->GetCPUDescriptorHandleForHeapStart());25.創建索引緩沖(index buffer)
填充內存
? ? ? ? 這里使用了vector,如果使用數組,由于數組太大可能會棧溢出。一個精靈(四邊形)有四個頂點,劃分為兩個三角形,則需要6個索引。
? ? ? ? 頂點繞序為 012、023,如下圖所示:
vector<UINT32> indices; indices.resize(Constant::NUM_2D_INDICES);UINT32 n = 0; for (UINT32 i = 0; i < Constant::NUM_2D_INDICES; i += 6) {indices[i + 0] = 0 + n;indices[i + 1] = 1 + n;indices[i + 2] = 2 + n;indices[i + 3] = 0 + n;indices[i + 4] = 2 + n;indices[i + 5] = 3 + n;n += 4; }創建默認堆(heap default)
? ? ? ? 我們需要在顯存上創建索引緩沖資源,通過以下方式創建:
CD3DX12_HEAP_PROPERTIES heapProps2(D3D12_HEAP_TYPE_DEFAULT); auto desc2 = CD3DX12_RESOURCE_DESC::Buffer(Constant::NUM_2D_INDICES * sizeof(UINT32));hr = g_dx._device->CreateCommittedResource(&heapProps2,D3D12_HEAP_FLAG_NONE,&desc2,D3D12_RESOURCE_STATE_COPY_DEST,nullptr,IID_PPV_ARGS(&_indexBuffer)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建索引緩存失敗(DEFAULT)!");return; }? ? ? ?通過標記 D3D12_HEAP_TYPE_DEFAULT創建的CD3DX12_HEAP_PROPERTIES 結構說明資源在顯存上,再通過?CD3DX12_RESOURCE_DESC::Buffer(size);指定了緩沖區的大小。D3D12_RESOURCE_STATE_COPY_DEST指定資源的狀態是復制目標(copy dest)狀態,資源必須是這個狀態才能通過內存上傳資源。
????????通過標記 D3D12_HEAP_TYPE_DEFAULT創建的資源叫默認堆,它是沒有CPU訪問權限的,只能通過上載堆給它上傳資源,這個過程就是內存到顯存的過程。
?上載堆(heap upload)的創建
? ? ? ? ?同樣上載堆是類似的創建方式,只不過堆類型改為了D3D12_HEAP_TYPE_UPLOAD,然后資源的狀態必須填D3D12_RESOURCE_STATE_GENERIC_READ,這是規定。它是可以被CPU訪問的,也就是存在于某片內存上。
com_ptr<ID3D12Resource> indexBufferUploadHeap; CD3DX12_HEAP_PROPERTIES heapProps3(D3D12_HEAP_TYPE_UPLOAD); auto desc3 = CD3DX12_RESOURCE_DESC::Buffer(Constant::NUM_2D_INDICES * sizeof(UINT32));hr = g_dx._device->CreateCommittedResource(&heapProps3,D3D12_HEAP_FLAG_NONE,&desc3,D3D12_RESOURCE_STATE_GENERIC_READ,nullptr,IID_PPV_ARGS(&indexBufferUploadHeap)); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"創建索引緩存失敗(Upload)!");return; }? ? ? ? 可以通過Map、CopyBufferRegion等接口來寫入,但d3dx12有封裝好的函數,簡單的整體復制我們直接使用即可,先不考慮那么多(寫入一部分、寫入紋理等不盡相同)。
// Copy data to the intermediate upload heap and then schedule a copy // from the upload heap to the index buffer. D3D12_SUBRESOURCE_DATA indexData = {}; indexData.pData = (void*)indices.data(); indexData.RowPitch = Constant::NUM_2D_INDICES * sizeof(UINT32); indexData.SlicePitch = indexData.RowPitch;UpdateSubresources<1>(g_dx._commandList.get(), _indexBuffer.get(), indexBufferUploadHeap.get(), 0, 0, 1, &indexData);? ? ? ? 這里使用到了命令列表,說明這個操作需要GPU同步,可以理解,從內存復制到顯存,必然是需要顯卡告訴我們完成了此操作。
? ? ? ? 但命令列表的執行,只是記錄了操作,并沒有實際的執行操作,等到命令隊列執行它,才真正的開始,那個時候我們才需要等待GPU完成操作。因為vector<UINT32> indices;和com_ptr<ID3D12Resource> indexBufferUploadHeap;都是局部變量,生命周期結束后,內存也就失效了。但顯然這時候內存失效是錯誤的,因為資源還沒從內存全部復制到顯存。
轉換資源狀態
? ? ? ? 前面創建的索引緩沖默認堆一開始是D3D12_RESOURCE_STATE_COPY_DEST狀態,要作為索引緩沖資源,還需要通過資源轉換屏障轉換到D3D12_RESOURCE_STATE_INDEX_BUFFER狀態。因為上面的創建,并沒有出現index buffer相關的標記,顯卡不知道這個資源的用途是索引緩沖。
auto num_barrier1 = CD3DX12_RESOURCE_BARRIER::Transition(_indexBuffer.get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_INDEX_BUFFER); g_dx._commandList->ResourceBarrier(1, &num_barrier1);?創建索引緩沖視圖(ibv)
D3D12_INDEX_BUFFER_VIEW _indexBufferView;// Describe the index buffer view. _indexBufferView.BufferLocation = _indexBuffer->GetGPUVirtualAddress(); _indexBufferView.Format = DXGI_FORMAT_R32_UINT; _indexBufferView.SizeInBytes = Constant::NUM_2D_INDICES * sizeof(UINT32);? ???? ? ? ? 在繪制時,通過_commandList->IASetIndexBuffer(&_indexBufferView);即可設置要使用索引緩沖。
26.執行命令列表
? ? ? ? 命令列表記錄完操作后,需要Close表示完成記錄,再提交給命令隊列執行。在最后使用WaitForGpu等待顯卡完成操作。
//關閉并 添加到命令隊列 // Close the command list and execute it to begin the initial GPU setup. hr = g_dx._commandList->Close(); if (FAILED(hr)) {dnd_debug(DL::DAMAGE, L"命令列表Close失敗!");return; }ID3D12CommandList* ppCommandLists[] = { g_dx._commandList.get() }; g_dx._commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);//這里必須等待資源上傳到GPU g_dx.WaitForGpu();27.繪制流水線
? ? ? ? 到這里,所有基礎組件就創建完成了,接下來是幀函數循環調用的繪制部分。
重置命令分配器和命令列表
? ? ? ? 在創建命令列表時,我們就依賴于命令分配器。這里每幀重新記錄,都需要Reset。
//重新記錄需要reset hr = _commandAllocator[_frameIndex]->Reset(); if (FAILED(hr)) {dnd_debug(DL::ERR, L"命令分配器Reset失敗!");return; }// However, when ExecuteCommandList() is called on a particular command // list, that command list can then be reset at any time and must be before // re-recording. hr = _commandList->Reset(_commandAllocator[_frameIndex].get(), _2d._pipelineState[Dx2D::PsoType::NORMAL].get()); if (FAILED(hr)) {dnd_debug(DL::ERR, L"命令列表Reset失敗!");return; }rtv、stv清屏
? ? ? ?將rtv從呈現狀態(D3D12_RESOURCE_STATE_PRESENT),轉換到繪制目標狀態(D3D12_RESOURCE_STATE_RENDER_TARGET),以進行接下來的清屏與繪制操作。順便也將dsv清空到初始值。
//資源屏障 呈現 -> 繪制目標 // Indicate that the back buffer will be used as a render target. auto res_barrier0 = CD3DX12_RESOURCE_BARRIER::Transition(_renderTargets[_frameIndex].get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET); _commandList->ResourceBarrier(1, &res_barrier0);//設置 當前繪制目標 CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(_rtvHeap->GetCPUDescriptorHandleForHeapStart(), _frameIndex, _sizeDescriptorRtv); CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(_dsvHeap->GetCPUDescriptorHandleForHeapStart()); _commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);//清屏 // Record commands. const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f }; _commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); _commandList->ClearDepthStencilView(_dsvHeap->GetCPUDescriptorHandleForHeapStart(), D3D12_CLEAR_FLAG_DEPTH, DEPTH_VALUE_START, 0, 0, nullptr);繪制
? ? ? ? 這一步放在后面再講。
轉換rtv狀態
? ? ? ? 完成繪制后,反過來操作一遍,將rtv從繪制目標狀態轉換到呈現狀態。
// Indicate that the back buffer will now be used to present. auto res_barrier1 = CD3DX12_RESOURCE_BARRIER::Transition(_renderTargets[_frameIndex].get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); _commandList->ResourceBarrier(1, &res_barrier1);關閉命令列表并執行
//關閉 hr = _commandList->Close(); if (FAILED(hr)) {dnd_debug(DL::ERR, L"命令列表關閉失敗!");return; }// 執行命令列表 ID3D12CommandList* ppCommandLists[] = { _commandList.get() }; _commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);呈現并進行下一幀
HRESULT hr = _swapChain->Present(_bVsync ? 1 : 0, 0); if (FAILED(hr)) {dnd_debug(DL::ERR, L"Present失敗!");return; }MoveToNextFrame(); void DirectX::MoveToNextFrame() {HRESULT hr = E_FAIL;// Schedule a Signal command in the queue.const UINT64 currentFenceValue = _fenceValues[_frameIndex];hr = _commandQueue->Signal(_fence.get(), currentFenceValue);if (FAILED(hr)){dnd_debug(DL::ERR, L"命令隊列Signal失敗!");return;}// Update the frame index._frameIndex = _swapChain->GetCurrentBackBufferIndex();// If the next frame is not ready to be rendered yet, wait until it is ready.if (_fence->GetCompletedValue() < _fenceValues[_frameIndex]){hr = _fence->SetEventOnCompletion(_fenceValues[_frameIndex], _fenceEvent);if (FAILED(hr)){dnd_debug(DL::ERR, L"圍欄設置同步對象失敗!");return;}WaitForSingleObjectEx(_fenceEvent, INFINITE, FALSE);}// Set the fence value for the next frame._fenceValues[_frameIndex] = currentFenceValue + 1; }28.繪圖流程
//設置根簽名 _commandList->SetGraphicsRootSignature(_rootSignature.get());//設置使用的堆描述符 ID3D12DescriptorHeap* ppHeaps[] = { _cbvSrvHeap.get() ,_samplerHeap.get() }; _commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);//設置采樣器 _commandList->SetGraphicsRootDescriptorTable(1, _samplerHeap->GetGPUDescriptorHandleForHeapStart());//設置PSO _commandList->SetPipelineState(_pipelineState[PsoType::NORMAL].get());//設置視口 _commandList->RSSetViewports(1, &g_dx._viewport);//設置裁剪 _commandList->RSSetScissorRects(1, &g_dx._scissorRect);//設置srv偏移 CD3DX12_GPU_DESCRIPTOR_HANDLE cbvSrvHandleTex(_cbvSrvHeap->GetGPUDescriptorHandleForHeapStart(), INT(iter._canvas->_id), g_dx._sizeDescriptorCbvSrv); _commandList->SetGraphicsRootDescriptorTable(0, cbvSrvHandleTex);//設置cbv偏移 CD3DX12_GPU_DESCRIPTOR_HANDLE cbvSrvHandleMat(_cbvSrvHeap->GetGPUDescriptorHandleForHeapStart(), INT(_vecCanvas.size() + mat_id), g_dx._sizeDescriptorCbvSrv); _commandList->SetGraphicsRootDescriptorTable(2, cbvSrvHandleMat);//設置繪制圖元 _commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);//設置索引緩存 _commandList->IASetIndexBuffer(&_indexBufferView);//設置頂點緩存 _commandList->IASetVertexBuffers(0, 1, &iter._canvas->_vertexBufferView);//繪制 _commandList->DrawIndexedInstanced(UINT((iter._end - iter._beg) / 2 * 3), 1, 0, INT(iter._beg), 0);29.代碼倉庫
? ? ? ? 完整的2d繪圖封裝,可以參考我的倉庫,我還將實現圖像加載、音效、文本、網絡、ui等等一系列游戲相關的組件,歡迎提出bug,或參與進來。
DND3D: 基于DirectX12的3D引擎,嘗試。
總結
以上是生活随笔為你收集整理的【DirectX12】1.基本组件创建和绘图流程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【LeetCode】6.Z 字形变换
- 下一篇: 字符编码简介