☰
DirectX9随书源码实战:环境配置、渲染管线拆解与调试工具改造
2026/10/19 2:33:12 网站建设 项目流程

简介:这份资源是《Beginning DirectX9》一书的配套随书源码,面向希望踏入游戏开发与3D图形编程领域的初学者,帮助读者通过可运行的工程实例理解DirectX9的核心概念。压缩包共210个文件,约1.16MB,以71个cpp源文件、34个h头文件为主体,配合39个vcproj工程与39个sln解决方案,可直接在Visual Studio中打开编译;另有17个bmp位图、5个x模型、1个wav音频及rc资源脚本等素材,覆盖纹理、精灵与粒子等演示场景。源码内容涉及Direct3D设备初始化与基本图形绘制、固定功能与可编程图形管线、纹理映射、光照与阴影、深度缓冲与视口变换、动画与帧率控制,以及DirectInput输入处理和DirectSound音频播放等模块。已有127人学习关注,适合对照书本逐例调试,在动手实践中掌握3D图形编程的基础流程与排错思路。

1. 从一份随书源码说起:DirectX9 到底还能不能跑起来

翻出《Beginning DirectX9》的随书源码,多半是因为两个场景:要么是手头有个老项目要维护,渲染层还压在 D3D9 上;要么是想从固定管线入手,把图形学里那些矩阵变换、光照模型、纹理采样的概念先跑通一遍再上现代 API。这份源码包的价值不在于代码多新,而在于它把 D3D9 的完整调用链拆得足够细——设备创建、顶点缓冲、索引缓冲、纹理加载、渲染状态、着色器装配,每一块都有独立可编译的示例。它适合两类人:一类是需要在老引擎里定位渲染问题的从业者,另一类是想通过可运行的最小工程理解 GPU 管线的新手。但直接双击 .sln 大概率会翻车,因为这套代码的编译环境、字符集、依赖库和现代 Windows SDK 之间有一堆需要手动对齐的地方。下面按“先跑通、再拆解、后避坑”的顺序,把这份源码从解压到改造成自己能用的工程讲清楚。

2. 把随书源码跑起来:环境对齐与第一个可编译工程

2.1 为什么 DirectX9 工程在现代 VS 上会编译失败

D3D9 的 SDK 从 2010 年之后就不再随 DirectX SDK 单独分发了,而是并入 Windows SDK。但《Beginning DirectX9》随书源码的编写年代,工程文件里引用的还是旧版dxerr.lib、d3dx9.lib以及June 2010那套头文件路径。现代 Visual Studio 打开后,最常见的报错集中在三处:d3dx9.h找不到、dxerr.h缺失、以及LPDIRECT3D9相关符号未定义。根因是旧工程把 DirectX SDK 的 include 和 lib 路径写死在.vcxproj的AdditionalIncludeDirectories里,而新机器上这个路径根本不存在。

常见做法是装一个旧版 DirectX SDK(June 2010),但更干净的方式是只补d3dx9系列库,把dxerr换成 Windows SDK 自带的comdef.h或直接去掉错误处理宏。我一般会先看工程属性里的“附加包含目录”,把不存在的路径删掉,再手动补$(DXSDK_DIR)Include和$(DXSDK_DIR)Lib\x86。如果不想装完整 SDK,也可以只把d3dx9.h、d3dx9.lib、d3dx9math.h这几个文件从旧 SDK 里拷出来放到工程目录下,用相对路径引用。

2.2 用 VS2022 建一个最小 D3D9 工程验证链路

与其在旧工程里修修补补,不如先新建一个空工程,把 D3D9 设备创建的最小链路跑通,确认环境没问题,再把随书源码里的示例逐个搬进来。下面这段代码是 D3D9 初始化的骨架,编译通过说明头文件和库都对齐了。

// d3d9_init.cpp : 最小 D3D9 设备创建与清屏 #include <windows.h> #include <d3d9.h> #pragma comment(lib, "d3d9.lib") #pragma comment(lib, "d3dx9.lib") LPDIRECT3D9 g_pD3D = NULL; LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; HRESULT InitD3D(HWND hWnd) { // 创建 D3D9 接口对象,这是所有 D3D9 调用的入口 if (NULL == (g_pD3D = Direct3DCreate9(D3D_SDK_VERSION))) return E_FAIL; D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = TRUE; // 窗口模式,调试期不要用全屏 d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // 后台缓冲交换后丢弃,性能最好 d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; // 窗口模式下用桌面格式 d3dpp.EnableAutoDepthStencil = TRUE; // 自动创建深度模板缓冲 d3dpp.AutoDepthStencilFormat = D3DFMT_D16; // 16 位深度,够用且省显存 // 用 HAL 设备,软件顶点处理,兼容性最好 return g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, // 默认显示适配器 D3DDEVTYPE_HAL, // 硬件抽象层 hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pd3dDevice); } void Cleanup() { if (g_pd3dDevice) g_pd3dDevice->Release(); if (g_pD3D) g_pD3D->Release(); }

这段代码里几个参数值得单独说。D3D_SDK_VERSION是编译期常量,用来防止头文件和运行库版本不匹配,如果这里返回 NULL,基本就是 d3d9.h 和 d3d9.dll 版本对不上。D3DCREATE_SOFTWARE_VERTEXPROCESSING表示顶点变换交给 CPU,虽然慢,但在虚拟机或老显卡上最稳;如果确认显卡支持硬件顶点处理,可以换成D3DCREATE_HARDWARE_VERTEXPROCESSING,性能会明显提升。D3DFMT_D16是深度缓冲格式,随书源码里大量示例用的是D3DFMT_D24S8,如果模板缓冲用不到,D16 更省资源。

2.3 把随书示例逐个接入:顶点缓冲与纹理加载

环境验证通过后,就可以把随书源码里的示例按依赖顺序搬进来。建议的顺序是:先搬顶点缓冲和索引缓冲的示例,再搬纹理加载,最后搬光照和着色器。顶点缓冲的创建在 D3D9 里是固定套路,但随书源码里有些示例用了D3DXCreateBuffer这类辅助函数,现代 SDK 里已经移除,需要手动改成CreateVertexBuffer。

// 创建顶点缓冲并填充三角形数据 struct CUSTOMVERTEX { FLOAT x, y, z; // 位置 DWORD color; // 顶点颜色 }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE) LPDIRECT3DVERTEXBUFFER9 g_pVB = NULL; HRESULT InitVB() { CUSTOMVERTEX vertices[] = { { -1.0f, -1.0f, 0.0f, 0xffff0000 }, // 红 { 1.0f, -1.0f, 0.0f, 0xff00ff00 }, // 绿 { 0.0f, 1.0f, 0.0f, 0xff0000ff }, // 蓝 }; // 创建顶点缓冲,长度 3 个顶点,FVF 指定灵活顶点格式 if (FAILED(g_pd3dDevice->CreateVertexBuffer( 3 * sizeof(CUSTOMVERTEX), 0, // 用途标记,静态缓冲填 0 D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, // 默认池,显存中 &g_pVB, NULL))) return E_FAIL; // 锁定缓冲写入数据,写完必须解锁 void* pVertices = NULL; if (FAILED(g_pVB->Lock(0, sizeof(vertices), (void**)&pVertices, 0))) return E_FAIL; memcpy(pVertices, vertices, sizeof(vertices)); g_pVB->Unlock(); return S_OK; }

D3DPOOL_DEFAULT表示缓冲放在显存里,读写需要 Lock/Unlock,适合静态几何体;如果顶点数据每帧都变,应该用D3DPOOL_MANAGED,D3D9 会自动在显存和内存之间同步。随书源码里有些示例用了D3DPOOL_SYSTEMMEM,那是给软件顶点处理用的,搬到硬件路径上会报错。纹理加载部分,旧代码常用D3DXCreateTextureFromFile,这个函数在d3dx9.lib里还在,但要注意文件路径是相对当前工作目录而不是工程目录,调试时经常因为路径问题加载失败,建议用绝对路径或把资源拷到输出目录。

3. 拆解渲染管线:从固定管线到可编程着色器的过渡

3.1 固定管线里的渲染状态到底在控制什么

《Beginning DirectX9》随书源码的前半部分大量使用固定管线,也就是通过SetRenderState、SetTextureStageState、SetTransform这些接口来配置 GPU 行为,不写着色器。这套东西现在看很古老,但它是理解渲染状态机的最好材料。固定管线里每个渲染状态对应硬件的一个开关或参数,比如D3DRS_LIGHTING控制是否启用光照,D3DRS_ZENABLE控制深度测试,D3DRS_CULLMODE控制背面剔除。随书源码里有一个示例专门用滑块实时调整这些状态,跑起来后能直观看到每个状态对画面的影响。

我一般会建议先把这个示例跑通,然后逐个注释掉SetRenderState调用,观察画面变化。比如把D3DRS_ZENABLE设为 FALSE,两个三角形的前后遮挡关系会立刻乱掉;把D3DRS_CULLMODE从D3DCULL_CCW改成D3DCULL_NONE,背面会露出来。这种“改一个参数看一个效果”的方式,比看文档记状态名有效得多。随书源码里还用了D3DXMatrixLookAtLH和D3DXMatrixPerspectiveFovLH来设置视图矩阵和投影矩阵,这两个函数在d3dx9math.h里,现代 SDK 里对应的是DirectXMath库,但 D3D9 工程继续用 D3DX 版本也没问题,只是要注意D3DXMATRIX和XMMATRIX不能混用。

3.2 顶点着色器和像素着色器的装配流程

随书源码的后半部分开始引入可编程管线,用D3DXCompileShaderFromFile编译.hlsl文件,然后CreateVertexShader和CreatePixelShader创建着色器对象,最后SetVertexShader和SetPixelShader装配到设备上。这个流程和现代 API 的思路是一致的,只是接口名字不同。D3D9 的着色器模型最高到vs_3_0和ps_3_0,随书源码里的示例基本停留在vs_1_1和ps_1_1,因为那个年代的显卡普遍只支持到这一档。

// 编译并装配顶点着色器 LPD3DXBUFFER pCode = NULL; LPD3DXBUFFER pError = NULL; LPDIRECT3DVERTEXSHADER9 g_pVS = NULL; HRESULT InitVS() { // 从文件编译 vs_2_0 着色器,入口函数名是 VS_Main if (FAILED(D3DXCompileShaderFromFile( L"shader.vs", // 着色器源文件 NULL, // 宏定义数组,没有就传 NULL NULL, // include 接口,没有就传 NULL "VS_Main", // 入口函数名 "vs_2_0", // 着色器模型版本 0, // 编译选项 &pCode, // 编译后的字节码 &pError, // 错误信息 NULL))) // 常量表 { // 编译失败时 pError 里有详细报错,务必输出 if (pError) OutputDebugStringA((LPCSTR)pError->GetBufferPointer()); return E_FAIL; } // 用编译好的字节码创建顶点着色器对象 HRESULT hr = g_pd3dDevice->CreateVertexShader( (DWORD*)pCode->GetBufferPointer(), &g_pVS); pCode->Release(); return hr; }

这里最容易踩的坑是着色器模型版本和设备能力不匹配。如果显卡只支持vs_1_1,你传vs_2_0进去,CreateVertexShader会返回D3DERR_INVALIDCALL。稳妥的做法是先调GetDeviceCaps查VertexShaderVersion和PixelShaderVersion,再决定用哪个版本编译。随书源码里有些示例直接写死了vs_1_1,搬到新显卡上反而没问题,因为新硬件向下兼容。另一个坑是常量表更新,D3D9 用SetVertexShaderConstantF传矩阵和向量,寄存器编号必须和着色器里的register(c0)对应,错一位画面就全黑,这个只能靠调试器逐帧看常量值来定位。

3.3 用随书示例做一次完整的三角形渲染实验

把顶点缓冲、着色器、渲染循环串起来,就是一个完整的 D3D9 渲染帧。随书源码里每个示例的Render函数结构基本一致:Clear清屏、BeginScene开始、设置状态和着色器、DrawPrimitive绘制、EndScene结束、Present呈现。这个顺序不能乱,BeginScene和EndScene之间才能发绘制命令,Present之后后台缓冲才会翻到前台。

void Render() { if (g_pd3dDevice == NULL) return; // 清空后台缓冲和深度缓冲 g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0); if (SUCCEEDED(g_pd3dDevice->BeginScene())) { // 装配顶点缓冲和 FVF g_pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX)); g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); // 如果用了着色器,这里换成 SetVertexShader 和 SetPixelShader g_pd3dDevice->SetVertexShader(g_pVS); // 绘制一个三角形,从第 0 个顶点开始 g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); g_pd3dDevice->EndScene(); } // 呈现后台缓冲 g_pd3dDevice->Present(NULL, NULL, NULL, NULL); }

DrawPrimitive的第一个参数是图元类型,D3DPT_TRIANGLELIST表示每三个顶点组成一个三角形;第二个参数是起始顶点索引;第三个参数是图元数量,这里画一个三角形所以填 1。如果换成DrawIndexedPrimitive,就需要额外绑定索引缓冲,随书源码里有对应示例。Present的四个参数在窗口模式下都可以传 NULL,全屏模式下需要传后台缓冲和窗口句柄,否则可能黑屏。

4. 避坑与排查:随书源码在现代环境下的五个高频问题

4.1 编译报错 LNK2019:无法解析的外部符号

现象是链接阶段报一堆D3DXCreateTextureFromFile、D3DXMatrixLookAtLH未解析。原因是工程没有链接d3dx9.lib,或者链接了但库路径不对。解决方式是在工程属性的“附加依赖项”里加上d3dx9.lib;d3d9.lib;dxguid.lib,并确认“附加库目录”指向了正确的 SDK 路径。如果用的是 Windows SDK 自带的 D3D9,d3dx9.lib可能不在默认搜索路径里,需要手动指定。

4.2 运行时黑屏但程序不崩

现象是窗口出来了,但画面全黑,调试输出没有报错。常见原因有三个:一是Clear的颜色和背景一样,看起来像黑屏;二是视图矩阵或投影矩阵设错,几何体被裁掉了;三是着色器常量没更新,矩阵全是零。排查时先把Clear颜色改成亮红色,确认渲染循环在跑;再把视图矩阵设为单位矩阵,看几何体是否出现在原点附近;最后用SetVertexShaderConstantF手动传一个单位矩阵进去,确认常量寄存器编号没错。

4.3 纹理加载失败返回 D3DERR_INVALIDCALL

现象是D3DXCreateTextureFromFile返回失败,但文件确实存在。原因通常是文件格式不被支持,比如用了带 Alpha 的 PNG 但设备创建时没指定D3DFMT_A8R8G8B8,或者图片尺寸不是 2 的幂。D3D9 对非 2 的幂纹理支持有限,老硬件直接拒绝。解决方式是把纹理尺寸改成 64x64、128x128 这类 2 的幂,或者用D3DXCreateTextureFromFileEx指定格式和尺寸,让 D3DX 自动缩放。

4.4 着色器编译通过但画面全白

现象是D3DXCompileShaderFromFile返回成功,CreateVertexShader也成功,但画出来是纯白。原因是像素着色器没有输出颜色,或者顶点着色器没有传递纹理坐标。D3D9 的像素着色器必须给COLOR寄存器赋值,否则默认输出白色。检查.ps文件里是否有mov oC0, c0这类输出指令,以及顶点着色器的输出结构里TEXCOORD0是否和像素着色器的输入对应。

4.5 窗口缩放后画面拉伸或消失

现象是拖动窗口边缘改变大小后,画面比例不对或者直接没了。原因是后台缓冲尺寸没有跟着窗口变,投影矩阵的宽高比还是旧的。解决方式是在WM_SIZE消息里重置设备,先Release所有D3DPOOL_DEFAULT的资源,再调Reset传新的D3DPRESENT_PARAMETERS,最后重建资源。随书源码里有些示例没处理WM_SIZE,直接跑没问题,但一缩放就翻车。我一般会在Reset之前把所有显存资源释放掉,Reset之后再重新创建,这个顺序不能反。

5. 把随书源码改造成自己的 D3D9 调试工具

随书源码跑通之后,最有价值的用法不是照着抄,而是把它改成一个渲染状态调试器。我的习惯是保留一个最小工程,把SetRenderState、SetSamplerState、SetTextureStageState的调用全部抽到一个配置结构体里,然后用键盘按键实时切换。比如按 Z 键切换深度测试,按 X 键切换背面剔除,按 C 键切换线框模式。这样在排查老项目的渲染问题时,可以快速定位是哪个状态设错了。

// 用键盘消息实时切换渲染状态 void OnKeyDown(WPARAM key) { switch (key) { case 'Z': // 切换深度测试 g_bZEnable = !g_bZEnable; g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, g_bZEnable); break; case 'X': // 切换背面剔除 g_dwCullMode = (g_dwCullMode == D3DCULL_CCW) ? D3DCULL_NONE : D3DCULL_CCW; g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, g_dwCullMode); break; case 'C': // 切换线框填充 g_dwFillMode = (g_dwFillMode == D3DFILL_SOLID) ? D3DFILL_WIREFRAME : D3DFILL_SOLID; g_pd3dDevice->SetRenderState(D3DRS_FILLMODE, g_dwFillMode); break; } }

这个调试器的关键在于状态切换后不需要重建资源,SetRenderState是立即生效的。但要注意D3DRS_FILLMODE在有些驱动上对点图元和线图元无效,只对三角形有效。另外,如果工程里用了着色器,固定管线的状态有一部分会被着色器覆盖,比如D3DRS_LIGHTING在可编程管线里就不起作用了,光照计算完全由着色器代码决定。

验证改造是否成功的方法很简单:找一个已知会受状态影响的场景,比如两个交叉的三角形,切换深度测试看遮挡关系是否变化,切换剔除模式看背面是否出现,切换填充模式看是否变成线框。如果三个状态都能正确响应,说明调试器链路是通的。从那以后我每次接手 D3D9 老项目,都会先把这个调试器跑一遍,确认渲染状态没有被上层代码意外覆盖,再去看具体的绘制逻辑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询