告别Hello World!用DirectX12和VS2019画你的第一个彩色三角形(附完整源码)
2026/6/2 11:49:55 网站建设 项目流程

从零构建DirectX12彩色三角形:现代图形编程的里程碑实践

在计算机图形学领域,绘制第一个三角形被视为相当于编程世界中的"Hello World"——它标志着开发者正式踏入GPU编程的大门。与传统的OpenGL相比,微软的DirectX 12(简称D3D12)以其接近硬件的底层控制和多线程优化能力,正在成为高性能图形应用开发的新标准。本文将带领读者从零开始,使用Visual Studio 2019构建一个完整的D3D12彩色三角形渲染程序,不仅涵盖技术实现细节,更着重解析每个步骤背后的设计哲学。

1. 开发环境准备与基础概念

1.1 硬件与软件需求检查

现代图形编程首先需要确保开发环境满足最低要求。通过Windows内置的dxdiag工具,我们可以验证显卡是否支持D3D12:

dxdiag

在"显示"选项卡中,检查"功能级别"是否包含"12_x"。这意味着硬件支持Direct3D 12的全部特性。值得注意的是,即使较旧的显卡也可能支持D3D12,但功能级别可能受限。

开发工具方面,Visual Studio 2019是当前最稳定的选择,需要安装10.0.19041.0或更高版本的Windows SDK。这个SDK包含了所有必要的头文件和库,是D3D12开发的基础。

1.2 D3D12核心架构理解

与前任版本不同,D3D12采用了更显式的设计理念,将更多控制权交给开发者。这种设计带来了性能优势,同时也增加了复杂性。主要组件包括:

  • 设备(Device):代表GPU的抽象,是所有资源的创建工厂
  • 命令队列(Command Queue):GPU工作项的提交入口
  • 命令列表(Command List):记录GPU执行的指令序列
  • 资源(Resource):存储在GPU内存中的数据(纹理、缓冲区等)
  • 描述符(Descriptor):资源视图,告诉GPU如何解释资源
  • 管线状态对象(PSO):封装了渲染状态(着色器、混合模式等)

这种架构使得CPU可以并行准备多个命令列表,然后批量提交给GPU执行,显著提升了多核CPU的利用率。

2. 项目初始化与窗口创建

2.1 创建Win32应用程序框架

D3D12程序仍然基于传统的Win32窗口,我们需要先建立一个基本的窗口框架:

// 窗口过程函数 LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hWnd, message, wParam, lParam); } // 创建窗口 HWND CreateWindow(const wchar_t* title, int width, int height) { WNDCLASSEX wc = { sizeof(WNDCLASSEX) }; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WindowProc; wc.hInstance = GetModuleHandle(nullptr); wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.lpszClassName = L"D3D12WindowClass"; RegisterClassEx(&wc); RECT rect = { 0, 0, width, height }; AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); return CreateWindow( L"D3D12WindowClass", title, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, GetModuleHandle(nullptr), nullptr); }

2.2 集成DirectX Headers

D3D12的开发需要额外的头文件支持。微软开源的DirectX-Headers项目提供了这些必要的文件:

  1. 从GitHub克隆DirectX-Headers仓库
  2. DirectX-Headers-main\include\directx目录添加到项目包含路径
  3. 在代码中包含核心头文件:
#include <directx/d3d12.h> #include <directx/dxgi1_6.h> #pragma comment(lib, "d3d12.lib") #pragma comment(lib, "dxgi.lib")

3. D3D12核心组件初始化

3.1 创建设备与命令队列

设备是D3D12的核心接口,代表物理GPU的抽象:

ComPtr<ID3D12Device> device; ComPtr<IDXGIFactory6> factory; // 创建DXGI工厂 ThrowIfFailed(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory))); // 枚举适配器并创建设备 ComPtr<IDXGIAdapter1> adapter; for (UINT adapterIndex = 0; factory->EnumAdapterByGpuPreference(adapterIndex, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) { DXGI_ADAPTER_DESC1 desc; adapter->GetDesc1(&desc); if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device)))) { break; } }

命令队列是GPU工作的提交入口,不同类型的队列用于不同目的(图形、计算、复制):

D3D12_COMMAND_QUEUE_DESC queueDesc = {}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; ComPtr<ID3D12CommandQueue> commandQueue; ThrowIfFailed(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue)));

3.2 交换链与渲染目标配置

交换链管理着多个缓冲区(通常是2-3个),用于实现平滑的画面渲染:

DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; swapChainDesc.BufferCount = FrameCount; 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; ComPtr<IDXGISwapChain1> swapChain; ThrowIfFailed(factory->CreateSwapChainForHwnd( commandQueue.Get(), hWnd, &swapChainDesc, nullptr, nullptr, &swapChain));

渲染目标视图(RTV)是GPU绘制结果输出的地方,每个交换链缓冲区都需要一个RTV:

// 创建RTV描述符堆 D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; rtvHeapDesc.NumDescriptors = FrameCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; ComPtr<ID3D12DescriptorHeap> rtvHeap; ThrowIfFailed(device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap))); // 为每个交换链缓冲区创建RTV CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart()); for (UINT i = 0; i < FrameCount; i++) { ThrowIfFailed(swapChain->GetBuffer(i, IID_PPV_ARGS(&renderTargets[i]))); device->CreateRenderTargetView(renderTargets[i].Get(), nullptr, rtvHandle); rtvHandle.Offset(1, rtvDescriptorSize); }

4. 渲染管线配置

4.1 根签名与着色器编译

根签名定义了着色器能够访问的资源布局,是CPU和GPU之间的契约:

CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); ComPtr<ID3DBlob> signature; ComPtr<ID3DBlob> error; ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)); ThrowIfFailed(device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)));

HLSL着色器是GPU执行的小程序,我们需要编译顶点和像素着色器:

// 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.0f); result.color = color; return result; } float4 PSMain(PSInput input) : SV_TARGET { return input.color; }

编译着色器到字节码:

ComPtr<ID3DBlob> vertexShader; ComPtr<ID3DBlob> pixelShader; UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; ThrowIfFailed(D3DCompileFromFile(L"Shader.hlsl", nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vertexShader, nullptr)); ThrowIfFailed(D3DCompileFromFile(L"Shader.hlsl", nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &pixelShader, nullptr));

4.2 管线状态对象(PSO)创建

PSO封装了所有渲染状态,是D3D12中最复杂的对象之一:

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 } }; D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) }; 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.DepthEnable = FALSE; psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; psoDesc.SampleDesc.Count = 1; ComPtr<ID3D12PipelineState> pipelineState; ThrowIfFailed(device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState)));

4.3 顶点缓冲区与命令列表

定义三角形顶点数据并上传到GPU:

struct Vertex { float3 position; float4 color; }; Vertex triangleVertices[] = { { { -0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } }, // 左下,红色 { { 0.0f, 0.5f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } }, // 上中,绿色 { { 0.5f, -0.5f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } } // 右下,蓝色 }; // 创建上传堆并复制数据 ComPtr<ID3D12Resource> vertexBuffer; ThrowIfFailed(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(sizeof(triangleVertices)), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertexBuffer))); // 映射内存并复制数据 void* pVertexDataBegin; CD3DX12_RANGE readRange(0, 0); ThrowIfFailed(vertexBuffer->Map(0, &readRange, &pVertexDataBegin)); memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices)); vertexBuffer->Unmap(0, nullptr); // 创建顶点缓冲区视图 D3D12_VERTEX_BUFFER_VIEW vertexBufferView; vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress(); vertexBufferView.StrideInBytes = sizeof(Vertex); vertexBufferView.SizeInBytes = sizeof(triangleVertices);

命令列表记录GPU执行的具体指令:

// 重置命令分配器和命令列表 ThrowIfFailed(commandAllocator->Reset()); ThrowIfFailed(commandList->Reset(commandAllocator.Get(), pipelineState.Get())); // 设置视口和裁剪矩形 commandList->RSSetViewports(1, &viewport); commandList->RSSetScissorRects(1, &scissorRect); // 资源屏障:从呈现状态转换到渲染目标状态 commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition( renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET)); // 设置渲染目标 CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle( rtvHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize); commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr); // 清除渲染目标 const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f }; commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); // 设置图元拓扑和顶点缓冲区 commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); commandList->IASetVertexBuffers(0, 1, &vertexBufferView); // 绘制调用 commandList->DrawInstanced(3, 1, 0, 0); // 资源屏障:从渲染目标状态转换回呈现状态 commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition( renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT)); // 关闭命令列表 ThrowIfFailed(commandList->Close());

5. 渲染循环与同步

5.1 执行命令列表与呈现

命令列表准备好后,需要提交到命令队列执行:

ID3D12CommandList* ppCommandLists[] = { commandList.Get() }; commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // 呈现交换链 ThrowIfFailed(swapChain->Present(1, 0));

5.2 CPU-GPU同步机制

围栏(Fence)用于同步CPU和GPU的执行:

// 创建围栏 ComPtr<ID3D12Fence> fence; ThrowIfFailed(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence))); UINT64 fenceValue = 1; HANDLE fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); // 等待前一帧完成 const UINT64 currentFenceValue = fenceValue; ThrowIfFailed(commandQueue->Signal(fence.Get(), currentFenceValue)); fenceValue++; if (fence->GetCompletedValue() < currentFenceValue) { ThrowIfFailed(fence->SetEventOnCompletion(currentFenceValue, fenceEvent)); WaitForSingleObject(fenceEvent, INFINITE); } frameIndex = swapChain->GetCurrentBackBufferIndex();

6. 调试与性能优化技巧

6.1 常见错误排查

D3D12开发中常见的错误包括:

  1. 资源状态错误:忘记设置正确的资源屏障
  2. 描述符越界:访问超出描述符堆范围的描述符
  3. 命令列表未关闭:在ExecuteCommandLists之前忘记关闭命令列表
  4. 内存泄漏:未正确释放COM对象

启用D3D12调试层可以捕获许多错误:

ComPtr<ID3D12Debug> debugController; if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))) { debugController->EnableDebugLayer(); }

6.2 性能优化建议

  1. 多线程命令列表生成:利用D3D12的多线程特性,并行生成命令列表
  2. 资源重用:避免每帧创建和销毁资源
  3. 描述符管理:使用描述符表而非根描述符提高性能
  4. 管线状态缓存:避免频繁切换PSO

7. 进阶方向与扩展思考

成功渲染第一个三角形只是D3D12之旅的起点。接下来可以考虑:

  1. 添加深度缓冲:实现3D场景的正确遮挡
  2. 纹理映射:为几何体添加表面细节
  3. 常量缓冲区:实现动态参数传递
  4. 计算着色器:探索GPU通用计算能力
  5. 多线程渲染:充分利用现代CPU的多核特性

D3D12的显式设计和底层控制虽然增加了学习曲线,但为性能优化提供了前所未有的灵活性。掌握这些基础概念后,开发者可以逐步构建更复杂的渲染引擎,实现令人印象深刻的图形效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询