【译】DirectX 12 简明入门教程

您所在的位置:网站首页 directx视频教程 【译】DirectX 12 简明入门教程

【译】DirectX 12 简明入门教程

#【译】DirectX 12 简明入门教程| 来源: 网络整理| 查看: 265

原文链接:https://alain.xyz/blog/raw-directx12作者:Alain Galvan

本文将介绍如何编写一个简单的 Hello Triangle DirectX 12 应用程序。DirectX 12 是最新的底层图形 API,用来编写 Windows 应用程序,支持光线追踪、计算、光栅化等特性。

Github仓库

下载范例

DirectX 12是微软专有计算机图形 API 的最新版本,用于 Windows 和 Xbox 平台。与 Vulkan、Apple Metal 和 WebGPU 等较新的图形 API 一样,DirectX 12旨在让开发者可以更接近 GPU 的底层架构,从而能开发出不再复杂、运行更快的驱动程序。这意味着应用程序开发者能直接创建管线状态,指定使用队列执行哪些命令,并能选择使用多线程来构建这些数据结构。

DirectX 专注于实时渲染,因此专为游戏开发人员计算机辅助设计(CAD)软件工程师而设计。作为计算机图形 API 的行业标准,几乎所有兼容的硬件都能对 DirectX 提供强大支持,使它成为商业项目的标准。

从 Marmoset Toolbag、Adobe PhotoShop、Unreal Engine 等三维或图像创作软件,再到像 Activision Blizzard 的 OverWatch 和 Call of Duty、Epic 的 Fortnite 的商业游戏、以及 Valve 的 Steam 上的绝大多数游戏等等,DirectX 这个图形 API 是最受欢迎并且无处不在,尽管它对某些平台不兼容。

当然,这并不是说 DirectX 在 3D 渲染之外的领域没有用途。Google Chrome 使用 DirectX 渲染 HTML 和 SVG 元素。Tensorflow 采用了 DirectX 12 后端为使用 DirectML 执行机器学习提供支持。采用计算管线(compute pipeline)的 GPGPU 计算可以执行各种工作负载,包括物理仿真、构造 BVH 等等。

DirectX 12 目前支持以下设备:

Windows 7 - 11(Windows 7 可以通过微软的 D3D12 On 7 部分支持DirectX 12)✖️ Xbox One - Xbox Series X/S

虽然 DirectX 12 的官方语言或许是 C 和 C++,但还有不少语言同样支持 DirectX 12:

CC++C#Rust(借助 dx-sys)

我已经准备了一个Github仓库,其中包含入门所需的一切内容。我将用 C++ 介绍一个 Hello Triangle 应用程序,这个程序使用了光栅图形管线创建三角形并将它渲染到屏幕上。

安装

首先安装:

GitCMake集成开发环境(IDE),比如 Visual Studio、XCode,或者是编译器,比如 GCC

然后在任何终端(如 VS Code 的集成终端)中,输入以下内容。

# Clone 代码仓 git clone https://github.com/alaingalvan/directx12-seed --recurse-submodules # 定位至文件夹 cd directx12-seed # 如果你忘记了 `recurse-submodules` ,你始终可以运行: git submodule update --init # 创建一个 build 文件夹,并将你的项目文件存放在里面: # ️ 在 Windows 上构建 Visual Studio 64位版本的解决方案 cmake . -B build -A x64 # 在 Mac OS 上构建 XCode 项目 cmake . -B build -G Xcode # 在 Linux 上构建 .make 文件 cmake . -B build # 适用在任何平台上进行构建 cmake --build build概述

DirectX 12 的开发文档建议使用ComPtr来作为std::shared_ptr的替代,这样做的好处是可以更好地调试,更容易初始化 DirectX 12 的数据结构。

无论你是否选择使用ComPtr,使用 DirectX 12 渲染光栅图形的步骤与其他现代图形 API 非常相似:

初始化 API:创建IDXGIFactory 、IDXGIAdapter、ID3D12Device、ID3D12CommandQueue、ID3D12CommandAllocator、ID3D12GraphicsCommandList 。设置帧后台缓冲:为后台缓冲区创建IDXGISwapChain、ID3D12DescriptorHeap,为ID3D12Resource后台缓冲区渲染目标视图,创建ID3D12Fence以检测帧何时完成渲染。初始化资源:创建三角形数据,例如ID3D12Resource顶点缓冲区、ID3D12Resource索引缓冲区等,创建ID3D12Fence以检测上传到 GPU 内存的时间。加载着色器ID3DBlob、常量缓冲区ID3D12Resource及其ID3D12DescriptorHeap,描述使用ID3D12RootSignature可以访问哪些资源,并构建ID3D12PipelineState 编码指令:向ID3D12GraphicsCommandList编写你希望执行的管线指令,确保将ResourceBarrier放在适当的位置。渲染:更新 GPU 常量缓冲区数据(Uniforms),使用ExecuteCommandLists将命令提交至ID3D12CommandQueue,Present交换链,然后等待下一帧。销毁:使用Release()来销毁正在使用的数据结构,或者使用ComPtr对数据结构解除分配。

下面我将对 Github 仓库中代码片段进行解释,省略了某些部分,并且成员变量 (mMemberVariable) 内联声明,不带前缀m,以便你可以更容易地分辨它们的类型,这里给出的示例代码可以独立运行。

创建窗口(Window Creation)

我们使用 CrossWindow (译者注:原文给出的链接失效了,可以从这个 github仓库 来访问)来创建支持跨平台的窗口。创建 Win32 窗口并对其进行更新,操作非常简单:

#include "CrossWindow/CrossWindow.h" #include "Renderer.h" #include void xmain(int argc, const char** argv) { // ️ 创建窗口 xwin::WindowDesc wdesc; wdesc.title = "DirectX 12 Seed"; wdesc.name = "MainWindow"; wdesc.visible = true; wdesc.width = 640; wdesc.height = 640; wdesc.fullscreen = false; xwin::Window window; xwin::EventQueue eventQueue; if (!window.create(wdesc, eventQueue)) { return; }; // 创建一个渲染器 Renderer renderer(window); // 引擎循环 bool isRunning = true; while (isRunning) { bool shouldRender = true; // ♻️ 更新事件队列 eventQueue.update(); // 事件迭代: while (!eventQueue.empty()) { // 更新事件 const xwin::Event& event = eventQueue.front(); // 响应窗口改变大小: if (event.type == xwin::EventType::Resize) { const xwin::ResizeData data = event.data.resize; renderer.resize(data.width, data.height); shouldRender = false; } // ❌ 响应窗口关闭: if (event.type == xwin::EventType::Close) { window.close(); shouldRender = false; isRunning = false; } eventQueue.pop(); } // ✨ 更新可视化部分 if (shouldRender) { renderer.render(); } } }

如果你想替代 CrossWindow,可以使用其它库,比如 GLFW,SFML,SDL,QT,或者直接调用 Win32 或 UWP API 接口。

初始化 API Initialize API)工厂(Factory)

工厂是 DirectX 12 API 的入口,它允许你查找那些支持 DirectX 12 命令的适配器(译者注:可以理解为显卡)。

你还可以在工厂附近创建一个调试控制器(Debug Controller),它可以启用 API 使用情况验证。这只能在 debug 模式中使用。

参考:IDXGIFactory7// 声明 DirectX 12 句柄(Handle) IDXGIFactory4* factory; ID3D12Debug1* debugController; // 创建 Factory UINT dxgiFactoryFlags = 0; #if defined(_DEBUG) // 创建一个 Debug Controller 来追踪错误 ID3D12Debug* dc; ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&dc))); ThrowIfFailed(dc->QueryInterface(IID_PPV_ARGS(&debugController))); debugController->EnableDebugLayer(); debugController->SetEnableGPUBasedValidation(true); dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG; dc->Release(); dc = nullptr; #endif HRESULT result = CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory)); 适配器(Adapter)

Adapter 提供了有关 DirectX 设备物理属性的信息。你可以查询当前 GPU 的名称、制造商、显存容量以及其它更多信息。

有两种类型的适配器:软件适配器和硬件适配器。微软 Windows 系统总是会包含一个基于软件层面的DirectX 实现,让你可以在没有专用硬件(如独立或集成 GPU )的情况下使用 API 。

参考:IDXGIAdapter4// 声明句柄 IDXGIAdapter1* adapter; // 创建 Adapter for (UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != factory->EnumAdapters1(adapterIndex, &adapter); ++adapterIndex) { DXGI_ADAPTER_DESC1 desc; adapter->GetDesc1(&desc); // ❌ 不要选用 Basic Render Driver adapter. if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) { continue; } // ✔️ 检查适配器是否支持 Direct3D 12, 如果支持就选择它来供程序运行并返回 if (SUCCEEDED(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0, _uuidof(ID3D12Device), nullptr))) { break; } // ❌ 否则的话,不再使用这个迭代器 adapter,所以要进行释放 adapter->Release(); } 设备(Device)

设备是 DirectX 12 API 的主要入口点,可用来访问 API 的内部。这是访问重要数据结构和函数(如管线、着色器 blob、渲染状态、资源屏障等)的关键。

参考:IDXGIDevice4// 声明句柄 ID3D12Device* device; // 创建 Device ThrowIfFailed(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device)));

调试 DirectX 创建的数据结构可能非常困难。而一个 Debug Device 对象允许你使用 DirectX 12 的调试模式。这样,你将能够防止数据泄露,或者能验证程序是否正确创建或使用 API。

参考:ID3D12DebugDevice// 声明句柄 ID3D12DebugDevice* debugDevice; #if defined(_DEBUG) // 获取 debug device ThrowIfFailed(device->QueryInterface(&debugDevice)); #endif 命令队列(Command Queue)

命令队列让你可以一次性提交多组 draw call(称为命令列表 command lists)来按顺序来执行命令,从而让 GPU 保持充分工作并优化执行速度。

参考:ID3D12CommandQueue// 声明句柄 ID3D12CommandQueue* commandQueue; // 创建 Command Queue D3D12_COMMAND_QUEUE_DESC queueDesc = {}; queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; ThrowIfFailed(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue))); 命令分配器(Command Allocator)

命令分配器让你可以创建命令列表,你可以在其中定义希望 GPU 执行的功能。

参考:ID3D12CommandAllocator// 声明句柄 ID3D12CommandAllocator* commandAllocator; // 创建命令分配器 ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator))); 同步(Synchronization)

DirectX 12 具有大量同步图元(synchronization primitives),可以告知驱动程序如何使用资源、GPU 何时完成任务等。

围栏(Fence)能让程序知道 GPU 在什么时候执行了哪些特定任务,无论是上传了哪些资源到 GPU 专用内存,还是程序什么时候完成向屏幕的提交(present),都能获取到这些信息。

参考:ID3D12Fence1// 声明句柄 UINT frameIndex; HANDLE fenceEvent; ID3D12Fence* fence; UINT64 fenceValue; // 创建 fence ThrowIfFailed(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)));

屏障(Barrier)能让驱动程序知道如何在即将提交的命令中使用资源。比如说,程序正在写入纹理,并且想要将这个纹理复制到另一个纹理(例如交换链的渲染附件),这会很有用。

// 声明句柄 ID3D12GraphicsCommandList* commandList; // 创建 Barrier D3D12_RESOURCE_BARRIER barrier = {}; barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; barrier.Transition.pResource = texResource; barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; commandList->ResourceBarrier(1, &barrier); 交换链(Swap Chain)

交换链能处理交换和分配后台缓冲区,用来显示正在渲染到给定窗口的内容。

// 声明数据变量 unsigned width = 640; unsigned height = 640; // 声明句柄 static const UINT backbufferCount = 2; UINT currentBuffer; ID3D12DescriptorHeap* renderTargetViewHeap; ID3D12Resource* renderTargets[backbufferCount]; UINT rtvDescriptorSize; // ⛓️ 声明交换链 IDXGISwapChain3* swapchain; D3D12_VIEWPORT viewport; D3D12_RECT surfaceSize; surfaceSize.left = 0; surfaceSize.top = 0; surfaceSize.right = static_cast(width); surfaceSize.bottom = static_cast(height); viewport.TopLeftX = 0.0f; viewport.TopLeftY = 0.0f; viewport.Width = static_cast(width); viewport.Height = static_cast(height); viewport.MinDepth = .1f; viewport.MaxDepth = 1000.f; if (swapchain != nullptr) { // 通过交换链创建渲染目标附件(Render Target Attachments) swapchain->ResizeBuffers(backbufferCount, width, height, DXGI_FORMAT_R8G8B8A8_UNORM, 0); } else { // ⛓️ 创建交换链 DXGI_SWAP_CHAIN_DESC1 swapchainDesc = {}; swapchainDesc.BufferCount = backbufferCount; swapchainDesc.Width = width; swapchainDesc.Height = height; swapchainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; swapchainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapchainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; swapchainDesc.SampleDesc.Count = 1; IDXGISwapChain1* newSwapchain = xgfx::createSwapchain(window, factory, commandQueue, &swapchainDesc); HRESULT swapchainSupport = swapchain->QueryInterface( __uuidof(IDXGISwapChain3), (void**)&newSwapchain); if (SUCCEEDED(swapchainSupport)) { swapchain = (IDXGISwapChain3*)newSwapchain; } } frameIndex = swapchain->GetCurrentBackBufferIndex(); // 描述并创建渲染目标视图(render target view, RTV) 描述符堆 D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; rtvHeapDesc.NumDescriptors = backbufferCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; ThrowIfFailed(device->CreateDescriptorHeap( &rtvHeapDesc, IID_PPV_ARGS(&renderTargetViewHeap))); rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); // ️ 创建帧资源 D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle(renderTargetViewHeap->GetCPUDescriptorHandleForHeapStart()); // 为每帧创建RTV for (UINT n = 0; n GetBuffer(n, IID_PPV_ARGS(&renderTargets[n]))); device->CreateRenderTargetView(renderTargets[n], nullptr, rtvHandle); rtvHandle.ptr += (1 * rtvDescriptorSize); } 初始化资源(Initialize Resources)描述符堆(Descriptor Heaps)

描述符堆是用来处理内存分配的,这些内存存储着色器引用的对象描述。

参考:ID3D12DescriptorHeapID3D12DescriptorHeap* renderTargetViewHeap; D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; rtvHeapDesc.NumDescriptors = backbufferCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; ThrowIfFailed(device->CreateDescriptorHeap( &rtvHeapDesc, IID_PPV_ARGS(&renderTargetViewHeap))); 根签名(Root Signature)

根签名定义了着色器可以访问资源类型的对象,包括常量缓冲区、结构化缓冲区、采样器、纹理、结构化缓冲区,等等。

参考:ID3D12RootSignature// 声明句柄 ID3D12RootSignature* rootSignature; // 判断是否能得到1.1版本的根签名: D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {}; featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1; if (FAILED(device->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &featureData, sizeof(featureData)))) { featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0; } // 私有的GPU资源 D3D12_DESCRIPTOR_RANGE1 ranges[1]; ranges[0].BaseShaderRegister = 0; ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV; ranges[0].NumDescriptors = 1; ranges[0].RegisterSpace = 0; ranges[0].OffsetInDescriptorsFromTableStart = 0; ranges[0].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE; // ️ GPU资源组 D3D12_ROOT_PARAMETER1 rootParameters[1]; rootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; rootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX; rootParameters[0].DescriptorTable.NumDescriptorRanges = 1; rootParameters[0].DescriptorTable.pDescriptorRanges = ranges; // 所有布局 D3D12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1; rootSignatureDesc.Desc_1_1.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; rootSignatureDesc.Desc_1_1.NumParameters = 1; rootSignatureDesc.Desc_1_1.pParameters = rootParameters; rootSignatureDesc.Desc_1_1.NumStaticSamplers = 0; rootSignatureDesc.Desc_1_1.pStaticSamplers = nullptr; ID3DBlob* signature; ID3DBlob* error; try { // 创建根签名 ThrowIfFailed(D3D12SerializeVersionedRootSignature(&rootSignatureDesc, &signature, &error)); ThrowIfFailed(device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature))); rootSignature->SetName(L"Hello Triangle Root Signature"); } catch (std::exception e) { const char* errStr = (const char*)error->GetBufferPointer(); std::cout Release(); signature = nullptr; }

虽然程序运行起来没有问题,但如果使用无绑定资源(bindless resources),那么开发起来要容易得多,Matt Pettineo(@MyNameIsMJP)在《Ray Tracing Gems II》中写过关于这方面的内容。

堆(Heaps)

堆是 GPU 显存中的对象。你可以使用堆将顶点缓冲区或纹理等资源上传到 GPU 显存中。

参考:ID3D12Resource// 上传: // 声明句柄 ID3D12Resource* uploadBuffer; std::vector sourceData; D3D12_HEAP_PROPERTIES uploadHeapProps = {D3D12_HEAP_TYPE_UPLOAD, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1u, 1u}; D3D12_RESOURCE_DESC uploadBufferDesc = {D3D12_RESOURCE_DIMENSION_BUFFER, 65536ull, 65536ull, 1u, 1, 1, DXGI_FORMAT_UNKNOWN, {1u, 0u}, D3D12_TEXTURE_LAYOUT_ROW_MAJOR, D3D12_RESOURCE_FLAG_NONE}; result = device->CreateCommittedResource( &uploadHeapProps, D3D12_HEAP_FLAG_NONE, &uploadBufferDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, __uuidof(ID3D12Resource), ((void**)&uploadBuffer)); uint8_t* data = nullptr; D3D12_RANGE range{0, SIZE_T(chunkSize)}; auto hr = spStaging -> Map(0, &range, reinterpret_cast(&data)); if (FAILED(hr)) { std::cout

顶点缓冲区将每个顶点信息作为属性存储在顶点着色器中。所有缓冲区都是 DirectX 12 中的ID3D12Resource对象,无论是顶点缓冲区、索引缓冲区、常量缓冲区等。

参考:D3D12_VERTEX_BUFFER_VIEW// 声明数据结构 struct Vertex { float position[3]; float color[3]; }; Vertex vertexBufferData[3] = {{{1.0f, -1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}}, {{-1.0f, -1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}}, {{0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}}; // 声明句柄 ID3D12Resource* vertexBuffer; D3D12_VERTEX_BUFFER_VIEW vertexBufferView; const UINT vertexBufferSize = sizeof(vertexBufferData); D3D12_HEAP_PROPERTIES heapProps; heapProps.Type = D3D12_HEAP_TYPE_UPLOAD; heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; heapProps.CreationNodeMask = 1; heapProps.VisibleNodeMask = 1; D3D12_RESOURCE_DESC vertexBufferResourceDesc; vertexBufferResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; vertexBufferResourceDesc.Alignment = 0; vertexBufferResourceDesc.Width = vertexBufferSize; vertexBufferResourceDesc.Height = 1; vertexBufferResourceDesc.DepthOrArraySize = 1; vertexBufferResourceDesc.MipLevels = 1; vertexBufferResourceDesc.Format = DXGI_FORMAT_UNKNOWN; vertexBufferResourceDesc.SampleDesc.Count = 1; vertexBufferResourceDesc.SampleDesc.Quality = 0; vertexBufferResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; vertexBufferResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE; ThrowIfFailed(device->CreateCommittedResource( &heapProps, D3D12_HEAP_FLAG_NONE, &vertexBufferResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertexBuffer))); // 向顶点缓冲区拷贝三角形数据 UINT8* pVertexDataBegin; // 不会在 CPU 中读取这些资源 D3D12_RANGE readRange; readRange.Begin = 0; readRange.End = 0; ThrowIfFailed(vertexBuffer->Map(0, &readRange, reinterpret_cast(&pVertexDataBegin))); memcpy(pVertexDataBegin, vertexBufferData, sizeof(vertexBufferData)); vertexBuffer->Unmap(0, nullptr); // 初始化顶点缓冲视图. vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress(); vertexBufferView.StrideInBytes = sizeof(Vertex); vertexBufferView.SizeInBytes = vertexBufferSize; 索引缓冲区(Index Buffer)

索引缓冲区包含要绘制的每个三角形、线、点的各个索引。

参考:D3D12_INDEX_BUFFER_VIEW// 声明数组 uint32_t indexBufferData[3] = {0, 1, 2}; // 声明句柄 ID3D12Resource* indexBuffer; D3D12_INDEX_BUFFER_VIEW indexBufferView; const UINT indexBufferSize = sizeof(indexBufferData); D3D12_HEAP_PROPERTIES heapProps; heapProps.Type = D3D12_HEAP_TYPE_UPLOAD; heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; heapProps.CreationNodeMask = 1; heapProps.VisibleNodeMask = 1; D3D12_RESOURCE_DESC vertexBufferResourceDesc; vertexBufferResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; vertexBufferResourceDesc.Alignment = 0; vertexBufferResourceDesc.Width = indexBufferSize; vertexBufferResourceDesc.Height = 1; vertexBufferResourceDesc.DepthOrArraySize = 1; vertexBufferResourceDesc.MipLevels = 1; vertexBufferResourceDesc.Format = DXGI_FORMAT_UNKNOWN; vertexBufferResourceDesc.SampleDesc.Count = 1; vertexBufferResourceDesc.SampleDesc.Quality = 0; vertexBufferResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; vertexBufferResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE; ThrowIfFailed(device->CreateCommittedResource( &heapProps, D3D12_HEAP_FLAG_NONE, &vertexBufferResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&indexBuffer))); // 向 DirectX 12 显存拷贝数据: UINT8* pVertexDataBegin; // 不会在 CPU 中读取这些资源 D3D12_RANGE readRange; readRange.Begin = 0; readRange.End = 0; ThrowIfFailed(indexBuffer->Map(0, &readRange, reinterpret_cast(&pVertexDataBegin))); memcpy(pVertexDataBegin, indexBufferData, sizeof(indexBufferData)); indexBuffer->Unmap(0, nullptr); // 初始化索引缓冲区视图 indexBufferView.BufferLocation = indexBuffer->GetGPUVirtualAddress(); indexBufferView.Format = DXGI_FORMAT_R32_UINT; indexBufferView.SizeInBytes = indexBufferSize; 常量缓冲区(Constant Buffer)

常量缓冲区描述了在程序绘制时将发送到着色器阶段的数据。通常,你在这里存放着模型视图投影矩阵或任何特定的变量数据,如颜色。

// 声明数据结构 struct { glm::mat4 projectionMatrix; glm::mat4 modelMatrix; glm::mat4 viewMatrix; } cbVS; // 声明句柄 ID3D12Resource* constantBuffer; ID3D12DescriptorHeap* constantBufferHeap; UINT8* mappedConstantBuffer; // 创建常量缓冲区 D3D12_HEAP_PROPERTIES heapProps; heapProps.Type = D3D12_HEAP_TYPE_UPLOAD; heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; heapProps.CreationNodeMask = 1; heapProps.VisibleNodeMask = 1; D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {}; heapDesc.NumDescriptors = 1; heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; ThrowIfFailed(device->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&constantBufferHeap))); D3D12_RESOURCE_DESC cbResourceDesc; cbResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; cbResourceDesc.Alignment = 0; cbResourceDesc.Width = (sizeof(cbVS) + 255) & ~255; cbResourceDesc.Height = 1; cbResourceDesc.DepthOrArraySize = 1; cbResourceDesc.MipLevels = 1; cbResourceDesc.Format = DXGI_FORMAT_UNKNOWN; cbResourceDesc.SampleDesc.Count = 1; cbResourceDesc.SampleDesc.Quality = 0; cbResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; cbResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE; ThrowIfFailed(device->CreateCommittedResource( &heapProps, D3D12_HEAP_FLAG_NONE, &cbResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&constantBuffer))); constantBufferHeap->SetName(L"Constant Buffer Upload Resource Heap"); // 创建常量缓冲区视图(CBV) D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {}; cbvDesc.BufferLocation = constantBuffer->GetGPUVirtualAddress(); cbvDesc.SizeInBytes = (sizeof(cbVS) + 255) & ~255; // CB size is required to be 256-byte aligned. D3D12_CPU_DESCRIPTOR_HANDLE cbvHandle(constantBufferHeap->GetCPUDescriptorHandleForHeapStart()); cbvHandle.ptr = cbvHandle.ptr + device->GetDescriptorHandleIncrementSize( D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) * 0; device->CreateConstantBufferView(&cbvDesc, cbvHandle); // 不会在 CPU 中读取这些资源 D3D12_RANGE readRange; readRange.Begin = 0; readRange.End = 0; ThrowIfFailed(constantBuffer->Map( 0, &readRange, reinterpret_cast(&mappedConstantBuffer))); memcpy(mappedConstantBuffer, &cbVS, sizeof(cbVS)); constantBuffer->Unmap(0, &readRange); 顶点着色器(Vertex Shader)

顶点着色器是逐顶点执行的,它非常适合对给定对象进行转换处理,比如根据混合形状执行每个顶点动画、GPU 蒙皮,等等。

cbuffer cb : register(b0) { row_major float4x4 projectionMatrix : packoffset(c0); row_major float4x4 modelMatrix : packoffset(c4); row_major float4x4 viewMatrix : packoffset(c8); }; struct VertexInput { float3 inPos : POSITION; float3 inColor : COLOR; }; struct VertexOutput { float3 color : COLOR; float4 position : SV_Position; }; VertexOutput main(VertexInput vertexInput) { float3 inColor = vertexInput.inColor; float3 inPos = vertexInput.inPos; float3 outColor = inColor; float4 position = mul(float4(inPos, 1.0f), mul(modelMatrix, mul(viewMatrix, projectionMatrix))); VertexOutput output; output.position = position; output.color = outColor; return output; }

你可以使用传统的 DirectX 着色器编译器(已包含在 DirectX 11/12 API 中)来编译 shader ,但最好使用较新的官方编译器。

dxc.exe -T lib_6_3 -Fo assets/triangle.vert.dxil assets/triangle.vert.hlsl

然后,你就能以为二进制文件形式加载着色器:

inline std::vector readFile(const std::string& filename) { std::ifstream file(filename, std::ios::ate | std::ios::binary); bool exists = (bool)file; if (!exists || !file.is_open()) { throw std::runtime_error("failed to open file!"); } size_t fileSize = (size_t)file.tellg(); std::vector buffer(fileSize); file.seekg(0); file.read(buffer.data(), fileSize); file.close(); return buffer; }; // 声明句柄 D3D12_SHADER_BYTECODE vsBytecode; std::string compiledPath; std::vector vsBytecodeData = readFile(compCompiledPath); vsBytecode.pShaderBytecode = vsBytecodeData.data(); vsBytecode.BytecodeLength = vsBytecodeData.size(); 像素着色器(Pixel Shader)

像素着色器是按输出的每个像素来执行的,包括与这个像素坐标对应的其他附件。

struct PixelInput { float3 color : COLOR; }; struct PixelOutput { float4 attachment0 : SV_Target0; }; PixelOutput main(PixelInput pixelInput) { float3 inColor = pixelInput.color; PixelOutput output; output.attachment0 = float4(inColor, 1.0f); return output; } 管线状态(Pipeline State)

管线状态描述了执行给定的光栅绘制指令所用到的全部内容。

参考:ID3D12GraphicsCommandList5// 声明句柄 ID3D12PipelineState* pipelineState; // ⚗️ 定义图形管线 D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; // 输入装配布局 D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = { {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}, {"COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}}; psoDesc.InputLayout = {inputElementDescs, _countof(inputElementDescs)}; // 资源 psoDesc.pRootSignature = rootSignature; // 顶点着色器 D3D12_SHADER_BYTECODE vsBytecode; vsBytecode.pShaderBytecode = vertexShaderBlob->GetBufferPointer(); vsBytecode.BytecodeLength = vertexShaderBlob->GetBufferSize(); psoDesc.VS = vsBytecode; // ️ 像素着色器 D3D12_SHADER_BYTECODE psBytecode; psBytecode.pShaderBytecode = pixelShaderBlob->GetBufferPointer(); psBytecode.BytecodeLength = pixelShaderBlob->GetBufferSize(); psoDesc.PS = psBytecode; // 光栅化 D3D12_RASTERIZER_DESC rasterDesc; rasterDesc.FillMode = D3D12_FILL_MODE_SOLID; rasterDesc.CullMode = D3D12_CULL_MODE_NONE; rasterDesc.FrontCounterClockwise = FALSE; rasterDesc.DepthBias = D3D12_DEFAULT_DEPTH_BIAS; rasterDesc.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP; rasterDesc.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS; rasterDesc.DepthClipEnable = TRUE; rasterDesc.MultisampleEnable = FALSE; rasterDesc.AntialiasedLineEnable = FALSE; rasterDesc.ForcedSampleCount = 0; rasterDesc.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF; psoDesc.RasterizerState = rasterDesc; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; // 颜色/混合 D3D12_BLEND_DESC blendDesc; blendDesc.AlphaToCoverageEnable = FALSE; blendDesc.IndependentBlendEnable = FALSE; const D3D12_RENDER_TARGET_BLEND_DESC defaultRenderTargetBlendDesc = { FALSE, FALSE, D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD, D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD, D3D12_LOGIC_OP_NOOP, D3D12_COLOR_WRITE_ENABLE_ALL, }; for (UINT i = 0; i CreateGraphicsPipelineState( &psoDesc, IID_PPV_ARGS(&pipelineState))); } catch (std::exception e) { std::cout

为了执行 draw call,你需要一个编写命令的地方。命令列表(Command List)可以对 GPU 要执行的许多命令进行编码,包括设置屏障(barrier)、设置根签名等等。

参考:ID3D12GraphicsCommandList5// 声明句柄 ID3D12CommandAllocator* commandAllocator; ID3D12PipelineState* initialPipelineState; ID3D12GraphicsCommandList* commandList; // 创建命令列表 ThrowIfFailed(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator, initialPipelineState, IID_PPV_ARGS(&commandList)));

然后,对这些命令进行编码并提交:

// 重置命令列表并添加新的命令 ThrowIfFailed(commandAllocator->Reset()); // ️ 开始调用光栅图形管线 ThrowIfFailed(commandList->Reset(commandAllocator, pipelineState)); // 设置资源 commandList->SetGraphicsRootSignature(rootSignature); ID3D12DescriptorHeap* pDescriptorHeaps[] = {constantBufferHeap}; commandList->SetDescriptorHeaps(_countof(pDescriptorHeaps), pDescriptorHeaps); D3D12_GPU_DESCRIPTOR_HANDLE cbvHandle(constantBufferHeap->GetGPUDescriptorHandleForHeapStart()); commandList->SetGraphicsRootDescriptorTable(0, cbvHandle); // ️ 指派back buffer,用作render target D3D12_RESOURCE_BARRIER renderTargetBarrier; renderTargetBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; renderTargetBarrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; renderTargetBarrier.Transition.pResource = renderTargets[frameIndex]; renderTargetBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; renderTargetBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; renderTargetBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; commandList->ResourceBarrier(1, &renderTargetBarrier); D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart()); rtvHandle.ptr = rtvHandle.ptr + (frameIndex * rtvDescriptorSize); commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); // 记录光栅命令. const float clearColor[] = {0.2f, 0.2f, 0.2f, 1.0f}; commandList->RSSetViewports(1, &viewport); commandList->RSSetScissorRects(1, &surfaceSize); commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); commandList->IASetVertexBuffers(0, 1, &vertexBufferView); commandList->IASetIndexBuffer(&indexBufferView); commandList->DrawIndexedInstanced(3, 1, 0, 0, 0); // ️ 指派back buffer 随即提交(present) D3D12_RESOURCE_BARRIER presentBarrier; presentBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; presentBarrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; presentBarrier.Transition.pResource = renderTargets[frameIndex]; presentBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; presentBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; presentBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; commandList->ResourceBarrier(1, &presentBarrier); ThrowIfFailed(commandList->Close()); 渲染(Render)

在 DirectX 12 中渲染非常简单:你需要在刷新时改动常量缓冲区数据、提交要执行的命令列表、提交交换链来更新 Win32 或 UWP 窗口,同时向程序发出已完成提交的信号。

// 声明句柄 std::chrono::time_point tStart, tEnd; float elapsedTime = 0.0f; void render() { // ⌚ 将帧率锁定至60fps tEnd = std::chrono::high_resolution_clock::now(); float time = std::chrono::duration(tEnd - tStart).count(); if (time Map( 0, &readRange, reinterpret_cast(&mappedConstantBuffer))); memcpy(mappedConstantBuffer, &cbVS, sizeof(cbVS)); constantBuffer->Unmap(0, &readRange); setupCommands(); ID3D12CommandList* ppCommandLists[] = {commandList}; commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // 提交,然后等待GPU执行 swapchain->Present(1, 0); const UINT64 fence = fenceValue; ThrowIfFailed(commandQueue->Signal(fence, fence)); fenceValue++; if (fence->GetCompletedValue() SetEventOnCompletion(fence, fenceEvent)); WaitForSingleObject(fenceEvent, INFINITE); } frameIndex = swapchain->GetCurrentBackBufferIndex(); } 销毁句柄(Destroy Handles)

如果你使用的是ComPtr数据结构,那么就像使用共享指针一样,你无需担心什么时候销毁已创建的句柄。如果没使用ComPtr,那你可以调用内置在每个 DirectX 数据结构中的Release()函数进行销毁。

结论

DirectX 12 是一个功能丰富、性能强大的计算机图形 API,非常适合商业项目。它在设计上遵循现代图形 API,同时它也是硬件驱动程序工程师和商业项目工程师主要在维护的 API。这篇文章回顾了基于光栅的绘图,但没有涉及到更多方面,而它们同样值得探讨,例如:

DirectML:硬件加速机器学习模型。DirectX Raytracing:硬件加速光线追踪和场景遍历。计算着色器(Compute Shader):基于 GPGPU 执行任意任务,例如图像处理、物理等。网格着色器(Mesh Shader):一种替代传统基于光栅渲染技术构建基元的方法。可变速率着色(Variable Rate Shading,VRS):对给定命令集的着色速率进行更精细的控制。

我还写了更多关于 DirectX 的文章:

Raw DirectX 12 book 介绍了计算管线、光线追踪管线等。Raw DirectX 11 blog post 回顾了早期的 DirectX 11,并且可以把这篇文章作为学习 DirectX 很不错的入门材料,因为这里面的许多概念都沿用到了 DirectX 12。更多资源

你一定不要错过下面这些文章、工具和项目:

文章在微软 DirectX 12文档中有一个页面,包含了初始化 DirectX 的所用到的全部数据结构;微软在这个链接里有关于 D3D12 和 11 的规范文档;Jendrik Illner(@jendrikillner)写了一篇 DirectX 12 学习计划;Braynzarsoft,一个 DirectX 教程社区;Riko Ophorst 的一篇 DirectX 12 光线追踪论文;Riccardo Loggini 的 D3D12 博客文章;英特尔关于 D3D12 博客文章;NVIDIA 关于 D3D12 和相关主题的博客文章;Diligent Graphics (@diligentengine) 写了篇关于 DirectX 12 的文章;Jeremiah van Oosten的 DirectX 12 系列教程;Alex Tardif (@longbool)的 A Gentle Introduction to D3D12;范例微软官方的 DirectX 12 范例代码仓库;英特尔官方的 GitHub Organization Game Tech Dev;NVIDIA 的官方 GitHub Organization GameWorks;Matthäus G. Chajdas(@NIV_Anteru)发布的 HelloD3D12,它是 AMD 的 GPUOpen 库和 SDK 的一部分;工具由 Adam Sawicki(@Reg__)开发的 D3D12 Memory Allocator ,他同样还是 Vulkan Memory Allocator 的开发者;Tim Jones(@tim_jones) 发布了一个VS Code 插件 HLSL Tools,可让你更轻松地对编写 shader;

你可以在这个Github仓库中找到本篇文章提到的所有源代码。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3