简介:一份配合《Beginning DirectX 9》入门教程的随书源码包,面向DirectX9及3D图形编程初学者,通过可编译运行的示例展示Direct3D渲染、DirectInput输入、DirectSound音频等核心组件的实际用法。包内共210个文件,以cpp、h源码和vcproj、sln工程文件为主,辅以bmp贴图、wav音频、rc资源与x模型文件,整体仅1.16MB,结构紧凑,便于按章节对照学习。源码覆盖窗口与设备初始化、几何图形绘制、固定/可编程图形管线、纹理映射、光照阴影、深度缓冲与视口变换、动画帧率控制、输入响应和背景音乐播放等主题,能够帮助读者把书中的概念落实到具体代码,理解从创建窗口到渲染一帧画面的完整流程。已有127人学习/下载,适合正在阅读入门教程、希望借助实际工程提升DirectX9实践能力的开发者。
1. 十多年后翻出《Beginning DirectX 9》随书源码,它还有没有价值
DirectX 9 是图形编程里几乎不可能绕开的中间站,而这套 Begining DirectX9 随书源码恰好落在这个时代的正中间。它不是把若干接口调用堆在一起,而是把窗口创建、设备初始化、顶点缓冲、纹理、光照和第一批 HLSL 着色器串成几十个能直接运行的小工程。翻它的人未必是在怀旧,更多是想弄明白“渲染管线到底从哪一步开始”。想从零学图形学、想逆向老游戏、想把旧接口代码迁到现代图形栈的人,都能拿它当垫脚石,前提是先把编译环境踩通。这套源码最大的价值不是“能跑”,而是它逼着你亲手完成一个窗口从无到有的全过程,这个过程放在今天依旧稀缺。
2. 打开随书源码先冷静三分钟:盘目录、盯依赖、分清运行时与SDK
2.1 先按章节编号找工程,别被密密麻麻的 cpp 文件带跑
我拿到这种随书源码包,第一件事从来不是找 README,而是先展开目录数序号。源码通常按书的原始章节切成目录,第一屏一定是环境初始化,顶点与几何体在中间,纹理光照靠后,着色器示例往往放在末尾。这个顺序本身比注释更说明问题:先让窗口出现,再谈怎么把三角形画上去,最后才讲像素如何被着色器接管。练手时也应该照这个顺序走,而不是挑长得最花哨的例子下手。
目录盘完,接下来要面对的是依赖。DX9 时代源码的依赖比现代图形栈简单得多,但简单不代表不出事,D3DX 这个辅助库就是最容易翻车的地方。很多示例开头只有三行 include,藏着的信息量却非常大,一行对应一个要备齐的库。
2.2 依赖表:d3d9、d3dx9、dxguid 各负责什么
源码里 cpp 文件开头那几行#include,基本就决定了这个示例需要在编译期备齐哪些东西。常见的是下面这几组:
| 头文件 / 库 | 作用 | 运行期 | 编译期准备 |
|---|---|---|---|
| d3d9.h / d3d9.lib | Direct3D 9 核心接口,设备创建、渲染状态、顶点缓存 | 系统自带 d3d9.dll | 一般可直接用 |
| d3dx9.h / d3dx9.lib | D3DX 辅助:纹理加载、数学矩阵、效果文件解析 | 不一定随系统提供 | 需要早年 DirectX SDK 或手工准备的库 |
| dxguid.lib | 设备枚举时用到的 GUID 定义 | 运行期不需要 | 一般可用系统 SDK 提供 |
| windows.h / user32.lib | 窗口创建、消息循环 | 系统自带 | 自动链接 |
这张表背后有个经验:源码里的链接错误,八成不是 d3d9 本身的问题,而是 d3dx9 那一行。后来的系统 SDK 早把 D3DX 从开发者包里摘出去了,头文件和 lib 都不再默认提供。所以编译一个用了D3DXCreateTextureFromFile的示例,就会看到报错说找不到 d3dx9.h,或者到链接阶段告诉你_D3DXCreateTextureFromFileA@20无法解析。我一般会直接把这一行当作“需要单独准备 d3dx9 库”的信号,而不是傻傻去改 include 路径。
2.3 运行时和 SDK 是两个世界:源码跑不起来九成是编译期的事
“运行时(Runtime)”和“SDK”这两件事经常被混着说。运行时是 exe 启动时加载的 dll,SDK 是编译时需要的头文件和 lib 文件。Win10、Win11 上基本都还带着 Direct3D 9 运行时,所以随手拿一个当年编译好的 demo exe 双击,窗口往往还能弹出来,功能也都正常。到了自己动手编译时就发现,d3d9.h、d3dx9.h 不在默认搜索路径里,或者 lib 缺失。记住这一条:能运行不代表能编译,编译过不去不是系统坏了,而是编译器搜索路径上缺材料。
提示:排查“源码编译失败”时,先把“运行 demo 需要什么”和“编译工程需要什么”分成两张清单,能少走很多弯路。
检查运行时的健康程度,我会用系统自带的诊断工具看一眼显卡驱动里关于 D3D9 的项,确认不是硬件加速被禁用。那里面会把 DirectX 版本、驱动日期、AGP 纹理加速状态都列出来。这也顺带解释了“代码没问题,画面却异常”里的一种常见情况:虚拟机里跑 D3D9 经常只有软件渲染,性能表现会差到让你误以为写错了代码。
3. 用最小命令把第一个示例跑出画面:从空白窗口到三角形
3.1 建好目录、摆好头文件与库文件,再把选中示例整理成一个小工程
我的做法不是一次性编译整个源码包,而是先清理出一个小目录,只放当前要用的文件。这样错误边界很小,出了问题能立刻定位是源码问题还是环境问题。
mkdir dx9-samples && cd dx9-samples mkdir include lib src # 把老 SDK 里的 d3d9.h、d3dx9.h 拷到 include/ 下 # 把 d3d9.lib、d3dx9.lib、dxguid.lib 拷到 lib/ 下 # 最后从随书源码里挑一个最基础的窗口示例放入 src/ 下这段命令有三个关键动作。第一是头文件与 lib 分开摆放,编译器找 include,链接器找 lib,两者搜索路径不同,混在一起会带来“头文件可见但链接失败”的诡异局面。第二是只挑一个示例进 src,随书源码里的工程之间偶尔会共享公共文件,全丢进来会让编译器把多个 main 函数撞在一起。第三是保留原始源码目录不动,方便回头对照。
3.2 一个教科书级的 D3D9 初始化片段:照着随书源码精简
下面的代码几乎能在每个早期示例里找到影子,我把它收拢成一个最小版本。它做的事只有三件:创建 D3D9 对象、填参数、创建设备。
#include <d3d9.h> #include <windows.h> IDirect3DDevice9 *g_dev = nullptr; // 初始化 Direct3D9:创建对象、填参数、创建设备 bool init_d3d(HWND wnd, int width, int height) { // 入口固定是 Direct3DCreate9,参数必须传 D3D_SDK_VERSION IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION); if (!d3d) return false; D3DPRESENT_PARAMETERS pp; memset(&pp, 0, sizeof(pp)); pp.BackBufferWidth = width; pp.BackBufferHeight = height; pp.BackBufferFormat = D3DFMT_A8R8G8B8; pp.MultiSampleType = D3DMULTISAMPLE_NONE; pp.SwapEffect = D3DSWAPEFFECT_DISCARD; pp.hDeviceWindow = wnd; pp.Windowed = TRUE; // 默认适配器、硬件光栅化,出错时先检查 D3DDEVTYPE HRESULT hr = d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, wnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &pp, &g_dev); d3d->Release(); // 设备创建完成,D3D 对象就可以释放了 return SUCCEEDED(hr); }参数说明里最容易踩的是两个地方。第一,BackBufferFormat用D3DFMT_A8R8G8B8是最保守的选择,随书源码里偶尔写D3DFMT_UNKNOWN表示跟随窗口表面,在部分驱动上会得到预期之外的格式。第二,D3DDEVTYPE_HAL是硬件光栅化,如果图省事改成D3DDEVTYPE_REF,程序会像幻灯片一样慢,因为那是软件模拟器在逐像素模拟。顶点处理方式我习惯先用D3DCREATE_SOFTWARE_VERTEXPROCESSING,等读到后面着色器章节再换硬件处理,这样固定管线阶段不容易被显卡驱动的差异干扰。
3.3 编译命令与链接库:链接顺序也有它的规矩
把代码存成 main.cpp 之后,用命令行编译比新建 IDE 工程更省事,因为它第一分钟就会暴露缺了哪些库。
g++ src/main.cpp -o demo.exe \ -Iinclude -Llib \ -ld3d9 -ldxguid-Iinclude指向我们摆好头文件的目录,-Llib指向 lib 目录,这两个路径只对本次编译生效,不会污染系统全局。-ld3d9对应 d3d9.lib,-ldxguid对应 dxguid.lib。链接参数不是随便排的,依赖方要放在被依赖方前面,这行命令里两个库互相引用,实际顺序已经是最稳定的一种。如果示例里出现D3DXCreateTextureFromFile,要加-ld3dx9,并且确保 d3dx9.h 已经包含进来。
链接阶段还有一个容易忽视的细节:编译参数需要区分 Debug 和 Release。随书源码里的很多示例是在 Debug 下写的,会调一堆断言和调试辅助接口,如果你用 Release 编译,一些宏定义行为会变化,最典型的是D3D_DEBUG_INFO没有被定义,某些设备能力查询返回的结构体大小不一致,表现成“编译能过、运行崩溃”。
3.4 窗口循环里的 Present:画面没动静先看这里
初始化成功不代表画面会出现,DX9 里渲染结果显示到屏幕的最后一环是Present。你会在每个示例的渲染循环尾部看到它:
// 清屏为深蓝色,然后提交后台缓冲区到前台 g_dev->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 64), 1.0f, 0); g_dev->BeginScene(); // 这里才是画三角形、画网格的代码 g_dev->EndScene(); g_dev->Present(nullptr, nullptr, nullptr, nullptr);BeginScene与EndScene必须成对出现,所有绘制调用都夹在两者之间。Present的三个参数里前两个是源和目标区域,传nullptr表示整张后台缓冲都上屏。随书源码里有一部分示例在窗口失焦时会停止渲染,这是消息循环里对WM_ACTIVATE的常见处理,如果你改掉了窗口焦点逻辑,画面就会在切换窗口后永久卡死。
4. 随书源码编译运行高频坑:五个翻车现场与排查思路
4.1 找不到 d3d9.h:先查编译器搜索路径,再看是不是混用了 SDK
现象:编译刚开始就报fatal error: d3d9.h: No such file or directory。
原因:d3d9.h 不在编译器默认搜索路径里,多见于后来安装的系统 SDK 不包含旧头文件,或者你把 64 位和 32 位头文件目录搞混了。很多源码工程默认按 32 位编译,而部分新版 IDE 默认生成 64 位目标,两套库路径完全不同。
解决:我一般用命令行先验证头文件位置,确认d3d9.h确实在 include 目录里后,再在编译命令里显式指定-Iinclude。不要同时把多个 SDK 目录塞进全局搜索路径,那会导致头文件版本互相打架,出现“两个 d3d9.h 都找到了”的警告,进而引发更隐蔽的类型不匹配。
4.2 链接时报 unresolved external symbol:d3d9.lib 和 dxguid.lib 缺一不可
现象:源文件编译通过,链接阶段报unresolved external symbol Direct3DCreate9。
原因:头文件让你骗过了编译器,但链接器没找到对应实现。常见于只加了头文件搜索路径,没加 lib 搜索路径,或者 lib 文件名拼写错误。d3d9.lib 是动态库的导入库,它本身不包含实现,而是告诉你运行时去 d3d9.dll 里找函数地址。丢掉它,Direct3DCreate9 就悬空了。
解决:在链接参数里补上-ld3d9 -ldxguid。确认 dxguid.lib 也不能省,设备枚举、格式查询这类代码会引用大量 GUID 常量,它们住在 dxguid 里。排查时我用一个最笨但有效的办法:逐个删掉链接参数,看到底哪个调用对应哪个库,能顺手把源码的依赖关系摸清楚。
4.3 运行时窗口全黑、白屏或花屏:先查 BackBuffer 格式和窗口尺寸
现象:程序能启动,窗口也在,但里面什么都没有,或者整块颜色不对,偶尔伴随屏幕闪烁。
原因:像素格式不匹配是最大嫌疑。窗口客户区尺寸和 BackBuffer 尺寸不一致时,交换链拉伸会出现黑边或错位;D3DFMT_UNKNOWN在部分显卡上解析出的格式不是你要的 32 位颜色,Clear 的颜色也会显示异常。
解决:把BackBufferWidth和BackBufferHeight显式设为窗口客户区大小,格式固定成D3DFMT_A8R8G8B8。还有一个我踩过的细节:创建窗口时如果用了CreateWindow的默认尺寸而没调AdjustWindowRect,客户区已经比你预想的小了一圈,渲染出来的场景会被裁掉一块。源码里这类窗口尺寸计算代码通常被一堆宏包着,调试时直接把尺寸写死成 640x480 最省心。
4.4 高 DPI 下窗口被拉伸或 UI 错位:补上感知设置
现象:窗口字体发虚、画面模糊,鼠标坐标和渲染内容对不上,像是整块画面被放大过。
原因:系统按默认 DPI 缩放窗口,而渲染输出尺寸还是逻辑像素值,两者比例不同导致画面被强行拉伸。随书源码年代距今太远,没有高 DPI 感知的概念,这个问题几乎无解。
解决:在程序入口最前面调SetProcessDPIAware(),让系统不再自动缩放;更彻底的做法是在工程里放一份 manifest 文件声明dpiAware=true。调完这一处后,窗口在 150% 缩放下也会保持像素级清晰。这个坑通常不会误以为坏了,但会严重干扰后续几何体绘制时对坐标的判断。
4.5 用了 D3DX 的示例全部编译失败:归根结底还是 d3dx9.lib 缺失
现象:前面几个纯 d3d9 示例都通过了,一到纹理加载、字体绘制、效果文件相关的示例就报错,错误集中在D3DX*函数上。
原因:D3DX 是独立于核心 D3D9 的辅助库,源码里常见的D3DXCreateTextureFromFile、D3DXCreateFont、D3DXCompileShader全在这个库里。后来的开发者 SDK 不再打包 D3DX,需要自己准备早年 DirectX SDK 留下的头文件、lib 和 dll。我把这件事列为“编译期准备”,而不是“系统问题”。
解决:我一般从旧 SDK 的安装目录里把 d3dx9.h、d3dx9.lib 和对应 dll 一并收集到工程目录,保持本地化。运行时如果目标机器没有 d3dx9.dll,可以把 dll 放到 exe 同目录,比装全局服务更干净。这套源码是初学者拿来练手的,强烈建议所有 D3DX 依赖都做成文件夹内局部引用,不给系统留垃圾。
5. 读源码比跑源码更值钱:固定管线到可编程管线的对照
5.1 SetRenderState 与光照:这是固定管线的主线
DX9 初期的示例里,你几乎见不到真正意义上的着色器,而是用SetRenderState控制一个黑匣子般的状态机。光照、雾化、剔除、混合全是一行状态设置,代码结构异常直白:
// 开启光照与雾化,颜色混合交给固定管线内部处理 g_dev->SetRenderState(D3DRS_LIGHTING, TRUE); g_dev->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(40, 40, 40)); g_dev->SetRenderState(D3DRS_FOGENABLE, TRUE);这套写法的优点是入门阻力小:你不必理解着色器编译过程,只要看状态开关就能猜出效果。缺点是可编程性弱,所有效果都被限定在预设的光照模型里。读这部分源码时,我建议你把每个SetRenderState的枚举名抄下来,再去查它对应的状态组,比如光照、光栅化、混合分别属于不同阶段。做一次这样的整理,后面看 D3D11 的状态对象就顺理成章。
5.2 随书源码里第一段 HLSL 效果文件:可编程管线的起点
到了书中后半段,示例里开始出现 .fx 或直接内嵌的 HLSL 代码。这是整套源码价值最高的地方,因为固定管线与可编程管线的分界就在这里。一个最简单的效果文件长这样:
// 最简单的像素着色器,不管顶点如何,都返回红色 float4 ps_main() : COLOR0 { return float4(1.0, 0.0, 0.0, 1.0); }编译并应用这个着色器的动作,把渲染从“设置状态”推向了“编写代码”。你会看到随书源码里开始出现CreateVertexShader、SetPixelShader这类调用,顶点声明的结构体里也多了纹理坐标。我读这段时得到一个非常重要的认识:固定管线的SetRenderState只是一个配置过程,而着色器才真正定义了“每个顶点/像素如何被计算”,前者是选择,后者是编程。这个区别想通之后,再去看 DX9 后期示例里复杂的多遍渲染,就不会被吓退。
5.3 从 D3D9 想明白 D3D11:一张对照表讲清状态与资源的差异
很多人学完 DX9 直接跳 D3D11,感觉像是换了套图形栈。其实两者在同一画面上是一致的,差别主要是显式化了:
| D3D9 习惯 | D3D11 对应物 | 变化点 |
|---|---|---|
| SetRenderState 全局状态 | ID3D11State 对象(光栅化、深度、混合) | 状态改为创建后绑定,不再是全局黑匣子 |
| 顶点缓冲 + FVF 声明 | ID3D11Buffer + ID3D11InputLayout | 输入布局需要在绘制前明确指定 |
| 隐式管线 | 着色器显式编译并绑定 | Shader Model 进入可编程主线 |
| D3DX 辅助函数 | DirectXTex 等替代库 | 第三方工具链取代官方辅助库 |
| 立即模式 | 立即上下文 + 延迟上下文 | 多线程渲染成为正式能力 |
这张表不是让你把 DX9 忘掉,而是帮你在读随书源码时建立“同一概念在不同接口里的不同位置”的映射。比如 D3D9 的SetRenderState(D3DRS_CULLMODE, …),到 D3D11 就是先创建一个D3D11_RASTERIZER_DESC,再调用CreateRasterizerState,最后在绘制前用RSSetState绑定。概念没变,变的只是对象化。这也能解释为什么许多老工程师能快速迁移到现代图形学:真正值钱的是脑子里那张状态流转图,而不是某个具体接口的签名。
6. 把源码改造成个人调试环境:一个最小 D3D9 验证窗口的惯用手法
6.1 维护一个只做两件事的迷你工程:初始化与 Present
我后来把随书源码的初始化部分抽成一个固定脚本,任何新环境都先编译它,验证通过再开始折腾新功能。它只负责创建窗口、创建设备、清屏、Present,没有纹理、没有顶点、没有输入处理。这个迷你可执行文件是判断环境是否正常的金标准:它能跑,说明 d3d9 运行时、头文件、lib 匹配都没问题;它不能跑,问题在你机器,不在业务代码。我会把编译命令存成一个简单的批处理脚本,换机器后只改路径,不碰代码。
我自己也有一个执念:这套老源码里的D3DADAPTER_DEFAULT在双显卡笔记本上偶尔不按预期工作,集成显卡与独立显卡的切换会把设备枚举结果搞乱。所以我的最小工程里会保留一段GetAdapterCount的循环,把每个适配器的名称和显存打出来,肉眼确认到底用的是哪块卡,避免“代码没变,换台电脑就翻车”的尴尬。
6.2 用一个帧率输出确认环境健康
我在最小工程里加一个简单的帧率统计,每秒把 FPS 打到窗口标题栏上。DX9 程序如果跑在硬件加速下,哪怕只是清屏,轻松也有几百 FPS;如果跌到个位数,多半是掉进了软件渲染。这个方法比任何调试器都直观,因为 Direct3D 9 不会主动告诉你它正在用 REF 设备运行。养成这个习惯之后,每次拿到新显卡、新系统,我都先跑一次这个窗口,再决定要不要去调示例代码。
血的教训来自一次迁移演示就绪。某次在虚拟机里给学生演示随书源码的三角形示例,画面卡到无法忍受,我当时以为是显卡驱动问题,折腾半天才发现虚拟机默认只提供软件 D3D9 支持,D3DDEVTYPE_HAL根本没有硬件设备,程序自动回退到软件模拟。后来我把“验证 HAL 设备是否存在”写进了最小工程里,失败就明确弹一行文字,而不是继续硬跑。这套源码教会我的不是某个接口怎么调,而是先确认环境,再谈效果。希望帮到你。
最小验证工程的检查顺序: 1. 系统工具里确认 D3D9 加速处于启用状态。 2. 编译最小工程并运行,查看 FPS 是否远高于 60。 3. 枚举适配器名称,确认使用的是目标显卡。 4. 再打开随书源码示例,维持同样的窗口尺寸与格式。本文还有配套的精品资源,点击获取