DirectX 9随书源码实战指南:从编译环境到HLSL改造
2026/9/8 1:48:22 网站建设 项目流程

简介:这份源码包是《Begining DirectX9》入门教程的随书示例,适合刚开始接触3D图形编程与游戏开发的初学者,通过实际代码样例理解图形渲染管线和游戏循环。压缩包共210个文件,以cpp源文件、vcproj与sln工程文件、h头文件为主,另附bmp贴图、wav音频等资源,整体仅1.16MB,结构紧凑,可按章节独立编译运行。示例围绕DirectX9的核心组件展开,Direct3D部分涵盖窗口初始化、基本几何体绘制、固定功能管线与可编程着色器,包括顶点处理、像素着色、光照阴影、深度缓冲与视口变换等关键环节;同时给出纹理映射、动画控制和帧率管理的具体实现,并通过DirectInput演示键盘鼠标输入,通过DirectSound播放背景音乐与音效。每个例子都配有可直接打开的工程,并包含与演示对应的贴图或音频文件,读者可以修改参数即时观察渲染变化,便于将抽象概念转为直观体验。目前已有127人学习下载,是理解DirectX9渲染流程、迈入3D游戏开发领域的实用参考。 我翻硬盘翻出了一套《Beginning DirectX 9》的随书源码,文件夹里躺着十几个 VC6 时代的示例工程,单文件 main.cpp 居多,附带着当时的模型、贴图资源。当年我学图形编程就是靠它上手的:没废话,没有大型引擎框架,每个示例只解决一个问题,编译出来就是各种彩色三角形在窗口里旋转。时隔多年重新打开,我才发现把一套老源码在新系统上跑通要踩的坑,其实比当年学 D3D9 本身还多。所以这篇博文就想把这套随书源码彻底“盘活”,从目录结构讲到编译环境,从第一个三角形聊到如何用 HLSL 改造成现代写法,最后再给一份排错速查表。想入门 Direct3D 9,或者想拿老代码练手的同学,直接照着操作就行。

这套随书源码覆盖的 D3D9 示例,正好是从窗口初始化、顶点缓冲、矩阵变换到纹理光照的完整主线。DirectX SDK 虽然已经停止更新,但 API 设计思路放到今天依旧有价值:它足够底层,能让你建立图形管线的“肌肉记忆”,又不像 D3D12 那样把命令列表、内存屏障反复抽象。程序员之间常说的“先 D3D9,再 D3D12”,指的就是这个过渡阶段。

1. 为什么一份 2004 年的随书源码到今天还能用

1.1 看源码比看教程更直观

现在的图形学教程有一个通病:为了做“跨平台”或者“现代 API”,开头就丢给你一个 500 行的初始化类,窗口创建、设备创建、资源管理全部混在一起。你照着敲完,发现除了编译通过,脑子里什么都没剩下。而这份随书源码里的示例,大多只有 300 到 800 行,并且是可以用单步调试一行行跟进去的。你在Direct3DCreate9上打个断点,能看到 D3D 对象怎么诞生;你在Present上打断点,能看到一帧渲染在什么时候真正结束。

这种“最小可运行程序”的形态,比任何长篇大论的教程都更容易让人推导原理。源码里的注释不算多,但变量命名很直观:g_pD3Dg_pd3dDeviceg_pVB,一眼就能看出全局对象的作用。对于初学者,这就是最理想的入门材料。

1.2 随书源码覆盖了一条完整的学习主线

这本书的章节基本是一步步搭房子的逻辑。随书源码对应的目录会告诉你,作者不是随便堆代码,而是严格按 D3D9 的核心知识点安排示例:

  • 窗口类和 Direct3D 初始化
  • 绘制点、线、三角形的图元阶段
  • 顶点缓冲与灵活顶点格式 FVF
  • 世界矩阵、视图矩阵、投影矩阵的变换
  • 材质、光照、颜色混合
  • 纹理映射与多重纹理
  • 字体、精灵、渲染状态
  • 网格模型与 X 文件加载

每一章的示例都足够独立,并且通常是同一套窗口骨架下换一个核心函数。这种“只改一个文件就能看到新效果”的练习方式,特别适合搭建图形学知识体系。我第一次读的时候,就是把每个示例编译一遍,再跑到代码里改参数,看渲染结果怎么变化。

2. 摸清源码结构和开发环境,再动手不迟

2.1 压缩包里的目录到底长什么样

拿到随书源码之后先别急着双击.sln,因为根本不会有.sln。当时 Visual C++ 6.0 使用的工程文件是.dsw.dsp,目录结构通常像这样:

Beginning DirectX 9 Chapter 02 PointSprite PointSprite.dsw PointSprite.dsp main.cpp ... Chapter 03 Triangle Triangle.dsw Triangle.dsp main.cpp Chapter 04 ...

如果你直接用 Visual Studio 2015 以上版本打开.dsp,会弹出转换向导。这里我先说结论:与其让 VS 自动转换,我更建议手动新建空项目再添加源文件。自动转换确实省事,但它会重新安排字符集、运行库、附加依赖项,导致后续出现一大堆“找不到头文件”“字符常量怪癖”之类的编译问题。

2.2 编译前必须准备的开发环境

D3D9 编程依赖的头文件和库并不在 Windows SDK 里。Windows 10/11 自带的d3d9.dll是运行库,只能让已经编译好的程序跑起来,编译时你仍然需要 DirectX SDK。微软最后提供的独立版本是 June 2010,装上之后会设置环境变量DXSDK_DIR,通常指向C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)

编译老源码时另一个重点是目标平台。建议统一选择 Win32/X86,不要碰 x64。老示例大多没有做 64 位适配,D3DX9 数学库里也存在对齐相关的问题,在 64 位下莫名其妙的崩溃会劝退你。

配好工程后,把这几项填进项目属性里:

配置项建议值
目标平台x86(Win32)
附加包含目录$(DXSDK_DIR)Include
附加库目录$(DXSDK_DIR)Lib\x86
字符集使用多字节字符集
链接器-附加依赖项d3d9.lib d3dx9.lib winmm.lib
预处理器定义WIN32;_DEBUG;_WINDOWS

为什么要额外链winmm.lib?书里部分示例用到了timeGetTime、播放声音等 Windows 多媒体接口,不链接的话会出现一长串“无法解析的外部符号”。虽然有些示例没用到,但统一加上可以少折腾一轮。

3. 跟着源码跑通第一个 D3D9 渲染示例

3.1 新建工程并引入源码文件

第一步,在 Visual Studio 里新建一个“Windows 桌面应用程序”类型的空项目,然后把某个示例目录下的.cpp文件拖进来。比如Chapter 03里的三角形示例,把main.cpp添加进去。这里不建议直接使用控制台程序,否则WinMain入口会和 CRT 启动代码打架。

3.2 初始化的代码骨架与参数选择

绝大多数示例的初始化函数长这样,我加了点注释说明:

LPDIRECT3D9 g_pD3D = NULL; LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; HRESULT InitD3D(HWND hWnd) { g_pD3D = Direct3DCreate9(D3D_SDK_VERSION); D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = TRUE; // 窗口模式 d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; // 由系统决定格式 d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.BackBufferCount = 1; g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pd3dDevice); return S_OK; }

这里有两个关键点。第一,Direct3DCreate9的参数D3D_SDK_VERSION并不是让传“9.0”的意思,它是 SDK 头文件里的版本约定宏。老源码有时会手写一个旧版本宏,换成新 SDK 后会返回空指针,所以这个宏不要随意改。第二,D3DDEVTYPE_HAL表示走显卡硬件加速,如果运行在虚拟机或远程桌面环境里,设备创建可能失败,这时可以临时改成D3DDEVTYPE_REF使用微软的软件参考实现,画面慢但至少能跑。

3.3 顶点缓冲与真正画出一个三角形

书里的示例一般会定义一个自定义顶点结构:

struct CUSTOMVERTEX { float x, y, z; DWORD color; }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE)

创建顶点缓冲区之后,把三个顶点锁进显存:

CUSTOMVERTEX vertices[] = { { -1.0f, -1.0f, 0.0f, 0xffff0000 }, { 1.0f, -1.0f, 0.0f, 0xff00ff00 }, { 0.0f, 1.0f, 0.0f, 0xff0000ff }, }; g_pd3dDevice->CreateVertexBuffer( 3 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_DEFAULT, &g_pVB, NULL); void* pVertices = NULL; g_pVB->Lock(0, 0, &pVertices, 0); memcpy(pVertices, vertices, sizeof(vertices)); g_pVB->Unlock();

然后在每一帧的BeginScene / EndScene之间调用:

g_pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX)); g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

这里就能看到固定管线的核心逻辑:SetStreamSource告诉显卡“顶点流从哪里读、步长多少”,SetFVF声明顶点数据结构,DrawPrimitive决定图元类型和数量。这三个操作放在现代 API 里通常被拆成顶点输入布局、顶点缓冲绑定和绘制命令,但思路骨架完全一致。

3.4 消息循环里的渲染节奏

窗口程序的入口通常是一个while循环,不断处理消息,然后调用渲染函数:

MSG msg = {0}; while (msg.message != WM_QUIT) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { Render(); } }

Render()内部的固定搭配是Clear->BeginScene->EndScene->Present。老手喜欢把这个流程称为“一帧的节拍器”。尤其要注意BeginSceneEndScene必须成对出现,绘制调用也必须放在两者之间,否则会出现画面闪烁或内容消失的怪现象。

4. 老代码迁到现代 Visual Studio 的避坑记录

4.1 手动重建工程才是最快的路

我在初次尝试时,选了 Visual Studio 自动转换.dsp,结果 VS2019 转完后项目里多了很多自动生成的过滤文件,并且全部依赖路径都失效了。更糟的是,转换后的工程默认使用 Unicode 字符集,老代码里的字符串常量全部报错。所以后来我学乖了:直接新建一个空 Win32 项目,把要用到的.cpp文件添加进去,再手动配置表里的几个路径。整个过程十分钟内能完成,而且后面所有示例都能复用同一个工程模板。

4.2 d3dx9.h 与 Windows SDK 的头文件冲突

最容易让人崩溃的是,编译时d3dx9.h内部突然冒出大量的macro redefinitionC2061语法错误。原因在于 VS 默认会先搜索 Windows SDK 的 include 目录,而新版 Windows SDK 里的d3d9.hd3d9types.h与 DirectX SDK 的同名文件重复定义。解决方案是保证 DirectX SDK 的 include 路径排在 Windows SDK 之前。在项目属性“VC++ 目录”的“包含目录”里,把$(DXSDK_DIR)Include放在最上方;如果还不行,就加上$(DXSDK_DIR)Include到“附加包含目录”,并关闭 VS 的“从生成中排除 Microsoft 默认包含路径”之类的开关。

如果你不需要 D3DX 的数学工具库函数,也可以只使用 Windows SDK 自带的d3d9.h,然后把书里所有d3dx9.h相关函数自己替换一遍。但这样做工作量偏大,对新手不友好。所以核心原则就一句话:让 DXSDK 的头文件优先。

4.3 DLL 加载与链接器设置

编译通过只是第一步,运行起来才是真的“爽”。老源码直接编译出来的 exe 很大概率会弹窗提示缺少d3dx9_43.dll。这不是系统文件,而是 DirectX SDK 里的运行时组件。处理办法有两个:

  • 安装微软官方提供的 DirectX End-User Runtime June 2010;
  • 更推荐的做法:从 SDK 安装目录的Redist文件夹里找到d3dx9_43.dll,直接复制到 exe 同目录下。这样以后把 demo 分享给别人时,也不会因为目标机器没装 DirectX 运行时而翻车。

链接阶段的另一个问题是在“项目属性 -> 链接器 -> 附加依赖项”里忘了d3dx9.lib,结果报出unresolved external symbol D3DXMatrixPerspectiveFovLH之类的错误。这个符号来自 D3DX 实用库,跟直接 D3D9 API 不在同一个d3d9.lib中,所以必须把d3dx9.lib加进链接列表。

5. 从源码示例里提炼出真正能复用的图形基本功

5.1 把重复代码提炼成自己的 d3dutil

随书源码的示例有一个特点:每个工程都有差不多的初始化窗口代码和 D3D 设备创建代码。比起复制粘贴,更有价值的是自己动手抽一个工具模块,把窗口创建、设备创建、顶点缓冲的创建和释放统一封装起来。我当时就照着源码里的模式写了一个简单的d3dutil.h,把InitD3DCleanupD3DLinkVertexBuffer等方法收进去。这个抽象过程中你对 API 的理解会加深,因为你需要思考哪些参数是每个示例都一样的,哪些是应该暴露给调用方的。

现在的很多引擎库,比如 DXUT,正是从这种工具抽象里长出来的。理解源码给出的原始形态,再去看 DXUT 的封装,你会一下子明白它为什么要设计那些回调函数。

5.2 用 HLSL 重写固定管线示例

书里的光照和材质走的是固定功能管线,你看到的是SetRenderState(D3DRS_LIGHTING, TRUE)这样的状态开关。想往现代图形学靠拢,可以把示例里的绘制部分替换成一个最简单的 HLSL 着色器:

float4 VS(float4 pos : POSITION) : SV_POSITION { return pos; } float4 PS() : SV_TARGET { return float4(1.0f, 0.5f, 0.2f, 1.0f); }

在 D3D9 中可以通过CreateVertexShaderCreatePixelShader加载这段代码,然后在绘制前SetVertexShaderSetPixelShader。当你把固定管线的世界、视图、投影矩阵手动搬进顶点着色器时,才真正理解矩阵乘法的顺序为什么是“世界矩阵乘视图矩阵再乘投影矩阵”。这个理解的含金量,比单纯调用SetTransform高得多。

5.3 从源码里挖出模型与纹理加载的宝藏

书后面的示例还带了.X格式的模型文件和 2D 贴图资源。D3DX 库提供了一行加载模型的简化接口:

LPD3DXMESH g_pMesh = NULL; D3DXLoadMeshFromX( L"tiger.x", D3DXMESH_SYSTEMMEM, g_pd3dDevice, NULL, NULL, NULL, NULL, &g_pMesh);

配合D3DXCreateTextureFromFile就能把纹理贴到模型表面。随书源码里这些资源都是现成的,比自己去网上找测试模型省很多时间。把这些示例依次跑通,就相当于把“加载外部资产 -> 送入渲染管线”的整个链路都体验了一遍。这也是做游戏 Demo 前最好的一次预习。

6. 随书源码最常见问题速查与排查

6.1 问题现象和处理策略一览表

现象原因处理方案
编译时报d3dx9.h not found未配置 include 路径在项目里加$(DXSDK_DIR)Include
链接时报D3DXMatrixPerspectiveFovLH unresolved缺少 d3dx9.lib链接器附加依赖项加d3dx9.lib
运行时报缺少d3dx9_43.dll缺少 DirectX 运行库装 June 2010 运行库,或把 DLL 复制到 exe 目录
字符串常量报错、宽字符转换错误工程默认使用 Unicode把字符集改成“多字节字符集”
创建设备失败或窗口黑屏HAL 设备不可用临时换D3DDEVTYPE_REF测试
深度缓冲失效,物体相互穿透交换链没设置深度模板d3dpp中设置AutoDepthStencilFormatD3DFMT_D24S8,并在Clear时加入D3DCLEAR_ZBUFFER
全屏示例切回桌面时卡死丢失了WM_ALT+TAB等系统消息处理最简单的方法是先把Windowed = TRUE单跑
程序退出时报 COM 对象泄漏忘记释放设备、顶点缓冲Cleanup里对pVBpDevice调用Release()

6.2 我踩过的几个典型场景

第一次跑通三角形示例后,我想把窗口切换成全屏模式,结果程序黑屏,鼠标也不见了。查了半天才发现是BackBufferFormat不能填D3DFMT_UNKNOWN,全屏模式下必须明确指定一个如D3DFMT_X8R8G8B8的格式。所以从源码示例起步时,先用窗口模式,把渲染结果看顺眼了再去动全屏逻辑。

另一个印象深刻的问题是纹理示例在虚拟机里运行直接花屏。原因是虚拟机的显示驱动对某些 D3D9 纹理格式支持不完整,改用D3DFMT_A8R8G8B8这种最通用的格式后问题就消失了。如果你家的显卡或虚拟机也出现奇怪的花屏,优先怀疑格式支持范围,而不是代码本身。

最后再说一个很实际的经验:在老源码里搜索某个 API 的用法,比开浏览器查文档快得多。十几个示例都是围绕同一套 API 组织起来的,一个函数的调用参数、上下文和返回值,在源码里往往是连续出现的。当你需要查SetRenderState怎么设置混合模式时,直接在随书源码里搜一下,然后跳转到调用处看上下文,效果立竿见影。

这份随书源码的价值不在于代码本身多么现代,而在于它把图形编程最核心的那几个概念原原本本地摆在桌面上。花一个周末,把最简单的几个示例亲手编译、改写、调试一遍,比读十篇“图形渲染入门”文章都管用。挑一个窗口示例开头,接下来的路,代码会一步一步告诉你。

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

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

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

立即咨询