DirectX 教程第一部分:用于快速像素级绘制到 DirectX 表面的 DirectX 对话框模板






4.95/5 (9投票s)
2006 年 5 月 16 日
9分钟阅读

104430

2725
CDirectXDialog 是用于希望使用 DirectX 的对话框类的基类。
引言
这是我关于 DirectX 编程的文章系列的开篇:主题是“我们从哪里来?”。我将简要介绍一些在我看来 DirectX 的雏形可能还存在于盖茨先生工作台上的那个年代所使用的技术。这将使我们能更好地理解 DirectX 和现代图形硬件的奇妙之处。本教程共六部分,其中两部分将通过教程提供的代码给出如何使用的具体示例。
- DirectX 教程第一部分:DirectX 对话框模板
- DirectX 教程第二部分:在弹球游戏中使用的 CDirectXDialog 类
- DirectX 教程第三部分:基础 3D 数学
- DirectX 教程第四部分:在 Vectorballs 滚动器中使用 Vector 类
- DirectX 教程第五部分:从零开始的 Lambert 和 Gouraud 多边形填充器
- DirectX 教程第六部分:使用 Direct3D 的方式...
学完本教程后,您应该对 3D 数学、多边形填充器以及使用 DirectX 实现这些功能有多么容易有一个基本的了解。但首先,让我们通过一瞥 DirectX 快速访问表面内存的能力来开始我们的探索。这将使我们能够模拟 VGA 模式下的绘图到显存(还记得 0xA000 吗)。我们的 Lambert 和 Gouraud 多边形填充器将仅使用此功能来操作屏幕上的像素。
为了让您了解此像素例程的速度有多快,我决定计算一个数学图形(见上面的截图),该图形基于函数 f(x,y) = (x2+y2)2。如您所见,此函数关于两个轴都对称,因此我们只需计算一个四分之一的部分,然后将其值翻转到其他部分即可。但这并不能阻止我们绘制任何四分之一的所有像素。如果您在平均分辨率下最大化对话框,这意味着您需要绘制大约 780,000 个像素。在我的当前硬件配置(奔腾 IV,3GHz,...)下,这仅需大约 72 毫秒。顺便说一句,我大约在 15 年前(当时我拥有的第一台计算机是一台 Amiga,仅有 7.14MHz)就做过这件事。对于默认分辨率 320x256 = 81920 像素,打印整个图像大约需要 5 分钟(当时我使用 MC6800x0 汇编完成了所有编程)。对于硬件爱好者来说,让我们计算一下速度的提升:(780000 / 72ms) / (81920 / (5*60000ms)) = 39672.85!!!我认为这个值很惊人,不是吗?
编译要求
您需要下载并安装 Microsoft DirectX SDK 来编译本教程提供的示例项目。如果编译器找不到适当的 DirectX 头文件,请检查您的项目设置。您需要指定公共包含文件确切的路径。在我的特殊情况下,我将 DirectX SDK 安装在 C:\DXSDK。因此,我的项目设置的附加包含路径是 C:\DXSDK\Samples\C++\Common\Include。
DirectSurface 访问
IDirectDrawSurface7
接口提供了一种方法,用于获取 DirectX 表面内存以直接操作其像素。该方法称为 Lock
,您必须确保在完成到表面内存的绘图后释放锁定。相应的释放方法名为 Unlock
。当您的应用程序锁定表面内存时,任何其他 DirectX 单元都无法访问它,甚至图形硬件本身也不能。因此,在真实的 DirectX 应用程序中,您不应使用此方法,因为它会影响整体性能。但在我们的情况下,这不是很重要。最终,我们只对基于像素的直接绘图例程的直接实现感兴趣。
HRESULT Lock(LPRECT lpDestRect, LPDDSURFACEDESC2 lpDDSurfaceDesc, DWORD dwFlags, HANDLE hEvent ); HRESULT UnLock(LPRECT lpRect);
当您获得对表面内存的访问权限时,DirectX 运行时将在一个名为 DDSURFACEDESC2
的结构中返回有关该表面的信息。DirectX 表面的内存布局可能因您系统的显示设置而异。因此,我们将不得不仔细检查此结构,以了解哪些字节对应于屏幕上的哪个像素。表面内存可以分为可见区域和非可见区域。可见区域的宽度和高度由 DDSURFACEDESC2
结构中的 dwWidth
和 dwHeight
属性决定。我不太确定为什么会有非可见部分,但我猜测它与水平滚动有关。通过更改表面的起始地址,您可以操纵内存的哪个部分用于每个扫描行。效果是使非可见区域的像素可见,这看起来就像您在滚动表面一样。
现在,为了更改表面的像素,我们必须将屏幕坐标映射到表面内存中的位置。此映射可以通过一个线性数学函数来描述。
memory_position = start_address + x * bytes_per_pixel + y * bytes_per_line
每个像素根据您系统的显示设置在内存中占用一些位数。为了确定一个像素实际占用的字节数(变量 bytes_per_pixel
),我们可以查看 DDPIXELFORMAT
结构中的 dwRGBBitCount
属性。变量 bytes_per_line
对应于 DDSURFACEDESC2
结构中的 lPitch
属性。但是,计算特定像素的内存位置不足以设置正确的颜色。为了做到这一点,我们还必须检查哪些位属于哪个颜色通道。在 32 位模式下,实现非常直接。在这种情况下,您可以直接将 RGB 颜色放入表面内存,因为每个像素是四个字节宽。但在其他情况下,映射会更复杂一些。例如,在 16 位模式下,每个像素仅占用两个字节的表面内存。这意味着每个颜色通道的表示位都少于 8 位(确切地说是 5 位)。因此,我们将不得不屏蔽和移位我们的 RGB 值以匹配正确的像素格式。完成此任务所需的唯一信息是每个颜色通道的位掩码和位位置。对应的属性是 DDPIXELFORMAT
结构中的 dwRBitMask
、dwGBitMask
和 dwBBitMask
。
下表给出了 16 位模式下这些属性的值。如前所述,此模式下的掩码为 5 位宽,其中蓝色通道占据最低位,红色通道占据最高位。
颜色通道位掩码的值
Attribute | 值 |
---|---|
dwRBitMask |
01111100 00000000 |
dwGBitMask |
00000011 11100000 |
dwBBitMask |
00000000 00011111 |
为了确定位掩码中的位数,我们必须不断地将位掩码减一并应用逻辑 AND 操作。CDirectXDialog
类的以下操作执行此计算。
int CDirectXDialog::getNumberOfBits(DWORD mask) { int nob = 0; while (mask) { mask = mask & (mask - 1); nob++; } return nob; }
最后,可以通过不断地将位掩码与从右到左开始移位的变量位进行逻辑 AND 操作来识别位掩码的起始位。为完整起见,这里是确定此起始位位置的操作。
int CDirectXDialog::getBitMaskPosition(DWORD mask) { int pos = 0; while (!(mask & 1 << pos)) pos++; return pos; }
操作像素
DirectX 表面访问完全由 CDirectXDialog
类封装。当您想访问表面内存时,必须调用该类的 BackbufferLock()
操作。完成绘图后,您必须调用 BackbufferUnlock()
。成对调用这些操作至关重要,因此我决定将其设为私有。因此,您不能直接访问这些操作。您必须使用一个负责锁定和解锁表面的工作对象。这个工作对象类是 CDirectXDialog
的友元类,名为 CDirectXLockGuard
。
BackbufferLock()
中处理的第一个问题是 DirectX 表面的锁定。之后,检查表面以确定每个颜色通道的位掩码和位位置,以及俯仰值。CDirectXDialog
的实际 setPixel
操作可以通过成员函数指针访问。在 BackbufferLock()
中,根据当前设置的显示模式初始化此函数指针。用于在 DirectX 表面中设置像素的具体函数称为 CDirectXDialog::setPixelOPTIMIZED
和 CDirectXDialog::setPixelSECURE
。优化版本仅将 RGB 值复制到相应的内存位置。安全版本还会屏蔽和移位 RGB 颜色值以获得正确的像素格式,因此比优化版本慢得多。
inline void CDirectXDialog::setPixelOPTIMIZED(int x, int y, DWORD color) { *(unsigned int*)(backbuffervideodata + x*x_pitch + y*y_pitch) = color; } inline void CDirectXDialog::setPixelSECURE(int x, int y, DWORD color) { int offset = x*x_pitch + y*y_pitch; DWORD Pixel = *(LPDWORD)((DWORD)backbuffervideodata + offset); Pixel = (Pixel & ~ sDesc.ddpfPixelFormat.dwRBitMask) | ((RGB_GETRED(color) >> (8 - rbits)) << rpos); Pixel = (Pixel & ~ sDesc.ddpfPixelFormat.dwGBitMask) | ((RGB_GETGREEN(color) >> (8 - gbits)) << gpos); Pixel = (Pixel & ~ sDesc.ddpfPixelFormat.dwBBitMask) | ((RGB_GETBLUE(color) >> (8 - bbits)) << bpos); *(unsigned int*)(backbuffervideodata + offset) = Pixel; }
CDirectXDialog 类
CDirectXDialog
是一个抽象类。因此,您必须对其进行子类化并覆盖纯虚函数 displayFrame()
。每当对话框需要重绘时,框架都会调用此操作。本示例中的圆形图像是在 CDlgBackgroundArtDecoDlg
类的 displayFrame()
操作中绘制的。其他重要且可覆盖的操作包括:
initDirectDraw()
在对话框初始化时由框架调用。您可以在此处创建并初始化其他 DirectX 资源,例如其他表面。
restoreSurfaces()
当发生异常且 DirectX 绘图表面丢失时,由框架调用。您必须在此处重新创建和初始化您的 DirectX 资源。
freeDirectXResources()
当对话框将被销毁时,由框架调用。您可以在此处释放您创建的资源。
绘制图像
显示的图像在 CDlgBackgroundArtDecoDlg
类的 displayFrame()
操作中计算和绘制。我想比较 C++ 和纯汇编实现的性能优势。因此,您可以选择激活哪一个。如果定义了预处理器变量 IS_IT_WORTH_IT
,则绘图由 x86 汇编例程完成。否则,由 C++ 实现完成。顺便说一句,我使用成员函数指针从内联汇编代码片段中调用 setPixel
成员函数。如果您想了解更多关于此主题的信息,请参阅我的另一篇文章(如何从内联汇编代码段调用 C++ 成员操作)。
void CDlgBackgroundArtDecoDlg::displayFrame() { CRect rect; GetClientRect(rect); int width = rect.Width() / 2; int height = rect.Height() / 2; DWORD starttime,stoptime; starttime = GetTickCount(); { g_pDisplay.Clear(); CDirectXLockGuard lock(this); #if defined(IS_IT_WORTH_IT) setPixelPTR _setPixel = setPixel; int x, y, c, z = zoom; _asm { mov edx, width loop1: mov ebx, height loop2: mov eax, edx; //color = (x*x+y*y) imul eax, eax; mov ecx, ebx; imul ecx, ecx; add eax, ecx; imul eax, eax; //color = color*color; mov ecx, z; //zoom sar eax, cl; and eax, 0xFF; mov cl, al; and cl, 0x80; //if (color >= 128) color = color - 127; jz weiter; xor eax, 0x7F; weiter: shl eax, 8+1; //Backup mov x, edx; mov y, ebx; mov c, eax; push eax; //color mov eax, ebx; //y add eax, height; push eax; mov eax, edx; //x add eax, width; push eax; mov ecx, this; //this-call of member function pointer call _setPixel; mov eax, c; //color push eax; mov eax, y; //y add eax, height; push eax; mov eax, width; //x sub eax, x; push eax; mov ecx, this; //this-call of member function pointer call _setPixel; mov eax, c; //color push eax; mov eax, height;//y sub eax, y; push eax; mov eax, width; //x sub eax, x; push eax; mov ecx, this; //this-call of member function pointer call _setPixel; mov eax, c; //color push eax; mov eax, height;//y sub eax, y; push eax; mov eax, x; //x add eax, width; push eax; mov ecx, this; //this-call of member function pointer call _setPixel; //Reload mov edx, x; mov ebx, y; sub ebx, 1 jge loop2 sub edx, 1 jge loop1 } #else for (long x = 0; x < width; x++) for (long y = 0; y < height; y++) { long g = x*x + y*y; g = g * g; g = g >> zoom; g = g & 0xFf; if (g & 0x80) // if (g > 0x7f) g = 0x7f - g; g = 0x7f ^ g; g = g << 1; (this->*setPixel)(width + x, height + y,RGBA_MAKE(g,0,0,0)); (this->*setPixel)(width - x, height + y,RGBA_MAKE(0,g,0,0)); (this->*setPixel)(width + x, height - y,RGBA_MAKE(0,0,g,0)); (this->*setPixel)(width - x, height - y,RGBA_MAKE(g,g,g,0)); } #endif } stoptime = GetTickCount(); char buffer[128]; sprintf(buffer, "time: %4dms", stoptime - starttime); g_pTextSurface->DrawText(NULL, buffer, 0, 0, RGB(0,0,0), RGB(255,255,0)); g_pDisplay.Blt(20, 20, g_pTextSurface, NULL); CDialog::OnPaint(); }
还有什么要说的?
有些人可能会说,这篇文章有点令人困惑,因为一方面,我想解释比 DirectX 更早的技术,另一方面,我又在写关于 DirectX 的文章。但请让我向您保证,这些就是我们了解 DirectX 所需的全部内容。我使用 DirectX 的唯一原因是因为它速度快,并且我在这里提出的像素绘图例程与 DOS VGA 模式下的例程相对应。我本可以使用 Microsoft 的 GDI 来实现像素绘图例程,坦白说,我确实有一个基于 GDI 的版本。但它太慢了,无法创建快速的多边形填充器。因此,我认为这是一个不错的权衡。希望您仍然觉得这些文章有帮助且有趣。