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

MFC D3D 应用程序 - Direct3D 教程第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (13投票s)

2006 年 10 月 10 日

CPOL

17分钟阅读

viewsIcon

82626

downloadIcon

4

带有自定义 Direct3D 框架的分步 MFC + D3D 教程的第二部分。

引言

这是自定义 Direct3D 框架和 3D 教程的第二部分,封装在 CXD3D 类中。在 第一部分 中,我们介绍了​​一些 3D 概念、Direct3D 架构以及枚举对象;您也可以在那里找到演示项目。在本章中,我们将介绍从枚举结果中选择设置到渲染循环。

CXD3DSettings 类

当显卡适配器、显示模式和设备设置的枚举成功后,CXD3D::CreateD3D 会继续选择最佳的初始渲染设置。这意味着框架将填充一个 Settings 结构,这是 CXD3D 类的另一个内部设置对象。

//----------------------------------------------------------
// CXD3DSettings: Current D3D settings: adapter, device,
// mode, formats, etc.
//----------------------------------------------------------
class CXD3DSettings
{
public:
    AdapterInfoPtrArray AdapterInfos;

    UINT Windowed;

    UINT ndm[2]; // indices to the best dm within each ai
    UINT ndi[2]; // indices to the best di within each ai
    UINT ndc[2]; // indices to the best dc within each ai

    // indices into dc properties that can be changed
    UINT nDSFormat;

    // constructor
    CXD3DSettings();

    // 'Get' wrappers
    AdapterInfo*   GetAdapterInfo();
    DeviceInfo*    GetDeviceInfo();
    D3DDISPLAYMODE GetDisplayMode();

    VPTYPE              GetVPType();
    D3DFORMAT           GetBackBufferFormat();
    D3DFORMAT           GetDSFormat();
    D3DMULTISAMPLE_TYPE GetMSType();
    DWORD               GetMSQuality();
    DWORD               GetPresentInterval();

    // 'Set' wrappers
    void SetDSFormat(UINT nFmt);
};

此类函数的实现方式与原始 SDK 框架完全不同,但功能完全相同。

首先,我们有一个 AdapterInfo 指针数组,其中索引 0 存储最佳全屏适配器信息,索引 1 存储最佳窗口化适配器信息。接下来,我们有一个 Windowed 标志(0 或 1),用作前面数组的索引。

反过来,ndmndi UINT 数组将保存每个 AdapterInfo 中最佳 DisplayModeDeviceInfo 的相应索引;ndc UINT 数组保存每个 DeviceInfo 中最佳 DeviceCombo 的索引。这一切只不过是嵌套索引的集合,保存对枚举所提供的最佳设置的引用,而不是复制枚举结果。换句话说,Settings 对象始终指向 Enumeration 类所拥有的最佳 D3D 设置选择。

设置由 CXD3D::ChooseInitialSettings 初始化,该函数会分支到 CXD3D::FindBestFullscreenModeCXD3D::FindBestWindowedMode,两者都接受一组标志,指示我们是否需要 HAL 设备或参考设备。让我们逐一查看。

查找最佳渲染设置

FindBestFullscreenMode 遍历枚举的适配器信息。对于每个适配器,它会将当前的显示模式(桌面显示模式)保存在一个临时变量中。完成后,它会遍历每个适配器的 DeviceInfos,跳过那些不符合函数参数类型的设备。在第三个循环中,它会遍历每个 DeviceInfoDeviceCombo。我们只是向下遍历树状枚举结构(请记住检查 第一部分 的枚举结构图),直到我们到达一个 DeviceCombo

窗口化设备组合将被跳过;每个全屏 DeviceCombo 都将与当前的最佳组合进行比较,以最终确定哪个是真正的最佳组合,或者,如果不行,哪个“优于”任何其他组合。

最初,没有最佳 DeviceCombo,因此迭代中的第一个被视为最佳(字面意思是“比没有好”)。在后续的迭代中,HAL DeviceCombo 优于非 HAL DeviceCombo,当它俩的显示格式和后备缓冲区格式都与桌面匹配时,它就成为最佳,这意味着设备可以采用当前的显示模式,并且不需要颜色转换。

一旦我们得到了最佳 DeviceCombo(或“比没有好”的组合),我们就需要找到最佳 AdapterInfo 上使用其显示格式的显示模式,该模式在尺寸和刷新率方面尽可能接近最佳桌面显示模式。显然,最佳 AdapterInfo 是拥有最佳 DeviceInfo 的那个,而最佳 DeviceInfo 又是拥有最佳 DeviceCombo 的那个。

这种相当复杂的逻辑可以产生最佳的全屏设置,在可能的情况下,这些设置不会改变桌面的当前显示模式,并且不需要任何颜色转换。

FindBestWindowedMode 函数采用类似的方法,只是它永远不会改变显示模式;它将强制使用主适配器的当前显示模式,其假设是窗口化应用程序将始终在主适配器上启动。

指向最佳全屏和窗口化 AdapterInfo 的指针与索引一起保存为 Settings,这些索引指向其中最佳的 DisplayModeDeviceInfoDeviceCombo

这是框架在多显示器设置中可能出现严重问题的地方。拥有两个或更多显示适配器的 PC,或者具有多头显示的适配器,可能正在使用 Windows 的桌面扩展功能,或者每个适配器可能具有不同的显示格式。更糟糕的是,有些软件(如 MaxiVista)允许显示跨越网络中的多个显示器,并且可能还有其他正在运行的应用程序拥有独占的适配器所有权。

我没有在多显示器设置中测试过默认框架或此版本的框架,但直觉告诉我,当最佳窗口化适配器不是主适配器时,事情可能会变得糟糕;不过,让窗口化应用程序在辅助适配器上创建一个全屏渲染目标,同时在主适配器上显示菜单、对话框、工具栏和控件等标准 Windows 内容,对于关卡编辑器应用程序来说将很酷。如果能让全屏应用程序渲染到三个适配器,就像Microsoft Flight Simulator 所做的那样,那将更棒,但就本文而言,渲染到多个显示器是一个高级主题,我不会在此涵盖。请参阅 SDK 帮助中的Multihead 主题以获取更多信息。

设置类还提供了一组针对每个特定设置的“Get”包装器。当我们需要当前的深度/模板格式时,我们通过 Settings 对象使用 GetDSFormat 包装器来获取它。

D3DFORMAT CXD3DSettings::GetDSFormat()
{
    return (D3DFORMAT)AdapterInfos[Windowed]->
            DeviceInfos[ndi[Windowed]].
            DeviceCombos[ndc[Windowed]].
            DSFormats[nDSFormat];
}

(请忽略换行符;VC++ 编译器反正会忽略。)对于特定的设置,这可能会评估为

AdapterInfos[1]->DeviceInfos[0].DeviceCombos[2].DSFormats[2] == D3DFMT_D24X8

所以,你可以看出应用程序仔细设置正确索引的重要性。

列表中可能不止一种深度/模板格式,因此为了支持从例如索引 0 的 D3DFMT_D16 更改为索引 2 的 D3DFMT_D24X8,该类会跟踪深度/模板格式索引,在 nDSFormat UINT 中,由设置构造函数初始化为 0,并通过 SetDSFormat 更改,仅进行范围检查。

void CXD3DSettings::SetDSFormat(UINT nFmt)
{
    if (nFmt < AdapterInfos[Windowed]->
        DeviceInfos[ndi[Windowed]].
        DeviceCombos[ndc[Windowed]].
        DSFormats.Length())
        nDSFormat = nFmt;
}

由于这个改进的框架不允许更改设置,我只提供了上述 nDSFormat 索引和相应的“Get/Set”函数,作为其余“Set”包装器的指南;此时,如果您想为用户提供更改设置的方法,您应该能够自己完成,就像原始 SDK 框架通过设置对话框那样。

现在我们已经选择了最佳渲染设置,是时候初始化 Direct3D 环境了,也就是说,使用这些设置创建 Direct3D 设备。

创建设备

CXD3D::CreateD3D 成功创建 Direct3D 对象、枚举所有适配器、显示模式和设备,并从中选择最佳设置后,就可以创建 Direct3D 设备了。

不过,在此之前,它会给应用程序一个机会来创建或初始化任何不依赖于设备的自定义内容,通过调用 CXD3D::OneTimeSceneInit。这是一个可重写的函数,在基类实现中返回 S_OK非设备对象可能是光标Direct3D 字体Direct3D 网格对象或任何应用程序定义的 other objects。此函数与 CXD3D::FinalCleanup 配对,在其中应释放或删除此类对象。

CreateDevice Direct3D API(应用程序编程接口)函数接受以下参数:

  • 要创建设备的所需的适配器序号
  • 所需的设备类型
  • 一个窗口句柄
  • 一个创建行为,包含 VP 类型,以及可选的纯设备请求;
  • 一个演示参数结构,以及
  • 一个 DIRECT3DDEVICE 指针,用于返回设备。

请注意,从现在开始,我可能会以简略的方式称呼 Direct3D API 函数(或结构),例如 CreateDevice API。

回到当前主题,此时,框架拥有了大多数参数(要么来自枚举,由设置包装,要么来自应用程序本身)的(有效)值,所以让我们来检查一下尚未讨论的演示参数结构。请注意,这不是一个框架定义的结构;它是一个 Direct3D API 结构。

typedef struct _D3DPRESENT_PARAMETERS_
{
    UINT      BackBufferWidth;
    UINT      BackBufferHeight;
    D3DFORMAT BackBufferFormat;
    UINT      BackBufferCount;

    D3DMULTISAMPLE_TYPE MultiSampleType;
    DWORD               MultiSampleQuality;

    D3DSWAPEFFECT SwapEffect;
    
    HWND hDeviceWindow;
    
    BOOL Windowed;

    BOOL      EnableAutoDepthStencil;
    D3DFORMAT AutoDepthStencilFormat;
    
    DWORD Flags;

    /* FullScreen_RefreshRateInHz must be zero for Windowed mode */
    UINT FullScreen_RefreshRateInHz;
    UINT PresentationInterval;
} D3DPRESENT_PARAMETERS;

演示参数从设置类中选择;这不是一对一的对应关系,因此 CXD3D 函数负责将一个填充成另一个。

//----------------------------------------------------------
// BuildPresentParamsFromSettings(): 'builds' presentation
// parameters from the current settings
//----------------------------------------------------------
void CXD3D::BuildPresentParamsFromSettings()
{
    m_d3dpp.Windowed = Settings.Windowed;

    m_d3dpp.hDeviceWindow = m_hWndRender;

    m_d3dpp.BackBufferCount = 1;
    
    m_d3dpp.EnableAutoDepthStencil = Enumeration.AppUsesDepthBuffer;

    m_d3dpp.MultiSampleType    = Settings.GetMSType();
    m_d3dpp.MultiSampleQuality = Settings.GetMSQuality();
    
    m_d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    
    m_d3dpp.Flags = 0;

    if (Enumeration.AppUsesDepthBuffer)
    {
        m_d3dpp.Flags = D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL;
        m_d3dpp.AutoDepthStencilFormat = Settings.GetDSFormat();
    }

    if (Settings.Windowed)
    {
        m_d3dpp.BackBufferWidth  = m_rcClient.right - m_rcClient.left;
        m_d3dpp.BackBufferHeight = m_rcClient.bottom - m_rcClient.top;
        m_d3dpp.FullScreen_RefreshRateInHz = 0;
    }
    else
    {
        m_d3dpp.BackBufferWidth  = Settings.GetDisplayMode().Width;
        m_d3dpp.BackBufferHeight = Settings.GetDisplayMode().Height;
        m_d3dpp.FullScreen_RefreshRateInHz =
        Settings.GetDisplayMode().RefreshRate;
    }
    
    m_d3dpp.BackBufferFormat = Settings.GetBackBufferFormat();
    m_d3dpp.PresentationInterval = Settings.GetPresentInterval();
}

大多数参数直接来自设置,一个来自枚举,一些是硬编码的;让我们来回顾一下我们尚未讨论的参数。

  • 后备缓冲区数量:0(视为 1)、1、2 或 3。通常,应用程序使用单个后备缓冲区,也称为双缓冲。更多的缓冲区可以使您的帧率更平滑,但它们也可能导致输入延迟(按下按键到看到结果之间的延迟),并且它们还会消耗额外的内存。
  • 交换效果:还记得交换链吗?交换效果指示在呈现后对后备缓冲区的处理方式;要么丢弃其内容,要么保留。在内存消耗和性能方面,使用 D3DSWAPEFFECT_DISCARD 作为此参数将始终更有效。如果您的应用程序直接操作后备缓冲区,您将需要其他两种类型之一,即翻转复制,但框架默认会丢弃它们。此外,调整或玩弄后备缓冲区是一个高级主题,仅仅设置此标志是不够的;请在此处查看 SDK 帮助以获取更多信息。
  • EnableAutoDepthStencil 标志:如果设置为 true,表示设备将自动创建一个深度/模板缓冲区,并且 Direct3D 将管理它。在框架中,它默认设置为 true,这意味着它还必须在 AutoDepthStencilFormat 中设置格式,并且必须设置 D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL,以启用以与后备缓冲区相同的方式丢弃缓冲区,再次提高性能。顺便说一句,Direct3D 的调试运行时版本将在呈现深度/模板缓冲区后强制执行此标志。
  • FullScreen_RefreshRateInHz:显示适配器刷新屏幕的速率。对于窗口化模式,此值必须为 0 或 D3DPRESENT_RATE_DEFAULT,告诉运行时选择演示速率或采用当前速率;对于全屏模式,此值必须是 EnumAdapterModes API 返回的刷新率之一,或者再次是 D3DPRESENT_RATE_DEFAULT

因此,在用选定的设置填充演示参数后,我们终于可以发布 Direct3D 设备的创建了。

当设备成功创建后,我们将填充设备统计信息字符串,在我的笔记本上它显示为HAL software VP on S3 Graphics SuperSavage/IXC 1014,但在您的 PC 上,它可能显示为HAL pure hardware VP on NVIDIA Multi-GPU GeForce 7950 GX2,在这种情况下,我会嫉妒得脸红。

此时,在 3D 环境初始化中,我们已经有了一个设备,因此我们可以初始化任何设备相关对象。现在是另一个休息的好时机,因为我们将更深入地探讨 Direct3D 的内部工作原理。

设备相关对象,又名资源

设备相关对象是通过 IDirect3DDevice9 接口创建的对象(在框架中,是 CreateDevice 返回的 m_pd3dDevice 对象),它们就是Direct3D 资源:用于渲染 3D 场景的顶点缓冲区索引缓冲区纹理,保存渲染目标像素数据的表面后备缓冲区前置缓冲区深度/模板缓冲区,以及用于创建体积纹理的表面,无论它们是做什么用的。

资源具有一组属性,这些属性定义了它们的类型(顶点缓冲区、表面等)、它们的,或者它们将在哪种类型的内存中创建(例如,系统内存、显存等)、它们的格式(例如,2D 表面的像素格式)以及它们的用途,定义了资源将如何使用,例如作为渲染目标、纹理等。

框架明确区分可以自行恢复设备重置的资源和必须在设备重置后重新创建的资源。重置设备是一个重要主题,涉及设备的正常(运行)状态和丢失(非运行)状态,以及创建设备对象(资源)的池,因此我们将稍微偏离主题来解决这个问题。

当处于全屏模式且用户按下 Alt+TAB(将键盘焦点转移到另一个应用程序)时,当处于窗口化模式且窗口大小发生变化时,当初始化系统对话框时,当发生电源管理事件时,或者当另一个应用程序声明独占全屏操作时,Direct3D 设备会“丢失”。这些只是典型场景,但希望您明白了:有时,某些事情会“吓跑”设备,应用程序必须等到它回来;那件事可能像用户切换到计算器或调整应用程序窗口一样简单,但也可能像防病毒警告一样重要。

框架的处理设备重置的方式如下:在应用程序的渲染循环中,最终在设备窗口上呈现资源的那个循环,设备的 Present 方法可能会失败,返回 D3DERR_DEVICELOST,在这种情况下,我们会相应地设置“设备已丢失”状态标志。在接下来的迭代中,我们检测到该状态,并测试设备的服务级别,直到它返回 D3DERR_DEVICENOTRESET,表明它不再丢失,但需要重置;因此,它会调用环境重置,其中任何不会在重置中幸存的设备对象在设备重置之前失效(释放),并在设备成功重置之后恢复(重新创建)。明白了吗?

所有这些复杂性都归结为常识性的内存(资源)管理。我们有充分的理由调用 CXD3D::InvalidateDeviceObjects(我们刚刚丢失了设备!)。使资源失效相当于释放资源(因为它们是 COM 接口),特别是那些无法在重置中幸存的资源。在重新获得设备的情况下,我们在设备重置后,在最初创建它们的地方调用 CXD3D::RestoreDeviceObjects。顺便说一句,这类资源正是那些在 D3DPOOL_DEFAULT 内存池中创建的资源(本质上是显存)。

相比之下,CXD3D::InitDeviceObjects 是创建可以在重置后幸存的资源的地方,也就是那些在 D3DPOOL_MANAGEDD3DPOOL_SYSTEMMEMD3DPOOL_SCRATCH(本质上是非显存)中的资源。它与 CXD3D::DeleteDeviceObjects 配对,我们在此处释放这些资源,这次是响应应用程序的关闭。

好的,让我们稍微回顾一下:资源是通过设备接口创建的,您可以选择在哪个内存中创建它们。

  • 默认池资源放置在首选的设备访问内存中,前面已提到为驱动程序最优内存:本地显存和/或加速图形端口 (AGP) 内存(尽管如果驱动程序决定,它也可以是系统内存)。此池中的资源也称为设备敏感,因为它们必须满足设备在大小和格式方面的要求。在默认池中分配的顶点缓冲区资源在几乎所有情况下都能提供最佳性能
  • 托管资源会在需要时自动复制到显存,但始终以系统内存为后备。它们也是设备敏感的。对于大多数资源,托管是首选的内存池选择,但(也许)不包括顶点缓冲区。
  • 系统内存资源驻留在 PC 的物理和/或虚拟 RAM 中,设备通常无法访问,因此最适合您不想直接渲染的资源,例如将用于更新默认池中的其他纹理或表面的纹理或表面。
  • 临时资源也创建在系统 RAM 中,但设备永远无法访问它们,这意味着它们不能用作纹理或渲染目标。另一方面,它们不受设备大小或格式的限制,这意味着您可以使用任何格式来创建它们,因此它们是设备不敏感的。好的例子包括具有专有格式的纹理,非常大的纹理,应用程序将将其转换为或切成更小的、对设备友好的纹理,以及一个离屏平面表面,用于保存 3D 场景的快照。

您的应用程序中资源的内存池选择高度取决于它们的类型和用途,但当您有选择时,您必须再次权衡前面提到的性能与视觉质量之间的普遍权衡;在我笔记本的简陋 HAL 软件 VP 设备中,我没有太多选择,除了让 Direct3D 管理大多数资源,但纯粹的设备可能能够实现 3 倍的帧率,如果资源管理设置正确的话。

这就是关于资源的内容:在 InitDeviceObjects 中创建一些资源,在 RestoreDeviceObjects 中创建一些资源;在重置前使后者失效,在重置后重新创建它们;在关闭时,确保在 DeleteDeviceObjects 中清理前者;仔细选择在哪里创建什么,并调整选择来测试您全新的、顶级的、酷炫的显卡。

我知道这信息量太大,我甚至还没有提到丢失设备的其他细微之处。只需想象一下用户在某个时候从您的 3D 游戏切换到计算器;重置逻辑到位,可以暂停 3D 应用程序并为操作系统和其他应用程序释放一些内存,即在重新获得焦点时不会对 3D 应用程序性能产生明显影响的内存。一般来说,几何资源(顶点和索引缓冲区)在默认内存池(理想情况下是显存)中是最佳放置的,以便在设备丢失时可以丢弃它们,并在应用程序恢复时重新创建它们,但是重新加载时间最长的资源(即纹理或其他应用程序定义的 other objects)应该在重置过程中保留在内存中,以使过渡稍微平滑一些。当然,这只是一个指南,您会随着时间的推移学会管理内存。请记住,该逻辑涵盖了一个罕见的事件,也涵盖了应用程序关闭,因此为了避免任何内存泄漏,就像任何 Windows 应用程序一样,在某个时候进行清理是有意义的。

InitializeEnvironment 成功

我们创建了一个 Direct3D 设备并用它来创建资源,但仍然可能有什么东西失败了!当 HAL 的创建真正失败时,框架将尝试切换到参考设备,显示警告消息,然后重试,但参考设备可能根本无法渲染,在这种情况下,您也无能为力(除了切换到 Direct3D 的调试运行时版本,即零售版本,但没有保证)。无论如何,如果一切按计划进行,您应该会得到一个 Direct3D HAL 设备,现在应用程序可以开始渲染循环了。

渲染循环

//----------------------------------------------------------
// RenderEnvironment(): handles device resets and issues the
// app defined FrameMove and Render that will actually draw
// the scene.
//----------------------------------------------------------
HRESULT CXD3D::RenderEnvironment()
{
    HRESULT hr;

    if (m_bDeviceLost)
    {
        // test the cooperative level to see if it's okay
        // to render
        if (FAILED(hr = m_pd3dDevice->TestCooperativeLevel()))
        {
            // if the device was truly lost, (i.e., a
            // fullscreen device just lost focus), wait
            // until we get it back
            if (hr == D3DERR_DEVICELOST)
                return S_OK;

            // eventually, we will get this return value,
            // indicating that we can reset the device
            if (hr == D3DERR_DEVICENOTRESET)
            {
                // if we are windowed, read the desktop mode
                // and use the same format for the back buffer;
                // this effectively turns off color conversion
               if (m_bWindowed)
               {
                   m_pd3d->GetAdapterDisplayMode(
                   Settings.GetAdapterInfo()->AdapterOrdinal,
                   &Settings.GetDisplayMode());

                    m_d3dpp.BackBufferFormat =
                    Settings.GetDisplayMode().Format;
               }

               // now try to reset the device
               if (FAILED(hr = ResetEnvironment()))
                   return hr;
            }

            return hr;
        }

        // we have a device
        m_bDeviceLost = false;
    }

    // setup the app's timers
    FLOAT fTime        = DXUtil_Timer(TIMER_GETAPPTIME);
    FLOAT fElapsedTime = DXUtil_Timer(TIMER_GETELAPSEDTIME);

    // skip rendering if no time elapsed (the application
    // is paused)
    if (fElapsedTime == 0.0f)
        return S_OK;

    // store the time
    m_fTime        = fTime;
    m_fElapsedTime = fElapsedTime;

    // move the scene
    if (FAILED(hr = FrameMove()))
        return hr;

    // render the scene as normal
    if (FAILED(hr = Render()))
        return hr;

    // update the frame stats (fps)
    UpdateStats();

    // present the next buffer in the swap chain
    if (m_pd3dDevice->Present(NULL,
                                 NULL,
                                 NULL,
                                 NULL) == D3DERR_DEVICELOST)
        m_bDeviceLost = true;

    return S_OK;
}

有两个明显不同的部分:一个处理设备丢失状态(如前所述),另一个实际调用渲染函数。此外,如果应用程序处于暂停状态,则什么也不会发生。

框架的计时由 dxutil.h/dxutil.cpp 中的 DXUtil_Timer 函数处理,该函数是 QueryPerformanceFrequency() Windows API(或其缺陷中的 timeGetTime())的包装器。顺便说一句,RenderEnvironment 函数是框架中唯一查询经过时间的地方,所以它实际上是调用之间经过的时间。

每当设备处于运行状态并且调用之间经过一段时间时,演示将在两个单独的重写中进行准备:CXD3D::FrameMove 负责动画,CXD3D::Render 负责绘制。

//----------------------------------------------------------
// FrameMove: just rotate the world about the y-axis
//----------------------------------------------------------
HRESULT CD3DWnd::FrameMove()
{
    // just rotate the world about the y-axis
    FLOAT fTime = DXUtil_Timer(TIMER_GETABSOLUTETIME);

    D3DXMATRIX matWorld;

    D3DXMatrixRotationY(&matWorld, fTime / 150.0f);

    m_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld);

    return S_OK;
}
...
//----------------------------------------------------------
// Render: CXD3D overload that actually draws something
//----------------------------------------------------------
HRESULT CD3DWnd::Render()
{
    HRESULT hr;

    // clear the entire viewport with black
    m_pd3dDevice->Clear(0,
                           NULL,
                           D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
                           D3DCOLOR_XRGB(0,0,0),
                           1.0f,
                           0L);


    if (SUCCEEDED(hr = m_pd3dDevice->BeginScene()))
    {
        // set a material (no texture)
        m_pd3dDevice->SetMaterial(&m_Material); 
        m_pd3dDevice->SetTexture(0, NULL);

        // render the contents of some vertex buffer
        if (m_pVB)
        {
            m_pd3dDevice->SetStreamSource(0,
                                             m_pVB,
                                             0,
                                             sizeof(D3DXVECTOR3));

           m_pd3dDevice->DrawPrimitive(D3DPT_LINESTRIP, 0, nVertices);
        }

        // end the scene
        m_pd3dDevice->EndScene();
    }

    return S_OK;
}

请注意,这些只是关于在这些函数中做什么的示例重写,但一般来说,FrameMove 是应用所有矩阵代数和几何变换以使场景动画化的地方,而 Render 是清除视口(强制)并在(同样强制的)BeginScene/EndScene 对内调用设备方法来绘制场景的地方。

另外请注意,这两个函数中几乎发生的一切都通过设备的“Set”调用进行;这些方法定义了设备的渲染状态(3D 对象显示的内容、时间、地点和方式)。

想象一个第一人称射击 3D 游戏;首先渲染背景中的天空,然后在背景中渲染天际线的山脉,然后是地形,从左到右,从前到后延伸,然后是地形上的建筑物,然后是移动的敌人,然后是车辆或射击者,这是实际的视点,也称为“眼睛”或“摄像机”,也在移动,最后是所有东西之上的 HUD(平视显示器)。

提到的每个对象都由一个或多个资源在 Direct3D 中表示,按逻辑顺序呈现给设备;有些是静态的,有些是移动的(从一帧到下一帧进行变换),有些可能有时被遮挡,有时可见,一个对象始终可见,一个对象不断改变视图。渲染状态包含了整个动态场景。

当然,在我们能够呈现我们梦想的场景之前,还有一套关键的 3D 概念和 Direct3D 方法需要我们掌握。有大量的书籍和网页讨论这些,包括 SDK 帮助,但我承诺在这里分享我学到的一切,所以请准备好下一期的系列文章,我将深入探讨 3D 变换、3D 向量和矩阵代数、3D 几何,总的来说,以及它在 Direct3D 中的工作原理。

天才百分之一是灵感,百分之九十九是汗水 - 托马斯·爱迪生。

© . All rights reserved.