初次接触:DirectDraw
第六章 初次接触:DirectDraw本章,你将初次接触DirectX中最重要的组件:DirectDraw。DirectDraw可能是DirectX中最强大的技术,因为其贯穿着2D图形绘制同时其帧缓存层也是DirectX3D的基础。当然,DirectX8.0中DirectDraw已经完全集成到了DirectX3D里面。此外,如果你对DirectDraw深有了解,那将拥有创建任何DOS16/32类型的图形化程序的能力。DirectDraw是理解众多DirectX自身概念的钥匙。所以,听仔细了。
下面是本章你要解决掉的知识点:
·DirectDraw接口
·创建一个DirectDraw对象
·与windows合作
·深入研究模式
·奇妙的颜色
·构建显示表面
DirectDraw的众接口
DirectDraw由一系列的接口组成。如果你复习一下我们在第五章“DirectX基础与恐怖的COM”对组件对象模型(COM)的讨论,那你便会明白,接口不过是你用来与组件通信的函数或/和方法之集合。看下图6.1关于DirectDraw众接口的图示。记住,我不打算把版本好加在每个接口上,因为到目前为止我们的讨论都是抽象了。比如,IDirectDraw确实已经升级到7.0了,所以当我们用到DirectDraw时我们指的是IDirectDraw7,但现在,我只想展示一下DirectDraw的一些基础和关系。
图 6.1 DirectDraw的众接口
接口特性
如你所见,组成DirectDraw的接口只有五个:
IUnknown:所有的COM对象都必须从这个基接口派生,DirectDraw当然也不例外。IUnknown除了包含将被其他所有接口重载的Addref(), Release(),和 QueryInterface()函数之外什么也没有。
IDirectDraw:用使用DirectDraw工作就必须创建这个主接口。从IDirectDraw的字面意思便暗示了显卡以及相关硬件支持。十分有趣的是,由于win98/ME/XP/NT2000均支持MMS(多显示器支持),现在你可以在你的系统中装不止一个显卡于是便有了不止一个DirectDraw对象。但是,本书只假设电脑中只有一块显卡,而且即使系统中有多块显卡也总是选择默认的显卡来表示DirectDraw对象。
IDirectDrawSurface:其代表你使用DirectDraw创建、操作和显示的真实显示表面。一个DirectDraw表面可以存在于显卡自身的VRAM(显存)也可以存在于系统内存中。有两种基本的表面类型:主表面和副表面。
主表面用来表示显卡当前正被光栅化并显示的真实视频缓存。而副表面则常常是不显示的。大多数情况下,你将创建单个主表面以表示真实的视频播放,然后用一个或更多的副表面来表示位图对象和/或用后备缓存来代表用来画下一帧动画的非屏幕做图区(offscreen drawing areas)。在下一章我们将讨论关于表面的细节。但现在,还是看看图6.2的详细图示。
图6.2 DirectDraw表面
IDirectDrawPalette:DirectDraw可以用来处理任何色深,从1位单色到32位真彩色。故而,DirectDraw支持IDirectDrawPalette接口从而支持了256色或更少颜色数的视频模式。在制作一系列demo时256色模式得到广泛使用,因为用软件光栅化是256色是最快的视频模式。在第二卷对DirectX3D立即模式的讨论中你将转向24位色,因为那是D3D工作的基本视频模式。无论那种情况,IDirectDrawPalette接口用来创建、加载、操作调色板,以及把调色板附着到那些你为DirectDraw程序创建的表面上比如主表面或副表面。图6.3展示了绘图表面与调色板之间的关系。
图6.3 绘图表面与调色板间的关系
IDirectDrawClipper:这个接口用来帮助在可视显示表面的子集中进行DirectDraw光栅和位图的剪切操作。大多数情况下,你只需在窗口化的DirectX程序中和/或在剪切位图超出显示表面(无论是主表面还是副表面)的部分时使用DirectDraw剪切器。IDirectDrawClipper接口的酷表现在其将尽可能地利用硬件加速功能,以及替你完成在剪切位图以适应屏幕尺寸时那个麻烦的逐像素(pixel-by-pixel)或微潜像过程。
现在,在你开始创建一个DirectDraw对象前,让我们重新回顾一下上一章学到的关于如何使用COM对象的知识。DirectDraw和所有DirectX组件不断的发展着,所以其接口也不断地更新着。唉,尽管到本章为止我提到DirectDraw接口时都用IDirectDraw, IDirectDrawSurface, IDirectDrawPalette,和 IDirectDrawClipper来指代,但大多数情况下这些接口已经被更新过了。必要像前面提到过的,在DirectX7.0时IDirectDraw 已经更新到 IDirectDraw7。
这一切意味着,如果你要使用最新的软硬件功能,你必须总是用IUnknown::QueryInterface()查询到最新的接口修订版。但是要想确定最新版的接口,你还不得不查一下DirectX SDK文档。当然,在本书中你使用的是DirectX8.0,所以你已经知道这最新已经最新到哪了,但记住,当你升级到DirectX9.0是你将有一下更新的接口可供使用了。不过,本书的两卷都是关于写你自己的光栅化与3D化软件,所以我将尽可能简短些。大多数情况下,你用不到新版本DirectX的大多数新增功能。
综合使用各接口
下面,我将简单地综合使用各接口以创建一个DirectDraw程序。
1。创建一个主DirectDraw对象并通过QueryInterface()获得一个IDirectDraw7接口。或者使用DirectDrawCreateEx()直接获得一个IDirectDraw7接口。使用接口设置合作级别以及视频模式。
2。使用IDirectDrawSurface7接口创建至少一个用以绘图的主表面。针对表面的色深和视频模式,可能需要一个调色板。如果视频模式的色深小于8位每像素,则要用调色板。
3。用IDirectDrawPalette接口创建一个调色板,用RGB格式的信息初始化调色板,并把调色板与相关表面关联起来。
4。如果DirectDraw程序是窗口程序,或者如果你渲染的位图有可能超出可视DirectDraw表面的边界时,你至少要创建一个剪切器并将其调整到可视窗口的大小。见图6.4。
图6.4 DirectDraw剪切器
5。在主表面上绘图。
当然,我省略了大量技术性的细节,但是以上五点是使用不同接口共同的要点。记住这五点之后,让我们了解些细节并真正地使用下这些接口……
创建一个DirectDraw对象
用C++创建一个DirectDraw1.0对象时,你所要做的便是调用DirectDrawCreate(),如下所示:
HRESULT WINAPI DirectDrawCreate(GUID FAR *lpGUID, // guid of object
LPDIRECTDRAW FAR *lplpDD, // receives interface
IUnknown FAR *pUnkOuter ); // com stuff
lpGUID:这是你要使用的那个显示驱动的GUID。大多数情况下只需简单设为NULL以使用默认的硬件。
lplpDD:如果调用成功这将是个IDirectDraw接口的指针的指针。注意,这个函数返回一个IDirectDraw接口而非IDirectDraw7接口。
pUnkOuter:高级特性,总是设为NULL。
下面是你可能要用到的:创建一个基于IDirectDraw接口的默认DirectDraw对象。
LPDIRECTDRAW lpdd = NULL; // storage for IDirectDraw
// create the DirectDraw object
DirectDrawCreate(NULL, &lpdd, NULL);
如果函数调用成功,lpdd将是一个可用的IDirectDraw 1.0对象接口。但是,你可能喜欢最新版本的接口,IDirectDraw7。不过在你学习如何做到这之前让我们来谈谈错误处理问题吧。
DirectDraw的错误处理
DirectX的错误处理非常简洁。有许多的宏可以用来检测各种函数调用的成功与失败。Microsoft推荐在测试DirectX函数错误时使用两个宏:
FAILED():检测调用成功。
SUCCEEDED():检测调用失败。
知道了这些,你可以通过加入错误处理代码来使你的程序更正规。
if (FAILED(DirectDrawCreate(NULL, &lpdd, NULL)))
{
// error
} // end if
或者类似的,你可以对成功进行检测:
if (SUCCEEDED(DirectDrawCreate(NULL, &lpdd, NULL)))
{
// move onto next step
} // end if
else
{
// error
} // end else
我经常使用FAILED()宏,因为我不喜欢同时从两个逻辑方向考虑问题,不过你使用什么宏那可就随你了。使用宏的唯一问题是,宏告诉你的信息太少,其只能检查一些常规错误。如果你想要知道确切的错误,你可以察看函数的返回码。表6.1列出DirectX6.0 DirectDrawCreate()的可能的返回码。
表 6.1. DirectDrawCreate() 的返回码
返回码 描述
DD_OK 完全成功
DDERR_DIRECTDRAWALREADYCREATED DirectDraw对象以及创建过了
DDERR_GENERIC 未知错误
DDERR_INVALIDDIRECTDRAWGUID GUID设备未知
DDERR_INVALIDPARAMS 非法的参数
DDERR_NODIRECTDRAWHW 没有任何硬件
DDERR_OUTOFMEMORY 大胆猜测一些?
使用这些常量的唯一问题是Microsoft不保证他们不会完全改变所有的错误代码。但是,我想按下面的方式编码是绝对安全的。
if (DirectDrawCreate(...)!=DD_OK)
{
// error
} // end if
在任何时候,DirectDraw的所有函数都会定义DD_OK,所以你尽可放心使用。
对接口进行改进
如我所说,你可以用从调用DirectDrawCreate()返回的lpdd存储基本的IDirectDraw接口。或者,你可以通过任何DirectDraw接口都实现了的IUnknown接口的QueryInterface()方法查询一个新的接口从而达到使用最新版本接口的目的(不该这个接口是什么)。最新的DirectDraw接口,在DirectX7.0中,叫IDirectDraw7。所以你以下面方式获得这个接口的指针。
LPDIRECTDRAW lpdd = NULL; // standard DirectDraw 1.0
LPDIRECTDRAW lpdd7 = NULL; // DirectDraw 7.0 interface 7
// first create base IDirectDraw interface
if (FAILED(DirectDrawCreate(NULL, &lpdd, NULL)))
{
// error
} // end if
// now query for IDirectDraw7
if (FAILED(lpdd->QueryInterface(IID_IDirectDraw7,
(LPVOID *)&lpdd7)))
{
// error
} // end if
现在,这里有些重要的东西需要引起注意:
·调用QueryInterface()的方式。
·在访问IDirectDraw7接口时常量的使用,即IID_IDirectDraw7.
通常,从一个接口的所有函数调用都有如下的形式:
interface_pointer->method(parms...);
所有接口都以如下形式被标识:
IID_IDirectCD
这里,
C代表组件,其他的诸如Draw代表DirectDraw,Sound代表DirectSound,Input代表DirectInput,等等。
D代表一个从2到n的数,表示你想要的那个接口。你可以从DDRAW.H文件中找到所有这些常量。
继续这个例子。你现在可能对既有IDirectDraw接口又有IDirectDraw7接口感到困惑吧。干麻要这么做?既然你不需要旧版本的接口,简单的像下面这样扔掉它就是了:
lpdd->Release();
lpdd = NULL; // set to NULL for safety
从现在起,从新的接口IDirectDraw7进行所有的函数调用。
警告:由于有了IDirectDraw7的新功能,编码变的自由了而且所要做的琐碎的事也少了。之所以能这样,除了IDirectDraw7接口更加成熟和先进了之外,还因为大多数情况下其需要并且返回新的数据类型,而不是为DirectX1.0定义的数据类型。唯一确定这种不规则变化的途径便是去查看DirectX SDK文档,核实任何具体函数需要和(或)返回的数据类型的版本。但是这只是一般的警告,我将告诉你本书中你遇到的所有例子中的正确数据类型。我就是那么好。顺便说一句,我的生日是6.14。
除了使用QueryInterface()从一个初始化了的IDirectDraw 接口指针(lpdd),还有一个更直接的“COM 途径”去获得IDirectDraw7 接口。在COM中,只要你有接口ID或称IID,你便可以得到任何接口的接口指针,也即你想要的那个接口。大多数情况下,我个人不推崇使用低级COM函数,因为对此我已经深恶痛绝了。尽管如此,当你使用DirectMusic时你仍然不得不使用低级COM方法,所以此时至少也该介绍给你这种低级调用的过程。下面的代码直接获得IDirectDraw7 接口:
// first initialize COM, this will load the COM libraries
// if they aren't already loaded
if (FAILED(CoInitialize(NULL)))
{
// error
} // end if
// Create the DirectDraw object by using the
// CoCreateInstance() function
if (FAILED(CoCreateInstance(&CLSID_DirectDraw,
NULL,
CLSCTX_ALL,
&IID_IDirectDraw7,
&lpdd7)))
{
// error
} // end if
// now before using the DirectDraw object, it must
// be initialized using the initialize method
if (FAILED(IDirectDraw7_Initialize(lpdd7, NULL)))
{
// error
} // end if
// now that we're done with COM, uninitialize it
CoUninitialize();
上面这段代码是Microsoft推荐的创建DirectDraw对象的方法。但是这里它耍了一点小花样,它用了一个宏:
IDirectDraw7_Initialize(lpdd7, NULL);
你可以不这样,而完全用COM的方法取而代之:
lpdd7->Initialize(NULL);
不管上面两个调用的哪一个,NULL所在位置的参数表示显卡,而NULL代表使用默认的显卡驱动。总之,不难明白上面的宏是如何在代码中扩展开的吧。我想大概是这样做是想使事情变得简单点吧。现在,好消息是,Microsoft已经创建了一个无须中间步骤一下子就建出IDirectDraw7的函数。通常,通常这是不可能的,但在DirectX7.0中,他们创建了一个叫DirectDrawCreateEx()的新函数。关于这个函数,我在上一章提到过。他的原型声明如下:
HRESULT WINAPI DirectDrawCreateEx(
GUID FAR *lpGUID, // the GUID of the driver, NULL for active display
LPVOID *lplpDD, // receiver of the interface
REFIID iid, // the interface ID of the interface you are requesting
IUnknown FAR *pUnkOuter // advanced COM, NULL
);
这与DirectDrawCreate()很相似,但其多了一个参数,以使你能够创建任何版本的DirectDraw接口。于是,创建IDirectDraw7接口的函数调用如下:
LPDIRECTDRAW7 lpdd; // version 7.0
// create version 7.0 DirectDraw object interface
DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL);
唯一诡异的地方是接口指针的类型转换--注意那个(void **),此外你必须在参数idd中传一个接口。除此之外,它还是蛮简单高效的。
让我们花几分钟重复一些所有东西。如果我们想要用最新版本的DirectDraw,即7.0版(因为在DirectX8.0中Microsoft去除了DirectDraw),我们需要一个IDirectDraw7接口。我们可以使用基本的DirectDrawCreate() 创建一个IDirectDraw1.0版接口,然后使用QueryInterface()以获得IDirectDraw7接口。另一方面,我们可以使用低级COM技术直接获得IDirectDraw7。再或者,我们可以使用函数DirectDrawCreateEx()(从DirectX7.0才开始有这个函数)直接创建之。不错吧?DirectX有点像X Windows了,做同一件可以有5000种途径。
现在你知道了如何创建一个DirectDraw对象以及如何获得最新版本的方法。让我们在使DirectDraw工作起来的征程中再向前走一步,即设置合作级别。
与windows合作
如你所知,Windows是一个合作、共享的环境。尽管作为一个程序员,我一直不知道如何让系统与我的代码合作,但至少这是个理想状态。总之,DirectX与其他任何Win32应用程序一样,最起码的,它必须通知Windows它所要使用的各种资源,以便让让其他程序不在DirectX使用这些资源时申请(以及得到)这些资源。基本上,只要DirectX告诉Windows它在做什么,它便能得到它想要的资源。这看上去对我很公平。
在DirectDraw方面,你唯一感兴趣的是视频显示硬件。有两样东西你要关心的:
·全屏模式
·窗口模式
在全屏模式中,DirectDraw像老式的DOS程序那般行事。就是说,整个荧屏表面都分配给你的游戏,你直接写到视频硬件上。别的程序不会接触到视频硬件。窗口模式有点小不同。在窗口模式中,DirectDraw必须更多的与Windows合作,因为其他的程序可能需要更新他们各自的对用户可见的客户区窗口。因此,在窗口模式中,你对视频硬件的控制和独占是收限制的。
第七章“高级DirectDraw和位图绘图”将进一步讨论窗口DirectDraw应用程序,但他们可能有点复杂。本章的大部分将与全屏模式打交道,因为他们更容易操作,要记住这点。
现在,你知道一些关于为什么需要在Windows和DirectX之间建立合作的重要性了。让我们来看一下如何告诉Windows你需要的合作方式。要设置DirectDraw的合作级别,使用函数IDirectDraw7:: SetCooperativeLevel(),其是IDirectDraw7中的一个方法。
C++:如果你是C程序员,语法IDirectDraw7:: SetCooperativeLevel()可能有点神秘。::操作符叫做域操作符。该语法简单地表示SetCooperativeLevel()是IDirectDraw7接口的一个方法(或成员函数)。接口是个类,该类不过是一个具有虚函数的结构体罢了。有时我会省略掉前缀而直接写成SetCooperativeLevel()。但是,记住,所有DirectX函数都是接口的一部分,且必须使用函数指针类型的调用,入lpdd->function(...)
这里是IDirectDraw7:: SetCooperativeLevel()的原型声明:
HRESULT SetCooperativeLevel(HWND hWnd, // window handle
DWORD dwFlags);// control flags
如果成功返回DD_OK,否则返回错误代码。
有趣的是,这是第一次在DirectX函数中需要窗口句柄。DirectX(具体说是DirectDraw)需要hWnd参数以确定关联的窗口。你只需简单地使用你程序的主窗口句柄即可。
SetCoopertiveLevel()的第二个参数是dwFlags,其是影响DirectDraw与Windows协同方式的控制标志。表6.2列出了经常使用的一些标志,这些标志可以通过或运算联合在一起使用,以获得一个所要的合作级别。
表 6.2. SetCooperativeLevel()的控制标志
值 说明
DDSCL_ALLOWMODEX 允许使用Mode X (320x200,240,400)显示模式。只当使用DDSCL_EXCLUSIVE 和 DDSCL_FULLSCREEN 标志时方可使用。
DDSCL_ALLOWREBOOT 允许在独占模式(全屏模式)下检测 Ctrl+Alt+Del组合键。
DDSCL_EXCLUSIVE 请求使用独占级别。该标志必要与DDSCL_FULLSCREEN 标志同时使用。
DDSCL_FPUSETUP 表明调用函数希望使用优化了的DirectX3D浮点运算单元(FPU)(单精度且禁用异常处理)(single precision and exceptions disabled
DirectX3D就不需要每次都显式地设置FPU了。要了解更多信息,请查阅DirectX SDK文档“DirectDraw合作级别与FPU精度”。
DDSCL_FULLSCREEN 表明使用全屏模式。其他程序对GDI函数的调用将无法在屏幕上画图。本标志必须与DDSCL_EXCLUSIVE 标志同时使用。
DDSCL_MULTITHREADED 要求DirectDraw的行为满足多线程安全这一要求。就目前你的学习阶段而言还无需对此过于深究。
DDSCL_NORMAL 表明程序是一个普通的窗口程序。本标志不能和 DDSCL_ALLOWMODEX, DDSCL_EXCLUSIVE, 或 DDSCL_FULLSCREEN 同时使用
DDSCL_NOWINDOWCHANGES 表明不允许DirectDraw最小化或还原激活状态下的程序窗口。
如果你对上面的诸多标志都看的很仔细的话,你可能会发现好些标志好像有点多余。基本上,DDSCL_FULLSCREEN 和 DDSCL_EXCLUSIVE 必须同时使用。如果你想用 Mode X 模式,那必须同时使用DDSCL_FULLSCREEN, DDSCL_EXCLUSIVE, 和 DDSCL_ALLOWMODEX 。除此之外,标志的名字和其做的事情还是比较贴切的。大多数情况下,你将像下面这样设置全屏模式的程序:
lpdd7->SetCooperativeLevel(hwnd,
DDSCL_FULLSCREEN |
DDSCL_ALLOWMODEX |
DDSCL_EXCLUSIVE |
DDSCL_ALLOWREBOOT);
而普通窗口程序则如下般设置:
lpdd7->SetCooperativeLevel(hwnd, DDSCL_NORMAL);
当然,当你在本书后面学习多线程编程技术时,你可能就要加上标志DDSCL_MULTITHREADED 以使程序运行得更安全。总之,现在让我们看看创建DirectDraw对象和设置合作级别的完整过程:
LPDIRECTDRAW lpdd = NULL; // standard DirectDraw 1.0
LPDIRECTDRAW7 lpdd7 = NULL; // DirectDraw 7.0 interface 7
// first create base IDirectDraw interface
if (FAILED(DirectDrawCreateEx(NULL, (void **)&lpdd7, IID_IDirectDraw7, NULL)))
{
// error
} // end if
// now set the cooperation level for windowed DirectDraw
// since we aren't going to do any drawing yet
if (FAILED(lpdd7->SetCooperativeLevel(hwnd, DDSCL_NORMAL)))
{
// error
} // end if
注意:为了节省笔墨,我将省去诸如FAILED() 和(或) SUCCEEDED() 这些错误处理。但是你在编程时可要对错误处理时刻挂在心上哦。
此刻,你已经学会了创建一个生成窗口的完整DirectX程序的所有知识--先从DirectDraw开始,然后设置合作级别。尽管你还不知道如何绘图,但毕竟已经开始上路子了。作为一个例子,请看CD上的对应着DEMO6_1.EXE的DEMO6_1.CPP。当你运行程序时,你将看到如图6.5所示的画面。这个程序是基于前面我们写的T3D游戏控制台模板的,我试试在Game_Init() 和 Game_Shutdown() 中做了些修改以创建DirectDraw和设置合作级别。
图6.5 运行DEMO6_1.EXE
下面是在DEMO6_1.CPP中增加了DirectDraw代码的这两个函数,你会看到,创建DirectDraw是多么简单的一件事。
int Game_Init(void *parms = NULL, int num_parms = 0)
{
// this is called once after the initial window is created and
// before the main event loop is entered, do all your initialization
// here
// first create base IDirectDraw interface
if (FAILED(DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL)))
{
// error
return(0);
} // end if
// set cooperation to normal since this will be a windowed app
lpdd->SetCooperativeLevel(main_window_handle, DDSCL_NORMAL);
// return success or failure or your own return code here
return(1);
} // end Game_Init
/////////////////////////////////////////////////////////////
int Game_Shutdown(void *parms = NULL, int num_parms = 0)
{
// this is called after the game is exited and the main event
// loop while is exited, do all you cleanup and shutdown here
// simply blow away the IDirectDraw interface
if (lpdd)
{
lpdd->Release();
lpdd = NULL;
} // end if
// return success or failure or your own return code here
return(1);
} // end Game_Shutdown
技巧:如果你打算自己从头写DEMO6_1.CPP并试图编译它,请记得从DirectX8.0 SDK的LIB\目录中手动include DDRAW.LIB。同时把DirectX头文件所在文件夹增加到你的编译器的编译搜索路径中,并使给文件夹为编译器搜索头文件时最先搜索的文件夹。此外,你当然应当创建一个Win32应用程序,我每天至少从使用编译器的新手那里收到10封email,忘了include .lib文件的。你可别成下一个发此类email的人……
学习有关模式的一些知识
有关设置DirectDraw的下一步可能是所有设置步骤中最酷的一部分。一般的地,在DOS中把视频模式设置为ROM BIOS模式是十分自然的。但由于模式转换的弊端使得在Windows下想像DOS这般设置视频模式几乎是不可能的。不过,由于有DirectX,那就小菜一碟了。DirectDraw的一个主要设计目标便是使视频模式转换对于程序员而言简便且透明。无需进行更多的VGA/CRT控制注册编码,只需做一个简单的调用即可。任何类型的Presto模式{?Presto mode},只要你想--你可以。(当然,显卡得支持)
设置视频模式的函数叫 SetDisplayMode(),其为IDirectDraw7接口的成员函数,或用C++的话说:IDirectDraw7::SetDisplayMode()。下面是它的原型声明。
HRESULT SetDisplayMode(DWORD dwWidth, // width of mode in pixels
DWORD dwHeight, // height if mode in pixels
DWORD dwBPP, // bits per pixel, 8,16,24, etc.
DWORD dwRefreshRate, // desired refresh, 0 for default
DWORD dwFlags); // extra flags (advanced) 0 for default
与往常一样,函数的如果调用成功则返回DD_OK。
你现在可能想说:“天哪,棒极了!”。你曾经试图设置一个Mode X模式,比如320×400或800×600模式吗?即使你成功了,呵呵,渲染视频缓存是可要祝你好运了。不过,现在有了DirectDraw函数,你只要把宽、高、色深、发给函数,呵呵,搞定!DirectDraw处理所有具体显卡的细节特性,如果你要求的模式可以建立,那它就建立之。并且,视频模式建立后保证有一段线性的内存缓存……当然它还保证其他一些东西,晚些时候再说。
看看表6.3,对常用的视频模式和对应色深来次简要的复习。
Table 6.3. 常用视频模式
宽 高 色深 Mode X
320 200 8 *
320 240 8 *
320 400 8 *
512 512 8,16,24,32
640 480 8,16,24,32
800 600 8,16,24,32
1024 768 8,16,24,32
1280 1024 8,16,24,32
有趣的是,你可以要求任何你想要的模式。比如你可以选择400×400,而且如果显卡支持这种模式那这种模式就能工作。但,最好还是建成选择表6.3列出的模式,因为透明是最常用的。
技巧:事实上,有一个Win32 API函数可以用来设置视频模式,而且我过去也用过。但是这个函数对系统容易危害到系统,而且总把事情搞的一团糟。
回到SetDisplayMode()函数,前三个参数意如所表,自不多言。但是最后两个就需要一点解释了。dwRefreshRate用来对你要求的模式设定刷新率。因此,如果你要求一个320×200模式,默认的系统会把刷新率设为70Hz,不过通过这个函数,如果你想,你可以强制的设为60Hz。而且说实在的,我将不深入讨论刷新率,其实简单设为0就行了(这样将使用驱动默认的值)
最后一个参数dwFlags是一个额外的标志,用来表示一些琐碎的东西,而且用处也不大。目前,通过给这个参数传DDSDM_STANDARDVGAMODE标志,你可以为320×200使用VGA模式13h{?}从而代替Mode X 320×200。再次的,我不会深究这个问题,如果你写一个使用320×200的游戏,那你可以用这个标志做下试验,看看到底是VGA模式13h快还是Mode X 320×200快。不过说真的,差别不大。所以现在,就设成0吧。
预备知识到此结束,让我们了转换模式吧。要转换模式,你必须首先创建DirectDraw对象,设置合作级别,然后设置显式模式如下:
lpdd->SetDisplayMode(width,height,bpp,0,0);
比如,创建一个640×480 256色(8位色深),你将这样做:
lpdd->SetDisplayMode(640,480,8,0,0);
设置 800x600 16位色深, 你该这么做:
lpdd->SetDisplayMode(800,600,16,0,0);
现在,两种模式的区别要比设置它们时的区别大得多了。8位色深模式的工作方式与16位、24位模式完全不同。如果你还记得,前几章关于win32 GDI编程时提到调色板(第三章 高级Windows编程,和第四章 Windows GDI,控件,和杂感),同样的道理在DirectDraw中也适用。即当你创建一个8位模式时,你其实是在创建堆模式{?palletized mode},此刻你必须创建一个调试板并用RGB的8.8.8格式填充调色板。
另一方面,如果你创建一个色深为16、247、32色深的完全RGB模式,你便不需要想调色板了。你可以把数据编码直接写到视频缓存中。不过至少,你应当学会如何与DirectDraw调色板协同工作(这上下一个话题所要讨论的)。但在进一步学习前,让我们看看用640×480×8的模式创建一个全屏DirectX程序的完整例子。
CD上的DEMO6_2.CPP及其对应的可执行文件就是这个例子。我只是给你一个程序的轮廓,你所看到的不过是一个空白荧屏,因为这个demo是全屏程序麻。但是我确实向你展示了达到全屏功能的代码。与往常一样,这个demo是基于以前我们写的游戏控制台程序,不过做了些小修改,在Game_Init() 和 Game_Shutdown() 部分添加了一些与DirectX有关的改动。这些改动在下面从DEMO6_2.CPP节选的代码中可看出。仔细研究下哦,你会惊奇于它的简洁的……
int Game_Init(void *parms = NULL, int num_parms = 0)
{
// this is called once after the initial window is created and
// before the main event loop is entered, do all your initialization
// here
// first create base IDirectDraw interface
if (FAILED(DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL)))
{
// error
return(0);
} // end if
// set cooperation to full screen
if (FAILED(lpdd->SetCooperativeLevel(main_window_handle,
DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX |
DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT)))
{
// error
return(0);
} // end if
// set display mode to 640x480x8
if (FAILED(lpdd->SetDisplayMode(SCREEN_WIDTH,
SCREEN_HEIGHT, SCREEN_BPP,0,0)))
{
// error
return(0);
} // end if
// return success or failure or your own return code here
return(1);
} // end Game_Init
/////////////////////////////////////////////////////////////
int Game_Shutdown(void *parms = NULL, int num_parms = 0)
{
// this is called after the game is exited and the main event
// loop while is exited, do all your cleanup and shutdown here
// simply blow away the IDirectDraw7 interface
if (lpdd)
{
lpdd->Release();
lpdd = NULL;
} // end if
// return success or failure or your own return code here
return(1);
} // end Game_Shutdown
此刻,有两件事你没做:控制调色板以及访问显式缓存。让我们先关照下颜色问题。
奇妙的颜色
DirectDraw支持许多不同的色深,包括1、2、4、8、16、24、以及32位。显然,1、2和4位色深有点过时了,所以你就别对这些色深过多的研究了。另一方面,8、16、24、和32位模式非常流行。你写的许多游戏可能因为速度上的原因要在8位调色板模式(也是一个不错的用于学习的模式)下运行。或在16或在24以利用全RGB颜色。RGB模式通过向缓存中写入相同字节的数据来工作,如图6.6所示。调色板模式则通过使用一个对缓存中每个像素都索引化了的查找表来工作。你应该很熟悉这种工作方式不是吗,前文有所涉及的。
图 6.6. 不同色深的比较
你要学的是,创建一个256色调色板,并通知DirectDraw你要用它。下面让我们来看看所需步骤:
1 创建一个或多个调色板数据结构,比如一个有256个PALETTENTRY结构的数组。
2 从DirectDraw对象创建一个DirectDraw调色板接口对象IDirectDrawPalette。大多数情况下,这个调色板对象将直接映射到已注册的VGA硬件调色板上。
3 把调色板与绘图表面关联起来,比如与主表面关联。这样所有渲染到表面的数据均将以合适的颜色显式。
4 (可选)如果你想,你可以更改部分或全部调色板项。如果你跳过步骤1,在步骤2中创建的是一个空的调色板,那你就得在这步中做点什么了。基本上,我所要说的是:当你创建了一个调色板接口是,你需要同时设置调色板颜色。如果你不同时做,那你稍后也的做。所以,只要你记得在后面初始化调色板,那步骤2可以变成步骤1。
让我们开始创建调色板数据结构吧。它无非是是一个有256个调色板项的数组。调色板项是一个叫PALETTENTRY Win32结构,如下所示:
typedef struct tagPALETTEENTRY
{
BYTE peRed; // red component 8-bits
BYTE peGreen; // green component 8-bits
BYTE peBlue; // blue component 8-bits
BYTE peFlags; // control flags: set to PC_NOCOLLAPSE
} PALETTEENTRY;
看上去很眼熟吗?很好。
总之,建个调色板,你只需简单的创建一个该结构的数组,如下:
PALETTEENTRY palette[256];
然后,你用你喜欢的方式填充这个数组。但是,这里有条规矩:你必需把peFlags 域填为PC_NOCOLLAPSE.。这是必需的,因为你并不需要Win32或DirectX替你优化调色板。记得这些后,下面是一个创建随机调色板的例子。在第一个数组元素是黑色,最后一个元素是白色:
PALETTEENTRY palette[256]; // palette storage
// fill em up with color!
for (int color=1; color < 255; color++)
{
// fill with random RGB values
palette[color].peRed = rand()%256;
palette[color].peGreen = rand()%256;
palette[color].peBlue = rand()%256;
// set flags field to PC_NOCOLLAPSE
palette[color].peFlags = PC_NOCOLLAPSE;
} // end for color
// now fill in entry 0 and 255 with black and white
palette[0].peRed = 0;
palette[0].peGreen = 0;
palette[0].peBlue = 0;
palette[0].peFlags = PC_NOCOLLAPSE;
palette[255].peRed = 255;
palette[255].peGreen = 255;
palette[255].peBlue = 255;
palette[255].peFlags = PC_NOCOLLAPSE;
这就是全部的工作拉。当然,你可以创建多个调色板并分别对它们任意填充,这完全取决于你。
继续,下一步是创建实际的IDirectDrawPalette接口。幸运的是,这个接口自DirectX6.0以来就没怎么变过,所以你无需使用 QueryInterface() 或其他任何方法来查询最新版本的接口。下面是IDirectDraw7:: CreatePalette()的原型声明,其创建一个调色板对象。
HRESULT CreatePalette(DWORD dwFlags, // control flags
LPPALETTEENTRY lpColorTable, // palette data or NULL
LPDIRECTDRAWPALETTE FAR *lplpDDPalette, // received palette interface
IUnknown FAR *pUnkOuter); // advanced, make NULL
如果函数调用成功则返回DD_OK
让我们看看各参数吧。第一个参数dwFlags,其用来控制各种调色板的各种属性。下一个参数是一个指向用来初始化调色板的数据的指针,如果你不想指定则设为NULL即可。如果函数调用成功,则传给第三个参数的指针将存储真实的IDirectDrawPalette接口。最后,pUnkOuter是高级COM参数,一般简单的设为NULL即可。
唯一让人感兴趣的参数,当然,便是dwFlags。让我们更深入地看看你能对这个参数做些什么。表6.4列出了可能的值,你可以用或运算综合使用它们。
表 6.4. CreatePalette()的控制标志
值 描述
DDPCAPS_1BIT 1位色深,在调色板中有2项。
DDPCAPS_2BIT 2位色深,在调色板中有4项。
DDPCAPS_4BIT 4位色深,在调色板中有16项。
DDPCAPS_8BIT 5位色深,最常用的,在调色板中有256项。
DDPCAPS_8BITENTRIES 这是一个索引化调色板的高级属性,其用在1、2、4位色深的调色板上。记得别用它就是了。
DDPCAPS_ALPHA 表明,PALETTEENTRY结构的 peFlags 域被解释为在一个8位值中用于控制透明度的alpha值。一个用该标志创建的调色板只能与由 DDSCAPS_TEXTURE标志创建的D3D素材表面关联。再次的,这是个给高手准备的高级标志。
DDPCAPS_ALLOW256 表明,这个调色板的256项均可被定义。通常,0和255项分别留给黑色和白色,在某些系统上,比如NT系统,在任何情况下你都是不可以 向这两项写入值的。但是,大多数情况下,你并不需要这个标志,因为0项一般总是黑色,而大多数调色板把第255项定义成白色。当然用 不用,怎么用,随你。
DDPCAPS_INITIALIZE 由lpDDColorArray传递过来的数组初始化调色板的颜色。这个标志被用来是调色板数据能被下载到硬件调色板上。
DDPCAPS_PRIMARYSURFACE 这个调色板将被关联到主表面上。在这个调色板上改变颜色将立即反应在显式上,除非使用DDPSETPAL_VSYNC标志并且DDPSETPAL_VSYNC 是被支持的功能。
DDPCAPS_VSYNC 强迫调色板只在垂直空白间隙期间才更新颜色。这可以减小颜色异常和屏幕闪烁。目前还不是完全支持。
{译者注:显示器显式图形的过程是这样的,先从屏幕左上角开始自左向右水平移动电子枪;到达右端时电子枪移到下一行左端再水平移 动,如此往复。电子枪从上一行右端到下一行左端这个空隙是不发电子的,但时间也很短,故也无法利用。但当电子枪移到右下角时,此时 下一步它将移动到第一行的左端,即要行走右下角到左上角这个对角线,这时这个时间就长一些了,这个时间就是“垂直空白间隙”简记 为VBI。随便说说,不对的还请指正。yew98。}
如果你问我,你一定会说:好多费解的控制字啊。其实,你只需用8位调色板工作,所以你只要对几个控制字使用或运算即可。
DDPCAPS_8BIT | DDPCAPS_ALLOW256 | DDPCAPS_INITIALIZE
然后,如果你不在乎让0项和255项,你可以忽略DDPCAPS_ALLOW256。进一步的,如果你不想在调用CreatePalette()期间初始化调色板,则你可忽略DDPCAPS_INITIALIZE。
把这些知识都吸进脑子里吧。下面便是你如何用刚才的随机调色数据创建一个调色板对象。
LPDIRECTDRAWPALETTE lpddpal = NULL; // palette interface
if (FAILED(lpdd->CreatePalette(DDPCAPS_8BIT |
DDPCAPS_ALLOW256 |
DDPCAPS_INITIALIZE,
palette,
&lpddpal,
NULL)))
{
// error
} // end if
如果函数调用成功,lpddpal将指向一个有效的IDirectDrawPalette接口。同时,硬件调试板也会用发送给它的调色数据更新自己。在这里,便是256个随机颜色。
通常,在一节的末尾我都会给你一个demo。不过很不幸,这次我们遇到了个所谓“鸡与蛋”的难题。也即,你必需先会在屏幕上花东西然后才能看看画的颜色是什么,而画图你还没学呢。下面就学吧。
创建一个显式表面
如你所知,在屏幕上显式的图形无非是一系列着了色的像素,而这些像素的颜色又是由内存中的一些格式化数据所描述,要么是调色板,要么是RGB。换句话说,要让任何事情在屏幕上发生,你就需要知道如何在内存中绘画。不过,DirectDraw是设计者决定对显存进行一点抽象,这样不管你系统中(别人系统中)使用什么样的显卡,访问视频表面的方法对于你(程序员观点)而言是不变的。这样,DirectDraw便支持了我们所谓的“表面”概念。
看图6.7,表面是可以含有位图数据的矩形内存区域。进一步的,有两种不同的表面:主表面和副表面。
图 6.7. 表面可以是任何尺寸
主表面直接与显卡上真实的显存对应,并且始终可见。因此,在DirectDraw程序中你只能有一个主表面,并且它直接反应屏幕图形而且存储于VRAM(显存)中。当你操作主表面时,你会立刻看到结果显示在屏幕上。比如,如果你设置视频模式为640×480×256,则你必需创建一个同样是640×480×256的主表面,并且把它于显示设备,即IDirectDraw7对象,关联起来。
副表面,则相当灵活。它们可以是任何尺寸,可以或保存在VRAM中或保存在系统内存中,而且你爱可以创建任意多个,只要内存够用。大多数情况下,你需要一个到两个副表面(缓冲区)用以平滑动画。和主表面一样,副表面也有色深和绘图的工作要做。你在离屏(offscreen)表面为下一帧动画绘图,然后快速的把离屏表面拷贝或翻页(page flip)到主表面,这样便平滑了动画。这就是所谓的双或三缓存。下一章你会学到更多相关知识。
副表面的第二个用途是存储游戏中的位图或动画。这是DirectDraw的一个非常重要的特性,因为只有使用DirectDraw表面你才能在位图数据上调用硬件加速功能。如果你你自己去写位块传输程序以绘位图,那你将失去所有的加速功能带来的好处。
现在我有点超前了,所以我得放慢点脚步。我超前的原因只是想让你有机会思考思考。现在,让我们来看看如何创建一个与你的显示模式相同大小的主表面,然后你将学习如何在主表面上写数据以及在屏幕上绘像素。
创建一个主表面
好拉,要创建任何表面你必需安如下步骤进行:
1 填充DDSURFACEDESC2 数据结构,其是用来描述你创建的表面的。
2 调用IDirectDraw7::CreateSurface()以创建表面
下面是CreateSurface()的原型声明:
HRESULT CreateSurface(
LPDDSURFACEDESC2 lpDDSurfaceDesc2,
LPDIRECTDRAWSURFACE4 FAR *lplpDDSurface,
IUnknown FAR *pUnkOuter);
基本上,函数需要一个对你想创建的DirectDraw表面的描述、一个接收接口的指针,以及最后把表征高级COM属性的pUnkOuter设为NULL。呵呵,填写对表面进行描述的数据结构是件烦人的事情,但我会帮助你一步一步来的。首先,让我们看看 DDSURFACEDESC2:
typedef struct _DDSURFACEDESC2
{
DWORD dwSize; // size of this structure
DWORD dwFlags; // control flags
DWORD dwHeight; // height of surface in pixels
DWORD dwWidth; // width of surface in pixels
union
{
LONG lPitch; // memory pitch per row
DWORD dwLinearSize; // size of the buffer in bytes
} DUMMYUNIONNAMEN(1);
DWORD dwBackBufferCount; // number of back buffers chained
union
{
DWORD dwMipMapCount; // number of mip-map levels
DWORD dwRefreshRate; // refresh rate
} DUMMYUNIONNAMEN(2);
DWORD dwAlphaBitDepth; // number of alpha bits
DWORD dwReserved; // reserved
LPVOID lpSurface; // pointer to surface memory
DDCOLORKEY ddckCKDestOverlay; // dest overlay color key
DDCOLORKEY ddckCKDestBlt; // destination color key
DDCOLORKEY ddckCKSrcOverlay; // source overlay color key
DDCOLORKEY ddckCKSrcBlt; // source color key
DDPIXELFORMAT ddpfPixelFormat; // pixel format of surface
DDSCAPS2 ddsCaps; // surface capabilities
DWORD dwTextureStage; // used to bind a texture
// to specific stage of D3D
} DDSURFACEDESC2, FAR* LPDDSURFACEDESC2;
如你所见,很复杂的一个结构。更有甚者,70%的域都相当诡异。不过幸运的是你只需知道我加粗的那几项,让我们一个一个看:
dwSize:在任何DirectX数据结构中这个域都很重要。许多DirectX数据结构都是用地址来传递的,所以获得数据结构的函数或方法是不知道数据结构的大小的。但是如果这个32位域的值总是数据结构的大小,那么获得数据结构的函数将总能通过访问这第一个域来获知数据结构的大小。所以,DirectDraw以及DirectX数据结构在第一个域中标识数据结构自身尺寸。这乍看上去好像有点多余,但相信我,这确实是个很好的设计。填充这个域你只需像如下这么做:
DDSURFACEDESC2 ddsd;
ddsd.dwSize = sizeof(DDSURFACEDESC2);
dwFlags:这个域用来告诉DirectDraw,你给的数据是用来填充DDSURFACEDESC2的哪个域的。或者,如果你在一个查询操作中用这个结构,告诉DirectDraw你要获得DDSURFACEDESC2的哪个域的信息。看看表6.5,其列出了这个标志字可取的值。比如,你如果你要在 dwWidth 和 dwHeight 两个域中填入有效数据则应该像下面这样设置dwFlags域:
ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT;
这样,DirectDraw便知道去查找 dwWidth 和 dwHeight 域,然后放入有效值。就把单位dwFlags看成是引导数据的指示器吧。
表 6.5. DDSURFACEDESC2的dwFlags域的各种可能的标志
值 描述
DDSD_ALPHABITDEPTH 表明 dwAlphaBitDepth 有效
DDSD_BACKBUFFERCOUNT 表明 dwBackBufferCount 有效
DDSD_CAPS 表明 ddsCaps 有效
DDSD_CKDESTBLT 表明 ddckCKDestBlt 有效
DDSD_CKDESTOVERLAY 表明 ddckCKDestOverlay 有效
DDSD_CKSRCBLT 表明 ddckCKSrcBlt 有效
DDSD_CKSRCOVERLAY 表明 ddckCKSrcOverlay 有效
DDSD_HEIGHT 表明 dwHeight 有效
DDSD_LINEARSIZE 表明 dwLinearSize 有效
DDSD_LPSURFACE 表明 lpSurface 有效
DDSD_MIPMAPCOUNT 表明 dwMipMapCount 有效
DDSD_PITCH 表明 lPitch 有效
DDSD_PIXELFORMAT 表明 ddpfPixelFormat 有效
DDSD_REFRESHRATE 表明 dwRefreshRate 有效
DDSD_TEXTURESTAGE 表明 dwTextureStage 有效
DDSD_WIDTH 表明 dwWidth 有效
dwWidth:表明表面的像素宽度。当你创建一个表面是,这里便是你设定宽度的地方。320,640等等。此外,如果你要查询表面的属性,这个域将返回表面的宽度(如果你这么要求的话)。
dwHeight:表面表面的像素高度。与dwWidth类似,这里是你在创建表面时设定其高度的地方。300,240,480等等。
lPitch:这是个有趣的域。基本上它是你所选择模式的水平内存间距。看图6.8。lPitch,也被称为步幅或内存宽度,是在给定视频模式下每行的的字节数。基于如下原因,这是个非常重要的数据域:当你要求一个640×480×8的显示模式,你知道每行有640个像素,每个像素占8位内存(即1个字节)。所以,每行有640个字节,于是lPitch似乎便该设为640。对吗?不一定哦。
图 6.8. 访问一个表面
技巧:lPitch将根据VRAM的不同设计而不同。故而,当你在DirectDraw表面上从一行访问另一行的内存时,你必需使用lPitch来移动到下一行,而不是用宽度乘上每像素的字节数。这一点非常之重要。
大多数新显卡支持我们所谓的“线性内存模式”而且有硬件寻址功能,这些属性已经是现实了,但并不保证每块显卡都实现之。所以,你不能假设一个640×480×8视频模式在内存中每行占640个字节。而这便是lPitch域存在的理由。你必需在你计算内存地址时应用它以保证计算正确,这样你便可以从一行移动到另一行了。比如,要访问640×480×8(256色)视频模式下的任何一个像素,你可以使用下面的代码。假设你已经从DirectDraw得到lPitch值,并且lpSurface已经指向表面内存(我会在下面解释这个参数的)
ddsd.lpSurface[x + y*ddsd.lPitch] = color;
是不是很简单啊?大多数情况下,ddsd.lPitch 设为640以代表640x480x8模式。而对于640×480×16模式,ddsd.lPitch 将设为1280(两字节每像素=640×2)。但是,对于某些显卡,出于显存的布局原因,比如内在缓存的设立或其他什么东西,就使得事情不像上述这样了。所以最正规的方式是:在计算内存时总是使用lPitch,你便总是安全的。
技巧:尽管lPitch值并不总是等于你设置的视频模式的水平值,但使用其来测试水平值可以使你能够调用到其他优化函数。比如,在你完成初始化部分功能的代码中,你可以去获得lPitch值并与你选择的视频模式水平值比较。如果它们相等,则你可以切换到你为优化程序而硬编码每行字节数的函数上。
lpSurface:这个域获得指向你随创建表面的真实内存地址的指针。这些内存可能是VRAM也可能是系统内存,但是你无需为次担心。一旦你获得了指向该内存的指针,你便可以像操作其他内存一样操作它了,比如向它写数据或读数据,等等,这完全取决于你想如何填充像素。呵呵,让这个指针有效多容易啊!但是我们还是要在这里多停留一会。一般,你必需“锁定”表面内存,并且告诉DirectX你要在该内存上面工作了而其他的进程不许试图在该内存上读或写。进一步的,当你获得了这个指针时,根据不同的色深(8,16,24,32)你要经常对其进行类型转换并将其复值给一个工作指针。
dwBackBufferCount:这个域被用来设置和读取后备缓存(或与主表面关联的副离屏翻页缓存)的数目。如果你能回忆起来,通过创建一个或更多的虚主表面(与表面拥有同样的图样和色深的缓存),后备缓存可用来实现动画的平滑化,这也便是离屏缓存。然后你在后备缓存上绘图,这个后备缓存对于用户而言是不可见的。接着快速的翻页或拷贝后备缓存到主表面以供显示。如果你只有一个后备缓存,这个技术便成为“双缓存技术”。使用两个缓存便叫“三缓存技术”,当然后者比前者拥有更好的效果但也占用更多的内存。为了是事情简单点,大多数情况下,你将创建一个包括一个主表面和一个后备缓存的翻页链。
ddckCKDestBlt:这个域被用来控制目标颜色键。当进行块传输操作时,这个域控制写入颜色的方式。更多信息将在后面第七章“高级DirectDraw和位图图形”上介绍。
ddckCKSrcBlt:这个域表明源颜色键。即当你进行bitmapping操作时{?后面好像有这个术语,一时想不起来了,但翻到后面再该过来吧。yew98}你不想被块传递的颜色。这是一个你如何设置你位图透明色的的方法。详见第七章。
ddpfPixelFormat:这个域用来获得表明像素的格式。当你去查询表明的属性是这就非常重要了。下面的是其结构。你可能要去查询DirectX SDK以获得更多的细节信息。因为这些信息实在太多了,而且和现在的讨论关心也不大。
typedef struct _DDPIXELFORMAT
{
DWORD dwSize;
DWORD dwFlags;
DWORD dwFourCC;
union
{
DWORD dwRGBBitCount;
DWORD dwYUVBitCount;
DWORD dwZBufferBitDepth;
DWORD dwAlphaBitDepth;
DWORD dwLuminanceBitCount; // new for DirectX 6.0
DWORD dwBumpBitCount; // new for DirectX 6.0
} DUMMYUNIONNAMEN(1);
union
{
DWORD dwRBitMask;
DWORD dwYBitMask;
DWORD dwStencilBitDepth; // new for DirectX 6.0
DWORD dwLuminanceBitMask; // new for DirectX 6.0
DWORD dwBumpDuBitMask; // new for DirectX 6.0
} DUMMYUNIONNAMEN(2);
union
{
DWORD dwGBitMask;
DWORD dwUBitMask;
DWORD dwZBitMask; // new for DirectX 6.0
DWORD dwBumpDvBitMask; // new for DirectX 6.0
} DUMMYUNIONNAMEN(3);
union
{
DWORD dwBBitMask;
DWORD dwVBitMask;
DWORD dwStencilBitMask; // new for DirectX 6.0
DWORD dwBumpLuminanceBitMask; // new for DirectX 6.0
} DUMMYUNIONNAMEN(4);
union
{
DWORD dwRGBAlphaBitMask;
DWORD dwYUVAlphaBitMask;
DWORD dwLuminanceAlphaBitMask; // new for DirectX 6.0
DWORD dwRGBZBitMask;
DWORD dwYUVZBitMask;
} DUMMYUNIONNAMEN(5);
} DDPIXELFORMAT, FAR* LPDDPIXELFORMAT;
注意:我加粗的域是常用域
ddsCaps:这个域用来使那些被要求的但尚未在表面属性中定义的项有效。事实上,这个域又是另一个数据结构。下面显示了DDSCAPS2:
typedef struct _DDSCAPS2
{
DWORD dwCaps; // Surface capabilities
DWORD dwCaps2; // More surface capabilities
DWORD dwCaps3; // future expansion
DWORD dwCaps4; // future expansion
} DDSCAPS2, FAR* LPDDSCAPS2;
在99.9%的情况下,你只需设置该结构的第一个域。dwCaps.dwCaps2 是为3D准备的,而其他域dwCaps3 和 dwCaps4是为未来扩展之用,尚未使用。总之,表6.6列出了dwCaps可能设置的标志值。要看完整的列表,到DirectX SDK里去。
比如当要创建一个主表面是,你可以像下面这样设置ddsd.ddsCaps
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
我知道上面这种表达方式很复杂,在某种程度上确实如此。双重嵌套的控制标志确实有点痛苦。但是,忍受吧……
表 6.6. DirectDraw表面的属性控制设置
值 描述
DDSCAPS_BACKBUFFER 表明这个表面是表面翻页结构中的后备缓存。
DDSCAPS_COMPLEX 表明是一个复习表面。一个复杂表面是一个与一或多个串成翻页链的后备缓存关联的主表面。
DDSCAPS_FLIP 表明这个表面是一个表面翻页结构中的一部分。但这个属性被传递给 CreateSurface() 方法时, 一个前端缓存和一个或多个后备 缓存将被创建。
DDSCAPS_LOCALVIDMEM 表明这个表面优先存在于显存中。如果这个标志选中则 DDSCAPS_VIDEOMEMORY 也必需选中。
DDSCAPS_MODEX 表明这个表面是一个320x200 或 320x240 的Mode X 表面。
DDSCAPS_NONLOCALVIDMEM 表明这个表面优先存在于非本地显存中。如果这个标志选中则 DDSCAPS_VIDEOMEMORY 也必需选中。
DDSCAPS_OFFSCREENPLAIN 表明这个表面是个离屏表面。这样该表面便不能是某种特殊表面了,比如不是一个覆盖层,素材,z序,前端缓存,后备缓存,或者 alpha 表面。(Usually used for sprites。)
DDSCAPS_OWNDC 表明这个表面将长期与一个设备上下文关联。
DDSCAPS_PRIMARYSURFACE 表明这个表面是主表面。其描述了此刻为用户所见的东西。
DDSCAPS_STANDARDVGAMODE 表明该表面是一个标准的VGA模式的表面,而非 Mode X 模式。这个标志不能与 DDSCAPS_MODEX 标志同时使用。
DDSCAPS_SYSTEMMEMORY 表明这个表面创建在系统内存中。
DDSCAPS_VIDEOMEMORY 表明这个表面创建在显存中。
现在你对当你在创建表面时DirectDraw赋予你的强大而复杂的功能有个概念了吧。让我们把知识付诸实践,创建一个简单的与显示模式同样尺寸和色深(默认操作即如此)的主表面吧。下面是创建主表面的代码:
// interface pointer to hold primary surface, note that
// it's the 7th revision of the interface
LPDIRECTDRAWSURFACE7 lpddsprimary = NULL;
DDSURFACEDESC2 ddsd; // the DirectDraw surface description
// MS recommends clearing out the structure
memset(&ddsd,0,sizeof(ddsd)); // could use ZeroMemory()
// now fill in size of structure
ddsd.dwSize = sizeof(ddsd);
// enable data fields with values from table 6.5 that we
// will send valid data in
// in this case only the ddsCaps field is enabled, we
// could have enabled the width, height etc., but they
// aren't needed since primary surfaces take on the
// dimensions of the display mode by default
ddsd.dwFlags = DDSD_CAPS;
// now set the capabilities that we want from table 6.6
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
// now create the primary surface
if (FAILED(lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL)))
{
// error
} // end if
如果函数调用成功,则lpddsprdimary将指向新的表面接口,而你便可以从它上面调用函数了(有很多函数可用它上面调用的,比如在256色模式下关联调色板)。让我们来再通过重复调色板的例子来看看吧。
关联调色板
在前面关于调色板的章节中你已经做了所以的工作,除了把调色板与一个表面关联起来。你创建了一个调色板并填充了各个颜色项,但你并没有把调色板与某个表面关联起来,因为当时你还没有表面呢。现在,你有一个表面了(主表面),你终于可以完成整个步骤了。
要不调色板关联到任何一个表面上,你所要做的便是使用IDirectDrawSurface7::SetPalette()函数,如下所示:
HRESULT SetPalette(LPDIRECTDRAWPALETTE lpDDPalette);
这个函数只需要一个你想要被关联的调色板指针即可。就用你在前面调色板章节中创建的调色板吧。下面是你如何把调色板与主表面关联的代码:
if (FAILED(lpddsprimary->SetPalette(lpddpal)))
{
// error
} // end if
还不坏,对吧。此时,你已经具备了任何使你能效仿DOS32游戏的条件。你可以转换视频模式,设置调色板,已经创建主绘图表面以描绘生动的视频图像。但是,仍然有一些细节你需要学习,例如锁定主表面内存并且获得访问VRAM的能力以及绘一个像素。让我们现在就来学下这些东西。
绘制像素
想要在全屏DirectDraw模式下绘制一个(或一些)像素,你必需首先创建Dir
最后更新:2017-04-03 14:54:08