65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (9投票s)

2006 年 5 月 16 日

9分钟阅读

viewsIcon

104430

downloadIcon

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 结构中的 dwWidthdwHeight 属性决定。我不太确定为什么会有非可见部分,但我猜测它与水平滚动有关。通过更改表面的起始地址,您可以操纵内存的哪个部分用于每个扫描行。效果是使非可见区域的像素可见,这看起来就像您在滚动表面一样。

现在,为了更改表面的像素,我们必须将屏幕坐标映射到表面内存中的位置。此映射可以通过一个线性数学函数来描述。

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 结构中的 dwRBitMaskdwGBitMaskdwBBitMask

下表给出了 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::setPixelOPTIMIZEDCDirectXDialog::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 的版本。但它太慢了,无法创建快速的多边形填充器。因此,我认为这是一个不错的权衡。希望您仍然觉得这些文章有帮助且有趣。

© . All rights reserved.