简介: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 SDK(June 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 |
重点解释两行:看到 .vcproj(VS2008 及更早)时,用记事本打开看 ProductVersion,8.00 对应 VS2005,9.00 对应 VS2008,直接用新版 VS 打开会被问是否升级,选"是"即可。看到 .x 模型或 .dds 贴图时,注意它们和源码的相对位置,D3DXLoadMeshFromX 这类加载函数用的是相对路径,你如果把资源挪到别的目录,运行时就会出现找不到文件的黑屏。
在这个阶段你能顺便判断包内代码是固定管线还是可编程管线:有 .fx 文件说明它走效果框架,大概率调用 D3DXCreateEffectFromFile;没有 .fx 时,代码里用的多半是 SetRenderState 加 FVF 的固定管线。这个判断影响后面读代码的姿势,固定管线看 FVF 和 SetRenderState,可编程管线看顶点/像素着色器编译和 SetVertexShader/SetPixelShader。搞清楚这两条路,再看核心代码才不会走偏。
3. 示例代码讲解:D3D9 设备初始化与渲染循环的拆解
爱游戏
3.1 从 WinMain 到 CreateDevice:D3D9 设备初始化链路
找到 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 设为 TRUE,BackBufferWidth/Height 填 0 会让后备缓冲跟着窗口客户区大小走,省去手动响应 WM_SIZE 的同步逻辑,示例包如果写了固定尺寸,你改成 0 能少一半窗口尺寸相关的 bug。
顶点处理这里最容易误解:D3DCREATE_SOFTWARE_VERTEXPROCESSING 并不代表着色器一定是软件跑,它只决定固定管线顶点处理在哪执行。老示例为了兼容性普遍用这个,新硬件上想用 T&L 能力可以改成 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_cast<Vertex*>(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 SDK(June 2010)已安装,%DXSDK_DIR% 已定义,并在工程 Include 目录中把 %DXSDK_DIR%Include 放在 Windows SDK Include 之前。顺序很重要,两个 SDK 都有 d3d9.h,版本不一致时先搜到旧版能避免宏冲突。
4.2 现象:链接报 LNK2019,Direct3DCreate9 无法解析
原因:工程没链接 d3d9.lib,或者链接到了新 Windows SDK 里的同名库,后者没有 d3dx9 的导出。解决:在链接器输入里显式加 d3d9.lib 和 d3dx9.lib,并用 /LIBPATH 指到 %DXSDK_DIR%Lib\x86。用 IDE 的工程属性页操作时,注意库目录和附加依赖项是两个地方,只填附加依赖项不填库目录照样解析不了。
4.3 现象:最小化再恢复后窗口黑屏,全屏下 ALT+TAB 回来画面卡死
原因:设备丢失(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 位工程没有对应的导入库。解决:把解决方案平台切回 Win32(x86),这是老 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 参数,后面所有调用全是无效设备,任何黑屏都能追到初始化的某一行上。希望帮到你。
本文还有配套的精品资源,点击获取