DirectX 9 随书源码深度拆解:从固定管线到二次开发实战

发布时间:2026/10/19 7:15:38
DirectX 9 随书源码深度拆解:从固定管线到二次开发实战 简介这份资源是《Beginning DirectX9》一书的配套随书源码面向希望踏入游戏开发与3D图形编程领域的初学者帮助读者通过可运行的工程实例理解DirectX9的核心概念与技术脉络。压缩包共收录210个文件以71个cpp源文件、34个头文件为主体辅以39个vcproj工程与39个sln解决方案文件另有bmp位图、x模型、rc资源脚本及wav音频等素材整体约1.16MB工程结构完整、便于直接编译调试。源码覆盖Direct3D基本图形绘制、固定与可编程图形管线、纹理映射、光照与阴影、深度缓冲与视口变换、动画与帧率控制以及DirectInput输入处理和DirectSound音频播放等模块读者可对照代码理解设备初始化、顶点处理与像素着色等关键环节。目前已有127人学习关注适合作为理论学习的实践补充帮助读者在动手调试中积累3D图形编程经验。1. 从一份随书源码说起为什么 DirectX 9 仍然值得动手拆一遍很多人第一次接触 DirectX 9不是因为项目需求而是因为手里攥着一本讲 DirectX 9 的书书后附了一份随书源码。这份《Beginning DirectX9 随书源码》就是典型代表——它不是一个能直接上线跑业务的引擎而是一套按章节拆解、层层递进的示例工程集合。你打开它能看到从最简单的窗口初始化到顶点缓冲、纹理映射、光照、字体渲染再到网格模型加载的完整代码路径。适合谁适合想搞懂固定管线时代渲染流程的人适合需要维护老项目、接手遗留 3D 模块的开发者也适合把 DirectX 9 当作图形学入门跳板的学生。它最大的价值不是“能跑”而是“每一步都能看到底层在干什么”。下面我就按自己拆这套源码的顺序把结构、跑法、改法讲清楚。2. 工程结构与技术栈先看清这套源码的骨架2.1 目录组织与章节对应关系这套随书源码通常按书本章节编号组织目录常见形式是Chapter01、Chapter02这样一路排下去每个章节目录下是一个独立的 Visual Studio 解决方案或项目文件。早期版本多为.vcproj/.sln对应 Visual Studio 2003 到 2008 时代后期重印或配套下载里也能见到.vcxproj的迁移版本。每个章节内部一般包含main.cpp或WinMain.cpp程序入口负责注册窗口类、创建窗口、初始化 Direct3D。d3dApp.cpp / d3dApp.h对 Direct3D 设备、窗口消息循环的封装很多章节会复用这一对文件。d3dUtil.cpp / d3dUtil.h工具函数比如颜色转换、纹理加载辅助。Vertex.h或vertexTypes.h自定义顶点结构体与 FVF灵活顶点格式定义。shaders/或fx/部分章节会带.fx效果文件或.hlsl着色器。res/或media/纹理、模型、字体等资源文件。如果你拿到的是压缩包先别急着双击.sln。用文本编辑器打开一个.vcproj看里面Platform和Configuration的配置确认它原本是针对 Win32 还是 x64、Debug 还是 Release。这一步能省掉后面一半的编译报错。2.2 技术栈与依赖固定管线 D3DX 工具库这套源码的技术栈非常集中C 加 Win32 API 加 Direct3D 9辅助库是 D3DXDirect3D Extension。D3DX 在当年是“官方外挂”提供了纹理加载D3DXCreateTextureFromFile、网格加载D3DXLoadMeshFromX、字体ID3DXFont、精灵ID3DXSprite等高层封装。源码里大量出现D3DX前缀的函数说明它没有走纯手写管线而是借助工具库降低示例复杂度。注意D3DX 在较新的 Windows SDK 中已经不再默认提供需要单独安装旧版 DirectX SDK如 2010 年 6 月版。这是跑这套源码时第一个容易翻车的地方。依赖关系可以概括为Win32 窗口 → Direct3D9 设备 → D3DX 资源加载 → 渲染循环。没有引入物理引擎、没有场景图、没有第三方数学库所有矩阵运算直接用D3DXMATRIX和D3DXMatrix*系列函数。这种“裸奔”式结构对学习反而友好因为每一层都暴露在你面前。2.3 一个典型章节的代码入口以最基础的“创建 D3D 设备并清屏”章节为例核心代码通常长这样// d3dApp.cpp 片段初始化 Direct3D 设备 bool D3DApp::initD3D() { // 创建 IDirect3D9 接口用于枚举适配器和获取能力 m_pD3D Direct3DCreate9(D3D_SDK_VERSION); if (!m_pD3D) return false; // 填充呈现参数后台缓冲格式、刷新率、窗口模式等 D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(d3dpp, sizeof(d3dpp)); d3dpp.Windowed TRUE; // 窗口模式 d3dpp.SwapEffect D3DSWAPEFFECT_DISCARD; // 丢弃后台缓冲内容 d3dpp.BackBufferFormat D3DFMT_UNKNOWN; // 窗口模式下用桌面格式 d3dpp.EnableAutoDepthStencil TRUE; // 自动创建深度模板缓冲 d3dpp.AutoDepthStencilFormat D3DFMT_D24S8; // 24 位深度 8 位模板 // 创建设备使用 HAL硬件抽象层软件顶点处理 HRESULT hr m_pD3D-CreateDevice( D3DADAPTER_DEFAULT, // 默认显示适配器 D3DDEVTYPE_HAL, // 硬件加速 m_hWnd, // 窗口句柄 D3DCREATE_SOFTWARE_VERTEXPROCESSING, // 软件顶点处理兼容性更好 d3dpp, m_pd3dDevice); return SUCCEEDED(hr); }这段代码的逻辑说明Direct3DCreate9拿到全局 D3D 接口D3DPRESENT_PARAMETERS决定后台缓冲怎么分配、深度缓冲要不要CreateDevice才是真正向驱动申请设备。参数里D3DCREATE_SOFTWARE_VERTEXPROCESSING是很多老示例的默认选择因为当年显卡硬件顶点处理支持参差不齐软件处理能保证跑起来。如果你在现代机器上改可以换成D3DCREATE_HARDWARE_VERTEXPROCESSING提升性能但要注意某些集成显卡对纯硬件路径支持不完整。3. 把源码跑起来环境、编译与第一个窗口3.1 环境准备旧 SDK 与新 IDE 的搭配跑这套源码环境是最大的门槛。我的建议是Windows 10 或 Windows 11 系统安装 Visual Studio 2019 或 2022然后单独安装 DirectX SDKJune 2010。安装顺序很重要——先装 Visual Studio再装 DirectX SDK否则 SDK 的包含路径和库路径可能不会自动注册到 VS 的全局配置里。装完后在 VS 里新建一个空项目做验证手动添加包含目录$(DXSDK_DIR)Include$(DXSDK_DIR)Lib\x8632 位项目库目录同理。如果你不想动全局设置就在项目属性里单独配。常见做法是给每个章节项目都加一遍虽然繁琐但能避免不同章节之间互相干扰。提示DirectX SDK 安装时如果报 “S1023” 错误通常是因为系统里已有更高版本的 Visual C Redistributable。先卸载冲突的运行时装完 SDK 再装回来。3.2 编译第一个章节从报错到窗口弹出打开Chapter01或最基础的章节先别急着 F5。按这个顺序走右键项目 → 属性 → 配置属性 → 常规 → 平台工具集改成你当前 VS 的版本比如Visual Studio 2022 (v143)。C/C → 预处理器 → 预处理器定义确认有WIN32、_DEBUG或NDEBUG。链接器 → 输入 → 附加依赖项确认有d3d9.lib、d3dx9.lib、winmm.lib。如果报LNK2019: 无法解析的外部符号 _Direct3DCreate94说明d3d9.lib没链上回去检查第 3 步。编译通过后运行你应该看到一个空白窗口标题可能是 “DirectX 9 Demo” 之类。窗口能弹出、能用鼠标拖动、能正常关闭就说明设备创建和消息循环都通了。如果窗口一闪而过检查WinMain里消息循环是不是写成了while (msg.message ! WM_QUIT)但没处理WM_DESTROY里的PostQuitMessage。3.3 渲染循环与清屏验证设备是否真的在工作窗口出来后下一步是确认渲染循环在跑。典型代码// 消息循环中的渲染调用 int D3DApp::run() { MSG msg; ZeroMemory(msg, sizeof(msg)); while (msg.message ! WM_QUIT) { if (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } else { // 没有消息时执行渲染 render(); } } return (int)msg.wParam; } void D3DApp::render() { // 清空后台缓冲和深度缓冲 m_pd3dDevice-Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(100, 149, 237), // 矢车菊蓝 1.0f, 0); m_pd3dDevice-BeginScene(); // 这里后续章节会加入绘制调用 m_pd3dDevice-EndScene(); // 呈现后台缓冲到屏幕 m_pd3dDevice-Present(NULL, NULL, NULL, NULL); }逻辑说明PeekMessage是非阻塞的没有消息时才走render这样窗口不会卡死。Clear把后台缓冲刷成指定颜色同时清深度。BeginScene/EndScene之间是实际绘制命令的放置区。Present把后台缓冲交换到前台。参数D3DCOLOR_XRGB(100, 149, 237)是 RGB 分量改颜色就改这三个数。如果屏幕一直是黑的先确认Clear的颜色不是黑色再确认Present有没有被调用。4. 核心模块拆解顶点、纹理、光照与网格4.1 顶点缓冲与绘制从三角形到立方体顶点缓冲是 DirectX 9 里最基础的几何数据容器。源码中通常这样创建// 定义顶点结构位置 颜色 struct Vertex { float x, y, z; D3DCOLOR color; static const DWORD FVF D3DFVF_XYZ | D3DFVF_DIFFUSE; }; // 创建顶点缓冲 IDirect3DVertexBuffer9* vb nullptr; m_pd3dDevice-CreateVertexBuffer( 3 * sizeof(Vertex), // 缓冲区大小3 个顶点 0, // 用途标志0 表示默认 Vertex::FVF, // 灵活顶点格式 D3DPOOL_MANAGED, // 内存池托管由 D3D 自动管理 vb, NULL); // 填充数据 Vertex* vertices nullptr; vb-Lock(0, 0, (void**)vertices, 0); vertices[0] { -1.0f, -1.0f, 0.0f, D3DCOLOR_XRGB(255, 0, 0) }; vertices[1] { 0.0f, 1.0f, 0.0f, D3DCOLOR_XRGB(0, 255, 0) }; vertices[2] { 1.0f, -1.0f, 0.0f, D3DCOLOR_XRGB(0, 0, 255) }; vb-Unlock();参数说明D3DPOOL_MANAGED表示由 Direct3D 负责在显存和系统内存之间迁移适合静态或低频更新的数据。Lock/Unlock是写入数据的标准配对操作Lock的第三个参数拿到指针写完必须Unlock否则渲染会出问题。FVF告诉管线这个顶点里有什么D3DFVF_XYZ是位置D3DFVF_DIFFUSE是漫反射颜色。绘制时设置流源和格式m_pd3dDevice-SetStreamSource(0, vb, 0, sizeof(Vertex)); m_pd3dDevice-SetFVF(Vertex::FVF); m_pd3dDevice-DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);DrawPrimitive的第一个参数是图元类型D3DPT_TRIANGLELIST表示每三个顶点组成一个三角形第二个参数是起始顶点索引第三个是图元数量。这里画一个三角形所以是 1。4.2 纹理映射把图片贴到几何体上纹理部分通常用 D3DX 加载IDirect3DTexture9* tex nullptr; HRESULT hr D3DXCreateTextureFromFile( m_pd3dDevice, Lres/stone.jpg, // 纹理文件路径 tex); if (FAILED(hr)) { // 加载失败常见原因是路径不对或格式不支持 return false; } // 设置纹理采样状态 m_pd3dDevice-SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); m_pd3dDevice-SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); m_pd3dDevice-SetTexture(0, tex);逻辑说明D3DXCreateTextureFromFile会根据文件扩展名自动选择解码器支持.jpg、.png、.bmp、.dds等。SetSamplerState设置放大和缩小时的过滤方式D3DTEXF_LINEAR是双线性过滤效果比D3DTEXF_POINT平滑。SetTexture(0, tex)把纹理绑定到第 0 个采样阶段。如果模型是黑的先检查纹理路径是不是相对路径、工作目录对不对再检查顶点结构里有没有D3DFVF_TEX1和对应的纹理坐标。4.3 光照与材质固定管线的最后一块拼图DirectX 9 固定管线支持最多 8 盏灯。源码中常见做法// 设置材质 D3DMATERIAL9 mtrl; ZeroMemory(mtrl, sizeof(mtrl)); mtrl.Diffuse D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); mtrl.Ambient D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f); m_pd3dDevice-SetMaterial(mtrl); // 设置平行光 D3DLIGHT9 light; ZeroMemory(light, sizeof(light)); light.Type D3DLIGHT_DIRECTIONAL; light.Diffuse D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Direction D3DXVECTOR3(1.0f, -1.0f, 1.0f); m_pd3dDevice-SetLight(0, light); m_pd3dDevice-LightEnable(0, TRUE); // 开启光照 m_pd3dDevice-SetRenderState(D3DRS_LIGHTING, TRUE); m_pd3dDevice-SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(50, 50, 50));参数说明D3DMATERIAL9决定物体表面对光的反应Diffuse是漫反射颜色Ambient是环境光颜色。D3DLIGHT_DIRECTIONAL是平行光只有方向没有位置适合模拟太阳光。LightEnable(0, TRUE)开启第 0 号灯。D3DRS_LIGHTING是全局光照开关忘了开这个设了灯也没用。如果物体全黑先查D3DRS_LIGHTING是不是TRUE再查法线有没有在顶点结构里D3DFVF_NORMAL。4.4 网格模型加载.x 文件与 D3DXMesh.x是 DirectX 自带的模型格式源码里通常这样加载和绘制ID3DXMesh* mesh nullptr; std::vectorD3DMATERIAL9 materials; std::vectorIDirect3DTexture9* textures; DWORD numMtrls 0; HRESULT hr D3DXLoadMeshFromX( Lres/tiger.x, // .x 文件路径 D3DXMESH_MANAGED, // 网格内存池 m_pd3dDevice, NULL, // 邻接信息不需要可传 NULL materialsBuffer, // 材质缓冲区 texturesBuffer, // 纹理缓冲区 numMtrls, // 材质数量 mesh); // 绘制时按子集逐个绘制 for (DWORD i 0; i numMtrls; i) { m_pd3dDevice-SetMaterial(materials[i]); m_pd3dDevice-SetTexture(0, textures[i]); mesh-DrawSubset(i); }逻辑说明D3DXLoadMeshFromX把.x文件解析成网格、材质数组和纹理数组。DrawSubset(i)绘制第 i 个材质对应的子网格。如果模型加载失败先确认.x文件是不是文本格式还是二进制格式老版本 D3DX 对某些二进制.x支持不好再确认工作目录D3DXLoadMeshFromX用的是相对路径工作目录不对就找不到文件。5. 避坑与排查跑这套老源码时最容易翻车的几件事5.1 现象编译报错 “无法打开包括文件: d3dx9.h”原因DirectX SDK 没装或者装了但 VS 的包含路径没配。新版本 Windows SDK 里只有d3d9.h没有d3dx9.h。解决安装 DirectX SDKJune 2010然后在项目属性 → VC 目录 → 包含目录里加上$(DXSDK_DIR)Include。如果$(DXSDK_DIR)宏没定义就手动写绝对路径比如C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Include。5.2 现象链接报错 “无法解析的外部符号 _D3DXCreateTextureFromFileW12”原因d3dx9.lib没加到附加依赖项或者加的是d3dx9d.lib调试版但当前是 Release 配置。解决在链接器 → 输入 → 附加依赖项里加d3dx9.lib。Debug 配置可以用d3dx9d.lib但前提是 SDK 的 Lib 目录里有这个文件。最稳妥的做法是统一用d3dx9.libDebug 和 Release 都能链。5.3 现象程序运行后窗口一闪就没了原因消息循环写错或者WinMain提前返回。常见的是PeekMessage循环里没有正确处理WM_QUIT或者InitInstance失败后直接return FALSE但没弹错误框。解决在InitInstance每一步后面加MessageBox或输出日志确认是哪一步失败。重点查CreateWindow的返回值是不是NULL以及D3DApp::initD3D的返回值。5.4 现象纹理加载失败模型显示为纯白或纯黑原因路径不对、纹理格式不支持、或者设备丢失后没有重新加载资源。解决先用绝对路径测试确认文件存在。如果绝对路径能加载说明是工作目录问题在项目属性 → 调试 → 工作目录里设成$(ProjectDir)。如果还是失败检查图片是不是 CMYK 模式或带 Alpha 通道的 PNG老版本 D3DX 对某些 PNG 支持不完整转成 JPG 或 BMP 再试。5.5 现象切换分辨率或 AltTab 后画面花屏、设备丢失原因DirectX 9 设备在窗口模式切换、锁屏、远程桌面等场景下会进入 “lost” 状态必须重置。解决在render开头检查m_pd3dDevice-TestCooperativeLevel()如果返回D3DERR_DEVICELOST就跳过渲染返回D3DERR_DEVICENOTRESET就调用Reset重建资源。源码里很多示例没处理这个现代系统上跑就容易花屏。我一般会在D3DApp里加一个handleDeviceLost函数把所有D3DPOOL_DEFAULT的资源释放后重建。6. 二次开发与改造从示例代码到可复用模块6.1 把 D3DApp 抽成独立基类这套源码里每个章节都有一份d3dApp.cpp内容大同小异。二次开发第一步就是把它抽成一个静态库或独立头文件让所有章节共用。具体做法新建一个D3DBase项目把窗口创建、设备初始化、消息循环、设备丢失处理都放进去对外暴露init、run、render三个虚函数。后续章节只需要继承D3DBase重写render即可。这样改的好处是你新增一个实验时不用再复制粘贴几百行样板代码。6.2 用现代 C 封装资源生命周期源码里大量裸指针和手动Release容易漏。改造时可以用Microsoft::WRL::ComPtr或自己写一个简单的D3DResource模板类在析构里自动Release。比如template typename T class D3DRef { public: D3DRef() : ptr(nullptr) {} ~D3DRef() { if (ptr) ptr-Release(); } T** operator() { return ptr; } T* operator-() { return ptr; } operator T*() const { return ptr; } private: T* ptr; };这样IDirect3DTexture9* tex就可以写成D3DRefIDirect3DTexture9 tex加载时用tex使用时直接tex-不用再写Release。注意operator重载要小心别在需要取地址的地方误用。6.3 把固定管线逐步迁移到可编程管线DirectX 9 支持 Shader Model 2.0/3.0源码里部分章节可能已经带了.fx文件。改造思路是先保留固定管线作为 fallback再逐步把顶点处理和像素处理替换成 HLSL。比如把SetFVF换成SetVertexDeclaration把SetMaterial/SetLight换成常量缓冲区传参。这个过程能帮你理解固定管线到底替你做了什么。我一般会先改一个最简单的三角形确认 Shader 能编译、能输出颜色再往复杂模型上套。6.4 验证改造是否成功的三个检查点改完之后怎么确认没改坏我习惯走这三步跑一遍原始章节截图保存基准画面。跑改造后的版本用同样的窗口大小、同样的相机角度截图。用像素对比工具比如fc /b对比两张 BMP或者写个简单的逐像素差值脚本看差异是否在可接受范围内。如果差异很大先查渲染状态有没有漏设再查矩阵有没有转置错误。DirectX 9 默认是行主序但 HLSL 里常量缓冲区默认是列主序传矩阵时经常要D3DXMatrixTranspose一下这个坑我踩过不止一次。注意改造过程中不要一次性把所有章节都迁移到新架构先拿一个章节跑通确认没问题再批量替换。否则一旦出问题你很难定位是架构问题还是某个章节的特有逻辑。6.5 一个具体技巧用宏切换调试输出老源码里调试信息很少出问题只能靠猜。我一般会在D3DBase里加一个宏#ifdef _DEBUG #define D3D_CHECK(hr, msg) \ if (FAILED(hr)) { \ char buf[256]; \ sprintf_s(buf, [%s] HRESULT 0x%08X\n, msg, hr); \ OutputDebugStringA(buf); \ __debugbreak(); \ } #else #define D3D_CHECK(hr, msg) (hr) #endif然后在每个CreateDevice、CreateVertexBuffer、D3DXCreateTextureFromFile后面包一层D3D_CHECK(hr, CreateTexture)。这样一旦失败调试器会直接断在出错点输出窗口也能看到是哪个调用挂了。这个习惯是从这套源码里逼出来的——当年没有D3D_CHECK一个E_OUTOFMEMORY能查一下午。从那以后我每次接手老图形项目都强制先加一层 HRESULT 检查宏再开始改功能。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询