
简介这份「Begining DirectX9 随书源码」面向刚接触3D图形编程与游戏开发的初学者配合DirectX9入门教程使用通过可编译的示例帮助理解Direct3D渲染、输入处理与音频播放等核心概念。压缩包为rar格式共210个文件约1.16MB其中71个cpp源文件、34个头文件与39个vcproj项目文件、39个sln解决方案构成主要代码骨架并配有bmp纹理、x模型、rc资源脚本及wav音频等素材便于直接打开工程对照学习。已有127人学习说明其对入门读者具有一定参考价值。内容覆盖窗口与设备初始化、固定功能与可编程管线、纹理映射、光照与阴影、深度缓冲和视口变换、动画与帧率控制以及DirectInput、DirectSound的实际调用读者可沿示例拆解渲染流程、理解各组件协作方式并在此基础上完成课程实验或自学练习逐步建立从概念到可运行程序的实践能力。1. 这套随书源码值不值得翻从一份 2003 年的 D3D9 工程说起现在新项目基本不碰 D3D9 了但工业现场还留着一堆跑在 D3D9 上的老渲染代码、老工具和兼容层。Begining DirectX9 随书源码就是一份完整可编译的 D3D9 教学工程按章节切分从空窗口一路做到顶点缓冲、纹理、光照和 vs_2_0/ps_2_0 着色器。它不教你怎么写现代引擎但能让你把“设备创建—资源绑定—DrawPrimitive”这条链路一次走通。适合三类人要接手老 D3D9 代码的维护者、想理解固定管线到可编程管线过渡的渲染新手、拿它当课程设计脚手架的学生。源码是 C 加 Win32没有托管代码也没有第三方依赖除了 DirectX SDK 之外不需要再装别的东西。2. 把随书源码跑起来工程结构、工具链与第一个可编译窗口2.1 源码目录长什么样先看清再动手拿到压缩包先别急着双击 .sln。这套源码按章节编号分目录每一章一个独立工程工程内一般有 .cpp、.h、.rc以及一个放资源的子目录。不同章节的工程互相不引用各自能单独编译这一点对排查很友好——哪章编不过就单独修哪章不会牵连。先花两分钟扫一遍目录把下面几类文件认清楚后面改工程时能省很多事。文件类型作用常见问题.sln / .vcproj工程与解决方案文件老版本格式高版本 VS 打开会触发转换.cpp / .h章节示例代码部分用了 MBCS 字符集默认 Unicode 下报错.rc窗口资源、菜单、图标资源 ID 与代码里的常量要对得上.xDirectX 网格模型需要 D3DX 的网格加载接口路径写死居多.bmp / .dds纹理贴图尺寸不一定是 2 的幂采样状态要单独设.fx着色器效果文件profile 版本与设备能力要对齐资源路径是这套源码最容易翻车的地方。很多示例写的是相对路径比如D3DXCreateTextureFromFile(g_pDevice, media/brick.bmp, tex)而 VS 调试时的默认工作目录是工程目录不是输出目录。第一次跑起来画面全黑、纹理加载返回 D3DXERR_INVALIDDATA八成就是工作目录没对上。我的习惯是先把工程属性里的调试工作目录显式设成$(ProjectDir)再谈别的。2.2 工具链怎么配DirectX SDK、Windows SDK 与 VS 版本这套源码依赖的核心是 D3DX 工具库而 D3DX 从 Windows SDK 8 开始就不再随系统 SDK 分发了。也就是说光装 Visual Studio 和 Windows SDK 是编不过的必须额外装一份 DirectX SDKJune 2010 是最后一个带完整 D3DX 的版本。装完之后系统会写一个DXSDK_DIR环境变量工程属性里用这个变量拼 Include 和 Lib 路径换机器时不用改硬编码路径。配置顺序建议按下面走别跳步安装 DirectX SDK装完重启一次确认环境变量生效。在 VS 里新建一个属性表.props把 Include 目录设为$(DXSDK_DIR)IncludeLib 目录设为$(DXSDK_DIR)Lib\x8632 位工程。把这个属性表挂到每个章节工程上避免逐个项目改。平台工具集选当前 VS 的版本字符集先保持“使用多字节字符集”等能编过了再考虑改 Unicode。先跑一段命令确认 SDK 真的装对了比在 VS 里瞎猜快得多:: 检查 DirectX SDK 是否安装到位 if exist %DXSDK_DIR%Include\d3d9.h (echo D3D9 header OK) else (echo missing d3d9.h) if exist %DXSDK_DIR%Include\d3dx9.h (echo D3DX9 header OK) else (echo missing d3dx9.h) if exist %DXSDK_DIR%Lib\x86\d3dx9.lib (echo D3DX9 lib OK) else (echo missing d3dx9.lib)这三行分别验证头文件和库。d3d9.h在 Windows SDK 里也有所以它存在不代表 DirectX SDK 装好了真正要盯的是d3dx9.h和d3dx9.lib这两个才是 D3DX 的入口。如果第二条或第三条输出 missing说明要么 SDK 没装要么DXSDK_DIR指向了错误位置先去系统环境变量里核对不要在工程里硬写路径绕过去——硬写路径的工程换台机器就废。2.3 第一个能跑的空窗口WinMain 与消息循环书里最前面的示例是一个纯 Win32 窗口还没有创建设备。别小看这一步D3D9 的窗口过程、消息循环和普通 Win32 程序不完全一样渲染循环不能被GetMessage阻塞否则窗口一拖动画面就卡住。所以示例里用的是PeekMessage。// 最小 Win32 骨架先不创建设备只保证窗口能起来 #include windows.h LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int nCmdShow) { WNDCLASSA wc {0}; wc.style CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc WndProc; wc.hInstance hInst; wc.hCursor LoadCursor(NULL, IDC_ARROW); wc.lpszClassName D3D9BookWnd; RegisterClassA(wc); // 按客户区大小反推窗口大小否则 D3D 视口会被标题栏和边框吃掉 RECT rc {0, 0, 800, 600}; AdjustWindowRect(rc, WS_OVERLAPPEDWINDOW, FALSE); HWND hwnd CreateWindowA(D3D9BookWnd, D3D9 Sample, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInst, NULL); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); MSG msg {0}; while (msg.message ! WM_QUIT) { // 用 PeekMessage 而不是 GetMessage保证没有消息时也能继续渲染 if (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } } return (int)msg.wParam; }AdjustWindowRect这一行是新手最容易漏的。CreateWindow的宽高参数指的是整个窗口包括标题栏和边框如果直接把 800×600 传进去客户区实际只有七百多乘五百多D3D 设备按 800×600 创建后画面会被裁掉一圈。先用AdjustWindowRect把目标客户区尺寸换算成窗口尺寸再传给CreateWindow这一步做对后面视口才对得上。PeekMessage的最后一个参数用PM_REMOVE表示取到消息就从队列里移除。消息循环里没有消息时直接进入下一轮渲染代码就写在这个循环体里。这套骨架和书里后续所有示例一致先把窗口跑通再往里面加设备。2.4 把老工程迁到高版本 VS 的正确顺序直接用高版本 VS 打开 .vcproj会弹一个单向升级提示点完之后原工程文件就被改写了想退回去只能从压缩包重新解压。我的做法是先复制一份工程目录再升级原始目录留着做对照。迁移顺序按这个来先只升级工程格式、不改任何代码编译一次看报什么错再补 DirectX SDK 的 Include/Lib最后才动字符集和入口点。一次性把所有问题一起改出错时根本分不清是哪一步引入的。第一次编译大概率会看到两类错误一类是LNK2019找不到D3DXCreateTextureFromFileA之类的符号说明库没链上另一类是LNK2001找不到WinMain说明子系统或入口点设错了。这两类在第五章会展开这里先记住一个原则——编译错误从第一个开始修后面的很多错误是第一个错误的连锁反应。3. 拆开 D3D9 初始化链路设备、呈现参数与后备缓冲3.1 Direct3DCreate9 到 CreateDevice三段式初始化D3D9 的初始化可以拆成三段拿到IDirect3D9接口、填呈现参数、创建设备。三段之间任何一段失败后面都走不下去所以每一步都要判HRESULT不要图省事连着写。IDirect3D9* g_pD3D NULL; IDirect3DDevice9* g_pDevice NULL; bool InitD3D(HWND hwnd) { // 第一段拿 IDirect3D9参数必须是 D3D_SDK_VERSION g_pD3D Direct3DCreate9(D3D_SDK_VERSION); if (!g_pD3D) return false; // 第二段填呈现参数 D3DPRESENT_PARAMETERS pp {0}; pp.Windowed TRUE; pp.SwapEffect D3DSWAPEFFECT_DISCARD; // 窗口模式最稳 pp.BackBufferFormat D3DFMT_UNKNOWN; // 窗口模式必须 UNKNOWN pp.BackBufferWidth 800; pp.BackBufferHeight 600; pp.EnableAutoDepthStencil TRUE; pp.AutoDepthStencilFormat D3DFMT_D24S8; // 第三段创建设备硬件顶点处理失败时退回软件 HRESULT hr g_pD3D-CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, pp, g_pDevice); if (FAILED(hr)) { hr g_pD3D-CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, pp, g_pDevice); } return SUCCEEDED(hr); }Direct3DCreate9的参数只有一个但它必须是D3D_SDK_VERSION这个宏在编译时绑定到当前 SDK 版本。用旧 SDK 编出来的程序跑到新运行库上或者反过来都会返回 NULL。遇到Direct3DCreate9返回空第一反应应该是核对 SDK 版本而不是怀疑显卡。CreateDevice的六个参数里第三个是窗口句柄第四个是行为标志。D3DCREATE_HARDWARE_VERTEXPROCESSING表示顶点变换交给 GPU失败后我退回D3DCREATE_SOFTWARE_VERTEXPROCESSING让 CPU 做。这个回退不是可有可无的保险——老集成显卡、远程桌面会话、部分虚拟机里硬件顶点处理确实拿不到直接退出会让用户以为程序坏了。3.2 D3DPRESENT_PARAMETERS 每个字段怎么填这个结构体字段多但真正影响能否创建成功、画面是否正确的就那几个。下面这张表按窗口模式和全屏模式分开列方便对照。字段作用窗口模式典型值易错点Windowed是否窗口模式TRUE全屏时必须同时改 BackBufferFormatSwapEffect交换链行为D3DSWAPEFFECT_DISCARD用 COPY 在部分驱动上会掉帧BackBufferFormat后备缓冲格式D3DFMT_UNKNOWN窗口模式填具体格式会创建失败BackBufferWidth/Height后备缓冲尺寸800 / 600全屏时要与显示模式匹配EnableAutoDepthStencil自动深度模板TRUE关掉后要自己创建深度表面AutoDepthStencilFormat深度模板格式D3DFMT_D24S8部分老设备不支持 D24S8要降级PresentationInterval垂直同步D3DPRESENT_INTERVAL_IMMEDIATE改成 DEFAULT 会锁 60 帧MultiSampleType多重采样D3DMULTISAMPLE_NONE开之前先用 CheckDeviceMultiSampleType 探测窗口模式下BackBufferFormat必须填D3DFMT_UNKNOWN这是 D3D9 的硬约束填了具体格式直接返回D3DERR_INVALIDCALL。全屏模式反过来必须填一个当前显示模式支持的格式常见做法是先GetAdapterDisplayMode拿到桌面格式再原样填进去。PresentationInterval对帧率影响最直接。填D3DPRESENT_INTERVAL_IMMEDIATE是不锁帧渲染循环跑多快就多快填D3DPRESENT_INTERVAL_DEFAULT是跟随垂直同步一般锁在 60。做性能对比实验时用前者做正式演示时用后者否则画面会有撕裂。这一点书里示例没强调但实际调试时差别很大。3.3 设备丢失与 Reset全屏切换时的保命逻辑D3D9 设备不是创建完就一劳永逸的。全屏程序被 AltTab 切走、锁屏、分辨率切换都会导致设备丢失Present返回D3DERR_DEVICELOST。这时候继续调用绘制接口没用必须等TestCooperativeLevel返回D3DERR_DEVICENOTRESET后再Reset。void RenderFrame() { g_pDevice-Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xff202020, 1.0f, 0); g_pDevice-BeginScene(); // ... 这里放绘制调用 g_pDevice-EndScene(); HRESULT hr g_pDevice-Present(NULL, NULL, NULL, NULL); if (hr D3DERR_DEVICELOST) { // 设备丢了先看能不能重建 if (g_pDevice-TestCooperativeLevel() D3DERR_DEVICENOTRESET) { OnLostDevice(); // 释放 D3DPOOL_DEFAULT 资源 D3DPRESENT_PARAMETERS pp GetPresentParams(); g_pDevice-Reset(pp); // 重建 OnResetDevice(); // 重建资源 } return; // 本轮不画直接返回 } }关键在OnLostDevice和OnResetDevice这一对。D3DPOOL_DEFAULT池里的资源默认池的顶点缓冲、纹理、渲染目标在设备丢失时全部失效必须在Reset之前释放Reset之后重新创建。D3DPOOL_MANAGED池的资源由 D3D 自己管理不用手动重建。书里早期的示例大量用 MANAGED 池就是为了避开这个问题但一旦你开始用渲染到纹理、动态缓冲就必须自己处理丢失。顺序不能反先释放默认池资源再Reset再重建。反过来会在Reset时返回D3DERR_INVALIDCALL而且不告诉你具体是哪个资源没释放只能靠血泪经验一个个排。4. 顶点缓冲、索引与纹理把一章节的示例改成自己的场景4.1 FVF 与顶点结构体的对应关系D3D9 用 FVFFlexible Vertex Format描述顶点布局。FVF 是一组标志位的按位或标志的顺序必须和结构体里字段的顺序完全一致否则 GPU 会按错误的步长读数据画面要么错位要么全黑。// 位置 漫反射颜色 一组纹理坐标 struct CUSTOMVERTEX { float x, y, z; // 对应 D3DFVF_XYZ DWORD color; // 对应 D3DFVF_DIFFUSE float u, v; // 对应 D3DFVF_TEX1 }; // FVF 顺序必须与结构体字段顺序一致 #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)如果结构体里把 color 写在 xyz 前面FVF 也要跟着写成D3DFVF_DIFFUSE | D3DFVF_XYZ | D3DFVF_TEX1。这类错误编译器不会报运行时也不一定报表现就是画面颜色乱掉或者纹理坐标错位很难查。我一般会在创建缓冲前加一句sizeof(CUSTOMVERTEX)的静态断言把顶点大小钉死改结构体时能立刻发现步长变了。4.2 创建缓冲、填充数据、SetStreamSource 的完整顺序顶点缓冲的创建分三步CreateVertexBuffer分配、Lock拿指针写数据、Unlock交还。三步的顺序和参数都有讲究。bool CreateTriangle() { CUSTOMVERTEX verts[] { { -1.0f, -1.0f, 0.0f, 0xffff0000, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f, 0xff00ff00, 0.5f, 0.0f }, { 1.0f, -1.0f, 0.0f, 0xff0000ff, 1.0f, 1.0f }, }; HRESULT hr g_pDevice-CreateVertexBuffer( sizeof(verts), // 字节数不是顶点个数 0, // 用途标志静态缓冲填 0 D3DFVF_CUSTOMVERTEX, // FVF 要和结构体对得上 D3DPOOL_MANAGED, // 托管池设备丢失不用手动重建 g_pVB, NULL); if (FAILED(hr)) return false; void* pData NULL; hr g_pVB-Lock(0, sizeof(verts), pData, 0); if (FAILED(hr)) return false; memcpy(pData, verts, sizeof(verts)); g_pVB-Unlock(); return true; }CreateVertexBuffer的第一个参数是总字节数不是顶点数量新手在这里写3会得到一个只能装 3 字节的缓冲写数据时直接越界。第四个参数用D3DPOOL_MANAGEDD3D 会在显存和系统内存之间自动搬运设备丢失后不用手动重建。代价是每帧改数据时会有一次隐式拷贝动态几何体多的时候要换成D3DPOOL_DEFAULT加D3DUSAGE_DYNAMIC。绘制时的绑定顺序是固定的g_pDevice-SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX)); // stride 必须等于顶点大小 g_pDevice-SetFVF(D3DFVF_CUSTOMVERTEX); // 和创建时一致 g_pDevice-DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); // 从第 0 个顶点起画 1 个三角形SetStreamSource的最后一个参数是步长也就是每个顶点占多少字节。这个值和sizeof(CUSTOMVERTEX)不一致时GPU 读到的位置、颜色、纹理坐标全部错位画面会变成一堆乱飞的三角形。DrawPrimitive的第三个参数是图元数量三角形列表里一个图元是 3 个顶点所以画 1 个三角形传 1不是传 3。4.3 加一张纹理D3DXCreateTextureFromFile 与采样状态纹理加载用 D3DX 的一行调用就能完成但采样状态不设默认值在不同驱动上表现不一样最典型的是纹理边缘出现黑边或者整张图糊成一片。// 加载纹理注意路径相对于调试工作目录 HRESULT hr D3DXCreateTextureFromFileA(g_pDevice, media/brick.dds, g_pTexture); if (FAILED(hr)) return false; g_pDevice-SetTexture(0, g_pTexture); // 采样状态过滤方式 寻址方式不设会走驱动默认值 g_pDevice-SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); g_pDevice-SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); g_pDevice-SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR); g_pDevice-SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP); g_pDevice-SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);D3DXCreateTextureFromFileA用的是 ANSI 版本配套多字节字符集工程。工程改成 Unicode 之后要换成D3DXCreateTextureFromFileW否则编译报错。这也是第五章要讲的坑之一。纹理尺寸不要求是 2 的幂但非 2 的幂纹理在部分老设备上不支持重复寻址和 mipmapD3DXCreateTextureFromFile会自动帮你缩放或补齐代价是加载变慢、内存变大。如果美术给的图本身就是 2 的幂直接加载最省事如果不是先在资源管线里处理掉别留到运行时。4.4 索引缓冲从 6 个顶点到 4 个顶点画一个矩形用三角形列表要 6 个顶点其中两个是重复的。换成索引缓冲只要 4 个顶点加 6 个索引顶点变换的次数少三分之一。这套源码里从简单图元过渡到网格模型的章节基本都会切到索引缓冲。// 四个角逆时针顺序 CUSTOMVERTEX quad[] { { -1.0f, 1.0f, 0.0f, 0xffffffff, 0.0f, 0.0f }, { 1.0f, 1.0f, 0.0f, 0xffffffff, 1.0f, 0.0f }, { -1.0f, -1.0f, 0.0f, 0xffffffff, 0.0f, 1.0f }, { 1.0f, -1.0f, 0.0f, 0xffffffff, 1.0f, 1.0f }, }; WORD indices[] { 0, 1, 2, 2, 1, 3 }; // 两个三角形共用一条对角线 g_pDevice-CreateIndexBuffer(sizeof(indices), D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, g_pIB, NULL); void* pIdx NULL; g_pIB-Lock(0, sizeof(indices), pIdx, 0); memcpy(pIdx, indices, sizeof(indices)); g_pIB-Unlock();绘制改成DrawIndexedPrimitive六个参数依次是图元类型、基准顶点偏移、最小顶点索引、顶点数量、起始索引、图元数量g_pDevice-SetIndices(g_pIB); g_pDevice-DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, // BaseVertexIndex 0, // MinVertexIndex 4, // NumVertices参与绘制的顶点总数 0, // StartIndex从索引缓冲第几个索引开始 2); // PrimitiveCount2 个三角形NumVertices填错是常见问题。填成索引数量 6 不会报错但部分驱动会做越界检查直接丢弃整个 draw call画面什么都没有。这个参数的正确含义是“本次绘制涉及的顶点数量”不是索引数量记住这一点能省一半排查时间。5. 避坑与排查老 D3D9 工程在新工具链上的五类翻车现场5.1 找不到 d3dx9.hDirectX SDK 没装或路径没挂上现象编译报Cannot open include file: d3dx9.h但d3d9.h能正常包含。原因d3d9.h在 Windows SDK 里有d3dx9.h只在 DirectX SDK 里。VS 默认的包含路径只挂了 Windows SDK没挂 DirectX SDK。解决装 DirectX SDKJune 2010确认DXSDK_DIR环境变量存在然后在工程属性里把$(DXSDK_DIR)Include加到包含目录最前面。顺序很重要放在 Windows SDK 路径后面会被同名头文件抢先命中。5.2 dxerr9.lib 找不到库名在新 SDK 里改了现象链接报LNK1104: cannot open file dxerr9.lib。原因老版本 DirectX SDK 里错误处理库叫dxerr9.lib后来的版本统一改成dxerr.lib。书里的工程是按老版本写的。解决在链接器附加依赖项里把dxerr9.lib换成dxerr.lib如果代码里用了DXTrace之类的宏头文件也要从dxerr9.h改成dxerr.h。不想改的话直接把错误处理那几行注释掉也能编过但调试时会少一个定位手段。5.3 Unicode 改造后 MessageBox 报错字符集连锁反应现象把工程字符集从多字节改成 Unicode 后MessageBox、SetWindowText、D3DXCreateTextureFromFile全部报C2664无法转换参数。原因这些接口都有 A/W 两个版本工程字符集决定宏展开成哪个。代码里写的是窄字符串字面量Unicode 模式下就匹配不上。解决要么把字面量全部包一层_T(...)要么把接口显式写成MessageBoxA、D3DXCreateTextureFromFileA。我的建议是老工程先保持多字节字符集等所有功能跑通再统一改 Unicode别在排查渲染问题的同时叠加字符集问题。5.4 CreateDevice 一直失败但没提示调试运行库没装现象CreateDevice返回D3DERR_INVALIDCALL或D3DERR_NOTAVAILABLE但参数检查了一遍又一遍没发现问题。原因DirectX SDK 的调试运行库Debug Runtime没启用D3D 把详细的错误信息吞掉了只给你一个笼统的 HRESULT。解决在 DirectX SDK 的控制面板里把调试运行库切到 Debug 模式重新跑一遍输出窗口会打印具体是哪一项参数不合法。定位完之后记得切回 Retail调试运行库下帧率会掉得很厉害。5.5 纹理加载返回 D3DXERR_INVALIDDATA路径对了但文件读不到现象D3DXCreateTextureFromFile返回D3DXERR_INVALIDDATA确认文件存在、路径拼写没错。原因VS 调试时的默认工作目录是工程目录.vcproj 所在目录不是输出目录Debug/ 或 Release/。代码里的相对路径是相对工作目录解析的不是相对 exe 所在目录。解决工程属性 → 调试 → 工作目录填$(ProjectDir)或者把媒体文件拷到输出目录。前者改一次一劳永逸后者每换一次输出配置就要拷一遍。我一般用前者并在代码里加一句日志把GetCurrentDirectory打出来出问题时一眼就能看出工作目录对不对。6. 从示例到可用 Demo着色器版本、渲染循环与一帧的验证6.1 固定管线到可编程管线最小可运行的一对 vs/ps书里靠后的章节开始用着色器。DirectX 9 的着色器模型有 vs_1_1、vs_2_0、vs_3_0 几档随书源码主要用 2_0因为它在当时的主流显卡上覆盖最广。用 D3DX 编译着色器只需要一次调用比手写汇编指令省事得多。// 从文件编译顶点着色器入口函数名和 profile 必须与 .hlsl 里一致 ID3DXBuffer* pCode NULL; ID3DXBuffer* pError NULL; HRESULT hr D3DXCompileShaderFromFileA( shaders/basic.vs, // 文件路径同样受工作目录影响 NULL, // 宏定义数组没有就传 NULL NULL, // include 处理接口 VSMain, // 入口函数名 vs_2_0, // 编译目标 profile 0, // 编译标志 pCode, // 编译出的字节码 pError, // 错误信息失败时先看这个 NULL); if (FAILED(hr)) { // pError 里是带行号的编译错误比 HRESULT 有用得多 OutputDebugStringA((const char*)pError-GetBufferPointer()); return false; } g_pDevice-CreateVertexShader( (const DWORD*)pCode-GetBufferPointer(), g_pVS);pError这个输出参数一定要用。着色器编译失败时 HRESULT 只告诉你失败了pError里才有具体行号和原因比如某个语义写错、某个寄存器超限。我第一次调着色器时没看这个缓冲对着代码找了一下午后来养成习惯编译失败先把pError打到调试输出窗口。6.2 渲染循环的时序与一帧的验证渲染循环的顺序是固定的Clear清屏和深度、BeginScene、绑定资源、DrawPrimitive、EndScene、Present。顺序错了不会报错但画面会缺东西。比如Clear放在BeginScene之后部分驱动会直接忽略这次清除上一帧的残影就留在屏幕上。验证一帧到底画对没画对最直接的办法是把后备缓冲拷回系统内存读一个像素。不用装抓帧工具几行代码就能确认中心点是不是你期望的颜色。// 读回后备缓冲中心点的像素确认画面不是全黑 IDirect3DSurface9* pBack NULL; IDirect3DSurface9* pSys NULL; g_pDevice-GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, pBack); g_pDevice-CreateOffscreenPlainSurface(800, 600, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, pSys, NULL); g_pDevice-GetRenderTargetData(pBack, pSys); D3DLOCKED_RECT lr; pSys-LockRect(lr, NULL, D3DLOCK_READONLY); // 取 (400, 300) 这个点注意按行 pitch 偏移不是简单的 width * 4 DWORD pixel *(DWORD*)((BYTE*)lr.pBits lr.Pitch * 300 4 * 400); pSys-UnlockRect(); // pixel 形如 0xAARRGGBB和 Clear 传的颜色对比lr.Pitch是每一行的字节数可能大于width * 4驱动会在行尾做对齐。直接用width * 4算偏移读出来的像素是错的这个坑我在第一次写读回代码时就踩过读到的颜色和屏幕上的完全对不上还以为是渲染错了。GetRenderTargetData只能从默认池的渲染目标拷到系统内存池的表面两个表面的格式和尺寸必须一致否则返回D3DERR_INVALIDCALL。这套源码包里的示例按章节递进每一章只加一个概念适合当骨架改。我的习惯是每跑通一章就先把这一章的代码单独存一份能编译的副本等后面改崩了还能退回去对照。DirectX 9 这套东西现在资料散、驱动行为差异大能跑通的版本比什么都值钱。从那以后我每次碰老 D3D9 工程都先把工作目录、字符集、SDK 路径这三样固定死再动业务代码省下来的排查时间远比配置花的多。希望帮到你。本文还有配套的精品资源点击获取