DirectX/9.0/Direct3D/初始化
Direct3D 中最重要的部分之一是初始化。有两种方法可以实现它:正确的方法(读:繁琐、困难、烦人,但值得学习),本章的绝大部分内容都围绕它展开,以及简单的方法,它将用作介绍,因为某些部分无论你如何操作都必须使用。在本节结束时,我们将将其应用到我们的基准测试应用程序中。
开始初始化 Direct3D 时,您必须做的第一件事是创建一个 Direct3D 对象。此对象将在本书的其余部分中至关重要。幸运的是,这并不意味着它很难。实际上,您唯一需要做的就是调用一个函数
IDirect3D9* Direct3DCreate9( UINT SDKVersion );
此函数返回它创建的 IDirect3D9 接口的地址,即我们的 Direct3D 对象。您需要使用参数告诉它 SDK 的 DirectX 版本。通常您只传递 D3D_SDK_VERSION,除非您出于某种原因想要旧的接口。
IDirect3D9* d3dobject = Direct3DCreate9( D3D_SDK_VERSION );
//Please note that DirectX uses the same naming conventions as Win32,
//so you can also initialize the variable as a LPDIRECT3D9 instead of a IDirect3D9*.
//Also, it's a good idea to make sure the device was created.
//If it wasn't, d3dobject will be null, and DirectX9 or greater probably isn't installed
就这样。不幸的是,其余过程并不那么简单。您需要的下一件事是一组呈现参数,这些参数存储在 D3DPRESENT_PARAMETERS 结构中。如果您查看内部,您会注意到有很多变量。您不需要填充所有变量,甚至不需要填充大多数变量。只需确保它们都被清零即可。您唯一关注的两个变量是 Windowed 和 SwapEffect。对于 Windowed,您可以简单地将其设置为 true。SwapEffect 取决于您想要什么。对于本教程,最快的 D3DSWAPEFFECT_DISCARD 将可以正常工作。到目前为止,您的代码应如下所示
D3DPRESENT_PARAMETERS d3dpresent;
memset( &d3dpresent, 0, sizeof( D3DPRESENT_PARAMETERS ) );
d3dpresent.Windowed = TRUE;
d3dpresent.SwapEffect = D3DSWAPEFFECT_DISCARD;
现在我们可以真正创建设备了。这可以通过调用 d3dobject 引用的函数来完成
IDirect3D9::CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS * pPresentationParameters, IDirect3DDevice9 ** ppReturnedDeviceInterface );
这将返回 0(成功)或错误。有很多参数,但大多数参数的值总是有效的。第一个是适配器编号。现在,我们可以只使用 D3DADAPTER_DEFAULT,它总是有效的。DeviceType 也有一个总是有效的参数:D3DDEVTYPE_REF。这非常慢,除非绝对必要,否则不应该使用;但是现在,它就足够了(它也不能在没有安装 SDK 库的情况下使用,因此不要在其他计算机上尝试)。我们的下一个参数是我们窗口的句柄,这不在本书中介绍(请参阅 Windows 编程)。BehaviorFlags 将与 D3DCREATE_SOFTWARE_VERTEXPROCESSING 一起使用,这是另一个缓慢的选择。pPresentationParameters(为什么微软使用如此长的名称?)获取我们呈现参数结构的地址,而 ppReturnedDeviceInterface 获取我们要填充的指针的指针。因此,我们的调用如下所示
d3dobject->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, window, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpresent, &d3ddevice );
就是这样初始化设备的方法!当然,您几乎肯定想要一个更快、功能更强大的设备,您确实应该检查错误,这样即使您的程序崩溃,用户也不会丢失内存,但这将在下一节中介绍。
所有(良好)使用 DirectX 的商业游戏都会以正确的方式初始化设备。几乎所有(如果不是全部)都允许您选择其引擎运行的图形卡或适配器。因此,我们将从这里开始。确定可用的图形卡非常有用。首先,它为用户提供了一定程度的自由,这始终很重要。对于商业游戏,它还允许他们创建包含适合该卡的设置的列表,从而使用户更容易操作。我们当然会将此功能实现到我们的基准测试应用程序中,尽管这仅仅是为了第一个原因。您必须做的第一件事(当然是在初始化 IDirect3D9 接口之后)是确定可用的适配器数量。这是一个简单的函数
UINT IDirect3D9::GetAdapterCount( );
它返回可用的适配器数量。我们需要做的下一件事是获取 D3DADAPTER_IDENTIFIER9 结构数组。创建数组后,我们可以使用循环和调用来填充它
HRESULT IDirect3D9::GetAdapterIdentifier( UINT Adapter, DWORD Flags, D3DADAPTER_IDENTIFIER9 * pIdentifier );
如果参数有效,则它返回 D3D_OK,如果出现错误,则返回 D3DERR_INVALIDCALL。传递给适配器的值可以在 [0,GetAdapterCount()) 内。Flags 应为 0(除非我们希望 Windows 下载新的驱动程序证书,我们没有),而 pIdentifier 应该是当前索引的地址。现在,我们的代码如下所示
UINT adaptercount = d3dobject->GetAdapterCount( );
D3DADAPTER_IDENTIFIER9* adapters = ( D3DADAPTER_IDENTIFIER9* )malloc( sizeof( D3DADAPTER_IDENTIFIER9 ) * adaptercount );
for( int i = 0; i < adaptercount; i++ )
{
d3dobject->GetAdapterIdentifier( i, 0, &( adapters[i] ) );
}
然后,我们可以根据用户选择的视频卡(使用消息处理程序)或程序根据 D3DADAPTER_IDENTIFIER9 结构中的详细信息认为最佳的卡来选择应用程序将使用的适配器。
选择适配器后,我们可以使用 D3DCAPS9 结构找出它能做什么。不幸的是,这是一个有点漫长的过程。您需要调用
HRESULT IDirect3D9::GetDeviceCaps( INT Adapter, D3DDEVTYPE DeviceType, D3DCAPS9 * pCaps );
参数有点棘手。Adapter 和 pCaps 很简单,它们只是我们选择的适配器编号和我们要填充的 D3DCAPS 结构的指针。DeviceType 是问题。它必须使用另一个函数确定。我们可以使用以下方法检查硬件设备
HRESULT IDirect3D9::CheckDeviceType( UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT DisplayFormat, D3DFORMAT BackBufferFormat,
BOOL Windowed );
调用此函数后,我们可以根据它是否返回 D3D_OK 来检查设备类型是否有效。但是,参数需要另一个信息。对于 Adapter,我们提供适配器编号。DeviceType 是我们要检查的类型,应该是 D3DDEVTYPE_HAL。由于我们是窗口化的,因此应该是 TRUE。DisplayFormat 和 BackBufferFormat 是我们需要确定的,它们通常(对于全屏模式,它们必须)应该相同。它们可以通过嵌套的 if 语句在试错法中解决,从我们喜欢的格式开始,并遍历大多数格式。以下是有效的显示和后备缓冲区格式列表: http://msdn.microsoft.com/en-us/library/bb172558(VS.85).aspx 如果它从未返回 D3D_OK,则您只能使用 D3DDEVTYPE_REF 或 D3DDEVTYPE_SW。我们将跳过具有 alpha 字段的格式,因为它们不受全屏模式的支持,我们将找到 HAL 或 REF 的最佳格式,因为我们以后会需要它。以下是我们的代码的外观
//adapternum is to have been defined earlier in the program and contains the choice of adapter
//fullscreen is also to have been previously defined, and contains either TRUE or FALSE
D3DCAPS9 caps;
D3DFORMAT format;
D3DDEVTYPE devicetype; //We'll need this and format later
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_HAL, D3DFMT_X8R8G8B8, D3DFMT_X8R8G8B8, window ) != D3D_OK )
//Best format, almost always works
{
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_HAL, D3DFMT_X1R5G5B5, D3DFMT_X1R5G5B5, window ) != D3D_OK ) {
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_HAL, D3DFMT_R5G6B5, D3DFMT_R5G6B5, window ) != D3D_OK ) {
devicetype = D3DDEVTYPE_REF;
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_REF, D3DFMT_X8R8G8B8, D3DFMT_X8R8G8B8, window ) != D3D_OK ) {
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_REF, D3DFMT_X1R5G5B5, D3DFMT_X1R5G5B5, window ) != D3D_OK ) {
if( d3dobject->CheckDeviceType( adapternum, D3DDEVTYPE_REF, D3DFMT_R5G6B5, D3DFMT_R5G6B5, window ) != D3D_OK ) {
//Error - No valid format or device detected!
//You need to decide what to do - if it is windowed, you can try using D3DFMT_UNKNOWN
} else {
format = D3DFMT_R5G6B5;
} } else {
format = D3DFMT_X1R5G5B5;
} } else {
format = D3DFMT_X8R8G8B8;
} } else {
devicetype = D3DDEVTYPE_HAL;
format = D3DFMT_R5G6B5;
} } else {
devicetype = D3DDEVTYPE_HAL;
format = D3DFMT_X1R5G5B5;
} } else {
devicetype = D3DDEVTYPE_HAL;
format = D3DFMT_X8R8G8B8;
}
d3dobject->GetDeviceCaps( ( INT )adapternum, devicetype, &caps );
一些了解如何执行此操作的人可能会指出,您只需使用 IDirect3D9*->GetAdapterDisplayMode 即可确定窗口化应用程序的有效格式。我没有在这里执行此操作,因为它在全屏模式下不起作用,而上面的代码总是有效的。无论如何,您现在拥有该适配器的设备功能。我们该怎么用它呢?我们可以做的第一件事是找出硬件是否支持变换或光照。我们可以使用 caps 中的 DevCaps 变量来做到这一点。您只需使用一些按位运算即可查看它是否具有 D3DDEVCAPS_HWTRANSFORMANDLIGHT。如果有,我们将在 CreateDevice 中使用 D3DCREATE_HARDWARE_VERTEXPROCESSING 而不是 D3DCREATE_SOFTWARE_VERTEXPROCESSING。我们还能够提取有关我们可以通过它传递的其他信息,例如 D3DCREATE_PUREDEVICE(对调试不利,但对速度很有利),它需要硬件。
DWORD vtx_proc;
if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) {
vtx_proc = D3DCREATE_HARDWARE_VERTEXPROCESSING;
if( caps.DevCaps & D3DDEVCAPS_PUREDEVICE ) {
vtx_proc |= D3DCREATE_PUREDEVICE;
}
} else {
vtx_proc = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
}
我们还可以确定可以使用的呈现间隔。它将有效间隔存储为其 PresentationIntervals 字段中的位掩码。此页面上的任何值都可以使用: http://msdn.microsoft.com/en-us/library/bb172585(VS.85).aspx 。如果我们想查看设备是否支持某种形式,我们只需执行以下操作
DWORD presinterval;
if( caps.PresentationIntervals & D3DPRESENT_INTERVAL_FOUR )
{
presinterval = D3DPRESENT_INTERVAL_FOUR;
}
CAPS 结构中还有更多有关设备的信息,但目前用处不大(读:我不知道该如何使用它)。
获取有效的深度模板与获取有效格式非常相似。它需要调用
IDirect3D9::CheckDeviceFormat( UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT AdapterFormat, DWORD Usage,
D3DRESOURCETYPE RType, D3DFORMAT CheckFormat );
并在成功时返回 D3D_OK。非显而易见的参数是 Usage、RType 和 CheckFormat。Usage 指示我们要检查的格式类型;深度模板;并且应该是 D3DUSAGE_DEPTHSTENCIL。RType 指示我们使用的资源形式,应该是 D3DRTYPE_SURFACE。CheckFormat 是我们要检查的深度模板格式。确定此格式需要另一种试错法;并且使用格式列表中名称中带有“D”的格式: http://msdn.microsoft.com/en-us/library/bb172558(VS.85).aspx 。以下是一个示例
D3DFORMAT depthstencil;
if( d3dobject->CheckDeviceFormat( adapter, devicetype, format, D3DUSAGE_DEPTHSTENCIL, D3DRTYPE_SURFACE, D3DFMT_D32 ) != D3D_OK )
{
...
} else {
depthstencil = D3DFMT_32;
}
多重采样是另一个试错过程,幸运的是也是最后一个。我们将确定要使用的两个值:多重采样级别和多重采样的最大质量。这两个值都通过以下函数确定
HRESULT IDirect3D9::CheckDeviceMultiSampleType( UINT Adapter, D3DDEVTYPE DeviceType, D3DFORMAT SurfaceFormat,
BOOL Windowed, D3DMULTISAMPLE_TYPE MultiSampleType, DWORD* pQualityLevels );
如果多重采样类型有效,CheckDeviceMultiSampleType 返回 D3D_OK。它还会在 pQualityLevels 中提供最大质量级别。我们的 D3DFORMAT 参数应该是我们的后缓冲区格式,而 pQualityLevels 应该是 DWORD 的地址。D3DMULTISAMPLE_TYPE 可以是此处列出的任何值:http://msdn.microsoft.com/en-us/library/bb172574(VS.85).aspx 始终如一,以下是我们使用的示例
DWORD quality;
D3DMULTISAMPLE_TYPE multisample;
if( d3dobject->CheckDeviceMultiSampleType( adapter, devicetype, format, !fullscreen, D3DMULTISAMPLE_16_SAMPLES,
&quality ) != D3D_OK )
{
...
} else {
multisample = D3DMULTISAMPLE_16_SAMPLES;
}
我们的下一个目标是确定可用的最佳显示模式。与其他任务相比,这相当容易。有两种适用的方法可以做到这一点。第一个是简单地使用用户当前正在使用的模式。你可以通过对以下内容进行一次调用来实现这一点
HRESULT IDirect3D9::GetAdapterDisplayMode( UINT Adapter, D3DDISPLAYMODE * pMode );
pMode 是我们关心的返回值。它是一个结构,它提供了用户正在使用的分辨率、刷新率和格式。这种方法快捷简便,但适应性不如某些情况下所需的那样强。下一种方法更复杂,但适应性更强。你需要两个函数
UINT IDirect3D9::GetAdapterModeCount( UINT Adapter, D3DFORMAT Format );
和
HRESULT EnumAdapterModes( UINT Adapter, D3DFORMAT Format, UINT Mode, D3DDISPLAYMODE* pMode );
GetAdapterModeCount 很简单。它获取适配器和后缓冲区格式,并提供显示模式的数量。EnumAdapterModes 稍微复杂一些,但仍然很简单。它也获取适配器和后缓冲区格式,但它还需要一个数字和一个存储模式的位置。数字必须小于 GetAdapterModeCount 返回的值,而 pMode 只需要是某些可用的内存。你通常在循环中使用这两个函数来查找你想要的值。例如
D3DDISPLAYMODE dispmode, bestmode;
d3dobject->EnumAdapterModes( adapter, format, 0, &bestmode );
for( UINT i = 1; i < d3dobject->GetAdapterModeCount( adapter, format ); i++ )
{
d3dobject->EnumAdapterModes( adapter, format, i, &dispmode );
if( dispmode.Width > bestmode.Width )
{
bestmode.Width = dispmode.Width;
bestmode.Height = dispmode.Height;
bestmode.RefreshRate = dispmode.RefreshRate;
continue;
}
if( dispmode.Height > bestmode.Height )
{
bestmode.Height = dispmode.Height;
bestmode.RefreshRate = dispmode.RefreshRate;
continue;
}
if( dispmode.RefreshRate > bestmode.RefreshRate )
{
bestmode.RefreshRate = dispmode.RefreshRate;
continue;
}
}
此代码获取所选格式的绝对最佳显示模式,并将它放入 bestmode 中。
现在我们拥有了所有这些数据,我们可以按照应有的方式初始化我们的设备 - 快速且灵活!我们的第一个目标是填充 D3DPRESENT_PARAMETERS 结构。这是大部分数据存放的地方。以下是该结构的全部内容
struct D3DPRESENT_PARAMETERS {
UINT BackBufferWidth, BackBufferHeight;
D3DFORMAT BackBufferFormat;
UINT BackBufferCount;
D3DMULTISAMPLE_TYPE MultiSampleType;
DWORD MultiSampleQuality;
D3DSWAPEFFECT SwapEffect;
HWND hDeviceWindow;
BOOL Windowed;
BOOL EnableAutoDepthStencil;/
D3DFORMAT AutoDepthStencilFormat;
DWORD Flags;
UINT FullScreen_RefreshRateInHz;
UINT PresentationInterval;
}
以下是我们填充它的方法(以一种大部分线性的顺序)
d3dpresent.BackBufferWidth = bestmode.Width;
d3dpresent.BackBufferHeight = bestmode.Height;
d3dpresent.BackBufferFormat = format;
d3dpresent.MultiSampleType = multisample;
d3dpresent.MultiSampleQuality = quality;
d3dpresent.hDeviceWindow = window;
d3dpresent.Windowed = !fullscreen;
d3dpresent.AutoDepthStencilFormat = depthstencil;
d3dpresent.FullScreen_RefreshRateInHz = bestmode.RefreshRate;
d3dpresent.PresentationInterval = presinterval;
如果你注意到,我们没有填充所有参数。这是因为我们需要知道我们的应用程序将要做什么 - 我们对剩下的参数有(在很大程度上)自由的范围。我们第一个未填充的参数是 BackBufferCount。我们可以用 0 到 D3DPRESENT_BACK_BUFFERS_MAX 之间的任何值来填充它。通常我们希望它为 1。接下来是 SwapEffect。可以在此处找到有效的交换效果列表:http://msdn.microsoft.com/en-us/library/bb172612(VS.85).aspx。通常情况下,它是 D3DSWAPEFFECT_DISCARD。如果我们将 MultiSampleType 设置为除了 D3DMULTISAMPLE_NONE 之外的任何值,则它**必须**是 D3DSWAPEFFECT_DISCARD。现在我们需要设置 EnableAutoDepthStencil。它可以是 TRUE 或 FALSE。如果我们希望 Direct3D 管理深度模板(我们希望这样做),它应该是 TRUE。最后,我们需要为 Flags 获取一个值。它可以是 0 或此处列出的任何值:http://msdn.microsoft.com/en-us/library/bb172586(VS.85).aspx。我们将使用 0。以下是我们为其余参数编写的代码
d3dpresent.BackBufferCount = 1;
d3dpresent.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpresent.EnableAutoDepthStencil = TRUE;
d3dpresent.Flags = 0;
现在我们可以调用 CreateDevice 函数。如果你还记得,它看起来像这样
HRESULT IDirect3D9::CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS * pPresentationParameters, IDirect3DDevice9 ** ppReturnedDeviceInterface );
唯一需要解释的参数是 BehaviorFlags。BehaviorFlags 作为位掩码指定了许多内容。它可以包含的完整常量列表如下:http://msdn.microsoft.com/en-us/library/bb172527(VS.85).aspx。我们只将它用于我们想要的顶点处理类型。因此,我们只需插入我们的 vtx_proc 变量。这就是我们最终的设备初始化函数!
d3dobject->CreateDevice( adapter, devicetype, window, vtx_proc, &d3dpresent, &d3ddevice );
虽然不直接参与初始化,但讨论释放你在初始化部分分配的内存是很有必要的。使用 Direct3D 对象创建的所有设备以及对象本身都必须使用其 Release 函数释放。在这里,我们只分配了设备和对象,因此我们需要执行以下操作
d3ddevice->Release( );
d3dobject->Release( );