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

D3DImage 入门

starIconstarIconstarIconstarIconstarIcon

5.00/5 (72投票s)

2008 年 8 月 11 日

CPOL

24分钟阅读

viewsIcon

436225

downloadIcon

18564

.NET 3.5 SP1 来了!是时候展示你的 DirectX 技能了。本文提供了开始使用 WPF 中一个名为 D3DImage 的新 DirectX 互操作功能所需的信息。

引言

不要被名称所迷惑。Windows Presentation Foundation (WPF) 的最新版本,包含在 Microsoft .NET Framework 3.5 版本 Service Pack 1 中,它不仅仅是一个服务版本。它包含了几个全新且备受期待的功能,包括硬件加速位图效果和更好的 DirectX 互操作。大多数新功能已在去年五月由 Tim Sneath 的原始公告 中描述过,该公告针对 SP1 的测试版。

本文的目的是介绍一个名为 D3DImage 的新功能。D3DImage 是一个全新的 ImageSource 对象,它通过允许自定义 Direct3D (D3D) 表面与 WPF 的原生 D3D 表面混合,实现了 WPF 和 DirectX 之间更高水平的互操作性。尽管 Tim 在早期的公告中宣称了 D3DImage,但它实际上并未在 SP1 的测试版中提供。但是,它已在刚刚发布的黄金版本中可用。

在本文中,我们将看一个非常简单的 D3D 互操作示例。尽管我们的示例演示了一个自定义的 3D 场景,但请记住 D3D 不仅仅用于渲染 3D 内容。从 DirectX 版本 8 开始,D3D 已经包含了 DirectDraw(以前的 2D DirectX 图形技术)。因此,D3D 现在负责渲染 2D 和 3D 图形。

本文的范围

本文的主要目标是向开发人员提供开始在 WPF 应用程序中使用 D3DImage 所需的信息。要编译随附的示例,需要一个配置为同时支持 C# 和 C++ 项目的 Visual Studio 版本。建议使用 Visual Studio 2008 的 Service Pack 1;但是,仅使用 Microsoft .NET Framework 3.5 版本 Service Pack 1 也可以构建示例。如果您在构建示例时遇到问题,请参阅本文末尾的标题为 "构建 D3DImage 示例的提示" 的部分。

为了保持专注并使示例简洁,我特意选择了一个非常简单的 3D 场景。实际上,我只是从 DirectX SDK 中的示例(标题为 "教程 3:使用矩阵")中借用了一个场景。它只不过是一个绕 Y 轴旋转的多色三角形。这是我能找到的最简单的 D3D 示例,为了本文的目的,它甚至进一步简化了。需要明确的是,这并非旨在作为高质量 D3D 代码的示例。非托管代码特意保持极简。我已将其简化为一个代码文件。

在本文中,我们将更多地关注从 WPF 侧使用 D3DImage 对象。我们肯定会查看 D3D 要求(就创建正确类型的设备和表面而言),但我们不会花费任何时间解释纯 D3D 概念,这远远超出了 D3DImage 简介的范围。对于刚接触 DirectX 开发并计划使用 D3DImage 实现互操作解决方案的人员,绝对应该计划阅读一本关于 D3D 的好书。

什么是 ImageSource?

首先... WPF 中的 ImageSource 究竟是什么?嗯,顾名思义,ImageSource 是一个 MIL 对象*,可以作为渲染图像的源。ImageSource 最明显的用法是设置 Image 元素本身的 Source 属性。ImageSource 还用于指定其他 MIL 对象的源,如 ImageBrushImageDrawing

* “MIL 对象”指的是可通过 WPF 的媒体集成层 (MIL) 通过特定 MIL 接口(如 DUCE.IResource)直接访问的托管对象。MIL 是 WPF 中大部分非托管部分,负责图形和媒体渲染。它建立在 DirectX 之上。到目前为止,我们还无法将自定义 DirectX 表面与 MIL 的 DirectX 表面直接集成,但 D3DImage 现在赋予了我们这种能力。

我们可以使用 ImageSource 创建 Brush 的事实非常重要,因为它允许图像在任何使用 Brush 的地方渲染。在 WPF 中,Brush 可用于渲染许多视觉元素的背景,以及文本元素的前景,以及形状的描边和填充。通过利用 ImageBrush,所有此类视觉元素都可以使用图像进行渲染。

这使我们能够用图像制作出以下两种画笔

然后,做些这样的傻事

XAML

<Grid Background="{StaticResource IceBrush}">
  <TextBlock Foreground="{StaticResource FireBrush}"
      Text="Fire & Ice" />
</Grid>

好吧... 没那么引人注目... 我想这就是我永远不会成为设计师的原因。但在才华横溢的设计师手中,ImageBrush 是一种非常强大的工具。(我想你只能相信我的话了!)

D3DImage 是 ImageSource

如前所述,D3DImage 一个 ImageSource。因此,它允许将 3D 场景用作 Image 的源,或者更重要的是,用作 ImageBrush 的源。这意味着 D3D 表面可以绘制到任何通过 Brush 渲染的 WPF 元素上。因此,现在,有了 .NET 3.5 SP1,ImageBrush 可能会成为一个更强大的工具,前提是你的员工中有一位具备 D3D 技能的开发人员。

D3DImage 登场... 气隙限制退出

D3DImage 对象首先是一个互操作对象。它接受一个已使用纯非托管 D3D 代码创建和渲染的 D3D 表面,并将该表面交给 MIL,以便它可以与渲染的 WPF 场景混合。如果你熟悉迄今为止存在的 D3D 互操作支持,你会知道这种新方法是一个巨大的进步。

到目前为止,如果我们需要在 WPF 应用程序中托管非托管 D3D 表面,我们一直受限于涉及托管 HWND 的解决方案。这意味着我们必须解决某些“气隙”问题,如 此 MSDN 主题 中所述。简而言之,使用 SP1 之前的版本,我们被迫放弃 DirectX 表面存在的窗口区域,因为该区域由托管该表面的 HWND 拥有。因此,我们无法执行诸如在托管 D3D 场景上方通过 Alpha 混合呈现基于 WPF 的文本和图形注释之类的操作。

这些类型的气隙限制在 WPF 这样的框架中代表着巨大的局限性,在 WPF 中,元素组合用于创建非常丰富的用户体验。使用 D3DImage 解决方案,这些限制将不复存在!

将自定义 D3D 场景与 WPF 场景组合

本文附带的代码示例演示了如何将自定义 D3D 渲染目标与 WPF 应用程序的渲染目标混合。让我们首先从宏观层面介绍示例的托管端(WPF 代码),然后,我们将更仔细地查看一些细节。

WPF 应用程序首先创建一个 D3DImage,然后将其设置为 ImageBrush 的源。然后将 ImageBrush 作为资源存储在应用程序的 Window 上,以便在解析 XAML 时可用。

所有执行此操作的代码都包含在 Window 的构造函数中,如下所示

public Window1()
{
    // create a D3DImage to host the scene and
    // monitor it for changes in front buffer availability
    _di = new D3DImage();
    _di.IsFrontBufferAvailableChanged += OnIsFrontBufferAvailableChanged;
 
    // make a brush of the scene available as a resource on the window
    Resources["RotatingTriangleScene"] = new ImageBrush(_di);
 
    // begin rendering the custom D3D scene into the D3DImage
    BeginRenderingScene();
 
    // parse the XAML
    InitializeComponent();
}

我们稍后将仔细研究此例程的细节。首先,让我们看看 WPF 场景的标记,它包含在 Window1.xaml 中,如下所示

<Window x:Class="D3DImageSample.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Name="RootWindow" Title="Window1" 
    Height="300" Width="300">
  <Window.Background>
    <ImageBrush ImageSource="Forest.jpg" />
  </Window.Background>
  <Grid Background="{StaticResource RotatingTriangleScene}" />
</Window>

此 XAML 仅包含一个包含 GridWindowWindow 的背景用森林的 ImageBrush 绘制。这仅仅是为了创建一个有趣的背景,用于构成我们的自定义 D3D 场景。Grid 的背景用我们的 D3D 场景绘制,它仍然只是一个旋转的三角形。

当您构建并运行应用程序时,您会看到如下所示的内容(尽管这些图像无法展现实际场景的保真度)

多亏了 WPF 对分层窗口的支持,我们甚至可以更进一步,完全去除窗口边框。这将允许我们将自定义 D3D 内容直接渲染到屏幕,如下所示

好吧...好吧...可以说,除非你需要将其与 WPF 内容混合,否则你永远不会选择在 WPF 中托管 D3D 场景...但是,你可以这样做真是太酷了!

创建 D3DImage

让我们仔细看看 D3DImage 对象本身,我们使用 Window1 构造函数中的以下代码行创建并初始化了它

    // create a D3DImage to host the scene and
    // monitor it for changes in front buffer availability
    _di = new D3DImage();
    _di.IsFrontBufferAvailableChanged += OnIsFrontBufferAvailableChanged;
 
    // make a brush of the scene available as a resource on the window
    Resources["RotatingTriangleScene"] = new ImageBrush(_di);
 
    // begin rendering the custom D3D scene into the D3DImage
    BeginRenderingScene();

创建对象后,我们立即为 IsFrontBufferAvailableChanged 事件建立了一个处理程序。这非常重要,因为此事件将告诉我们何时有实际有效的 WPF D3D 表面,我们可以用它来构成我们的自定义 D3D 表面。(毕竟,如果没有 WPF 渲染表面,我们真的没有理由浪费周期更新我们的场景,因为我们没有地方渲染它。)稍后会详细介绍...

创建 D3DImage 后,我们使用它创建一个 ImageBrush 并将其存储在 WindowResources 集合中,名称为 "RotatingTriangleScene"。最后,我们调用一个名为 BeginRenderingScene() 的例程,以启动我们自定义 D3D 场景的渲染泵。

知道何时更新场景

如上所述,实际上有时更新我们的自定义场景是有意义的,而有时则没有。显然,我们没有理由比 WPF 场景本身更新更频繁地更新我们的场景。那只会浪费周期。因此,我们最多只能在渲染过程中更新我们的场景。

经验丰富的 WPF 开发人员知道,CompositionTarget 类的静态 Rendering 事件可以用来挂接到 WPF 的渲染引擎。Rendering 事件会在每次 WPF 的 D3D 表面渲染时触发。因此,这为我们在将自定义表面交给 WPF 与框架的渲染目标进行组合之前更新自定义表面提供了理想的时机。我们的 BeginRenderingScene() 方法用于为 CompositionTarget.Rendering 事件设置处理程序,如下所示

private void BeginRenderingScene()
{
    if (_di.IsFrontBufferAvailable)
    {
        // create a custom D3D scene and get a pointer to its surface
        // (this is a call into our custom unmanaged library)
        _scene = InitializeScene();
        
        // set the back buffer using the new scene
        _di.Lock();
        _di.SetBackBuffer(D3DResourceType.IDirect3DSurface9, _scene);
        _di.Unlock();

        // leverage the Rendering event of WPF's composition target to
        // update the custom D3D scene
        CompositionTarget.Rendering += OnRendering;
    }
}

你可能已经注意到,此调用被包装在一个条件 if 语句中,该语句检查我们的 D3DImage 上的 IsFrontBufferAvailable 属性。大多数情况下,创建对象后,前缓冲区将立即可用。在这种情况下,我们应该在 D3DImage 上设置后缓冲区,以便它可以传输到前缓冲区。(我们稍后将介绍如何完成此操作。)

但是,如果 WPF 由于任何原因丢失了其 D3D 设备,那么 IsFrontBufferAvailable 将变为 false。WPF 会很友好地在此发生时触发 IsFrontBufferAvailableChanged 事件,以便我们可以停止更新 D3DImage 的后缓冲区。回想一下,我们已经通过一个名为 OnIsFrontBufferAvailableChanged() 的处理程序监视此事件。该处理程序如下所示

private void OnIsFrontBufferAvailableChanged(object sender, 
        DependencyPropertyChangedEventArgs e)
{
    // if the front buffer is available, then WPF has just created a new
    // D3D device, so we need to start rendering our custom scene
    if (_di.IsFrontBufferAvailable)
    {
        BeginRenderingScene();
    }
    else
    {
        // If the front buffer is no longer available, then WPF has lost 
        // its D3D device so there is no reason to waste cycles rendering
        // our custom scene until a new device is created.
        StopRenderingScene();
    }
}

停止渲染场景的例程相当简单。它基本上是早期 BeginRenderingScene() 例程的逆操作

private void StopRenderingScene()
{
    // This method is called when WPF loses its D3D device.
    // In such a circumstance, it is very likely that we have lost 
    // our custom D3D device also, so we should just release the scene.
    // We will create a new scene when a D3D device becomes 
    // available again.
    CompositionTarget.Rendering -= OnRendering;
 
    // release the scene 
    // (this is a call into our custom unmanaged library)
    ReleaseScene();
    _scene = IntPtr.Zero;
}

当操作系统发出信号表明可以再次创建新的 D3D 设备时,WPF 将自动创建新的 D3D 设备。此时,当属性变为 true 时,IsFrontBufferAvailableChanged 将再次触发。我们也将创建一个新的 D3D 设备并恢复渲染我们的场景。

旁注:如果你不是 DirectX 开发人员,你可能会想知道 WPF 为什么会丢失其 D3D 设备。这实际上是一个相当常见的事件,并且可能发生在用户执行影响视频驱动程序分辨率或颜色设置的系统操作时;或者用户按下 Ctrl+Alt+Delete 调用 WinLogon 桌面时;或者另一个应用程序锁定屏幕时;或者用户启动全屏 D3D 应用程序时;等等。

更新自定义场景

如果我们的自定义 D3D 场景涉及动画,我们很可能希望将其作为 WPF 渲染过程的一部分进行更新。然后,对 3D 表面所做的任何更改都可以与 WPF 的表面集成。因此,我们建立了一个 OnRendering() 方法来处理 CompositionTargetRendering 事件。该方法如下所示

private void OnRendering(object sender, EventArgs e)
{
    // when WPF's composition target is about to render, we update our 
    // custom render target so that it can be blended with the WPF target
    UpdateScene();
}

在我们的示例中,我们将在每次渲染过程中更新场景。但是请注意,如果您希望限制自定义场景的更新速率,您当然可以将该逻辑插入到此 OnRendering() 方法中。

要更新我们的场景,我们必须执行以下四个步骤

  1. 锁定 D3DImage
  2. 更新自定义 D3D 表面(这涉及对非托管代码的调用)
  3. 通过将 D3DImage 上的受影响矩形区域标记为脏来使其无效
  4. 解锁 D3DImage

这些步骤可以在我们的 UpdateScene() 例程中看到,如下所示

private void UpdateScene()
{
    if (_di.IsFrontBufferAvailable && _scene != IntPtr.Zero)
    {
        // lock the D3DImage
        _di.Lock();
 
        // update the scene 
        // (this is a call into our custom unmanaged library)
        SIZE size = new SIZE();
        RenderScene(size);
 
        // invalidate the updated rect of the D3DImage (in this case, the 
        // whole image)
        _di.AddDirtyRect(new Int32Rect(0, 0, size.Width, size.Height));
 
        // unlock the D3DImage
        _di.Unlock();
    }
}

当场景渲染时,WPF 将检查 D3DImage 中是否有脏区域。如果发现脏矩形,则自定义 D3D 表面的那些部分将与 WPF 自己的 D3D 表面重新组合,然后才会被渲染。

旁注:需要注意的是,为了支持与 WPF 场景的组合,需要刷新 D3D 设备。这肯定会对性能产生影响。影响程度取决于渲染的场景和视频驱动程序执行此类刷新操作的成本。因此,性能可能会因不同的硬件甚至不同的驱动程序版本而异。

D3D 互操作要求简述

我们现在已经涵盖了方程的托管部分。希望一切都很有意义。请参阅 Window1.xaml.cs 以获取完整的代码文件,包括调用非托管库所需的互操作 (P/Invoke) 声明。

在进入非托管部分之前,我们花点时间看看一些关于性能良好的 D3D 互操作场景的要求...

D3DImage 可以在 Windows XP 和 Windows Vista 上使用,将 D3D 场景与 WPF 场景合成。但是,根据底层操作系统,DirectX 设备和表面的创建方式肯定存在差异。其中一些基于获得最佳性能。另一些则是 D3DImage 的纯粹要求,例如,如果您使用不正确的像素格式,它将根本无法工作。

首先需要指出的是,如果不是立即显而易见的话,一个通用要求是,任何使用 D3DImage 进行 DirectX 互操作的应用程序也必须以完全信任模式运行。D3D 表面通过 IntPtr 在托管代码和非托管代码之间传递。任何时候在托管代码和非托管代码之间传递原始指针时,都无法保证安全性。

以下是在特定操作系统上实现高性能 D3DImage 解决方案的具体要求

Vista 上高性能 D3DImage 解决方案的要求

  • WDDM 驱动程序
  • D3D 9Ex 设备
  • 32 位 RGB 或 32 位 ARGB 表面(最好是不可锁定的)

XP 上高性能 D3DImage 解决方案的要求

  • 可锁定的 32 位 RGB 表面

- 或 -

  • 可锁定的 32 位 ARGB 表面
  • SP3 (或 SP2 加 此补丁)

在 Vista 上,D3DImage 在使用 WDDM 视频驱动程序 时性能最佳。如果在 Vista 上使用 XDDM 驱动程序,性能可能会相当差,因为需要软件复制表面。

在任何操作系统上,创建的表面的像素格式必须是 32 位 RGB (D3DFMT_X8R8G8B8) 或 32 位 ARGB (D3DFMT_A8R8G8B8)。由于后一种格式支持 Alpha,它显然更具吸引力。但是,如果你希望在 XP 上使用 ARGB 格式,你需要安装 SP3,或者在 SP2 上安装 分层窗口补丁。另外,请注意 ARGB 表面假定为预乘(有时称为 PARGB)。

为了在 XP 上实现硬件加速,表面必须创建为可锁定的。然而,在 Vista 上,不可锁定的表面更受青睐(尽管不是必需的),因为它通常更快。

在 XP 上,必须使用 Direct3DCreate9() 函数创建 D3D 设备。但是,在 Vista 上,您应该使用 Direct3DCreate9Ex() 函数创建 D3D 9Ex 设备。通过创建 9Ex 设备,WPF 将能够使用您的设备创建其自己的与 MIL 表面共享的临时表面。然后,它可以通过高性能 GPU 例程将您的表面复制到其共享表面。

还值得注意的是,D3DImage 仅在 Vista 上使用 9Ex 设备时支持 多重采样抗锯齿 (MSAA)

牢记以上要求,现在让我们看看用于创建我们非常简单的 D3D 场景的本机(非托管)代码。

动态创建简单 D3D 场景

您可能已经注意到,我们的托管应用程序调用了我们非托管库中的三个例程:InitializeScene()RenderScene()ReleaseScene()。从后两个开始,RenderScene() 方法仅包含渲染场景的 D3D 代码,而 ReleaseScene() 方法仅释放先前调用 InitializeScene() 中获取的接口。我们不会进一步查看后两个函数,因为它们只包含纯 D3D 代码。

只有 InitializeScene() 方法包含与我们使用 D3DImage 真正相关的内容,因为这是我们创建场景的地方。重要的是,我们动态创建适合应用程序运行时环境的设备和表面。更具体地说,我们需要创建符合上一节中概述要求的设备和表面的代码。

创建设备

我们的要求之一是,在 Vista 上运行时,我们应该创建一个能够创建共享表面的 D3D 设备。这只能使用 D3D 9Ex 设备来完成。因此,首要任务是确定 9Ex 功能是否可用。如果可用,我们应该使用 Ex 函数初始化 D3D;否则,我们可以使用非 Ex 函数初始化 D3D。这在 InitializeScene() 方法中完成,代码如下

// Vista requires the D3D "Ex" functions for optimal performance.
// The Ex functions are only supported with WDDM drivers, so they 
// will not be available on XP. As such, we must use the D3D9Ex 
// functions on Vista and the D3D9 functions on XP.
 
// Rather than query the OS version, we can simply check for the
// 9Ex functions, since this is ultimately what we care about.
HMODULE hD3D9 = LoadLibrary(TEXT("d3d9.dll"));
g_pfnCreate9Ex = (DIRECT3DCREATE9EX)GetProcAddress(hD3D9, 
    "Direct3DCreate9Ex");
g_is9Ex = (g_pfnCreate9Ex != NULL);
FreeLibrary(hD3D9);
 
if (g_is9Ex)
{
    InitializeD3DEx(hWnd, d3dpp);
}
else
{
    InitializeD3D(hWnd, d3dpp);
}

为简单起见,我们创建了两个独立的函数来初始化 D3D 库和创建设备:InitializeD3D()InitializeD3DEx()。这些函数做的事情本质上是相同的,即它们获取 D3D 库的可用接口。InitializeD3D() 通过直接创建一个 D3D 9 对象,然后使用该对象创建一个 D3D 9 设备来完成此操作。InitializeD3DEx() 例程则创建一个 D3D 9Ex 对象,并使用该对象创建一个 D3D 9Ex 设备。

值得注意的是,在 Windows XP 上的 D3D9 库中,创建 9Ex 设备的函数甚至不可用。因此,我们示例应用程序中的非托管代码不包含对 Direct3DCreate9Ex() 的直接调用。相反,它通过加载 D3D9 库并通过 GetProcAddress() 按名称查找函数来确定可用性,然后访问此函数。这使得我们的库可以在 Vista 和 XP 上同样良好地运行。

还应注意的是,除了获取 9Ex 接口外,InitializeD3DEx() 例程还获取标准 D3D 对象和设备接口。这样做只是为了简化库,允许在整个库(初始化和渲染期间)使用标准接口。否则,我们将需要不断检查 g_is9Ex 标志,然后使用一个接口或另一个接口。

HWND 怎么了?

回到 InitializeScene() 例程... 您可能已经注意到,在初始化 D3D 之前,我们注册了一个窗口类并创建了一个实例。这允许我们在创建设备时提供所需的 Win32 窗口句柄(或 HWND)。尽管我们创建了这个窗口,但我们从未实际显示它,D3D 在我们的场景中也未使用它。

为了完整性,需要指出的是,这个 HWND 会产生非常小的性能开销。创建它需要一些周期,然后它会消耗少量内存。另一种选择是使用 WPF 应用程序窗口的句柄来创建 D3D 设备。窗口句柄可以通过 IntPtr 参数轻松地提供给非托管库,以供 InitializeScene() 调用。在 WPF 中,您可以使用以下调用获取 Window 实例的 HWND(其中 this 表示 Window 或其中的 Visual

IntPtr hwnd = (PresentationSource.FromVisual(this) as HwndSource).Handle;

当然,这种方法要求我们等待 HwndSource 创建完成后才能初始化 D3D 场景。这意味着我们不能在 Window 的构造函数中初始化场景,因为届时 PresentationSource 仍将为 null。对于本文随附的示例,创建私有、不可见的窗口感觉更简洁、更简单。

创建渲染目标表面

一旦我们有了合适的设备,我们就可以创建渲染表面了。回想一下,我们对该表面的要求是它在 XP 上是可锁定的,但在 Vista 上我们更喜欢不可锁定的表面。幸运的是,g_is9Ex 标志现在基本上告诉我们是运行在 Vista 还是 XP 上(再次强调,9Ex 功能仅在带有 WDDM 驱动程序的 Vista 上可用),所以我们可以在 CreateRenderTarget() 调用中使用此标志,如下所示

// create and set the render target surface
// it should be lockable on XP and nonlockable on Vista
if (FAILED(g_pd3dDevice->CreateRenderTarget(WIDTH, HEIGHT, 
    D3DFMT_A8R8G8B8, D3DMULTISAMPLE_NONE, 0, 
    !g_is9Ex, // lockable
    &g_pd3dSurface, NULL)))
{
    return NULL;
}
g_pd3dDevice->SetRenderTarget(0, g_pd3dSurface);

请注意,在此示例中,我们正在创建一个 32 位 ARGB 表面。在 XP 上,这需要 SP3(或带有前面提到的 分层窗口补丁 的 SP2)。

这差不多就完成了我们 D3D 表面的初始化。我们现在有一个指向 D3D 表面接口的指针,该指针可以通过 IntPtrInitializeScene() 函数返回到托管应用程序。托管应用程序将使用此指针来更新 D3DImage 元素的后缓冲区。

InitializeScene() 例程的其余部分执行设置 D3D 场景(剔除、光照、顶点等)所需的各种操作。再次强调,有关这些 D3D 操作的更多信息,请参阅 DirectX SDK。

其他 D3D 考虑事项

以下是在实现 D3D 互操作解决方案时需要记住的一些额外事项。

不要呈现你的缓冲区

如前所述,在基于 D3DImage 的场景中,创建 D3D 设备时提供的 HWND 实际上并未被使用。在纯 D3D 应用程序中,该窗口通常用于两个目的

  1. 监控焦点变化,以及
  2. 确定场景在显示器上的渲染位置。

对于后一个目的,HWND 只有在后缓冲区传输到前缓冲区时才起作用。在 D3D 中,这称为“呈现”场景。通过在 D3D 设备上执行命名恰当的 Present()(或 PresentEx())方法来完成。

一个非常有趣的事情是,对于 D3DImage 场景,我们实际上从不呈现我们的后缓冲区。相反,我们让 WPF 将我们自定义表面的内容复制到它自己的渲染目标。然后,WPF 将呈现它自己的后缓冲区(现在包含组合场景)。

WPF 维护对 D3D 表面的引用

回想一下,D3D 表面是通过对 D3DImage 实例调用 SetBackBuffer() 来提供给 WPF 的。建立后缓冲区后,WPF 会添加对该表面接口的引用。它将一直持有该引用,直到设备丢失或再次调用 SetBackBuffer()。当不再需要表面进行渲染时,我们应该(也必须)指示 WPF 释放其引用。为此,只需使用 IntPtr.Zero 调用 SetBackBuffer(),如下所示

_di.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero);

D3D 9Ex 设备的池考虑

Vista 上高性能解决方案需要 9Ex 设备这一事实实际上对我们的 D3D 代码还有一些其他影响。最主要的一点是,我们不能托管 D3D 资源 与 9Ex 设备一起使用。请注意,此警告不适用于本文随附的示例,因为我们没有创建任何池资源。但如果我们要创建,此限制基本上意味着在创建池资源时,我们需要使用 g_is9Ex 标志来确定何时指定 D3DPOOL_DEFAULT 而不是 D3DPOOL_MANAGED(或 D3DXMESH_MANAGED 等)。典型的调用可能如下所示

D3DXLoadMeshFromXInMemory(meshSrcData, meshSrcDataSize,
    g_is9Ex ? D3DPOOL_DEFAULT : D3DXMESH_MANAGED,
    g_pd3dDevice, NULL, NULL, NULL, NULL, ppMesh); 

关于支持多个视频适配器的说明

在我们的示例中,我们通过将 D3DADAPTER_DEFAULT 传递给 CreateDevice()CreateDeviceEx() 方法来为默认适配器创建设备。如果在某个适配器上创建表面并在另一个适配器上显示,性能将会下降。在 XP 上尤其如此。如果存在多个适配器,我们则会为特定适配器创建设备,以便我们可以为该适配器创建表面。

一个真正动态的方法是当窗口移动到由不同适配器驱动的监视器时重新创建表面。这需要在 WPF 应用程序(重新初始化场景)和非托管库(确保使用正确的适配器创建表面)中都增加额外的逻辑。一种方法是让 WPF 应用程序在其 InitializeScene() 调用中提供窗口的屏幕坐标。这些坐标可以用来确定创建 D3D 设备的适当适配器。为了简单起见,本文提供的代码示例并未演示这种方法。

运行 D3DImage 示例的提示

本文的其余部分旨在帮助您构建和/或运行所提供的示例。在与我联系并提出具体问题之前,请仔细阅读以下提示。当然,我将尽力解决与本文直接相关的任何未解决的问题。如果您在 D3DImage 中遇到错误或限制,请在 WPF 论坛 或通过 WPF Connect 网站 向 Microsoft 提出这些问题。对于 D3D 问题,最好的选择是 D3D 论坛

我已将 D3DImage 示例的编译版本 作为本文的一部分。如果您只想运行此应用程序,请遵循本节中的建议。但是,如果您打算自己构建示例,请跳至标题为 "构建 D3DImage 示例的提示" 的部分。

安装 .NET 3.5 SP1

请确保您已安装 Microsoft .NET Framework 3.5 版本 Service Pack 1

安装 DirectX 可再发行库

您还需要最新的 DirectX 可再发行库。请注意,我包含的预构建示例由针对 2008 年 6 月发布的 DirectX SDK 构建的 32 位二进制文件组成。它们与早期版本不进行二进制兼容。例如,如果您需要针对 2008 年 3 月发布的版本运行,则需要自己下载并重新编译示例项目。

构建 D3DImage 示例的提示

我还将 D3DImage 示例的源代码 作为本文的一部分包含在内。以下是编译和运行示例的一些提示

安装 Visual Studio 2008 SP1 - 或 - 安装 .NET 3.5 SP1

此示例需要 .NET 3.5 SP1 中的新 D3DImage 功能。如果您使用 Visual Studio 2008 作为开发环境,获取 .NET 3.5 的最佳方法是安装 Visual Studio 2008 Service Pack 1。另一个选择是直接安装 Microsoft .NET Framework 3.5 版本 Service Pack 1

安装 DirectX SDK

请确保您已安装最新的 DirectX SDK

配置 Visual Studio

如果 Visual Studio 配置不正确,您可能会看到以下消息之一

fatal error C1083: Cannot open include file: 'd3dx9.h': No such file or directory
fatal error LNK1104: Cannot open file 'd3dxof.lib'

要解决此问题,请确保在 Visual Studio 设置中正确设置了 DirectX 头文件和库文件的路径。要配置这些路径,请从“工具”菜单中打开“选项...”对话框。然后,展开左侧树中的“项目和解决方案”,并选择“VC++ 目录”。接下来,从“显示目录:”下拉列表中选择“包含文件”,并添加指向 DirectX 头文件的条目。该条目应为“$(DXSDK_DIR)include”,如下所示

您还需要从“显示目录:”下拉列表中选择“库文件”,并添加指向 DirectX 库文件的条目。该条目应为“$(DXSDK_DIR)lib\x86”(如果您定位 64 位平台,则为“$(DXSDK_DIR) lib\x64”),如下所示

启用非托管代码调试

请注意,D3DImageSample 项目已配置为托管代码调试,因为目标应用程序是一个 .NET 应用程序。但是,由于示例项目也包含非托管代码,您可能希望将 Visual Studio 配置为非托管代码调试。这将允许您直接从托管代码逐步进入非托管代码。

要启用非托管代码调试,您必须首先打开 Visual Studio 项目的属性页。有几种方法可以做到这一点,但一种简单的方法是在解决方案资源管理器中选择项目,然后从“项目”菜单中选择“D3DImageSample 属性...”。当属性页打开时,选择“调试”页,然后确保选中“启用非托管代码调试”选项,如下所示

保存并重建项目,您应该已准备好进行混合模式调试。您可以通过在 main.cpp 中的非托管函数中设置断点来验证这一点。然后,在调试器下运行以验证您的断点是否被命中。

鸣谢

我真诚地感谢 Jordan Parker(来自 WPF 3D 团队)和 Dwayne Need 在微软公司,感谢他们在我在学习新的 D3DImage 功能时,帮助我入门并纠正了我的许多错误!

去吧,创造酷炫!

现在,是时候让所有认真的 DirectX 开发人员创造一些真正的冰火世界了!我当然期待看到人们使用新的 D3DImage 功能在 WPF 中能够创造出所有酷炫和热门的东西。

© . All rights reserved.