尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

DirectX12示例包完全解读:从三角形到光线追踪的学习路径

DirectX12示例包完全解读:从三角形到光线追踪的学习路径 简介DirectX 12 作为现代 GPU 编程接口彻底颠覆了 D3D11 的状态机思维将资源屏障、描述符堆、命令列表等底层机制交到开发者手中。围绕一份典型的 DirectX12 示例包从基础三角形绘制到 Compute Shader、DXR 光线追踪梳理出一条循序渐进的学习路径先理解命令录制与资源状态切换再掌握纹理采样与多通道渲染最后深入并行计算与混合渲染。通过 Visual Studio 环境配置、GPU 调试层和 PIX 帧捕获工具可以高效定位运行时黑屏、资源状态丢失等典型问题。本文旨在帮助初学者从“跑通示例”进阶到“改造工程”在动手实践中真正理解 D3D12 的设计意图与性能优化思路。 拿到一个叫“各种 DirectX12 示例 .zip”的压缩包时多数人的第一反应是先解压看看里面有什么。我见过不少刚开始接触 D3D12 的朋友文件从网盘里拖下来解压后对着十几个解决方案文件夹发愣这些例子是按什么顺序排的先学哪个为什么有的能跑有的编译就报错这篇文章就把这份示例包掰开揉碎说一说里面每类示例到底在讲什么、按什么顺序学、怎么在 Visual Studio 里跑起来以及我在实际调试中踩过的一些坑。如果你是刚接触 DirectX 12 的初学者或者已经写过一些 D3D11 但被 12 的对象模型、资源状态、命令列表搞到怀疑人生那这篇文章应该能帮你省下不少时间。示例代码这种东西最大的价值不是“能跑”而是你能从里面看出 API 的设计意图。1. 这份示例包到底是什么1.1 示例包里的常见内容无论是从微软官方 Sample Browser 导出的合集还是社区整理的学习仓库一份命名为“各种 DirectX12 示例”的 zip 包里通常都会包含几类固定角色最基础的窗口初始化和设备创建示例绘制三角形、旋转立方体等入门级渲染示例纹理采样、贴图、多渲染目标MRT等基础管线示例阴影映射、后处理、粒子系统等中级效果示例Compute Shader 相关示例包括线程组、共享内存、原子操作部分较新的包还会包含 DXR 光线追踪、Mesh Shader、Work Graphs 等进阶示例这些示例的工程文件通常带.sln和.vcxproj打开就能编译。部分新版示例会改用 CMake这也算是一个趋势不过大部分教学型 zip 还是会保守地使用 Visual Studio 工程。我在实际打开这类示例包时第一件事不是看代码而是看README.md或者文档目录。很多示例作者会写清楚工程依赖的 Windows SDK 版本、显卡特性等级、是否需要独立显卡。这些信息直接决定了你手上这份文件到底能不能在你机器上跑起来。1.2 这些示例解决什么问题很多初学者对 DirectX 12 有个误解觉得它就是 D3D11 的“性能增强版”但实际上 D3D12 的思维方式完全不同。D3D11 是一个高层次的“状态机”接口你切换渲染状态、绑定资源驱动帮你做同步和内存管理。D3D12 把这一切都拆开了让你手动管理命令列表、描述符堆、资源屏障、显存生命周期。所以示例包里的每一个例子都不仅仅是在演示某个效果而是在演示某个底层能力三角形示例教你怎么构建最精简的渲染管线纹理示例教你怎么处理资源状态切换和描述符绑定阴影示例教你怎么使用深度缓冲区以及 PSO管线状态对象的切换Compute 示例教你怎么利用 GPU 执行非图形计算如果只看不跑或者跑了不改这些代码就还是别人的知识。这也是为什么我在下文会反复建议你拿到示例第一件事是改一个参数、删一段代码看看画面会不会崩、会不会变慢然后你才真正开始学会 DirectX 12。2. 为什么选这组示例D3D12 学习路径的设计逻辑2.1 从旧 API 迁移时的最大冲击从 D3D11 迁移到 D3D12 的人通常会在四个地方被卡住资源屏障Resource Barrier你不能再随便读写同一份资源必须明确告诉 GPU“这块资源从渲染目标变成着色器输入”。描述符堆Descriptor Heap资源视图SRV、RTV、DSV不再直接绑定在管线状态上而是放进堆里你用 CPU 句柄或者 GPU 句柄去引用。命令列表Command List所有绘制指令先在 CPU 侧录制再提交到命令队列执行顺序和同步全都要自己负责。内存管理默认堆、上传堆、回读堆以及静态缓冲区和动态缓冲区的分配策略。示例包如果编排得好应该逐个攻破这些点。比如第一个三角形示例通常就会用到一个非常简单的命令列表录制流程让你先接受“录制-提交-执行”这个概念。第二个或第三个示例就会引入描述符堆让你理解为什么要用句柄而不是直接绑资源。我看过不少社区整理的示例包排序其实是有讲究的先画面输出再资源管理再同步和依赖最后才是高端特性和性能优化。这个顺序和 GPU 渲染管线的实际执行逻辑是一致的。2.2 示例难度分级的意义很多示例包会在文件夹命名里带编号比如00-Basics、01-Advanced。这种分级不是随便分的它的核心逻辑是每一步都只引入一个新的不确定性。以三角形示例为例它要你做的事情是“跑通整个绘制流程”所以它会尽量减少资源类型、减少渲染特性。你打开代码会发现它只用到一个很小的顶点缓冲、一个常量缓冲、一个根签名、一个 PSO。这正是让你集中精力理解“渲染一帧需要哪些对象”的最佳方式。反过来说如果你一上来就看阴影映射示例代码里既有深度纹理、又有多个渲染通道、还要切换 PSO、还要处理视口变换你很可能看两天就放弃。所以我拿到示例包后一定会先看它的目录结构如果没有编号我会自己按依赖关系排序先有不依赖任何纹理的绘制再有单张纹理采样再到多通道渲染最后才到 GPU 计算和光线追踪。这里也分享一个我自己的习惯拿到示例包先建立一个表格记录每个示例的名称、用的特性、依赖的硬件等级、是否可运行。这样后续学习的时候你不会陷入“这个示例我是不是看过”的迷茫。3. 核心示例逐个拆解3.1 三角形绘制理解命令列表和描述符堆任何 DirectX 12 示例包的“第一课”都是绘制三角形。这个例子的画面简单得不能再简单但它背后涉及的概念链非常长。首先是设备ID3D12Device、命令队列ID3D12CommandQueue、交换链IDXGISwapChain的创建。这一步会把你从 D3D11 时代“设备上下文”的舒适区拉出来因为你发现所有操作都变成了“先录制命令再提交队列”。其次是顶点缓冲的创建。你会接触到一个非常重要的概念上传堆Upload Heap。D3D12 里GPU 不能直接读取 CPU 写的普通内存你需要把顶点数据从 CPU 侧拷贝到 GPU 侧的默认堆中这就需要用到ID3D12CommandAllocator和ID3D12GraphicsCommandList。初学者第一次看到CopyBufferRegion这个函数时往往一脸懵但它的本质就是“CPU 把数据拷给上传堆GPU 再从上传堆拷到显存”的两个步骤。第三步是描述符堆。即使是最简单的三角形你也至少需要一个 RTV渲染目标视图来描述交换链后台缓冲区。你需要创建描述符堆用CreateRenderTargetView把视图写到堆里然后在命令列表中OMSetRenderTargets指定这个描述符。这里不涉及 SRV但你已经能感受到 D3D12 和 D3D11 的核心差异一切视图都是内存里的描述符而不是随用随传的绑定方式。还有一个容易忽略的点是根签名Root Signature。三角形示例一般是根签名最简单的时候通常只有一个常量缓冲绑定在 root 参数里。写代码时你会发现设置根签名的时间点、PSO 和根签名的匹配关系都是 D3D12 特有的事情。这里我建议你无论如何都要亲手把三角形示例画出来之后试试改一改顶点数据在屏幕上画出两个三角形组成的矩形才对顶点缓冲和输入布局有更深的体感。3.2 纹理贴图资源状态与同步三角形示例跑通后纹理贴图是下一步。这个例子第一次让你体会到 D3D12 资源管理的琐碎一块纹理在创建后初始状态通常是D3D12_RESOURCE_STATE_COPY_DEST你从 CPU 侧把像素数据拷到上传堆再拷贝到纹理资源中拷完之后需要将资源状态转化为PIXEL_SHADER_RESOURCE这一步就是资源屏障的应用。我见过很多人第一次写纹理加载代码时忘了加屏障然后画面黑屏或者显示警告信息D3D12 ERROR: Resource state is invalid。这个报错在 D3D11 里根本不会出现因为驱动会帮你自动处理资源状态。但在 D3D12 里这是应用程序的责任。纹理示例还会带你认识描述符堆的“堆类型”问题。你可能会在同一个例子中看到 CPU 可见堆和 GPU 可见堆的区别有些堆只能在 CPU 端写入有些堆需要给着色器访问。特别是当你需要把纹理描述符和采样器描述符放到同一个堆里的不同区域时你会发现 D3D12 对描述符布局的管理非常严格。这里有一个值得实操的点D3D12_RESOURCE_DESC里的Format、Width、Height、MipLevels这些字段到底怎么填大多数示例会用一个 DDS 文件或自定义格式加载纹理但如果你要加载 PNG 或 JPG就需要用 DirectXTex 这类库先把图像解码成像素数组再填进上传堆。我在实际操作中通常会加一个调试打印把纹理的宽高、格式、MIP 层数打出来避免后续描述符创建时参数不匹配。学完纹理示例建议你把采样器Sampler相关的参数比如寻址模式、过滤方式改一改观察画面变化。这一步能让你对D3D12_SAMPLER_DESC的理解从背公式变成真正的掌握。3.3 阴影映射从基础到级联阴影阴影映射是很多示例包里第一个“多通道渲染”示例。它的逻辑很简单先从光源视角渲染深度图再从相机视角渲染场景采样深度图判断片段是否在阴影中。这个逻辑用 D3D11 写并不复杂但 D3D12 需要你手动处理大量对象切换。你会在阴影示例中第一次遇到多个 PSO。一个用于普通场景渲染一个用于深度图渲染。这两个 PSO 的根签名可以相同但也可以不同。实际操作中我会建议把阴影深度图的 PSO 设成最小的不绑定任何像素着色器只输出深度这样性能更好。多通道带来的另一个问题就是资源的双重使用同一张纹理既需要作为深度渲染目标被写入又需要作为阴影贴图被采样。这两者之间的切换正是资源屏障大展身手的地方。你可能需要这样一段逻辑第一次屏障把深度纹理从DEPTH_WRITE变成PIXEL_SHADER_RESOURCE或者反过来第二次屏障在第二个渲染通道之前切换相关资源状态如果状态切换的顺序写错了最常见的结果不是画面崩溃而是黑屏或者警告刷屏。我在排查这类问题时会打开 GPU 调试层也就是在创建设备时启用DXGI_CREATE_FACTORY_DEBUG和D3D12_DEBUG_LAYER这样驱动会告诉我哪一步资源状态不合法。还有一点值得注意级联阴影CSM示例会更复杂一些。它不仅涉及多张深度纹理还需要在 CPU 侧计算每个级联的视锥体和矩阵。这个例子对数学能力要求更高但学习价值也很大。如果你在示例包里看到 CSM 相关的目录我建议放在阴影基础之后再去研究不要一上来就看。3.4 Compute Shader 粒子系统计算队列的实际用途Compute Shader计算着色器示例是 D3D12 与 D3D11 差距体现最明显的地方之一。在 D3D11 里Dispatch 也就是一个 API 调用而在 D3D12 里你需要为 Compute 创建独立的 PSO 和根签名并且在一个命令列表里你要手动切换图形管线状态和计算管线状态。很多示例包里的粒子示例逻辑是这样的用一个 Compute Shader 更新粒子的位置和速度用一个 Vertex Shader Pixel Shader 将粒子渲染成点或者四边形每一帧先用SetPipelineState(computePSO)Dispatch 更新数据再用SetPipelineState(graphicsPSO)DrawInstanced 渲染这里最大的难点不在于写 Compute Shader而在于理解“同一个缓冲区先被 Compute 写入后被 Vertex Shader 读取”这个流程需要什么同步。在 D3D12 中你可能需要 UAV 屏障D3D12_RESOURCE_BARRIER_TYPE_UAV来保证计算着色器的写入完成后再让后续绘制指令读取。如果你漏了这一步粒子位置可能错乱或者闪烁。示例里还会涉及线程组的概念。比如numthreads(64, 1, 1)这个属性以及 Dispatch 的参数Dispatch((particleCount 63) / 64, 1, 1)。很多教程只是带过但我建议你亲手改一下线程组大小用 PIX 或 RenderDoc 看占用率你会明白“线程组大小不是随便填的”。我见过有新手直接把粒子数量改到一百万然后发现帧率掉了。排查后发现不是 GPU 能力不够而是 CPU 侧每一帧都从 GPU 回读数据用于排序。D3D12 里跨 GPU/CPU 回读是需要同步的如果你在粒子示例里看到类似操作最好想清楚是否真的需要还是可以用 GPU 端排序替代。3.5 DXR 光线追踪示例从光栅化到混合渲染如果示例包比较新里面大概率会有 DXRDirectX Raytracing相关示例。这一部分对硬件有要求需要支持硬件光线追踪的显卡一般标注为D3D12_RESOURCE_BINDING_TIER_3和 GeForce RTX / Radeon RX 6000 以上等级。DXR 示例里最常见的画面是光栅化场景的同时在屏幕一部分区域渲染反射或阴影。这就引入了“混合渲染”的概念。你需要同时拥有光栅化 PSO 和光线追踪 PSO以及一个 Shader Binding TableSBT。SBT 是 DXR 特有的概念它告诉 GPU 每个着色器阶段的入口参数是什么。这部分内容对图形学基础要求很高我建议把它放在示例包的最后一站。当你已经理解了根签名、描述符堆、资源屏障再回头去看 SBT会觉得它其实不过是“用 GPU 指针描述渲染参数”的一种方式。如果你运行的机器不支持 DXR不要硬开。很多示例包里会做特性检测跑不起来时会在调试窗口输出一段提示。这不代表你代码写错了只是硬件评估没过。学习时可以选择先看代码结构理解射线生成着色器、最近命中着色器、未命中着色器之间的关系。等你有合适硬件后再运行也不迟。4. 在 Visual Studio 里跑起来的完整流程4.1 环境准备拿到“各种 DirectX12 示例 .zip”之后第一步是解压到一个路径里没有中文、没有空格的目录下。这听起来像是老生常谈但真的救过我很多次某些 Windows SDK 组件和 HLSL 编译器对路径里的特殊字符很敏感一旦路径带空格就可能出现莫名其妙的找不到文件错误。然后确认你的 Windows 10/11 版本建议至少在 1809 以上。DirectX 12 的运行时随系统更新系统太老会导致部分新特性不可用。Visual Studio 版本建议 2019 或 2022安装时记得勾选“使用 C 的游戏开发”工作负载这里面包含了 Windows 10 SDK、MSBuild、HLSL 编译器等组件。打开示例的.sln文件后在解决方案资源管理器里找到主项目右键点击“设为启动项目”。然后配置项目属性C 语言标准选择 C17 或更高Windows SDK 版本选择本机已安装的最新版平台建议选择x64因为大多数示例没有为 Win32 平台做适配。有些示例包是 CMake 工程那就需要先安装 CMake。打开 CMake 时源码目录指向解压后的示例目录构建目录在外部新建一个build文件夹。生成时选择 Visual Studio 17 2022 作为生成器平台选 x64然后打开生成的.sln再编译。4.2 编译与运行编译时如果有错误先看是不是缺少依赖项。有些示例包使用外部库比如 DirectXTex、DirectXMesh、ImGui这些库可能以 Git 子模块的方式引用。如果你是从 zip 解压的示例包子模块不会自动下载需要在依赖目录下手动放置库文件或者修改项目属性里的包含目录。如果示例包自带vcpkg.json你还可以用 vcpkg 安装依赖vcpkg install directxtex directxmesh imgui然后在 Visual Studio 的 CMake 设置里把工具链文件指定为vcpkg.cmake的路径。这一步对新手来说稍微有点绕但好处是后续编译其他依赖第三方库的示例时会省事很多。编译成功后直接 F5 运行。大多数 D3D12 示例会弹出一个窗口标题上显示示例名称和帧率。如果窗口黑屏或闪烁先看输出窗口有没有 Debug 信息如果直接崩溃先看是不是显卡驱动太老。这里补一句千万不要在虚拟机里跑这些示例。D3D12 对 GPU 特性有要求虚拟机里的软件渲染设备可能只支持最基本的特性等级很多高级示例会直接报错。4.3 调试技巧调试 D3D12 代码最重要的开关就是 GPU 调试层。在代码初始化设备时加入这样的逻辑UINT dxgiFactoryFlags 0; #if defined(_DEBUG) dxgiFactoryFlags | DXGI_CREATE_FACTORY_DEBUG; #endif ComPtrID3D12Device device; D3D12CreateDevice( nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(device) );如果设备创建失败可以尝试不传nullptr而是枚举硬件适配器后再创建。调试层会输出很多有用的错误比如资源状态错误、描述符堆越界、PSO 不匹配等。刚开始你可能会被刷屏的警告吓到但习惯之后会发现这些信息比任何文档都精确。另一个常用工具是 PIXPerformance Investigator for Xbox它是 Windows 上做 GPU 捕获的利器。你可以用它对单帧进行捕获看到每个 DrawCall 的资源绑定、PSO 设置、资源状态。如果某个示例在运行时画面异常用 PIX 看一遍帧时间线基本能定位到具体是哪个 DrawCall 出了问题。我强烈建议在跑示例时把窗口调成窗口模式而不是全屏这样报错和调试器弹窗不会导致你切换到桌面时黑屏。全屏模式下的调试体验要差很多。5. 常见问题与排查技巧实录5.1 典型问题速查表我在跑各种 DirectX12 示例时总结了一些高频问题可以先看下面这张表问题表现常见原因排查方法编译报错找不到 d3d12.hWindows SDK 版本没配好项目属性里选最新已安装的 Windows SDK链接错误 unresolved external symbol缺少 dxgi.lib / d3d12.lib在链接器输入里显式添加对应 lib运行后黑屏无报错资源状态屏障缺失或顺序错误打开 GPU 调试层查看警告信息画面闪烁或撕裂交换链垂直同步设置不对检查 DXGI_SWAP_CHAIN_DESC 里 SwapEffect 和 SyncIntervalFPS 极低使用了 CPU 回读 GPU 数据用 PIX 看捕获找出 Stall 位置运行即崩溃显卡不支持示例所需特性等级查看代码里 CheckFeatureSupport 的输出纹理加载失败图片文件路径不对把路径改成绝对路径或确认工作目录摄像机无法移动输入处理部分被示例简化为固定轨迹查看是否有键盘/鼠标消息处理逻辑这张表里最值得你留意的是黑屏问题。D3D12 黑屏不一定意味着设备创建失败很多时候是资源状态没有同步好。这种情况在 Release 配置下可能偶尔不触发但在 Debug 配置下会立刻崩溃或警告这也是我建议你始终先跑 Debug 配置的原因。5.2 两个印象最深的调试案例第一个案例是纹理示例黑屏。我当时运行的是带 DDS 纹理的示例画面只有背景色没有模型。打开调试层后看到一条警告说资源放在D3D12_RESOURCE_STATE_COPY_DEST但被像素着色器采样时却需要D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE。问题就出在我把纹理上传完之后忘记加ResourceBarrier切换状态。修复方法很简单在提交命令列表之前加一条屏障但那一刻你会立刻明白什么叫“手动管理资源状态”。第二个案例是阴影示例闪烁。画面物体表面有一堆高频噪声看起来像阴影图分辨率不够但调到 4096 后仍然抖动。最后发现是因为我在渲染深度图时没有把深度格式设为D3D12_FORMAT_D32_FLOAT而是用了交换链后备缓冲区的格式。深度缓冲格式不匹配导致深度写入和采样不一致。这个例子让我养成了一个习惯每次创建纹理或缓冲区时都先打印D3D12_RESOURCE_DESC的字段确认格式和预期一致。调试这种事很多时候靠的不是天赋而是你有没有一套稳定的排查流程。我的流程是先看调试层输出再用 PIX 看帧捕获最后定位到具体命令列表或资源代码。这套流程跑通之后再奇怪的渲染 bug 也花不了太久。6. 从“跑起来”到“会改”推荐的学习路径与扩展思路6.1 推荐阅读顺序示例包本身已经按难度组织了但我还是建议你按下面这个顺序学习和修改三角形示例先跑通再改成其他颜色、多个图形最后加上一个旋转矩阵。纹理示例换一张自己的图片修改寻址模式和过滤方式观察效果。深度缓冲和模板缓冲示例理解D3D12_DEPTH_STENCIL_DESC的作用试试关闭深度测试后画面会如何。渲染到纹理示例把场景渲染到一张纹理上再贴到屏幕上的四边形理解 RenderTarget 和 SRV 的来回切换。阴影映射示例把阴影图分辨率改小观察阴影质量变化再改阴影偏移量。计算着色器示例用固定值初始化粒子数据观察 GPU 的并行更新逻辑。DXR 示例如果硬件支持可以尝试修改反射次数理解递归光线追踪的成本。这个顺序并不严格但基本上遵循“先建立管线再管理资源再处理多通道再并行计算”的逻辑。每一步都在前一步的基础上增加一个概念不会一次引入太多新知识。我还是建议你每看一个示例都顺手写一段简单的 README记录它演示了什么 API、用到了哪些新对象、当时的坑是什么。这样等你回头复习或者写自己的项目时索引效率高很多。6.2 性能分析与改造成自己项目的思路示例代码写得都比较清晰但直接搬到项目里通常不够用。你看完示例后可以试着把它改造成自己的小项目比如做一个“旋转的带纹理立方体”再加上鼠标控制摄像机。这个过程会让你发现示例代码里有很多“教学目的”而精简的结构比如常量缓冲区只有一份、没有做多帧同步、没有处理窗口尺寸变化。这些都是实际项目中必须考虑的事。性能分析方面在 Visual Studio 里可以用 PIX 的 GPU Capture 来抓帧。抓完之后你会看到每个 DrawCall 的耗时、顶点数量、像素填充率。从示例代码里学到的性能优化技巧通常都比较基础减少绑定状态的切换把同一 PSO 下的物件尽量一起绘制尽量复用描述符堆不要每帧重新创建资源屏障尽可能合并减少 GPU 等待动态数据使用动态上传堆并做环形缓冲而不是每帧创建新缓冲这些例子在 D3D11 里不太需要你操心但在 D3D12 里会直接影响性能。示例包的代码往往不会过度优化因为它的目标是教学但你学习的目的就是把这些“教学写法”转成“工程写法”。我自己在实际操作中体会最深的一点是DirectX 12 示例的价值不只在“编译通过并看到一个窗口”而是在这个过程中逼着你一遍遍去读文档、查描述符布局、思考同步问题。每解决一个问题你对整个渲染管线的理解就会深一层。所以别急着下载完就想着全部跑通也别因为一两个示例报错就放弃。最后分享一个小技巧把示例包里的 HLSL 着色器文件单独抽出来放到一个专门的目录里对照学习。每个着色器的入口函数、绑定空间、Semantic 都记录一下。等你写自己的着色器时这份对照表能帮你省掉大量查文档的时间。你要是手头这份 zip 里有哪个示例一直编译不过或者某个效果跑出来和预期不符不妨先关掉 Release 模式打开 Debug 配置把调试层打开跑一遍。多数情况下答案就藏在输出窗口的那几行警告里。本文还有配套的精品资源点击获取
返回列表