本节摘要:本节以 Direct3D 12 护送一个三角形走完管线全程:建顶点缓冲、定根签名、配描述符、造 PSO、录命令、提交、上屏。每一站都回答"为什么需要它",并复盘"画出来是黑的"这类经典事故的原因。
直接看一帧的旅程图,后面每一步都在这张图上:

按图施工。顶点数据先给出来,三角形在裁剪空间里的坐标可以直接绕过矩阵,先保证旅程通:
struct Vertex { float x, y, z; float r, g, b; }; const Vertex tri[] = { { 0.0f, 0.5f, 0.0f, 1, 0, 0 }, // 顶:红 { 0.5f, -0.5f, 0.0f, 0, 1, 0 }, // 右下:绿 { -0.5f, -0.5f, 0.0f, 0, 0, 1 } // 左下:蓝 };
坐标范围约定在负一到正一(裁剪空间),超出部分会被裁掉——这是"三角形不见了"最常见的原因:坐标写错一个数量级,图形整体落在裁剪区外。接着把数据搬上 GPU:
// 上传堆:CPU 可写,适合一次性数据;第六章会讲默认堆与上传堆的分工 ComPtr<ID3D12Resource> vbuf; CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_UPLOAD); CD3DX12_RESOURCE_DESC bufDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(tri)); device->CreateCommittedResource(&heapProps, D3D12_HEAP_FLAG_NONE, &bufDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(vbuf.GetAddressOf())); void* mapped = nullptr; vbuf->Map(0, nullptr, &mapped); memcpy(mapped, tri, sizeof(tri)); // CPU 直接拷入 vbuf->Unmap(0, nullptr); D3D12_VERTEX_BUFFER_VIEW vbv{}; vbv.BufferLocation = vbuf->GetGPUVirtualAddress(); vbv.StrideInBytes = sizeof(Vertex); vbv.SizeInBytes = sizeof(tri);
三个要点:其一,Map/Unmap 是上传堆的直通道,常驻显存资源要走"上传堆中转、复制到默认堆"的两段路,第六章细化。其二,VBV(顶点缓冲视图)是"指针 + 步长 + 总长"三元组——步长填错,GPU 会把颜色字节当坐标读,花屏就是这么来的。其三,资源创建时声明了初始状态 GENERIC_READ,资源状态是 D3D 12 的显式义务,漏标状态转换会在调试层被点名。
着色器用最简一对:顶点着色器原样传位置与颜色,像素着色器输出插值后的颜色。用运行时编译(D3DCompile)或离线编译好的字节码均可,HLSL 细节留给 3.3 节。
初始化完成后,每帧只做三件事。录制:
// 每帧录制前先重置分配器与列表(前提:上帧用它的 GPU 工作已完成——栅栏话题) allocator->Reset(); cmdList->Reset(allocator.Get(), pso.Get()); // 清成深灰 · 绑渲染目标 CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition( rt[frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET); cmdList->ResourceBarrier(1, &barrier); FLOAT clear[4] = { 0.1f, 0.1f, 0.12f, 1 }; cmdList->ClearRenderTargetView(rtvHandle[frameIndex], clear, 0, nullptr); cmdList->OMSetRenderTargets(1, &rtvHandle[frameIndex], FALSE, nullptr); // 绑管线与几何,画三个顶点 cmdList->SetGraphicsRootSignature(rootSig.Get()); cmdList->RSSetViewports(1, &viewport); cmdList->RSSetScissorRects(1, &scissor); cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); cmdList->IASetVertexBuffers(0, 1, &vbv); cmdList->DrawInstanced(3, 1, 0, 0); // 转回呈现状态,收队 CD3DX12_RESOURCE_BARRIER back = CD3DX12_RESOURCE_BARRIER::Transition( rt[frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); cmdList->ResourceBarrier(1, &back); cmdList->Close();
提交与上屏一气呵成:
ID3D12CommandList* lists[] = { cmdList.Get() }; queue->ExecuteCommandLists(1, lists); swapchain->Present(1, 0); // 1 = 等垂直同步;0 为不等待
逐处解读:ResourceBarrier 是状态转换的显式声明——"我要把这块缓冲从呈现态切成渲染目标态",驱动据此插入必要的硬件等待,漏写就是调试层报错与潜在的设备移除(第五章实录的主角)。DrawInstanced 的第一个参数是顶点数,三个顶点一个三角形。Present 的参数是同步间隔,与交换链参数一起决定帧节拍。
跑起来应看到:深灰画布上一个三色渐变的三角形——颜色是三个顶点色在片元间线性插值的结果,这是光栅化插值的直观演示。
解读这份最小程序,D3D 12 的"啰嗦"已经显形:为了画三个顶点,我们声明了资源状态、拼了视图、录了整段命令。但每个啰嗦点都是控制点:清屏色、视口、拓扑、绘制数全部显式在手。第一章说的"自由换责任",这里得到第一份实证。
变式一:让它动起来。 把顶点着色器加一个随时间旋转的矩阵,常量缓冲每帧更新角度——你会发现需要根签名声明常量缓冲、需要每帧往 GPU 写数据,这正是下一章 4.1 节描述符堆实验的入口。变式二:三角形长成立方体。 六个面十二个三角形、加索引缓冲共享顶点、开深度测试——深度缓冲与 OM 阶段从 3.1 节的知识变成手上代码,旋转立方体由此而来。变式三:换拓扑。 IASetPrimitiveTopology 改成线段列表,同一个顶点缓冲能画出线框——管线的数据驱动本性,改一个枚举就能验证。
旅程走通,接下来补齐装备的语言课:HLSL 怎么写、怎么编译、怎么绑定——下一节。