
简介DirectX 9c 初始化示例项目面向刚开始接触 DirectX 游戏编程的开发者演示如何在 Visual Studio 2012 中搭建一个可运行的 DirectX 9 游戏框架。项目源自日本技术站点 marupeke296围绕窗口创建、Direct3D 设备初始化、消息循环、资源加载与渲染输出等核心环节展开能帮助读者直观理解游戏程序从启动到绘制的基本骨架并为进一步学习较新的图形 API 打下基础。压缩包共 8 个文件整体仅 5KB以 cpp 源代码、vcxproj 工程配置、filters 筛选器和 txt 说明文档为主并带有 Git 版本管理配置便于查看项目结构与工程组织方式。目前已有 49 人学习浏览。对照源码与资源内容清单可以理清 DirectX 9 初始化各 API 的调用顺序掌握渲染循环中顶点数据与绘制指令的写法还能借鉴 Git 忽略规则等工程实践是一款轻量且适合入门阶段动手演练的 DirectX 9 学习样本。示例代码结构简洁便于逐行研读。1. 这份 directX9c 示例 .zip解压后是干净的 D3D9 起步工程这份 directX9c 示例 .zip解压后是一份很干净的 D3D9 起步工程能编译出独立窗口、画出一个彩色三角面的最小程序。DirectX 9c 定下的 COM 接口——IDirect3D9、IDirect3DDevice9、顶点缓冲——到今天仍是理解现代图形 API 的地基示例价值不在能跑而在把初始化链路完整摆在面前。它专治翻遍教程也拼不齐的细节D3DPRESENT_PARAMETERS 字段怎么填、HAL 与 REF 设备怎么选、顶点格式怎么声明、Present 为何必须在 EndScene 之后。适合接手老项目、被迫维护 D3D9 代码的从业者也适合刚入图形学、想直接看渲染流水线的学习者。前提先说清包内代码按 2000 年代中期的工程习惯写成拿到新版 Visual Studio 直接编译大概率缺头文件和库文件不是示例坏了是环境要先对齐。第 2 章从环境配置讲起。2. 环境搭建与包内解剖把 DirectX9c 示例变成能编译的工程2.1 选哪套开发环境DirectX SDK、VS 版本与 32 位依赖先别急着开 VS 编译。D3D9 时代有一个绕不开的依赖DirectX SDK 里的 d3dx9.h 和 d3dx9.lib。新版 Windows SDK 虽然还带 d3d9.h但 d3dx9 系列D3DXCreateTexture、D3DXMatrixRotationY 这类工具库早就被微软移除老示例只要用了 D3DX 工具函数就必然依赖旧 DirectX SDK。我装的是 DirectX SDKJune 2010这也是那个年代最后一个发布版装完会写入 DXSDK_DIR 环境变量后续所有工程都靠它定位头文件和库。VS 版本上2000 年代工程默认是 VS2003/2005.vcproj 的 ProductVersion 老到新版本不认识。我一般直接用 VS2019/2022 打开 .sln 让它自动升级工具集选 v140 或 v141升级后把字符集切到多字节因为很多老代码用 char* 而不是宽字符。另一个关键点是编译平台d3dx9.lib 只有 32 位版本平台务必选 x86不要碰 x64。这一步翻车的人数不少我后面第 4 章会单独列一条。先用命令行验证 DirectX SDK 是否真的装好了# 用命令行验证 DirectX SDK 是否装好 echo %DXSDK_DIR% dir %DXSDK_DIR%Include\d3d9.h dir %DXSDK_DIR%Include\d3dx9.h dir %DXSDK_DIR%Lib\x86\d3dx9.lib第一条命令如果输出空说明环境变量没写入后三条里只要 d3dx9.h 不存在后面编译 d3dx 相关代码就一定会报无法打开包括文件。也可以用 where d3d9.dll 确认系统里有没有运行时但那只是运行时不代表开发库齐全。确认依赖后命令行编译是验证示例代码本身能不能编的最快方式# 命令行编译示例x86 环境VS2019 命令提示符中执行 call C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat cl /nologo /EHsc /I%DXSDK_DIR%Include ^ main.cpp winmain.cpp ^ /link /LIBPATH:%DXSDK_DIR%Lib\x86 d3d9.lib d3dx9.lib user32.lib gdi32.lib/I 指定头文件搜索路径/LIBPATH 指定库搜索路径。链接 user32.lib 是因为 WinMain 和窗口过程都在 user32 里gdi32.lib 是 GDI 窗口绘图用的最简窗口也常会引到。命令行编译能绕开 VS 工程升级时的一堆配置弹窗适合验证代码本身能不能编。如果你坚持用 IDE把上面两个目录填进 VC 目录的 Include/Library 即可。2.2 解压与目录识别先看清示例包里有什么再动手先把 zip 解压。常见做法是新建一个纯英文路径的目录比如 D:\dev\d3d9_sample然后右键解压到当前文件夹或用 7-Zip 解压。别把包直接解到 C:\ 或 D:\ 根目录老工程里资源路径经常写死相对路径而且 Windows 的路径长度限制会给一层套一层的目录带来麻烦。解压时若看到要求输入密码的弹窗而这个包本身发布时没密码那就是 zip 伪加密用 7-Zip 时直接点取消或者拖拽目录出来即可绕过一个正常的示例包不会真的加密。解压完先别双击 .sln先看目录长什么样。一个典型的 D3D9 示例包会有几类东西源码文件、工程文件、还可能带着模型和纹理资源。拿到手后应该先看什么我列成一张表文件类型常见后缀在示例包里的作用拿到后第一步源码.cpp / .h窗口、D3D9 初始化、渲染循环先找 WinMain 和 Direct3DCreate9 调用点工程文件.vcproj / .vcxproj / .sln定义编译选项、链接库确认 ProductVersion选对应 VS 版本升级模型/纹理.x / .bmp / .dds顶点数据与贴图输入复制时保留相对路径别改目录结构效果文件.fx可编程着色器入口可选确认是否依赖 D3DXCreateEffectFromFile重点解释两行看到 .vcprojVS2008 及更早时用记事本打开看 ProductVersion8.00 对应 VS20059.00 对应 VS2008直接用新版 VS 打开会被问是否升级选是即可。看到 .x 模型或 .dds 贴图时注意它们和源码的相对位置D3DXLoadMeshFromX 这类加载函数用的是相对路径你如果把资源挪到别的目录运行时就会出现找不到文件的黑屏。在这个阶段你能顺便判断包内代码是固定管线还是可编程管线有 .fx 文件说明它走效果框架大概率调用 D3DXCreateEffectFromFile没有 .fx 时代码里用的多半是 SetRenderState 加 FVF 的固定管线。这个判断影响后面读代码的姿势固定管线看 FVF 和 SetRenderState可编程管线看顶点/像素着色器编译和 SetVertexShader/SetPixelShader。搞清楚这两条路再看核心代码才不会走偏。3. 示例代码讲解D3D9 设备初始化与渲染循环的拆解爱游戏3.1 从 WinMain 到 CreateDeviceD3D9 设备初始化链路找到 WinMain 后别顺着往下读先跳着找三件事Direct3DCreate9 的返回值是否判空、CreateDevice 的参数、窗口过程里 WM_PAINT 是不是空的。很多人第一次拿到示例会盯着 DrawPrimitive 看但 D3D9 里 80% 的启动失败都发生在 CreateDevice 之前。// 1. 创建 D3D9 对象D3D_SDK_VERSION 是必须的版本校验参数 IDirect3D9* d3d9 Direct3DCreate9(D3D_SDK_VERSION); if (d3d9 nullptr) return E_FAIL; // d3d9.dll 版本与头文件不符 // 2. 填写后备缓冲描述结构 D3DPRESENT_PARAMETERS pp {0}; pp.BackBufferWidth 800; // 窗口化时设为 0 可跟随客户区 pp.BackBufferHeight 600; pp.BackBufferFormat D3DFMT_A8R8G8B8; // 32 位颜色缓冲 pp.BackBufferCount 1; pp.MultiSampleType D3DMULTISAMPLE_NONE; // 先不开抗锯齿简化调试 pp.SwapEffect D3DSWAPEFFECT_DISCARD; pp.hDeviceWindow hWnd; pp.Windowed TRUE; // 窗口化模式 pp.EnableAutoDepthStencil TRUE; // 自动创建深度/模板缓冲 pp.AutoDepthStencilFormat D3DFMT_D24S8; // 3. 创建设备 IDirect3DDevice9* device nullptr; HRESULT hr d3d9-CreateDevice( D3DADAPTER_DEFAULT, // 默认主显卡 D3DDEVTYPE_HAL, // 硬件光栅化REF 为软件模拟 hWnd, // 渲染目标窗口 D3DCREATE_SOFTWARE_VERTEXPROCESSING, // 软件顶点处理兼容最广 pp, device); if (FAILED(hr)) { /* 此时看 hr 值不要用 GetLastError */ }逐项说。D3D_SDK_VERSION 是 d3d9.h 里的一个宏Direct3DCreate9 拿它和系统目录里 d3d9.dll 的导出版本比对不一致就返回空指针这是微软防止头文件和运行时版本错配的手段不要自己改成别的值。D3DADAPTER_DEFAULT 表示使用主显示器对应的显卡多显卡机器想用第二块卡要先 GetAdapterCount 再 GetAdapterIdentifier 枚举。D3DDEVTYPE_HAL 指硬件抽象层真正走显卡渲染D3DDEVTYPE_REF 是参考光栅化器纯 CPU 模拟用来调试但慢到没法用。窗口化程序里把 Windowed 设为 TRUEBackBufferWidth/Height 填 0 会让后备缓冲跟着窗口客户区大小走省去手动响应 WM_SIZE 的同步逻辑示例包如果写了固定尺寸你改成 0 能少一半窗口尺寸相关的 bug。顶点处理这里最容易误解D3DCREATE_SOFTWARE_VERTEXPROCESSING 并不代表着色器一定是软件跑它只决定固定管线顶点处理在哪执行。老示例为了兼容性普遍用这个新硬件上想用 TL 能力可以改成 D3DCREATE_HARDWARE_VERTEXPROCESSING如果示例里有自定义顶点着色器则必须用硬件标志或混合模式否则创建会失败。3.2 渲染循环Clear、BeginScene 与 Present 的固定顺序设备创建成功后示例会进入消息循环。一个 D3D9 窗口的渲染循环必须遵守固定顺序Clear 清屏、BeginScene 开始场景、绘制调用、EndScene 结束场景、Present 呈现。顺序错了API 会返回 D3DERR_INVALIDCALL但很多示例根本不检查返回值于是表现为黑屏或者画面卡死。// 每一帧的渲染函数 HRESULT render(IDirect3DDevice9* device, IDirect3DVertexBuffer9* vb) { // 1. 清屏清颜色缓冲和深度缓冲 device-Clear(0, nullptr, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(32, 64, 128), 1.0f, 0); // 2. 场景开始所有绘制调用必须夹在 BeginScene/EndScene 之间 device-BeginScene(); // 固定管线设置顶点流来源 device-SetStreamSource(0, vb, 0, sizeof(Vertex)); device-SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE); device-DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); device-EndScene(); // 3. Present 把后备缓冲翻到前台 device-Present(nullptr, nullptr, nullptr, nullptr); return S_OK; }Clear 的第一个参数是裁剪矩形数量填 0 表示全部渲染目标都清第二个参数是矩形数组指针全清时传 nullptr。第三个参数说明清哪些表面D3DCLEAR_TARGET 是颜色D3DCLEAR_ZBUFFER 是深度颜色值用 D3DCOLOR_XRGB 宏拼。BeginScene/EndScene 必须严格配对所有 SetTexture、SetTransform、DrawPrimitive 都写在这两者之间这是 D3D9 的硬性约束而非风格建议。消息循环的写法对这类示例影响很大。用 GetMessage 的写法在窗口没有消息时整个线程挂起等待渲染循环被卡住表现为窗口无响应、画面不刷新。示例包里常见的是老式 GetMessage 写法我一般直接改成 PeekMessage 轮询// 消息循环PeekMessage 保证无消息时继续渲染 MSG msg {0}; while (msg.message ! WM_QUIT) { if (PeekMessage(msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } else { render(device, vb); // 无消息时渲染 } }这个改动对固定管线的示例通常是肉眼可见的帧率提升因为窗口空闲时不再白白挂起。渲染函数里不必加 Sleep 限帧D3D9 的 Present 本身会随垂直同步节奏阻塞跑多快取决于驱动设置。3.3 顶点缓冲与 DrawPrimitive把三角形真正画出来渲染循环再完整没有顶点数据也只是一块空屏。D3D9 固定管线里顶点要放进顶点缓冲用 FVF 告诉设备每个顶点的内存布局。这段是最容易照着抄也黑屏的地方把完整链路写出来// 顶点结构位置 顶点色 struct Vertex { float x, y, z; DWORD color; }; // 创建顶点缓冲3 个顶点FVF 必须与结构一致 IDirect3DVertexBuffer9* vb nullptr; device-CreateVertexBuffer( 3 * sizeof(Vertex), // 缓冲大小 0, // 用法0 为普通默认 D3DFVF_XYZ | D3DFVF_DIFFUSE, // FVF 顶点格式 D3DPOOL_DEFAULT, // 默认池通常对应显存 vb, nullptr); // 填数据Lock 得到系统内存指针写完后 Unlock void* pData nullptr; vb-Lock(0, 0, pData, 0); Vertex* verts static_castVertex*(pData); verts[0] { 0.0f, 0.9f, 0.0f, D3DCOLOR_XRGB(255, 80, 80) }; verts[1] { -0.9f, -0.9f, 0.0f, D3DCOLOR_XRGB(80, 255, 80) }; verts[2] { 0.9f, -0.9f, 0.0f, D3DCOLOR_XRGB(80, 120, 255) }; vb-Unlock();CreateVertexBuffer 的第二个参数是 D3DUSAGE 标志0 表示默认用法需要动态更新时用 D3DUSAGE_DYNAMIC需要软件访问时加 D3DUSAGE_SOFTWAREPROCESSING示例里没有特殊需求就保持 0。FVF 的意义在于让 D3D9 知道如何解释顶点缓冲里的字节D3DFVF_XYZ 是三个 float 位置D3DFVF_DIFFUSE 是一个 DWORD 颜色结构体成员顺序必须和 FVF 里声明的顺序一致这是固定管线最大的潜规则。Lock 的第一个参数是偏移第二个是长度0/0 表示锁整个缓冲第四个标志在写入时也传 0只有在需要避免 GPU 读写的同步开销时才考虑 D3DLOCK_DISCARD 或 D3DLOCK_NOOVERWRITE。Unlock 之后不能再用指针D3D9 的内存内容是异步上传的这个界限在 D3D11 里更严格D3D9 里你侥幸读了也只是读到旧数据。DrawPrimitive 三个参数依次是图元类型、起始顶点、图元数量。D3DPT_TRIANGLELIST 表示每 3 个顶点构成一个三角形所以一个三角形传 1如果是 100 个三角形且顶点连续就传 100。索引版本用 DrawIndexedPrimitive参数里多了索引缓冲和基元顶点数那些老示例若是加载 .x 模型走的一定是索引路径别用非索引版本去套。4. DirectX9c 示例的常见问题排查五条踩坑记录下面五条是从这份示例编译、运行、改代码的全过程里筛出来的高频问题。每条按现象、原因、解决的顺序写你可以直接按图索骥。多数问题不是代码写错而是环境或生命周期管理没跟上。4.1 现象编译报无法打开包括文件 d3dx9.h原因示例代码里 include 了 d3dx9.h但当前工程的头文件搜索路径里没有 DirectX SDK。新版 Windows SDK 的 Include 目录里只有 d3d9.h没有 d3dx9.h。解决确认 DirectX SDKJune 2010已安装%DXSDK_DIR% 已定义并在工程 Include 目录中把 %DXSDK_DIR%Include 放在 Windows SDK Include 之前。顺序很重要两个 SDK 都有 d3d9.h版本不一致时先搜到旧版能避免宏冲突。4.2 现象链接报 LNK2019Direct3DCreate9 无法解析原因工程没链接 d3d9.lib或者链接到了新 Windows SDK 里的同名库后者没有 d3dx9 的导出。解决在链接器输入里显式加 d3d9.lib 和 d3dx9.lib并用 /LIBPATH 指到 %DXSDK_DIR%Lib\x86。用 IDE 的工程属性页操作时注意库目录和附加依赖项是两个地方只填附加依赖项不填库目录照样解析不了。4.3 现象最小化再恢复后窗口黑屏全屏下 ALTTAB 回来画面卡死原因设备丢失Device Lost。窗口被最小化、尺寸改变、分辨率切换或独占全屏切换时D3D9 设备会进入 lost 状态此时所有渲染调用都不生效Present 返回 D3DERR_DEVICELOST。解决在窗口过程处理 WM_SIZE 时调用 TestCooperativeLevel根据返回值决定是等待还是 Reset。// 最常见的设备丢失恢复写法 case WM_SIZE: { if (device nullptr) break; HRESULT hr device-TestCooperativeLevel(); if (hr D3DERR_DEVICELOST) { // 设备暂不可用等待系统恢复什么都不做 } else if (hr D3DERR_DEVICENOTRESET) { releaseDynamicResources(); // 先释放 DEFAULT 池资源 // presentParams 必须是 CreateDevice 时那组参数 device-Reset(presentParams); restoreDynamicResources(); } break; }这里有个许多人忽略的细节Reset 必须使用与 CreateDevice 时相同的 D3DPRESENT_PARAMETERS窗口化程序最好把 BackBufferWidth/Height 设为 0这样 Reset 时自动跟随当前客户区。DEFAULT 池资源在失焦时必须释放MANAGED 池不需要所以示例里的纹理若用 D3DPOOL_MANAGED 就能少处理一半恢复逻辑。4.4 现象Debug 版运行输出一堆调试信息画面正常但不敢继续用原因装了 DirectX 调试运行时后所有不合法调用都会往 OutputDebugString 输出。常见的有参数给 NULL、BeginScene 未配对、Lock 指针未 Unlock 直接使用。它不是崩溃却是设备状态错乱的前兆。解决借 PIX 或 VS 图形调试器抓帧选中 DrawPrimitive 调用检查输入装配阶段的顶点缓冲和 FVF 是否匹配。没有图形调试器时把调试输出全部贴出来按第一个警告出现在第几帧定位。4.5 现象x64 平台编译d3dx9.lib 报找不到或者一堆 LNK2019原因d3dx9.lib 从头到尾只有 x86 版本64 位工程没有对应的导入库。解决把解决方案平台切回 Win32x86这是老 D3D9 示例的标准用法。硬要在 64 位下跑只能放弃所有 D3DX 工具函数自己写矩阵、纹理加载和 .x 解析工作量远超示例本身。我第一次拿到这个包时也试过 64 位折腾半天结论是不是代码不行是 D3D9 的历史包袱只在 32 位里才完整。5. 进阶与验证把静止三角形改成旋转场景再确认每一帧都在干活示例跑出静止三角形之后大部分人都会做同一个动作让它转起来。这一步不仅验证渲染循环是否真的在跑还能顺便把 D3DX 的矩阵工具链走一遍。我一般这么改声明一个全局角度每帧累加然后构造旋转矩阵并 SetTransform 到世界矩阵。// 旋转状态放在渲染函数外 static float g_angle 0.0f; // 渲染函数内、BeginScene 之后 g_angle 0.02f; // 大约每秒 2 弧度60fps 下转一圈约 3 秒 if (g_angle D3DX_PI * 2) g_angle - D3DX_PI * 2; // 防止大数精度损耗 D3DXMATRIX world, rotY; D3DXMatrixRotationY(rotY, g_angle); D3DXMatrixIdentity(world); D3DXMatrixMultiply(world, rotY, world); device-SetTransform(D3DTS_WORLD, world);D3DXMatrixRotationY 是绕 Y 轴旋转角度的单位是弧度D3DXMatrixIdentity 把矩阵置为单位阵再把旋转矩阵乘上去是为了给后续叠加平移、缩放留位置。SetTransform 的 D3DTS_WORLD 只在固定管线里生效它和 D3DTS_VIEW、D3DTS_PROJECTION 一起构成模型、视角、投影三级变换。如果示例里已经用 D3DXMatrixLookAtLH 和 D3DXMatrixPerspectiveFovLH 设置了视角矩阵三角形应该在屏幕中央旋转如果画面没动先怀疑角度增量没生效再怀疑 SetTransform 放在了 BeginScene 外面。验证是否每帧都在干活我有三个习惯。第一在 render 函数第一行下断点用连续运行看命中次数命中不了就是消息循环被 GetMessage 挂住了。第二把 Clear 的颜色从固定值改成随时间变化的颜色画面有颜色渐变说明 Present 链路完整这比任何调试器都直接。第三用 VS 的图形调试器抓一帧左侧帧列表里能看到 Clear、DrawPrimitive、Present 三个事件只要三次调用齐全管线就是通的。这套静态到旋转的改动本质上是把固定管线里最容易被忽略的矩阵状态重新走了一遍世界矩阵每帧变、视角矩阵一次设置、投影矩阵只在窗口尺寸变化时更新。很多老示例把变换矩阵写死在初始化里画面静止时就看不出来一旦加了旋转矩阵没乘对或者单位矩阵没初始化的问题立刻暴露。如果你想拿一份能直接落地改代码的 D3D9 基线工程这份示例 zip 值得下载后按今天讲的链路跑一遍。这几年我每次拿到这类历史示例都先做最小复现关掉一切优化、保持 x86、先让三角形静止不动再动相机、动模型。D3D9 的报错链是连锁的丢一个 CreateDevice 参数后面所有调用全是无效设备任何黑屏都能追到初始化的某一行上。希望帮到你。本文还有配套的精品资源点击获取