news 2026/6/2 2:35:55

从“Hello World”到“Hello Triangle”:用DirectX12在Win32窗口画第一个彩色三角形的完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从“Hello World”到“Hello Triangle”:用DirectX12在Win32窗口画第一个彩色三角形的完整流程

从“Hello World”到“Hello Triangle”:DirectX12图形编程的现代入门仪式

在编程学习的传统中,"Hello World"一直是初学者接触新语言或框架的第一步。而在图形编程领域,渲染一个彩色三角形则扮演着类似的角色——它既是DirectX12入门的仪式,也是理解现代图形管线的基础。不同于简单的文本输出,这个三角形背后隐藏着GPU工作原理、资源管理、命令提交等核心概念。

对于从DirectX11迁移而来的开发者,D3D12最直观的变化是其显式的资源管理和更低级别的API控制。这种设计虽然增加了初始学习曲线,但也带来了显著的性能提升潜力。本文将采用"先理解为什么,再动手怎么做"的方式,带你完整走过从创建Win32窗口到渲染三角形的全流程,特别关注那些容易卡住的环节,如描述符堆、资源屏障和同步机制。

1. 环境准备与基础架构

1.1 开发环境配置

现代DirectX12开发推荐使用Visual Studio 2019及以上版本,搭配Windows 10 SDK (10.0.19041.0或更新)。验证开发环境是否支持D3D12的最快方式是运行dxdiag命令:

dxdiag

在"显示"标签页中,检查"功能级别"是否包含"12_x"。这是硬件支持DirectX12的标志。如果使用较旧的VS2017,需要单独安装对应版本的Windows SDK。

提示:DirectX-Headers项目提供了开源的DirectX头文件,可以作为传统Windows SDK的替代方案,特别适合需要跨平台编译的场景。

1.2 Win32窗口与D3D12的绑定

创建基础的Win32窗口是D3D12渲染的前提。与D3D11不同,D3D12需要开发者显式管理窗口与图形API的关联:

// 窗口过程函数示例 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句柄用于后续交换链的创建。现代图形API通常采用这种显式绑定的方式,为多窗口渲染和复杂应用场景提供灵活性。

2. D3D12核心对象创建流程

2.1 设备与命令队列初始化

D3D12设备(ID3D12Device)代表物理GPU的抽象,是所有资源创建的起点。创建过程需要经过DXGI工厂枚举适配器:

ComPtr<IDXGIFactory4> factory; CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory)); ComPtr<IDXGIAdapter1> adapter; for (UINT adapterIndex = 0; factory->EnumAdapters1(adapterIndex, &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; } }

命令队列(ID3D12CommandQueue)是D3D12执行工作的门户,不同类型的队列(图形、计算、复制)对应GPU上不同的执行引擎:

D3D12_COMMAND_QUEUE_DESC queueDesc = {}; queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue));

2.2 交换链与渲染目标视图

交换链管理着多个缓冲区(通常是2-3个)以实现平滑的画面渲染。D3D12的交换链创建需要窗口句柄和命令队列:

DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; swapChainDesc.BufferCount = frameCount; swapChainDesc.Width = width; swapChainDesc.Height = height; swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; ComPtr<IDXGISwapChain1> swapChain; factory->CreateSwapChainForHwnd( commandQueue.Get(), hWnd, &swapChainDesc, nullptr, nullptr, &swapChain);

渲染目标视图(RTV)是将交换链缓冲区绑定到渲染管线的桥梁。创建RTV需要描述符堆——这是D3D12管理各种视图的核心机制:

D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {}; rtvHeapDesc.NumDescriptors = frameCount; rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap));

3. 渲染管线的构建

3.1 根签名与着色器编译

根签名定义了着色器如何访问资源,是D3D12管线状态的重要组成部分。最简单的根签名可以是一个空签名:

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

着色器通常使用HLSL编写,编译时需要注意目标着色器模型版本。对于D3D12,至少需要顶点着色器和像素着色器:

// Shader.hlsl struct PSInput { float4 position : SV_POSITION; float4 color : COLOR; }; PSInput VSMain(float3 pos : POSITION, float4 color : COLOR) { PSInput result; result.position = float4(pos, 1.0f); result.color = color; return result; } float4 PSMain(PSInput input) : SV_TARGET { return input.color; }

3.2 管线状态对象(PSO)

PSO封装了几乎所有渲染状态,是D3D12中最复杂的对象之一。创建PSO需要之前准备的根签名和着色器:

D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {}; 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.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));

输入布局描述了顶点数据的结构,需要与HLSL中的输入签名匹配:

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 } }; psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };

4. 资源上传与渲染循环

4.1 顶点数据的上传

D3D12要求开发者显式管理资源的上传。顶点数据需要从CPU内存复制到GPU可见内存:

struct Vertex { XMFLOAT3 position; XMFLOAT4 color; }; Vertex vertices[] = { { XMFLOAT3(-0.5f, -0.5f, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }, { XMFLOAT3(0.0f, 0.5f, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) }, { XMFLOAT3(0.5f, -0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) } }; CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD); CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(vertices)); device->CreateCommittedResource( &uploadHeapProps, D3D12_HEAP_FLAG_NONE, &bufferDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertexBuffer)); void* pVertexDataBegin; vertexBuffer->Map(0, nullptr, &pVertexDataBegin); memcpy(pVertexDataBegin, vertices, sizeof(vertices)); vertexBuffer->Unmap(0, nullptr);

4.2 命令提交与同步

D3D12采用命令列表和命令队列的提交模式,配合围栏实现CPU-GPU同步:

// 记录命令列表 commandAllocator->Reset(); commandList->Reset(commandAllocator.Get(), pipelineState.Get()); commandList->SetGraphicsRootSignature(rootSignature.Get()); commandList->RSSetViewports(1, &viewport); commandList->RSSetScissorRects(1, &scissorRect); // 资源屏障:从呈现状态转换到渲染目标状态 CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition( renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET); commandList->ResourceBarrier(1, &barrier); // 设置渲染目标并清除 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); // 再次转换资源状态 barrier = CD3DX12_RESOURCE_BARRIER::Transition( renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); commandList->ResourceBarrier(1, &barrier); commandList->Close(); // 提交命令列表 ID3D12CommandList* ppCommandLists[] = { commandList.Get() }; commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists); // 呈现并等待帧完成 swapChain->Present(1, 0); const UINT64 fence = fenceValue; commandQueue->Signal(fence.Get(), fence); fenceValue++; if (fence->GetCompletedValue() < fence) { fence->SetEventOnCompletion(fence, fenceEvent); WaitForSingleObject(fenceEvent, INFINITE); }

这个看似简单的三角形渲染流程,实际上已经涵盖了现代图形API的核心概念。从显式的资源管理到精细的同步控制,D3D12的这些设计理念为高性能图形应用奠定了基础。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!