DirectX是一套用于图形渲染,并行计算等用途的API,而DirectX12是它的最新版本,关于它的介绍这里就不多说了,网上能找到很多。DirectX12本身和Metal,Vulkan一样是新一代的API,和旧OpenGL,旧DirectX这些有着很大不同,加入了“显存管理”,“多线程渲染”等新特性,朝异步和并行的方向发展。这些特性也使得DX12的使用更加复杂,举个例子,使用OpenGL在屏幕上画一个三角形可能只需要二三十行代码,而且代码也很直观;使用DX12则动不动就上百行代码,还有一堆新概念需要理解,否则你根本看不懂代码在干什么。
本篇会通过一个例子使用DX12逐步在屏幕上绘制一个三角形。(感觉在屏幕上画三角形算是图形学中的“Hello,World”了)
创建窗口
图形API只负责渲染,输入和窗口之类的事一概不管,我们只能自己创建窗口。龙书和一些教程上都是用Win32 API创建窗口,不过为了方便我会用glfw来创建窗口。
首先为项目创建一个目录,在此目录下新建CMakeLists.txt文件和src,include, libs目录。将glfw克隆到libs目录下,然后在src目录下新建main.cpp,最后将d3dx12.h下载到include目录中。这是微软的一个头文件,把许多常用的结构做了派生并添加了很多有用的方法。不使用它也完全可以,只是有了它会更加方便。
cmake_minimum_required(VERSION 3.5) project(dx12_example_1) set(CMAKE_CXX_STANDARD 17) # 对于MSVC编译器,指出文件为UTF-8编码 if(MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /source-charset:utf-8") endif() file(GLOB SOURCES src/*.cpp) # glfw会根据GLFW_LIBRARY_TYPE的值决定编译为静态库或动态库,这里我们使用静态库 set(GLFW_LIBRARY_TYPE STATIC) add_subdirectory(libs/glfw) add_executable(${PROJECT_NAME} ${SOURCES}) # 链接glfw库 target_link_libraries(${PROJECT_NAME} PRIVATE glfw) target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/include)
上面是cmake文件的内容,现在我们已经链接了glfw库并且可以正确编译。
#define GLFW_EXPOSE_NATIVE_WIN32 #include <GLFW/glfw3.h> #include <GLFW/glfw3native.h> #include <cstdio> const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; // 错误回调函数,当glfw内部出错的时候就会调用它 void errorCallback(int error, const char* description) { fprintf(stderr, "Error: %s\n", description); } // 也是一个回调函数,相应键盘事件 void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { glfwSetWindowShouldClose(window, GLFW_TRUE); } } int main() { //设置错误回调函数,应放在glfw初始化之前 glfwSetErrorCallback(errorCallback); // 初始化glfw glfwInit(); // 表示不需要使用opengl等api glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); // 创建一个新窗口 GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "GLFW Window", nullptr, nullptr); // 为新窗口设置回调函数 glfwSetKeyCallback(window, keyCallback); // 接下来是事件循环,当窗口还不应关闭的时候要不断轮询事件。 while(!glfwWindowShouldClose(window)) { glfwPollEvents(); } // 前面的事件循环结束只表示窗口应该关闭,实际的关闭通过这个函数完成。 glfwDestroyWindow(window); // glfw库会终结,并销毁各种资源,在程序结束前调用。 glfwTerminate(); return 0; }
将这段代码复制到main.cpp文件中运行,我们就成功创建了一个窗口,大部分代码的作用都在注释中。
添加头文件和库
#define GLFW_EXPOSE_NATIVE_WIN32 #include <GLFW/glfw3.h> #include <GLFW/glfw3native.h> #include <cstdio> #include <d3d12.h> #include <d3d12shader.h> #include <d3dcompiler.h> #include <dxgi1_6.h> #include <DirectXMath.h> #include <wrl.h> #ifdef _DEBUG #include <dxgidebug.h> #endif #include <iostream> #include <string> #include <d3dx12.h> using namespace DirectX; using Microsoft::WRL::ComPtr; #pragma comment(lib, "d3d12.lib") #pragma comment(lib, "d3dcompiler.lib") #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "dxguid.lib") class COMException { public: COMException(HRESULT hr) : error(hr) {} HRESULT Error() const { return error; } private: const HRESULT error; }; inline void THROW_IF_FAILED(HRESULT hr) { if(FAILED(hr)) throw COMException(hr); } const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const UINT BackBufferCount = 3; const auto BackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM; const auto DepthStencilFormat = DXGI_FORMAT_D32_FLOAT; const auto FeatureLevel = D3D_FEATURE_LEVEL_12_1;
引入dx12需要的头文件和库,#pragma comment(lib, “…”)预处理指令的作用就是链接某个库,当然你也可以用其他的方式链接。
引入wrl.h头文件主要是为了使用ComPtr模板类,它是为COM设计的智能指针。DX12很多功能都以COM接口的方式提供。关于COM接口就不再多说,只要知道ComPtr是智能指针即可。
除此之外还定义了一些常量以便之后使用。
创建DXGI
UINT factoryFlags = 0; #if defined(_DEBUG) { // 若在debug模式下,这段代码会开启DirectX的调试层,调试层在控制台上打印的信息对于debug非常有帮助。 ComPtr<ID3D12Debug> debugInterface; if(SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugInterface)))) { debugInterface->EnableDebugLayer(); factoryFlags |= DXGI_CREATE_FACTORY_DEBUG; } } #endif ComPtr<IDXGIFactory5> factory; THROW_IF_FAILED(CreateDXGIFactory2(factoryFlags, IID_PPV_ARGS(&factory)));
DX中很多函数或者类型名后面跟着一个数字,这代表它们的版本,默认情况没有数字为第一个版本,此后依次为1,2,3.。。。版本更新的的一般有更多的功能。
DXGI意思是DirectX图形基础结构,负责管理swapchain(什么是swapchain之后会说)以及与窗口打交道等基础工作。因此一般是我们第一个创建的。
枚举适配器,创建设备
ComPtr<IDXGIAdapter1> adapter; ComPtr<ID3D12Device4> device; for(UINT adapterIndex = 0; factory->EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) { DXGI_ADAPTER_DESC1 desc{}; adapter->GetDesc1(&desc); if(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) continue; //如果是软件适配器就跳过 if(SUCCEEDED(D3D12CreateDevice(adapter.Get(), FeatureLevel, _uuidof(device), nullptr))) break; } THROW_IF_FAILED(D3D12CreateDevice(adapter.Get(), FeatureLevel, IID_PPV_ARGS(&device)));
创建DXGI后需要枚举并选择一个合适的显示适配器(可以理解为显卡或者GPU),为什么说合适的呢?因为系统可能有多个显示适配器,笔记本电脑通常就有集成显卡和独立显卡两个。除此之外,Windows一般还会提供一个软件的显示适配器,所以我们要尽可能选择性能最强,功能最多的一个。
选择合适的适配器之后创建一个与之相对应的设备(device),可以这么理解,适配器对应物理上的适配器,而设备是适配器的逻辑抽象,适配器提供的各种功能一般都是通过设备来调用的。
创建设备的时候传入了一个FeatureLevel参数,它代表我们需要的DirectX等级,比如这里传入D3D_FEATURE_LEVEL_12_1,表示适配器至少应支持DirectX 12.1.
创建命令队列,命令分配器,命令列表
ComPtr<ID3D12CommandQueue> cmdQueue; ComPtr<ID3D12CommandAllocator> cmdAlloc; ComPtr<ID3D12GraphicsCommandList> cmdList; D3D12_COMMAND_QUEUE_DESC queueDesc{}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; THROW_IF_FAILED(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&cmdQueue))); THROW_IF_FAILED(device->CreateCommandAllocator(queueDesc.Type, IID_PPV_ARGS(&cmdAlloc))); THROW_IF_FAILED(device->CreateCommandList(0, queueDesc.Type, cmdAlloc.Get(), nullptr, IID_PPV_ARGS(&cmdList))); THROW_IF_FAILED(cmdList->Close());
每个GPU都至少维护着一个命令队列,当队列不为空的时候,GPU就会从中依次取出命令并执行。而CPU端,或者说在我们的代码中,可以通过命令列表记录命令,然后将其提交到命令队列中。一件很重要的事是理解CPU端与GPU端的异步性,我们提交的命令不一定会立即执行,因为在队列中可能还有其他的命令。
由命令列表记录的命令,实际存储在与之相关联的命令分配器内。命令列表有开启与关闭两种状态,为开启状态的时候可以记录命令,为关闭状态的时候才可提交。一个命令分配器可以与多个命令列表相关联,但在这些命令列表中同时只能有一个处于开启状态。命令列表创建时默认处于开启状态,所以如果你连续使用一个命令分配器创建多个命令列表就会发生错误。命令列表关闭之后,通过ExecuteCommandLists方法提交,之后便可通过Reset方法将其清空并再重新开启。由于命令实际存储在分配器中所以即使命令尚未完成,命令列表就可以重置,不过要注意命令分配器只能在其命令执行完毕后才能Reset清空。
创建交换链
ComPtr<IDXGISwapChain1> swapChain1; ComPtr<IDXGISwapChain3> swapChain; DXGI_SWAP_CHAIN_DESC1 swapChainDesc{}; swapChainDesc.BufferCount = BackBufferCount; // 后台缓冲区的数量 swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 缓冲区的用途 swapChainDesc.Width = SCR_WIDTH; // 缓冲区宽度 swapChainDesc.Height = SCR_HEIGHT; // 缓冲区高度 swapChainDesc.Format = BackBufferFormat; // 缓冲区格式 swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; swapChainDesc.SampleDesc = {1, 0}; // 这里1表示采样数,0表示采样质量 THROW_IF_FAILED(factory->CreateSwapChainForHwnd( cmdQueue.Get(), // 只有当该命令队列为空时,交换链才可翻转 glfwGetWin32Window(window), // 渲染到的窗口的句柄 &swapChainDesc, nullptr, nullptr, &swapChain1 )); THROW_IF_FAILED(swapChain1.As(&swapChain));
屏幕显示上的图像对应一块内存(或者是显存),通过对这块内存的写入就可以改变屏幕上的图像,但为了避免画面撕裂我们不能直接对其写入,而是先将图像写入另一块内存,然后系统只需要更改指针就能将前台缓冲区与后台缓冲区交换(或者称为翻转操作)。
创建RTV, DSV描述符
// 创建RTV描述符 ComPtr<ID3D12DescriptorHeap> rtvHeap; D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc{}; rtvHeapDesc.NumDescriptors = BackBufferCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; THROW_IF_FAILED(device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap))); UINT rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); ComPtr<ID3D12Resource> renderTargets[BackBufferCount]; CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart()); for(UINT i = 0; i < BackBufferCount; ++i) { THROW_IF_FAILED(swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i]))); device->CreateRenderTargetView(renderTargets[i].Get(), nullptr, rtvHandle); rtvHandle.Offset(1, rtvDescriptorSize); } // 创建DSV描述符 ComPtr<ID3D12DescriptorHeap> dsvHeap; D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc{}; dsvHeapDesc.NumDescriptors = 1; dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; THROW_IF_FAILED(device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&dsvHeap))); UINT dsvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV); auto depthDesc = CD3DX12_RESOURCE_DESC::Tex2D(DepthStencilFormat, SCR_WIDTH, SCR_HEIGHT); depthDesc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; D3D12_CLEAR_VALUE depthClearValue{}; depthClearValue.Format = DepthStencilFormat; depthClearValue.DepthStencil.Depth = 1.0; depthClearValue.DepthStencil.Stencil = 0; ComPtr<ID3D12Resource> depthStencilBuffer; THROW_IF_FAILED(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &depthDesc, D3D12_RESOURCE_STATE_DEPTH_WRITE, &depthClearValue, IID_PPV_ARGS(&depthStencilBuffer) )); D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc{}; dsvDesc.Format = DepthStencilFormat; dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D; device->CreateDepthStencilView(depthStencilBuffer.Get(), &dsvDesc, dsvHeap->GetCPUDescriptorHandleForHeapStart());
D3D的许多资源都通过描述符进行引用,大多数描述符都要储存在描述符堆中。首先创建一个RTV(渲染目标视图)描述符堆,然后为交换链中的每个后备缓冲创建描述符。 接着进行类似的操作创建DSV(深度模板视图),不过这里我们要自己创建深度模板缓冲区。描述符堆以及D3D中的其他堆可以理解为一个固定大小的数组,RTV描述符堆就是一个存储多个RTV的定长数组。然后我们还要获取RTV和DSV的大小,这样就可以根据索引得知每个描述符在内存中的位置了。
创建根签名
// 创建根签名 ComPtr<ID3D12RootSignature> rootSignature; CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); ComPtr<ID3D10Blob> signature; ComPtr<ID3D10Blob> error; THROW_IF_FAILED(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)); THROW_IF_FAILED(device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)));
类似函数签名给出了函数的输入,根签名定义了着色器程序的输入,着色器根据根签名来定位它们需要的资源,这里只创建了一个空的根签名,关于根签名的内容以后再说。
创建着色器
UINT compileFlags = 0; #if defined(_DEBUG) compileFlags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif ComPtr<ID3D10Blob> vertexShader; ComPtr<ID3D10Blob> pixelShader; auto result = D3DCompileFromFile(L"../../shaders/shader.hlsl", nullptr, nullptr, "VSMain", "vs_5_1", compileFlags, 0, &vertexShader, &error); if(!SUCCEEDED(result)) { std::string info; for(int i = 0; i < error->GetBufferSize(); ++i) info.push_back(((char*)error->GetBufferPointer())[i]); std::cout << info << std::endl; THROW_IF_FAILED(result); } result = D3DCompileFromFile(L"../../shaders/shader.hlsl", nullptr, nullptr, "PSMain", "ps_5_1", compileFlags, 0, &pixelShader, &error); if(!SUCCEEDED(result)) { std::string info; for(int i = 0; i < error->GetBufferSize(); ++i) info.push_back(((char*)error->GetBufferPointer())[i]); std::cout << info << std::endl; THROW_IF_FAILED(result); }
// shader.hlsl struct PSInput { float4 position : SV_POSITION; float4 color : COLOR; }; PSInput VSMain(float3 position : POSITION, float4 color : COLOR) { PSInput result; result.position = float4(position, 1.0); result.color = color; return result; } float4 PSMain(PSInput input) : SV_TARGET { return input.color; }
在D3D中,shader是用HLSL(高级着色器语言)编写的一段小程序,它并行地运行在GPU上,关于shader请查阅更专业地资料吧,这里就不多说了。
创建管线状态对象
ComPtr<ID3D12PipelineState> pso; D3D12_INPUT_ELEMENT_DESC inputElements[] = { {"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_GRAPHICS_PIPELINE_STATE_DESC psoDesc{}; psoDesc.InputLayout = {inputElements, _countof(inputElements)}; psoDesc.pRootSignature = rootSignature.Get(); psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get()); psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get()); 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] = BackBufferFormat; psoDesc.DSVFormat = DepthStencilFormat; psoDesc.SampleDesc = {1, 0}; THROW_IF_FAILED(device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pso)));
管线状态对象把渲染过程中需要地几乎所有信息组合到了一起,这样就只需要在开始时初始化一大堆PSO,然后在需要的时候绑定即可。这里有了d3dx12.h中的辅助结构会方便很多,否则代码还会更冗长。
创建顶点缓冲
ComPtr<ID3D12Resource> vertexBuffer; D3D12_VERTEX_BUFFER_VIEW vertexBufferView{}; Vertex vertices[] = { {{0.0, 0.5, 0.5}, {1.0, 0.0, 0.0, 1.0}}, {{0.5, -0.5, 0.5}, {0.0, 1.0, 0.0, 1.0}}, {{-0.5, -0.5, 0.5}, {0.0, 0.0, 1.0, 1.0}} }; auto vertexBufferSize = sizeof(vertices); THROW_IF_FAILED(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertexBuffer) )); UINT8* mappedData = nullptr; CD3DX12_RANGE readRange(0, 0); THROW_IF_FAILED(vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&mappedData))); memcpy(mappedData, vertices, vertexBufferSize); vertexBuffer->Unmap(0, nullptr); vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress(); vertexBufferView.SizeInBytes = vertexBufferSize; vertexBufferView.StrideInBytes = sizeof(Vertex);
首先在上传堆中分配区域,将其映射到内存中,再将顶点数据复制进去,解除映射。最后创建顶点缓冲视图。
创建Fence相关对象
UINT64 fenceValue; ComPtr<ID3D12Fence> fence; HANDLE fenceEvent; THROW_IF_FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence))); fenceEvent = CreateEvent(nullptr, false, false, nullptr); if(fenceEvent == nullptr) THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError())); fenceValue = 1; CD3DX12_VIEWPORT viewport{0.f, 0.f, static_cast<float>(SCR_WIDTH), static_cast<float>(SCR_HEIGHT)}; CD3DX12_RECT scissorRect = CD3DX12_RECT{0, 0, static_cast<LONG>(SCR_WIDTH), static_cast<LONG>(SCR_HEIGHT)};
命令列表的方法调用时仅仅是将其记录了下来,实际执行在其提交之后,我们在命令执行完之前不能销毁或重用那些需要使用的资源。一个简单的办法就是强制等待,直到GPU执行完某个或全部命令。这也正是Fence(围栏)的作用。
Update & Draw
终于准备完成!剩下的就是在每个消息循环中更新信息并绘制了。由于这只是个小例子没什么需要更新的,所以这里只需要绘制。
// 设置状态 auto curFrameInex = swapChain->GetCurrentBackBufferIndex(); THROW_IF_FAILED(cmdAlloc->Reset()); THROW_IF_FAILED(cmdList->Reset(cmdAlloc.Get(), pso.Get())); cmdList->SetGraphicsRootSignature(rootSignature.Get()); cmdList->RSSetViewports(1, &viewport); cmdList->RSSetScissorRects(1, &scissorRect); // 将缓冲区转换为渲染目标的状态 cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[curFrameInex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET)); // 清除并设置渲染目标和深度缓冲区 CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart(), curFrameInex, rtvDescriptorSize); const float clearColor[] = {0.37f, 0.85f, 0.36f, 1.0f}; cmdList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); cmdList->ClearDepthStencilView(dsvHeap->GetCPUDescriptorHandleForHeapStart(), D3D12_CLEAR_FLAG_DEPTH, 1.0, 0, 0, nullptr); cmdList->OMSetRenderTargets(1, &rtvHandle, true, &dsvHeap->GetCPUDescriptorHandleForHeapStart()); // 绘制 cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); cmdList->IASetVertexBuffers(0, 1, &vertexBufferView); cmdList->DrawInstanced(3, 1, 0, 0); // 将缓冲区转化为待交换的状态 cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[curFrameInex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT)); // 关闭命令列表 THROW_IF_FAILED(cmdList->Close()); // 执行命令列表 ID3D12CommandList* cmdLists[] = {cmdList.Get()}; cmdQueue->ExecuteCommandLists(_countof(cmdLists), cmdLists); // 交换后备缓冲 THROW_IF_FAILED(swapChain->Present(1, 0)); // 等待队列中的命令执行完 auto waitValue = fenceValue++; THROW_IF_FAILED(cmdQueue->Signal(fence.Get(), waitValue)); if(fence->GetCompletedValue() < waitValue) { THROW_IF_FAILED(fence->SetEventOnCompletion(waitValue, fenceEvent)); WaitForSingleObject(fenceEvent, INFINITE); }
大功告成
此时我们终于可以在窗口中绘制一个三角形了,虽然功能简单但已经涉及了很多东西,更多高级的内容以后慢慢来吧。
完整代码如下:
#define GLFW_EXPOSE_NATIVE_WIN32 #include <GLFW/glfw3.h> #include <GLFW/glfw3native.h> #include <cstdio> #include <d3d12.h> #include <d3d12shader.h> #include <d3dcompiler.h> #include <dxgi1_6.h> #include <DirectXMath.h> #include <wrl.h> #ifdef _DEBUG #include <dxgidebug.h> #endif #include <iostream> #include <string> #include <d3dx12.h> using namespace DirectX; using Microsoft::WRL::ComPtr; #pragma comment(lib, "d3d12.lib") #pragma comment(lib, "d3dcompiler.lib") #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "dxguid.lib") class COMException { public: COMException(HRESULT hr) : error(hr) {} HRESULT Error() const { return error; } private: const HRESULT error; }; inline void THROW_IF_FAILED(HRESULT hr) { if(FAILED(hr)) throw COMException(hr); } const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; const UINT BackBufferCount = 3; const auto BackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM; const auto DepthStencilFormat = DXGI_FORMAT_D32_FLOAT; const auto FeatureLevel = D3D_FEATURE_LEVEL_12_1; // 错误回调函数,当glfw内部出错的时候就会调用它 void errorCallback(int error, const char* description) { fprintf(stderr, "Error: %s\n", description); } // 也是一个回调函数,相应键盘事件 void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { glfwSetWindowShouldClose(window, GLFW_TRUE); } } struct Vertex { XMFLOAT3 pos; XMFLOAT4 color; }; int main() { //设置错误回调函数,应放在glfw初始化之前 glfwSetErrorCallback(errorCallback); // 初始化glfw glfwInit(); // 表示不需要使用opengl等api glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); // 创建一个新窗口 GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "GLFW Window", nullptr, nullptr); // 为新窗口设置回调函数 glfwSetKeyCallback(window, keyCallback); // 创建DXGI UINT factoryFlags = 0; #if defined(_DEBUG) { // 若在debug模式下,这段代码会开启DirectX的调试层,调试层在控制台上打印的信息对于debug非常有帮助。 ComPtr<ID3D12Debug> debugInterface; if(SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugInterface)))) { debugInterface->EnableDebugLayer(); factoryFlags |= DXGI_CREATE_FACTORY_DEBUG; } } #endif ComPtr<IDXGIFactory5> factory; THROW_IF_FAILED(CreateDXGIFactory2(factoryFlags, IID_PPV_ARGS(&factory))); // 枚举适配器并创建设备 ComPtr<IDXGIAdapter1> adapter; ComPtr<ID3D12Device4> device; for(UINT adapterIndex = 0; factory->EnumAdapters1(adapterIndex, &adapter) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) { DXGI_ADAPTER_DESC1 desc{}; adapter->GetDesc1(&desc); if(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) continue; if(SUCCEEDED(D3D12CreateDevice(adapter.Get(), FeatureLevel, _uuidof(device), nullptr))) break; } THROW_IF_FAILED(D3D12CreateDevice(adapter.Get(), FeatureLevel, IID_PPV_ARGS(&device))); //创建命令队列,命令分配器,命令列表 ComPtr<ID3D12CommandQueue> cmdQueue; ComPtr<ID3D12CommandAllocator> cmdAlloc; ComPtr<ID3D12GraphicsCommandList> cmdList; D3D12_COMMAND_QUEUE_DESC queueDesc{}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; THROW_IF_FAILED(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&cmdQueue))); THROW_IF_FAILED(device->CreateCommandAllocator(queueDesc.Type, IID_PPV_ARGS(&cmdAlloc))); THROW_IF_FAILED(device->CreateCommandList(0, queueDesc.Type, cmdAlloc.Get(), nullptr, IID_PPV_ARGS(&cmdList))); THROW_IF_FAILED(cmdList->Close()); // 创建交换链 ComPtr<IDXGISwapChain1> swapChain1; ComPtr<IDXGISwapChain3> swapChain; DXGI_SWAP_CHAIN_DESC1 swapChainDesc{}; swapChainDesc.BufferCount = BackBufferCount; swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapChainDesc.Width = SCR_WIDTH; swapChainDesc.Height = SCR_HEIGHT; swapChainDesc.Format = BackBufferFormat; swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; swapChainDesc.SampleDesc = {1, 0}; THROW_IF_FAILED(factory->CreateSwapChainForHwnd( cmdQueue.Get(), glfwGetWin32Window(window), &swapChainDesc, nullptr, nullptr, &swapChain1 )); THROW_IF_FAILED(swapChain1.As(&swapChain)); // 创建RTV描述符堆 ComPtr<ID3D12DescriptorHeap> rtvHeap; D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc{}; rtvHeapDesc.NumDescriptors = BackBufferCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; THROW_IF_FAILED(device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap))); UINT rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); ComPtr<ID3D12Resource> renderTargets[BackBufferCount]; CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart()); for(UINT i = 0; i < BackBufferCount; ++i) { THROW_IF_FAILED(swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i]))); device->CreateRenderTargetView(renderTargets[i].Get(), nullptr, rtvHandle); rtvHandle.Offset(1, rtvDescriptorSize); } // 创建DSV描述符 ComPtr<ID3D12DescriptorHeap> dsvHeap; D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc{}; dsvHeapDesc.NumDescriptors = 1; dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; THROW_IF_FAILED(device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&dsvHeap))); UINT dsvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV); auto depthDesc = CD3DX12_RESOURCE_DESC::Tex2D(DepthStencilFormat, SCR_WIDTH, SCR_HEIGHT); depthDesc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; D3D12_CLEAR_VALUE depthClearValue{}; depthClearValue.Format = DepthStencilFormat; depthClearValue.DepthStencil.Depth = 1.0; depthClearValue.DepthStencil.Stencil = 0; ComPtr<ID3D12Resource> depthStencilBuffer; THROW_IF_FAILED(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &depthDesc, D3D12_RESOURCE_STATE_DEPTH_WRITE, &depthClearValue, IID_PPV_ARGS(&depthStencilBuffer) )); D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc{}; dsvDesc.Format = DepthStencilFormat; dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D; device->CreateDepthStencilView(depthStencilBuffer.Get(), &dsvDesc, dsvHeap->GetCPUDescriptorHandleForHeapStart()); // 创建根签名 ComPtr<ID3D12RootSignature> rootSignature; CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); ComPtr<ID3D10Blob> signature; ComPtr<ID3D10Blob> error; THROW_IF_FAILED(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)); THROW_IF_FAILED(device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature))); // 创建着色器 UINT compileFlags = 0; #if defined(_DEBUG) compileFlags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif ComPtr<ID3D10Blob> vertexShader; ComPtr<ID3D10Blob> pixelShader; auto result = D3DCompileFromFile(L"../../shaders/shader.hlsl", nullptr, nullptr, "VSMain", "vs_5_1", compileFlags, 0, &vertexShader, &error); if(!SUCCEEDED(result)) { std::string info; for(int i = 0; i < error->GetBufferSize(); ++i) info.push_back(((char*)error->GetBufferPointer())[i]); std::cout << info << std::endl; THROW_IF_FAILED(result); } result = D3DCompileFromFile(L"../../shaders/shader.hlsl", nullptr, nullptr, "PSMain", "ps_5_1", compileFlags, 0, &pixelShader, &error); if(!SUCCEEDED(result)) { std::string info; for(int i = 0; i < error->GetBufferSize(); ++i) info.push_back(((char*)error->GetBufferPointer())[i]); std::cout << info << std::endl; THROW_IF_FAILED(result); } // 创建管道状态对象 ComPtr<ID3D12PipelineState> pso; D3D12_INPUT_ELEMENT_DESC inputElements[] = { {"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_GRAPHICS_PIPELINE_STATE_DESC psoDesc{}; psoDesc.InputLayout = {inputElements, _countof(inputElements)}; psoDesc.pRootSignature = rootSignature.Get(); psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get()); psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get()); 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] = BackBufferFormat; psoDesc.DSVFormat = DepthStencilFormat; psoDesc.SampleDesc = {1, 0}; THROW_IF_FAILED(device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pso))); // 创建顶点缓冲 ComPtr<ID3D12Resource> vertexBuffer; D3D12_VERTEX_BUFFER_VIEW vertexBufferView{}; Vertex vertices[] = { {{0.0, 0.5, 0.5}, {1.0, 0.0, 0.0, 1.0}}, {{0.5, -0.5, 0.5}, {0.0, 1.0, 0.0, 1.0}}, {{-0.5, -0.5, 0.5}, {0.0, 0.0, 1.0, 1.0}} }; auto vertexBufferSize = sizeof(vertices); THROW_IF_FAILED(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertexBuffer) )); UINT8* mappedData = nullptr; CD3DX12_RANGE readRange(0, 0); THROW_IF_FAILED(vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&mappedData))); memcpy(mappedData, vertices, vertexBufferSize); vertexBuffer->Unmap(0, nullptr); vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress(); vertexBufferView.SizeInBytes = vertexBufferSize; vertexBufferView.StrideInBytes = sizeof(Vertex); // 创建Fence相关对象 UINT64 fenceValue; ComPtr<ID3D12Fence> fence; HANDLE fenceEvent; THROW_IF_FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence))); fenceEvent = CreateEvent(nullptr, false, false, nullptr); if(fenceEvent == nullptr) THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError())); fenceValue = 1; CD3DX12_VIEWPORT viewport{0.f, 0.f, static_cast<float>(SCR_WIDTH), static_cast<float>(SCR_HEIGHT)}; CD3DX12_RECT scissorRect = CD3DX12_RECT{0, 0, static_cast<LONG>(SCR_WIDTH), static_cast<LONG>(SCR_HEIGHT)}; // 接下来是事件循环,当窗口还不应关闭的时候要不断轮询事件。 while(!glfwWindowShouldClose(window)) { glfwPollEvents(); auto curFrameInex = swapChain->GetCurrentBackBufferIndex(); THROW_IF_FAILED(cmdAlloc->Reset()); THROW_IF_FAILED(cmdList->Reset(cmdAlloc.Get(), pso.Get())); cmdList->SetGraphicsRootSignature(rootSignature.Get()); cmdList->RSSetViewports(1, &viewport); cmdList->RSSetScissorRects(1, &scissorRect); cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[curFrameInex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET)); CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart(), curFrameInex, rtvDescriptorSize); const float clearColor[] = {0.37f, 0.85f, 0.36f, 1.0f}; cmdList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); cmdList->ClearDepthStencilView(dsvHeap->GetCPUDescriptorHandleForHeapStart(), D3D12_CLEAR_FLAG_DEPTH, 1.0, 0, 0, nullptr); cmdList->OMSetRenderTargets(1, &rtvHandle, true, &dsvHeap->GetCPUDescriptorHandleForHeapStart()); cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); cmdList->IASetVertexBuffers(0, 1, &vertexBufferView); cmdList->DrawInstanced(3, 1, 0, 0); cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[curFrameInex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT)); THROW_IF_FAILED(cmdList->Close()); ID3D12CommandList* cmdLists[] = {cmdList.Get()}; cmdQueue->ExecuteCommandLists(_countof(cmdLists), cmdLists); THROW_IF_FAILED(swapChain->Present(1, 0)); auto waitValue = fenceValue++; THROW_IF_FAILED(cmdQueue->Signal(fence.Get(), waitValue)); if(fence->GetCompletedValue() < waitValue) { THROW_IF_FAILED(fence->SetEventOnCompletion(waitValue, fenceEvent)); WaitForSingleObject(fenceEvent, INFINITE); } } // 前面的事件循环结束只表示窗口应该关闭,实际的关闭通过这个函数完成。 glfwDestroyWindow(window); // glfw库会终结,并销毁各种资源,在程序结束前调用。 glfwTerminate(); return 0; }