news 2026/10/24 2:34:02

DirectX 9随书源码拆解:D3D9渲染链路、避坑与二次开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DirectX 9随书源码拆解:D3D9渲染链路、避坑与二次开发

简介:这份 DirectX9 随书源码面向准备入门 DirectX9 与 3D 图形编程的初学者,配合教材章节提供可编译运行的示例,帮助读者从窗口创建、Direct3D 设备初始化一路理解渲染流程,解决只看理论难以落地的问题。压缩包共 210 个文件,约 1.16MB,以 cpp 源码、vcproj 工程与 sln 解决方案、h 头文件为主体,另含 bmp 纹理、x 模型及 wav 音频等配套素材,工程组织便于按章节对照实验。已有 127 人学习,说明其作为入门练习材料具备一定参考价值。内容覆盖基本几何绘制、固定功能与可编程管线、纹理映射、光照与阴影、深度缓冲和视口变换,也涉及动画与帧率控制、DirectInput 输入处理及 DirectSound 音频播放;读者可通过修改参数、跟踪调用与调试运行,把设备创建、资源管理、渲染循环和输入响应串成完整认识,为后续游戏逻辑与视觉特效实践打基础。

1. DirectX 9 随书源码:压缩包名字里少了个 n,但内容是完整的

解压之后第一个感觉是朴素:没有 CMake,没有包管理器,一个知识点一个文件夹,每个文件夹里躺着一份能单独编译的 .sln。这套 DirectX 9 随书源码的价值恰恰在于它不做任何封装——创建 D3D 设备、锁顶点缓冲、加载 .x 网格、轮询 DirectInput、Present 换缓冲,全部赤裸裸摊在你面前,想看哪一步就得自己写哪一步。适合三类人:图形学课程要交个小 Demo 的学生、接手了老 D3D9 项目想快速摸清渲染链路的开发者、以及想拿最原始的 API 去对照理解现代图形管线的人。下面按我拆包的顺序讲。

2. 源码包骨架:一个 Demo 从 WinMain 到 Present 的完整链路

先别急着编译,把目录翻一遍比直接点 F5 有用得多。这套示例的组织方式非常“教材”:知识点即目录,目录即工程,工程之间几乎不共享代码。理解了这个组织方式,你才知道哪些文件是用来抄的,哪些是用来扔的。

2.1 解压之后先看什么:目录结构与示例工程的分布

顶层的目录名基本对应章节顺序,越往后越接近完整的小引擎。每个示例目录里通常是这样几样东西:

路径作用打开频率
*.sln/*.vcproj独立工程文件,老版本格式,新 VS 打开会提示升级每次
main.cpp窗口创建、D3D 初始化、消息循环、Render 全在里面天天看
d3dutil.h/d3dutil.cpp各示例之间复制粘贴的公共函数,比如颜色转换、矩阵工具偶尔
media/贴图、.x模型、音效,示例的资源都在这里按需
*.fx后半部分的着色器示例才出现,早期章节没有中后期

有个细节值得注意:公共代码在示例之间是复制而不是引用。同一个InitD3D你能在十个工程里看到十个版本,参数还略有出入。别急着吐槽,这反而是好事——你想知道某个参数为什么要那么设,横向对比几个版本比翻文档快。我一般会先挑一个章节靠中间、功能最全的工程做基准,其他工程只当差异对照看。

还有一点:老工程的.vcproj里带的绝对路径可能指向别人机器上的 SDK 目录。用文本编辑器打开看一眼<ToolFile RelativePath>和AdditionalIncludeDirectories,能省掉半小时的“为什么头文件找不到”。

2.2 共用骨架:WinMain、消息循环和一帧的生命周期

所有示例共用一套骨架,差别只在Render()里画什么。这段骨架不长,但它是后面所有讨论的地基:

// main.cpp —— 每个示例都长这样,变的只是 Render 里的内容 #include <d3d9.h> #include <d3dx9.h> #pragma comment(lib, "d3d9.lib") #pragma comment(lib, "d3dx9.lib") IDirect3D9* g_pD3D = NULL; IDirect3DDevice9* g_pDevice = NULL; const UINT g_nWidth = 800; const UINT g_nHeight = 600; int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int nCmdShow) { // 1. 注册窗口类、CreateWindow,样板代码,各示例基本一致 // 2. 初始化 D3D 设备和输入设备 if (!InitD3D(hWnd)) return 0; // 3. PeekMessage 而不是 GetMessage:没消息时也要继续渲染 MSG msg = { 0 }; while (msg.message != WM_QUIT) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { Render(); // 空闲就画一帧,帧率不受消息驱动 } } Cleanup(); return (int)msg.wParam; }

PeekMessage这一行是整个骨架的关键。换成GetMessage,程序在没有任何消息输入时会阻塞,画面就停在第一帧不动了,新手上来最容易在这里卡住。Render()自己则严格遵守“Clear → BeginScene → 绘制 → EndScene → Present”的顺序:

void Render() { // 清颜色缓冲和深度缓冲,深蓝灰比纯黑好看,也方便判断是否真的清了 g_pDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(45, 45, 48), 1.0f, 0); if (SUCCEEDED(g_pDevice->BeginScene())) { // ... 本示例的绘制内容全部塞在这里 g_pDevice->EndScene(); } g_pDevice->Present(NULL, NULL, NULL, NULL); // 后缓冲换到前台 }

BeginScene/EndScene之间才能下绘制命令,把SetTexture或者DrawPrimitive写到EndScene后面,运行时不会崩,但屏幕上什么都没有——这种“静默失败”在 D3D9 里特别多,后面第 5 章专门说。Present的三个NULL参数分别对应源矩形、目标矩形、目标窗口,全默认表示整块后缓冲铺满整个客户区,绝大多数示例都用这个写法。

2.3 编译环境:VS 版本、DirectX SDK 和字符集的搭配

这套 DirectX 9 源码跑不起来的头号原因不是代码,是环境。d3d9.h在 Windows SDK 里一直有,但d3dx9.h、d3dx9.lib属于已经停止更新的 D3DX 库,只在最后一个独立发布的 DirectX SDK 里存在。Windows 10/11 的系统 SDK 中找不到它,编译直接就卡在#include <d3dx9.h>这一行。

常见做法是装完那个老版 DirectX SDK 之后,在工程属性的“VC++ 目录”里手动加两条,并且排在 Windows SDK 前面:

# 包含目录,注意顺序,放后面会被系统 SDK 里的同名头文件抢走 $(DXSDK_DIR)Include $(WindowsSDK_IncludePath) # 库目录,32 位工程用 x86,64 位用 x64 $(DXSDK_DIR)Lib\x86

$(DXSDK_DIR)是安装器写进系统环境变量的,装完必须重启 VS 才认。我自己踩过一次:装完直接点重新生成,一直报找不到d3dx9.lib,折腾半天发现是 VS 没重启。

字符集也得留意。示例工程大多是早期的多字节字符集(MBCS),代码里写D3DXLoadMeshFromX("media/skull.x", ...)传窄字符串。你把它升级到新 VS,默认字符集变成 Unicode,同一行代码就会报“无法从 const char[] 转换为 LPCWSTR”。改法有两个:工程属性里把字符集切回“使用多字节字符集”,或者老老实实给字符串加L前缀。前者省事,后者规范,我一般在新工程里选后者,老工程能不动就不动。

3. 渲染主链路拆解:设备创建、顶点缓冲、纹理与矩阵栈

骨架搭好之后,真正干活的是四件事:拿到设备、把顶点塞进显存、把贴图绑上去、把坐标算对。这四步在示例里被拆成了不同章节,实际写代码时是连在一起的,这里按执行顺序串一遍。

3.1 设备创建:D3DPRESENT_PARAMETERS 里每个字段都在管什么

IDirect3DDevice9是所有绘制命令的入口,创建它的过程就是把“我要一块什么样的后缓冲”讲清楚。示例里这段代码出现了几十次,注释却很少,值得逐字段过一遍:

bool InitD3D(HWND hWnd) { g_pD3D = Direct3DCreate9(D3D_SDK_VERSION); // 版本号必须传,否则返回 NULL if (!g_pD3D) return false; D3DPRESENT_PARAMETERS d3dpp = { 0 }; d3dpp.Windowed = TRUE; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // 换完就丢,别读后缓冲 d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; // 窗口模式交给系统挑 d3dpp.BackBufferWidth = g_nWidth; d3dpp.BackBufferHeight = g_nHeight; d3dpp.BackBufferCount = 1; // 双缓冲,够用 d3dpp.EnableAutoDepthStencil = TRUE; d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; // 24 位深度 + 8 位模板 d3dpp.hDeviceWindow = hWnd; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; // 不锁垂直同步 HRESULT hr = g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, // 主显卡 D3DDEVTYPE_HAL, // 硬件光栅化,软件是 REFERENCE hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, &d3dpp, &g_pDevice); return SUCCEEDED(hr); }

几个容易忽略的点。D3DCREATE_MULTITHREADED在多线程里调 D3D 会加锁,性能有损耗,示例加它是为了保险;自己写 Demo 单线程完全可以去掉。D3DDEVTYPE_HAL在没装显卡驱动的机器上会创建失败,排查时把这里换成D3DDEVTYPE_REF,软件光栅化能跑就说明代码没问题、是驱动的事。PresentationInterval设成D3DPRESENT_INTERVAL_IMMEDIATE会跑满帧、风扇狂转,想稳一点改成D3DPRESENT_INTERVAL_ONE跟显示器刷新率对齐。

窗口模式下把BackBufferWidth/Height设成 0 也行,D3D 会用客户区大小。但一旦你响应WM_SIZE去Reset设备,宽高就必须写真实值,否则下次切换全屏会拿到一个奇怪的分辨率。

3.2 顶点缓冲与 FVF:把一屏三角形塞进显存

D3D9 的顶点数据必须先放进IDirect3DVertexBuffer9,再用 FVF 告诉管线这段内存怎么解释。FVF 是个位掩码,D3DFVF_XYZ表示坐标是三个 float,D3DFVF_DIFFUSE表示带一个 32 位颜色,D3DFVF_TEX1表示一组纹理坐标。声明和结构体顺序必须严格对应,错一位,画面就是一堆乱飞的三角形:

struct CUSTOMVERTEX { float x, y, z; // 对应 D3DFVF_XYZ DWORD color; // 对应 D3DFVF_DIFFUSE float u, v; // 对应 D3DFVF_TEX1 }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1) CUSTOMVERTEX verts[] = { { -1.0f, -1.0f, 0.0f, 0xffffffff, 0.0f, 1.0f }, { -1.0f, 1.0f, 0.0f, 0xffffffff, 0.0f, 0.0f }, { 1.0f, -1.0f, 0.0f, 0xffffffff, 1.0f, 1.0f }, { 1.0f, 1.0f, 0.0f, 0xffffffff, 1.0f, 0.0f }, }; // 创建后立刻填充,D3DPOOL_MANAGED 让 D3D 自己管显存和内存的同步 g_pDevice->CreateVertexBuffer(sizeof(verts), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_MANAGED, &pVB, NULL); void* pData = NULL; pVB->Lock(0, sizeof(verts), &pData, 0); // 最后一个参数 0 表示不关心旧内容 memcpy(pData, verts, sizeof(verts)); pVB->Unlock();

D3DPOOL_MANAGED是示例里最常见的选法,省心:设备丢失后不用手动重建。代价是每次 Lock 都可能触发一次内部拷贝,顶点要每帧更新的话就换成D3DPOOL_DEFAULT加D3DUSAGE_DYNAMIC,并且Lock时传D3DLOCK_DISCARD,让驱动直接给你一块新的写指针,避免等 GPU 读完旧数据。这是性能优化的第一个分水岭,静态模型用 Managed,粒子、UI 这种每帧变的用 Dynamic。

绘制本身只有三行,但顺序不能反:

g_pDevice->SetStreamSource(0, pVB, 0, sizeof(CUSTOMVERTEX)); // 数据源和步长 g_pDevice->SetFVF(D3DFVF_CUSTOMVERTEX); // 怎么解释这段数据 g_pDevice->SetTexture(0, pTex); // 采样器 0 绑贴图 g_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // 4 个顶点 = 2 个三角形

DrawPrimitive的第三个参数是图元个数不是顶点数,三角带里 N 个三角形对应 N+2 个顶点,写错就是画一半或者越界。用D3DPT_TRIANGLELIST时更直观,三个顶点一个三角形,但顶点重复多,示例里两种都有。

3.3 纹理加载与采样状态:为什么贴图会糊、会花

示例里加载贴图基本都用D3DXCreateTextureFromFile的扩展版本,因为它能一次性把 mipmap、格式转换、缩放全办了:

IDirect3DTexture9* pTex = NULL; D3DXCreateTextureFromFileEx( g_pDevice, L"media/crate.jpg", D3DX_DEFAULT, D3DX_DEFAULT, // 宽高,DEFAULT 表示保持原图 D3DX_DEFAULT, // mip 级数,DEFAULT 生成完整 mip 链 0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED, D3DX_FILTER_TRIANGLE | D3DX_FILTER_MIRROR, // 缩放滤镜 D3DX_FILTER_TRIANGLE | D3DX_FILTER_MIRROR, // mip 生成滤镜 0, NULL, NULL, &pTex);

加载成功不代表显示正确,采样状态得自己设。没设的话默认是D3DTEXF_POINT,靠近了看是一格一格的马赛克:

g_pDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); // 缩小时 g_pDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); // 放大时 g_pDevice->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR); // mip 之间 g_pDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP); // 越界平铺 g_pDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);

纹理坐标超过 [0,1] 时怎么取,由ADDRESSU/V决定。WRAP是平铺,CLAMP是拉伸边缘,MIRROR是镜像。画地面砖块用 WRAP,画 UI 图标一定要用 CLAMP,否则边缘会采样到对面去,出现一条莫名其妙的接缝。

带透明通道的贴图还要开混合,而且要注意绘制顺序:

g_pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); g_pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); g_pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); // 半透明物体默认不写深度,否则会挡住后面的东西 g_pDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);

透明物体按到相机的距离从远到近画,这是固定管线的老规矩,示例里有的章节做了排序,有的没做——没做的那个 Demo 从某个角度看就是花的,不是你的问题。

3.4 世界/视图/投影矩阵与固定管线光照

三个矩阵决定顶点最终落在屏幕哪里。世界矩阵管模型自己怎么摆,视图矩阵等价于相机,投影矩阵决定 FOV 和远近裁剪面:

D3DXMATRIX matWorld, matRot, matTrans, matView, matProj; D3DXMatrixRotationY(&matRot, GetTickCount() / 1000.0f); // 绕 Y 轴匀速转 D3DXMatrixTranslation(&matTrans, 0.0f, 0.0f, 5.0f); // 推到相机前方 D3DXMatrixMultiply(&matWorld, &matRot, &matTrans); // 先转再推 D3DXMatrixLookAtLH(&matView, &D3DXVECTOR3(0.0f, 2.0f, -8.0f), // 相机位置 &D3DXVECTOR3(0.0f, 0.0f, 0.0f), // 看向原点 &D3DXVECTOR3(0.0f, 1.0f, 0.0f)); // 上方向 D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4.0f, (float)g_nWidth / g_nHeight, 1.0f, 1000.0f); g_pDevice->SetTransform(D3DTS_WORLD, &matWorld); g_pDevice->SetTransform(D3DTS_VIEW, &matView); g_pDevice->SetTransform(D3DTS_PROJECTION, &matProj);

矩阵乘法顺序是新手第一道坎。D3D 用行向量,顶点变换写成v * M,所以“先旋转再平移”对应matRot * matTrans,而不是反过来。写反的表现是模型绕着远处一个点公转,而不是自转。

光照部分,示例用的是固定管线的D3DLIGHT9:

D3DLIGHT9 light = { 0 }; light.Type = D3DLIGHT_DIRECTIONAL; light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Direction = D3DXVECTOR3(0.5f, -1.0f, 0.3f); D3DXVec3Normalize((D3DXVECTOR3*)&light.Direction, (D3DXVECTOR3*)&light.Direction); // 方向必须归一化 g_pDevice->SetLight(0, &light); g_pDevice->LightEnable(0, TRUE); g_pDevice->SetRenderState(D3DRS_LIGHTING, TRUE); g_pDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(60, 60, 60));

开了D3DRS_LIGHTING之后,顶点必须带法线,FVF 里要加D3DFVF_NORMAL,结构体里补三个 float。忘了加的表现是整个模型一片死黑,因为法线被当成 0 向量,光照算出来就是零。这个坑太常见了,先记住结论:要么关光照用纯色顶点,要么带法线,没有中间态。

4. 交互与资源层:DirectInput 轮询、.x 网格与释放顺序

渲染跑通之后,示例开始往里加“活的东西”:能按键操作、能加载现成模型、能放声音。这部分代码量不大,但资源管理的坏习惯基本都是在这些章节里养成的。

4.1 键盘鼠标的轮询式输入

DirectInput 的用法是固定套路:创建IDirectInput8,创建键盘设备,设数据格式和协作级别,然后每帧轮询。注意是轮询,不是靠 Windows 消息,所以窗口失去焦点时它不会自动停:

IDirectInput8* g_pDI = NULL; IDirectInputDevice8* g_pKeyboard = NULL; BYTE g_keys[256] = { 0 }; bool InitInput(HWND hWnd) { if (FAILED(DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&g_pDI, NULL))) return false; g_pDI->CreateDevice(GUID_SysKeyboard, &g_pKeyboard, NULL); g_pKeyboard->SetDataFormat(&c_dfDIKeyboard); // 标准 256 字节数组 g_pKeyboard->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); // 前台、非独占 g_pKeyboard->Acquire(); // 不 Acquire 读不到数据 return true; } void PollInput() { if (FAILED(g_pKeyboard->GetDeviceState(sizeof(g_keys), g_keys))) { g_pKeyboard->Acquire(); // Alt+Tab 回来必须重新抢,否则永远读失败 return; } if (g_keys[DIK_ESCAPE] & 0x80) PostQuitMessage(0); // 只取最高位,第 7 位为 1 表示按下 }

g_keys[DIK_ESCAPE] & 0x80这个写法很多人第一次见会懵。DirectInput 返回的每个字节只有最高位有效,表示按键当前是否按下,低 7 位是设备相关的,不要拿去判断。

DISCL_NONEXCLUSIVE表示不独占键盘,切出去别的程序还能用。想读手柄或者需要独占的鼠标,改成DISCL_EXCLUSIVE,但那样 Alt+Tab 切走时会丢焦点,恢复时要重新Acquire,示例里一般只在WM_ACTIVATE里处理一下。

4.2 .x 网格加载与多材质子集绘制

.x是 D3D9 时代的模型格式,示例后半段几乎全靠它。加载函数一次返回三样东西:网格本体、材质数组、贴图文件名数组,材质和子集是一一对应的:

ID3DXMesh* pMesh = NULL; ID3DXBuffer* pMatBuf = NULL; DWORD dwNumMat = 0; D3DMATERIAL9* pMats = NULL; IDirect3DTexture9** ppTex = NULL; // ppEffectInstances 是给 .x 里内嵌 effect 用的,普通模型传 NULL D3DXLoadMeshFromX(L"media/skull.x", D3DXMESH_MANAGED, g_pDevice, NULL, &pMatBuf, NULL, &dwNumMat, &pMesh); pMats = (D3DMATERIAL9*)pMatBuf->GetBufferPointer(); ppTex = new IDirect3DTexture9*[dwNumMat]; D3DXMATERIAL* pMat = (D3DXMATERIAL*)pMatBuf->GetBufferPointer(); for (DWORD i = 0; i < dwNumMat; ++i) { ppTex[i] = NULL; if (pMat[i].pTextureFilename) // 有的材质没贴图,必须判空 D3DXCreateTextureFromFileA(g_pDevice, pMat[i].pTextureFilename, &ppTex[i]); }

绘制时按子集循环,每个子集一套材质和贴图:

for (DWORD i = 0; i < dwNumMat; ++i) { g_pDevice->SetMaterial(&pMats[i]); g_pDevice->SetTexture(0, ppTex[i]); pMesh->DrawSubset(i); // 子集编号就是材质下标 }

有个必须知道的坑:D3DXLoadMeshFromX是宏,会根据工程的字符集映射到...FromXA或...FromXW。你传了L"..."但工程是多字节字符集,编译直接报错;反过来传窄字符串但工程是 Unicode,同样报错。要么统一字符集,要么显式写FromXA/FromXW,别赌。

另外.x里的贴图路径是相对路径,程序的工作目录不是工程目录时就会加载失败,ppTex[i]静默变成NULL,模型显示成白色但没有报错。我自己习惯在加载后打一行日志把失败次数打出来,省得靠眼睛猜。

4.3 资源释放顺序与设备丢失恢复

示例里很多小工程的Cleanup是空的,靠进程退出兜底。真拿去做二次开发就必须补上,COM 对象忘了Release就是实打实的内存泄漏:

void Cleanup() { // 顺序:先子后父,先设备资源后设备本身 if (pMesh) { pMesh->Release(); pMesh = NULL; } for (DWORD i = 0; i < dwNumMat; ++i) if (ppTex[i]) { ppTex[i]->Release(); ppTex[i] = NULL; } if (pVB) { pVB->Release(); pVB = NULL; } if (pTex) { pTex->Release(); pTex = NULL; } if (g_pKeyboard) { g_pKeyboard->Unacquire(); g_pKeyboard->Release(); g_pKeyboard = NULL; } if (g_pDI) { g_pDI->Release(); g_pDI = NULL; } if (g_pDevice){ g_pDevice->Release(); g_pDevice = NULL; } // 设备在资源之后 if (g_pD3D) { g_pD3D->Release(); g_pD3D = NULL; } // D3D 最后 }

比释放更难缠的是设备丢失。全屏状态下按 Alt+Tab、锁屏、换分辨率,Present会返回D3DERR_DEVICELOST,之后所有绘制调用都无效:

HRESULT hr = g_pDevice->Present(NULL, NULL, NULL, NULL); if (hr == D3DERR_DEVICELOST) { // 必须等系统允许才能 Reset,抢跑会一直失败 if (g_pDevice->TestCooperativeLevel() == D3DERR_DEVICENOTRESET) { ReleaseDefaultPoolResources(); // 释放 D3DPOOL_DEFAULT 的资源 g_pDevice->Reset(&g_d3dpp); // 参数和创建时一致 RestoreDefaultPoolResources(); // 重建,包括顶点缓冲、贴图、字体 } return; // 本帧直接跳过,不要继续画 }

D3DPOOL_MANAGED的资源不用管,Reset之后还在;D3DPOOL_DEFAULT的必须手动释放重建,这是最容易漏的地方——恢复回来画面正常,但字体、RenderTarget 全没了,程序卡在某个NULL指针上。窗口模式下这个问题少见,全屏 Demo 一定要处理。

5. 避坑与排查:五个把示例跑起来时最容易翻车的地方

这套源码我前后在两个机器上编译过,下面五条是最花时间的,每条都按“现象 → 原因 → 解决”记下来,遇到同类问题可以直接对号入座。

5.1 编译链接期:库找不到、字符集对不上

第一条:无法打开源文件 d3dx9.h或者链接时报LNK1104 无法打开文件 d3dx9.lib。原因不复杂,就是没装那个独立发布的 DirectX SDK,或者装了但 VS 里的包含目录/库目录没配、配了但顺序在 Windows SDK 之后被覆盖。解决方法是先确认DXSDK_DIR这个环境变量在命令行里echo得出来,然后在工程属性里把$(DXSDK_DIR)Include和$(DXSDK_DIR)Lib\x86加到最前面,再重启 VS。64 位工程记得换x64那一档,不换的话链接期会报一堆“模块计算机类型不匹配”。

第二条:无法从 "const char [N]" 转换为 "LPCWSTR",一编译就是几十个错。原因是工程被升级到新 VS 之后字符集从 MBCS 变成了 Unicode,而示例代码里到处是窄字符串字面量。解决方式二选一:工程属性 → 高级 → 字符集改成“使用多字节字符集”,改完立刻能编;或者全局给字符串加L前缀,同时把D3DXLoadMeshFromX这类宏显式写成...FromXW。我建议老工程用前者、新写的代码用后者,混着来最难受。

5.2 运行期:黑屏、D3DERR_INVALIDCALL 与 Alt+Tab

第三条:编译通过,运行起来一个纯黑窗口,什么都不显示。这是最高频的一类,原因有好几种,按概率排:绘制命令写在了BeginScene/EndScene之外;Clear的颜色被写成了0x00000000而且确实没画东西;相机位置在模型内部或者matProj的远裁剪面比模型距离还小。排查手法很土但有效:把Clear的颜色改成一个很扎眼的颜色(比如亮洋红),先确认清屏生效了,再在EndScene前加一行DrawPrimitive画个固定位置的全屏三角形。三角形出得来,问题在矩阵;出不来,问题在状态或者渲染顺序。

第四条:某次改动之后程序静默退出,或者CreateDevice返回D3DERR_INVALIDCALL。九成是D3DPRESENT_PARAMETERS里有字段不合法:窗口模式下BackBufferFormat写了具体格式而不是D3DFMT_UNKNOWN,全屏模式下宽高或刷新率显卡不支持,或者MultiSampleType开了但硬件不支持。最省事的做法是别靠猜,先用IDirect3D9::CheckDeviceFormat和CheckDeviceMultiSampleType问一遍,或者在创建失败时把D3DPRESENT_PARAMETERS的所有字段打到日志里逐个比对。我自己的习惯是写一个ValidatePresentParams(),创建前跑一遍,比事后翻日志快得多。

5.3 画面类:模型透明、贴图发花

第五条:加了透明贴图之后,物体背后的东西看不见了,或者从某些角度看穿帮。原因是开了D3DRS_ALPHABLENDENABLE但没关D3DRS_ZWRITEENABLE,半透明像素照样写深度,后画的物体就被挡住;另一种情况是开了混合但没按距离排序,近的先画、远的后画,混合结果就反了。解决办法是先关深度写入,把场景里的不透明物体照常画完,再把所有半透明物体按到相机的距离从远到近排一次序,逐个绘制。示例里有的章节做了这一步,有的图省事没做,你拿去做正式 Demo 之前一定补上,否则换个视角就是另一幅画面。

6. 二次开发:把示例骨架改成你自己的 Demo

这套 DirectX 9 源码拿来直接用意义不大,它的价值是当积木。我一般会做两件事。

6.1 抽一个 MiniRenderer 出来

把InitD3D、Render的骨架、Cleanup、设备丢失处理抽成一个MiniRenderer类,示例目录里的东西只保留绘制逻辑。这样换个示例只需要改一个OnRender()虚函数,不用再复制一遍窗口创建代码。顺手把资源包装一层 RAII,Release写进析构,能挡掉一大半泄漏:

class MiniRenderer { public: bool Init(HWND hWnd, UINT w, UINT h); void Frame(); // Clear + BeginScene + OnRender + EndScene + Present void Shutdown(); protected: virtual void OnRender() = 0; // 子类只关心画什么 virtual void OnLostDevice() {} // D3DPOOL_DEFAULT 资源在这里释放 virtual void OnResetDevice() {} // 以及在这里重建 IDirect3DDevice9* m_pDevice = nullptr; };

6.2 改完之后怎么验证没改坏

渲染问题最怕“看起来对”。我的验证流程是三步:先看Clear色是否稳定、帧率是否锁在预期值;再用抓帧工具截一帧,确认 Draw Call 数量和顶点数跟预期一致,少一次多一次都能看出来;最后把窗口拖到不同尺寸、按 Alt+Enter 切全屏再切回来,专门测设备丢失路径。

有个血泪教训:早期我改完不测 Alt+Tab,交付前一天演示时切了个窗口,回来整个场景全白,字体和 RenderTarget 全丢。从那以后我每次动设备相关的代码,都强制走一遍“切全屏 → 失焦 → 恢复 → 再切窗口”的流程,哪怕只改了一行Reset参数。这套 DirectX 9 源码里的示例虽然老,但把这条链路补齐之后,拿它做课程设计或者图形学实验的底子是完全够用的。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 2:33:31

procamcalib:投影仪-相机联合标定的工程化实践

简介&#xff1a;这是一套面向计算机视觉与三维重建研究者的Java开源工具包&#xff0c;专为投影仪-相机&#xff08;ProCam&#xff09;系统几何与颜色联合校准设计&#xff0c;兼顾多相机多投影仪配置需求&#xff0c;也支持纯相机阵列标定&#xff0c;适合具备基础图像处理知…

作者头像 李华
网站建设 2026/10/24 2:33:29

DevC++贪吃蛇小游戏实战:主循环、字符渲染与输入避坑

简介&#xff1a;这份面向 C 初学者、课程设计练习者与游戏开发入门者的 DevC 贪吃蛇小游戏源码包&#xff0c;围绕控制台游戏开发提供可编译运行的完整示例&#xff0c;帮助读者把结构体、数组、条件判断、循环和键盘输入等语法落实到具体项目中。压缩包共 9 个文件&#xff0…

作者头像 李华
网站建设 2026/10/24 2:33:27

12306抢票脚本原理:HTTP协议级购票链路解析

简介&#xff1a;这是一份面向Python初学者与自动化需求用户的12306火车票自动抢购脚本资源&#xff0c;聚焦节日期间&#xff08;如中秋、国庆、春节&#xff09;返乡购票难痛点&#xff0c;提供轻量级电脑端自动化解决方案。资源压缩包为RAR格式&#xff0c;共8个文件&#x…

作者头像 李华
网站建设 2026/10/24 2:32:44

论文降重实用方法汇总 高效合规完成重复率优化的实操指南

对于研究生来说&#xff0c;查文献、读论文、做实验和写综述往往需要投入大量时间。现在&#xff0c;AI工具可以辅助完成资料检索、长文本阅读、代码分析和内容整理。不同工具适合不同场景&#xff0c;合理搭配使用&#xff0c;能够减少重复劳动&#xff0c;提高科研效率。 **…

作者头像 李华
网站建设 2026/10/24 2:32:24

少样本微调该动哪几层?探针定位 × 测试时训练的对照实验

实验目标大模型少样本学新东西&#xff0c;现在有两条热门路线各走各的路&#xff1a;一条是测试时训练&#xff08;Test-Time Training&#xff0c;TTT&#xff09;——推理时拿几个示例做几步梯度更新&#xff0c;让知识真正进权重&#xff1b;另一条是定位选层——用探针或激…

作者头像 李华