ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DirectX 9随书源码实战指南:从编译环境到HLSL改造

2026/9/8 1:48:24 拓冰建站 浏览量
DirectX 9随书源码实战指南:从编译环境到HLSL改造 简介这份源码包是《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_pD3D、g_pd3dDevice、g_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 位下莫名其妙的崩溃会劝退你。配好工程后把这几项填进项目属性里配置项建议值目标平台x86Win32附加包含目录$(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。老手喜欢把这个流程称为“一帧的节拍器”。尤其要注意BeginScene和EndScene必须成对出现绘制调用也必须放在两者之间否则会出现画面闪烁或内容消失的怪现象。4. 老代码迁到现代 Visual Studio 的避坑记录4.1 手动重建工程才是最快的路我在初次尝试时选了 Visual Studio 自动转换.dsp结果 VS2019 转完后项目里多了很多自动生成的过滤文件并且全部依赖路径都失效了。更糟的是转换后的工程默认使用 Unicode 字符集老代码里的字符串常量全部报错。所以后来我学乖了直接新建一个空 Win32 项目把要用到的.cpp文件添加进去再手动配置表里的几个路径。整个过程十分钟内能完成而且后面所有示例都能复用同一个工程模板。4.2 d3dx9.h 与 Windows SDK 的头文件冲突最容易让人崩溃的是编译时d3dx9.h内部突然冒出大量的macro redefinition或C2061语法错误。原因在于 VS 默认会先搜索 Windows SDK 的 include 目录而新版 Windows SDK 里的d3d9.h、d3d9types.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把InitD3D、CleanupD3D、LinkVertexBuffer等方法收进去。这个抽象过程中你对 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 中可以通过CreateVertexShader、CreatePixelShader加载这段代码然后在绘制前SetVertexShader、SetPixelShader。当你把固定管线的世界、视图、投影矩阵手动搬进顶点着色器时才真正理解矩阵乘法的顺序为什么是“世界矩阵乘视图矩阵再乘投影矩阵”。这个理解的含金量比单纯调用SetTransform高得多。5.3 从源码里挖出模型与纹理加载的宝藏书后面的示例还带了.X格式的模型文件和 2D 贴图资源。D3DX 库提供了一行加载模型的简化接口LPD3DXMESH g_pMesh NULL; D3DXLoadMeshFromX( Ltiger.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中设置AutoDepthStencilFormat为D3DFMT_D24S8并在Clear时加入D3DCLEAR_ZBUFFER全屏示例切回桌面时卡死丢失了WM_ALTTAB等系统消息处理最简单的方法是先把Windowed TRUE单跑程序退出时报 COM 对象泄漏忘记释放设备、顶点缓冲在Cleanup里对pVB、pDevice调用Release()6.2 我踩过的几个典型场景第一次跑通三角形示例后我想把窗口切换成全屏模式结果程序黑屏鼠标也不见了。查了半天才发现是BackBufferFormat不能填D3DFMT_UNKNOWN全屏模式下必须明确指定一个如D3DFMT_X8R8G8B8的格式。所以从源码示例起步时先用窗口模式把渲染结果看顺眼了再去动全屏逻辑。另一个印象深刻的问题是纹理示例在虚拟机里运行直接花屏。原因是虚拟机的显示驱动对某些 D3D9 纹理格式支持不完整改用D3DFMT_A8R8G8B8这种最通用的格式后问题就消失了。如果你家的显卡或虚拟机也出现奇怪的花屏优先怀疑格式支持范围而不是代码本身。最后再说一个很实际的经验在老源码里搜索某个 API 的用法比开浏览器查文档快得多。十几个示例都是围绕同一套 API 组织起来的一个函数的调用参数、上下文和返回值在源码里往往是连续出现的。当你需要查SetRenderState怎么设置混合模式时直接在随书源码里搜一下然后跳转到调用处看上下文效果立竿见影。这份随书源码的价值不在于代码本身多么现代而在于它把图形编程最核心的那几个概念原原本本地摆在桌面上。花一个周末把最简单的几个示例亲手编译、改写、调试一遍比读十篇“图形渲染入门”文章都管用。挑一个窗口示例开头接下来的路代码会一步一步告诉你。本文还有配套的精品资源点击获取