简介:ID3DXSpriteTest1029是一份面向DSP编程与Visual C++开发者的Direct3D 2D图形渲染实战项目,聚焦ID3DXSprite接口的批量绘制与音频处理技巧,适用于游戏开发、实时可视化及图形界面设计等性能敏感场景。压缩包共43个文件,大小6.25MB,包含bmp位图资源、cpp/h源码、obj编译中间件、res资源文件、manifest配置文件以及vcproj/sln工程配置,结构完整,便于直接打开项目对照代码学习。项目详解设备创建、Sprite对象初始化与绘制状态设置,涵盖精灵的定位、旋转、缩放及颜色调整,并深入剖析批处理优化以减少API调用和GPU负载;同时涉及DirectSound缓冲区管理、音频加载与播放、混音及3D音效实现,助力开发者构建沉浸式音频体验。在Visual C++集成方面,展示了头文件引用、库链接、项目配置与错误调试等关键技巧。已有96人学习,对希望系统掌握2D渲染与音频编程的初中级开发者是一份实用且完整的参考资料。
1. ID3DXSpriteTest1029:老 VC++ 工程里的 2D 渲染样板,值得拆开看
程序员拿到这种资源包,第一反应往往是“一个测试工程,能有多大乾坤”,这个判断得翻过来看。ID3DXSpriteTest1029 是一个 Visual C++ 解决方案,核心代码围绕 Direct3D 的 ID3DXSprite 接口展开:初始化 Sprite 对象、批处理绘制、旋转缩放、Alpha 混色,同一套工程里还保留了 DirectSound 的演示段落,也就是资料上说的 DSP 编程。这个工程真正的价值不在代码量,而在能跑。把它编译起来、跑一遍绘制循环,比读十篇理论文章都管用。它适合三类人:想捡起 D3D9 时代 2D 渲染手艺的游戏开发者;做上位机界面又绕不开老 DirectX 的桌面开发者;以及正被“批处理、状态切换、设备丢失”这些名词卡住的新人。接口是老的,思路一点不过时。
2. ID3DXSprite 在渲染管线里的位置:批处理的底层逻辑
先别急着改代码。这个包虽然小,但它背后那条渲染链路值得先捋直。D3D9 时代画一张 2D 图,常规做法是做一个由两个三角形拼成的四边形,给顶点坐标和纹理坐标,然后绑纹理、提交图元。这套流程每走一遍,就多一次状态切换、多一次 API 调用。几十张图还好,到了几百张,CPU 就被这些调用消耗掉了,GPU 反而在等数据。
ID3DXSprite 做的事情是把这些四边形攒起来。你在 Begin 和 End 之间调用 Draw,Sprite 内部维护一个顶点缓冲和绘制队列,等 End 才真正提交图元。这意味着你从“每张图一次提交”变成“一批图一次提交”,API 调用次数直接降一个数量级。这就是批处理,它解决的是 CPU 侧的绘制开销。
还要先区分一个概念:D3DX 是 DirectX 9 SDK 里的扩展库,ID3DXSprite 只是里面负责 2D 精灵的组件。它不接管设备、不管窗口,只依赖一个已经创建好的 IDirect3DDevice9 指针。所以这个包里设备的创建一定在 Sprite 创建之前,这个顺序在代码里非常明确。
2.1 为什么 ID3DXSprite 的绘制方式比直接画四边形划算
直接用 D3D9 画一张图,大致是下面的流程:设置顶点格式、填充顶点缓冲、绑纹理、设置采样状态、DrawPrimitive。如果一百个精灵各做一遍,就是一百次顶点缓冲填充加一百次 DrawPrimitive。ID3DXSprite 的 Draw 不直接触发设备调用,它往内部的顶点数组里追加数据,End 时才提交。100 个精灵的顶点数据合并到同一个顶点缓冲里,一次 DrawIndexedPrimitive 就能画完。
更讲究的是 D3DXSPRITE_SORT_TEXTURE 标志。如果你在这一批里按纹理排序,Sprite 会先按纹理句柄把绘制顺序排好,减少纹理切换。纹理切换在 GPU 上并不便宜,尤其在旧显卡驱动下更明显。ID3DXSpriteTest1029 这个测试工程里 Begin 用的标志就是这一组,我在第 4 章会展开说每个标志的实际作用。这里只记一个结论:把所有绘制塞进尽量少的 Begin/End 块,是 2D 性能的第一个台阶。
两种绘制方式在调用层面差异很大,我给一个对比表,方便你评估这个包的价值:
| 绘制阶段 | 直接 D3D9 画四边形 | 用 ID3DXSprite |
|---|---|---|
| 每张图 | 设置顶点格式、填缓冲、绑纹理、提交 | 调用一次 Draw,内部追加顶点 |
| 纹理切换 | 每张图都可能切换 | 开启 SORT_TEXTURE 后按纹理排序 |
| 100 张图提交次数 | 至少 100 次 DrawPrimitive | 1 次(单次 End) |
| 状态管理 | 手动恢复 | Begin/End 包裹,内部处理好 |
这套机制放到今天看就是 2D 引擎里 SpriteBatch 的雏形。这个测试包规模小,正好把最原始的形态暴露给你:没有复杂的渲染队列、没有图集管理,就是一个 Sprite 对象加一个绘制循环。看懂了它,再去看别的引擎源码就不会觉得批处理是什么玄学。
2.2 从包里的文件结构看工程年代与开发习惯
把 rar 解开,能确认到的工程级文件是下面这几个,源码文件在解压后的工程子目录里。先列表看一下每个文件的用途:
| 文件 | 作用 | 需要留意的点 |
|---|---|---|
| ID3DXSpriteTest.sln | 解决方案文件,双击它打开工程 | 老工程换新 VS 版本时可能提示升级 |
| ID3DXSpriteTest.suo | 解决方案用户选项,记录断点与窗口布局 | 每个开发者保留自己的即可,不该进版本库 |
| ID3DXSpriteTest.ncb | IntelliSense 缓存,智能提示索引 | 删掉不影响编译 |
| ID3DXSpriteTest 工程目录 | 存放主源码与测试资源 | 阅读时优先定位主 cpp 文件 |
我拿到这种包有个固定动作:先把 .ncb 删掉再打开解决方案。这个东西只是一堆智能提示缓存,留着反而让第一次打开变慢,下次编译还会重新生成。.suo 同理,里面记录的是打开过的标签页、断点位置这些个人状态,和代码逻辑没有任何关系,时间戳再旧也不用担心。
你要理解的是 .sln 内部的项目配置:Debug/Release 两个配置、字符集设置、附加依赖项。老 Visual C++ 工程在字符集上用的多半是多字节,而新版本 VS 默认 Unicode,直接打开可能报些莫名其妙的字符串转换错误。这个细节第 3 章配置环境时会碰到,先有个心理预期。
3. 跑通第一步:VC++ 环境配置与 ID3DXSprite 初始化
如果你直接在 VS 里打开这个 sln,错误列表里可能立刻跳出一条“无法打开包含文件 d3dx9.h”。这太常见了,因为 D3DX 头文件不在 VC++ 默认搜索路径里。要先把 DirectX SDK 的 include 和 lib 路径挂到工程上,这是跑通的第一步,绕不开。
配置路径这件事,网上说法很多,但本质上就两步:告诉编译器到哪里找 d3dx9.h,告诉链接器到哪里找 d3dx9.lib。Visual C++ 环境里,这两个路径属于“VC++ 目录”配置。老版本 VS 在工具→选项→VC++ 目录里改,新版本 VS 在项目属性→VC++ 目录里改,二选一,效果等同。
3.1 头文件与库依赖:d3dx9.h 从哪来
先交代一个背景:d3d9.h 和 d3d9.lib 是系统 SDK 的一部分,Windows 自带;d3dx9.h 和 d3dx9.lib 属于 DirectX SDK,需要单独安装。所以系统里只有 Visual Studio、没有装 DirectX SDK 的话,报错就卡在 d3dx9.h 上。
配置方法是在项目属性里把 SDK 目录填进去:
- 包含目录:
C:\DXSDK\Include(按你实际安装盘符调整) - 库目录:
C:\DXSDK\Lib\x86
老工程的解决方案里一般也自带这些路径设置。如果打开后发现路径指向一个根本不存在的盘符,改回你自己安装路径即可。
这个测试工程属于典型的老 Direct3D 程序,源码顶部的头文件引用和库链接大致长这样:
// 老 Direct3D 工程的标配头文件集合 #include <windows.h> #include <d3d9.h> #include <d3dx9.h> // 用 pragma 声明链接依赖,省去工程属性里手动配置 #pragma comment(lib, "d3d9.lib") #pragma comment(lib, "d3dx9.lib") #pragma comment(lib, "winmm.lib")用#pragma comment是一种很常见的做法,好处是链接依赖跟着源码走,工程配置里配漏了也不会丢;坏处是它把依赖关系硬编码在源文件里,换用不同版本的库时不够灵活。我一般会坚持用 pragma,因为老工程重开几次后,工程属性里的配置太容易丢失,源码里的声明反而最稳。
d3dx9.lib 有一个容易踩的坑:它不只一个新版本。DirectX SDK 的每个发行版都带对应版本的 d3dx9.lib,装了两个版本 SDK 的话,搜索顺序会决定用到哪一个。这时候没必要纠结,保持路径里只放一个 SDK 的 include 和 lib,避免多个版本互相覆盖。
3.2 创建设备与 Sprite:先设备、后精灵、再纹理
ID3DXSprite 依赖一个现成的 IDirect3DDevice9,所以初始化顺序是死的:创建 D3D 对象、创建设备、创建 Sprite、加载纹理。这个工程里的初始化段代码非常标准,你把它抄出来当模板用都行:
LPDIRECT3D9 g_pD3D = NULL; LPDIRECT3DDEVICE9 g_pDevice = NULL; ID3DXSprite* g_pSprite = NULL; IDirect3DTexture9* g_pTexture = NULL; // 1. 创建 D3D9 接口对象 g_pD3D = Direct3DCreate9(D3D_SDK_VERSION); if (!g_pD3D) return E_FAIL; // 2. 填充设备参数 D3DPRESENT_PARAMETERS pp; ZeroMemory(&pp, sizeof(pp)); pp.Windowed = TRUE; pp.SwapEffect = D3DSWAPEFFECT_DISCARD; pp.BackBufferFormat = D3DFMT_UNKNOWN; pp.BackBufferCount = 1; // 3. 创建设备 g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &pp, &g_pDevice); // 4. 创建 Sprite 与纹理 D3DXCreateSprite(g_pDevice, &g_pSprite); D3DXCreateTextureFromFile(g_pDevice, L"sprite.png", &g_pTexture);这段代码里值得细看的有四个点。
Direct3DCreate9(D3D_SDK_VERSION)里的D3D_SDK_VERSION是版本检查,头文件版本和 DLL 版本对不上时返回 NULL。窗口模式下 BackBufferFormat 设为D3DFMT_UNKNOWN是允许的,系统会按显示模式自动匹配。D3DDEVTYPE_HAL表示使用硬件光栅化,这是最常规的取值。
D3DCREATE_SOFTWARE_VERTEXPROCESSING在 2D 精灵场景下几乎没有性能差别,但兼容性最好。老显卡驱动可能不支持硬件顶点处理,用软件顶点处理反而更稳。如果你的机器比较新,改成D3DCREATE_HARDWARE_VERTEXPROCESSING也能跑,这个测试工程两种都验过。
D3DXCreateTextureFromFile加载纹理的路径是相对当前工作目录的,不是相对 exe 所在目录。所以工程里如果改了输出目录,容易发生“代码里写了 sprite.png 却始终加载不到”的局面。加载失败时函数会返回一个有效值但不是 D3D_OK,日志里看不到,表现就是屏幕空白。
提示:D3D9 遗留路径对非 2 的幂纹理处理并不友好。测试素材尽量用 64x64、128x128 这类尺寸,避免出现采样边缘异常。
3.3 窗口与消息循环:老工程最常见的组织方式
这个测试工程没有用现成的游戏框架,就是最朴素的 Win32 窗口加消息循环。main 函数里做了三件事:注册窗口、初始化 D3D、进入消息循环并在空闲时调渲染。对这种单文件工程,我建议保持它原本的组织方式,别急着塞进类里,跑通了再改造。
你可能会注意到 WM_PAINT 里没有绘制代码,渲染全在消息循环的 PeekMessage 空闲分支里做。这是游戏工程和普通 GUI 程序最大的区别:普通程序是被动等待系统重绘,游戏程序是主动刷帧。如果你把消息循环改成了 GetMessage 阻塞式,窗口会卡在第一次绘制后不动,这是很容易翻车的地方。
4. Begin/End 之间:绘制调用、变换矩阵与颜色控制
初始化完成后就进入核心绘制循环。ID3DXSprite 的绘制分三层:Begin 把一批状态包起来,SetTransform 和 Draw 决定每张图长什么样,End 统一提交。很多新手在这一层乱了,把 SetTransform 放在 Begin 外,或者 Draw 脱离了 Begin 直接调用,就会出现渲染错乱。下面从 Draw 的参数一个个拆。
4.1 Draw 的五个参数:源矩形、中心、位置、颜色
这个工程里最频繁调用的是 ID3DXSprite::Draw,它的完整签名是五个参数:纹理、源矩形、中心点、位置、颜色。先看一段典型的绘制循环:
void RenderFrame() { g_pDevice->Clear(0, NULL, D3DCLEAR_TARGET, 0xFF000000, 1.0f, 0); g_pDevice->BeginScene(); g_pSprite->Begin(D3DXSPRITE_ALPHABLEND | D3DXSPRITE_SORT_TEXTURE); // 源矩形:从纹理上截取一块 RECT src = { 0, 0, 64, 64 }; D3DXVECTOR3 pos(100.0f, 50.0f, 0.0f); D3DCOLOR color = 0xFFFFFFFF; g_pSprite->Draw(g_pTexture, &src, NULL, &pos, color); g_pSprite->End(); g_pDevice->EndScene(); g_pDevice->Present(NULL, NULL, NULL, NULL); }Draw 的第二个参数是源矩形。整张图就传{0, 0, 宽, 高},做图集动画时,这就是一个格子。动画播放就是每帧改变src.left和src.top,这是精灵动画最基础的手段。
第三个参数是中心点,传 NULL 表示不做中心偏移,旋转缩放默认以纹理左上角为基准。传入D3DXVECTOR3(32.0f, 32.0f, 0.0f)就可以让旋转中心落在 64x64 纹素的正中央。
第四个参数是位置,用 D3DXVECTOR3 传,z 值在正交投影下通常为 0。第五个参数是颜色,0xFFFFFFFF 是完全不调制的白色;把它改成 0xFF0000FF,整张图会变成纯蓝色调。这个颜色值是按顶点执行的,所以通过它做闪烁效果非常便宜,不用换纹理。
D3DXSPRITE_ALPHABLEND标志让 Sprite 在绘制时启用 Alpha 混合,和颜色参数配合,可以让纹理的透明部分正常显示。如果去掉这个标志,PNG 里透明区域会变成黑色块。
4.2 旋转与缩放:D3DXMatrixTransformation2D 的六个参数
ID3DXSprite 本身不直接接受旋转角度和缩放比例,它只认变换矩阵。所以画一张旋转的精灵,要先构造矩阵再调用 SetTransform。这个工程里用的核心函数是 D3DXMatrixTransformation2D:
D3DXMATRIX mat; D3DXVECTOR2 scale(1.0f, 1.0f); D3DXVECTOR2 center(32.0f, 32.0f); float rotation = D3DXToRadian(90.0f); D3DXVECTOR2 position(200.0f, 100.0f); D3DXMatrixTransformation2D(&mat, NULL, // 缩放中心,NULL 表示以原点缩放 0.0f, // 缩放旋转角度,0 表示不做二次旋转 &scale, // X/Y 缩放比例 ¢er, // 旋转中心 rotation, // 旋转弧度 &position); // 平移量 g_pSprite->SetTransform(&mat); g_pSprite->Draw(g_pTexture, &src, NULL, NULL, 0xFFFFFFFF);这个函数的参数顺序不是按你直觉来的,它内部的变换顺序是:先平移缩放中心、再缩放、再旋转、再平移回旋转中心、最后做整体平移。所以 scale 和 center 的先后容易搞混。
最容易翻车的点就是旋转中心。如果 center 传 NULL,旋转以纹理左上角为轴,90 度转完,精灵会跑到你完全不期望的位置上。我一般会让 center 等于纹理宽高的一半,这样旋转时精灵是原地转,视觉上像是“以自身中心转动”。注意这里传给函数的弧度,用D3DXToRadian把角度转成弧度,直接传 90 进去是 90 弧度,转完都快两圈了。
SetTransform 的调用位置很重要:必须在 Begin 和 End 之间,且在 Draw 之前。同一批次里每张图可以有自己的矩阵,只要 Draw 之前 SetTransform 一次就行。矩阵会一直保留到下一次 SetTransform,所以连续绘制两张图时,第二张忘了设矩阵就会沿用第一张的旋转和缩放。这个状态的惯性效应在复制粘贴代码时很容易踩中。
4.3 DSP 段:DirectSound 缓冲区的创建与播放
资料里说的 DSP 编程,在这个包里对应的是 DirectSound 那段代码。先说明一个历史遗留问题:DSP 在多数技术语境里是数字信号处理,但老教程里确实有人用 DSP 缩写指代 Direct Sound Programming。这个包既然这么写了,就直接对应到 DirectSound 8 那套 API。
音频代码和图形代码相对独立,核心是创建缓冲区、填数据、播放。简化后的流程如下:
LPDIRECTSOUND8 pDSound = NULL; DirectSoundCreate8(NULL, &pDSound, NULL); // 协作级别告诉系统这个应用要不要独占声卡 pDSound->SetCooperativeLevel(hWnd, DSSCL_PRIORITY); DSBUFFERDESC dsbd; ZeroMemory(&dsbd, sizeof(dsbd)); dsbd.dwSize = sizeof(DSBUFFERDESC); dsbd.dwFlags = DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLVOLUME; WAVEFORMATEX wfx; wfx.wFormatTag = WAVE_FORMAT_PCM; wfx.nChannels = 1; wfx.nSamplesPerSec = 22050; wfx.wBitsPerSample = 16; wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8; wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; dsbd.lpwfxFormat = &wfx; dsbd.dwBufferBytes = wfx.nAvgBytesPerSec; // 1 秒长度 LPDIRECTSOUNDBUFFER pBuffer = NULL; pDSound->CreateSoundBuffer(&dsbd, &pBuffer, NULL); // 播放,DSBPLAY_LOOPING 表示循环 pBuffer->Play(0, 0, DSBPLAY_LOOPING);DSSCL_PRIORITY这个协作级别表示本应用可以改动主缓冲区格式,又不独占声卡。DSBCAPS_GLOBALFOCUS的作用很实用:窗口去焦点时声音继续播放,不暂停。DSBPLAY_LOOPING是循环播放标志,要播一次就传 0。
真正要往缓冲区里填 PCM 数据时,用Lock拿到两个指针再 memcpy,这是 DirectSound 最啰嗦的部分。测试工程里一般会省略,或者只生成一个很短的静音音频验证链路。你看包的时候留意一下有没有 Lock/Unlock 段,如果有,那就是一个完整的播放器雏形。音频代码在包里的位置相对独立,就算暂时跳过去也不影响图形部分的学习。
5. 避坑与常见问题:老 DirectX 工程最容易翻车的五个现场
从网上拿回来的老工程,能一次编译通过的少,翻车的多。凡是能让人卡一晚上的,基本逃不出下面五类。每条按“现象 → 原因 → 解决”来说,都是血泪经验。
5.1 双击程序秒退,连窗口都没看到
现象:编译成功,运行起来没有窗口,也没有任何错误提示。不是崩溃弹窗,是直接退出。
原因:两方面的可能。一是 D3D 设备初始化失败,比如窗口句柄为空,Direct3DCreate9返回了 NULL,代码里又没有做错误拦截,直接往下一步走就静默退出了。二是目标机器缺 VC++ 运行库,进程在启动阶段就挂了。
解决:先区分是哪一环。在Direct3DCreate9和CreateDevice后加返回值检查,打印到 OutputDebugString。如果是双击一个已经编译好的 exe 闪退,多半是运行库问题。老工程的运行库版本对不上系统时,表现非常典型。VC++ 6.0 时代的程序在 Win11 上闪退是重灾区:
| 编译器时代 | 涉及的运行库 | 典型症状 |
|---|---|---|
| VC++ 6.0(VS6) | msvcp60.dll、mfc42.dll | 老程序在 Win10/Win11 上无提示闪退 |
| VS2003 | msvcp71.dll | 程序启动即退出 |
| VS2005 | msvcp80.dll | 需要 2005 redistributable (x86) |
| VS2008 | msvcp90.dll | 需要 2008 redistributable (x86) |
这个包是 32 位工程,只在 x86 运行库层面排查即可。顺手把对应的 VC++ redistributable 装了,很多老游戏的“缺运行库”报错也能一并解决。比如电影大亨这类老单机游戏弹的运行时库缺失,和这个包面对的其实是同一类问题。
5.2 弹窗提示找不到 d3dx9_43.dll 或 d3dx9_30.dll
现象:程序能启动,但弹窗说“无法启动此程序,因为计算机中丢失 d3dx9_XX.dll”。
原因:d3dx9 系列 DLL 不是 Windows 系统自带的,它属于 DirectX 的最终用户运行库,且每个版本号对应一个独立的 DLL。_43 和 _30 的差别就是 SDK 发布时间的差别。
解决:安装对应年代的 DirectX 运行时库。注意区分:VC++ redistributable 负责解决 msvcpXX.dll,DirectX 运行库负责解决 d3dx9_XX.dll,两者经常被混为一谈,但补齐的是两套东西。调试阶段最简单的办法是把缺的那个 DLL 放到 exe 同目录,但那只是自用验证,发布时必须走完整安装包流程。
5.3 编译报错:无法打开包含文件 d3dx9.h
现象:打开 sln 后按 F7,错误列表出现 C1083。
原因:include 路径没设置,或者 DirectX SDK 安装后没重启 VS。另一种情况是系统里装了多个 SDK,老工程配置里写的路径已经失效,新找到的 sdk 里偏偏不含 D3DX 头文件。
解决:项目属性→VC++ 目录→包含目录,把 SDK 的 Include 路径放到最上面。库目录同理放到 Lib\x86。如果你发现 d3d9.h 能找到,只有 d3dx9.h 找不到,那基本可以确定是 D3DX 对应的 SDK 组件没装全。这是最稳的排查顺序:先确认安装,再确认路径,最后确认搜索次序。
5.4 精灵不显示:白屏、黑屏、画在角落
现象:程序能跑,窗口是主角色的背景,但精灵就是不出现。
原因:最常见的是 Begin/End 不匹配。Draw 调用了,但 End 没被调到,内部顶点缓冲一直没提交。另一个常见原因是 Draw 的坐标超出了视口范围,比如用了 SetTransform 之后整体平移到了窗口外。还有一种是图集切错了源矩形,画出来的内容不在可见区域。
解决:先清空逻辑,确认背景色能正常显示。然后在 Draw 之前临时写死位置为 (0, 0),这样能排除坐标问题。再检查 Begin 和 End 是否成对出现在同一个代码路径里。最后把源矩形改成整张纹理测试一次,如果显示了就不是纹理问题而是矩形参数问题。
5.5 退出时崩溃,或调试器报堆错误
现象:程序运行正常,关闭窗口那一刻崩了,有时候 IDE 会报访问冲突。
原因:释放顺序错了。ID3DXSprite 内部持有设备的顶点缓冲和状态块,你先释放了设备,Sprite 再释放时访问的是已经是无效的设备指针。COM 对象的释放顺序要严格与创建顺序相反。
解决:按创建的逆序释放,这是标准解法:
if (g_pTexture) { g_pTexture->Release(); g_pTexture = NULL; } if (g_pSprite) { g_pSprite->Release(); g_pSprite = NULL; } if (g_pDevice) { g_pDevice->Release(); g_pDevice = NULL; } if (g_pD3D) { g_pD3D->Release(); g_pD3D = NULL; }每个 COM 接口都要单独 Release,不能图省事只放设备。纹理、Sprite、设备、D3D 对象四步,顺序反过来就是创建顺序。如果不关注引用计数,Sprite 和纹理的泄漏在测试工程里看不出影响,但一放到长时间运行的软件里就会暴露。
6. 把测试工程改装成随身可用的精灵管理器:验证一次百张绘制
测试工程最值得改造的地方,就是把绘制循环里那串散落的 Draw 调用提成一个精灵批次类。不必引入引擎,二十行代码就够。这个改造能把包里的演示逻辑变成业务可直接调用的组件,也顺便验证批处理的实际收益。
6.1 精灵批次类:把 SetTransform 与 Draw 封装成两步
class SimpleSpriteBatch { public: void Begin(ID3DXSprite* s) { sp = s; sp->Begin(D3DXSPRITE_ALPHABLEND | D3DXSPRITE_SORT_TEXTURE); } void Draw(IDirect3DTexture9* tex, RECT* src, const D3DXVECTOR2& pos, const D3DXVECTOR2& scale, float rotation, D3DCOLOR color) { D3DXMATRIX m; D3DXMatrixTransformation2D(&m, NULL, 0.0f, &scale, NULL, rotation, &pos); sp->SetTransform(&m); sp->Draw(tex, src, NULL, NULL, color); } void End() { sp->End(); } private: ID3DXSprite* sp; };这个类的封装方式是“一次 Begin 后可以多次 Draw 再 End”,符合 ID3DXSprite 的批处理设计。旋转中心按 NULL 处理,如果你需要纹理中心旋转,把¢er再加一个参数传进来即可。类内部不持有任何 COM 指针,生命周期完全交给外部管理,避免上一章说的释放顺序问题。
改造后的调用方式非常直观:游戏循环里先batch.Begin,然后对每个精灵调用batch.Draw,最后batch.End。业务逻辑里不用再碰矩阵和状态标志,这是这个测试包最有价值的产出物。
6.2 验证方法:同样 500 次绘制,批处理的帧开销对比
改造完成后,可以用性能计数器验证批处理是不是真的有效。对比两组数据:一组是把 500 个精灵放在同一个 Begin/End 块里绘制,另一组是每个精灵单独 Begin/End。代码结构如下:
LARGE_INTEGER freq, t0, t1; QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&t0); // 500 次 Draw,只调用一次 End for (int i = 0; i < 500; ++i) { batch.Draw(tex, &src, pos, scale, rotation, color); } batch.End(); QueryPerformanceCounter(&t1); double ms = (double)(t1.QuadPart - t0.QuadPart) / freq.QuadPart * 1000.0;把 End 挪进循环里,就是那组对比样本。这个测试数据做出来,通常会差一个数量级。用这个数字去衡量,后面要不要做纹理图集合并、要不要继续减少状态切换就都有依据了,不用靠猜。
从那以后,我每次接手这种老 DirectX 工程都强制自己走一遍固定开头:先列运行库清单、再配 SDK 路径、最后跑通原始 Demo 再谈改造。这套习惯就是拆这类包拆出来的,少走了很多弯路。希望这个顺序也能帮到你。
本文还有配套的精品资源,点击获取