DirectX 因素

使用 Direct2D 效果打破 Z 屏障

Charles Petzold

下载代码示例

Charles Petzold作为孩子,我们了解如何画之前我们学会如何读和写,和我们无疑收集到几个经验教训。我们发现在画布上绘画的时空过程体现在的油漆的分层。我们早些时候油漆可能部分地掩盖和被我们后来油漆遮盖。

为此原因,甚至有人完全不熟悉计算机图形学的力学能大概猜出如何在图像图 1 呈现:很显然,背景是彩色的灰色第一 ; 下一步来的蓝色三角形,其次是绿色,,最后由红色,就是在其他一切。它是没有惊喜呈现数字从后方到前方的过程被称为"画家算法"。

Three Overlapping Triangles
图 1 三个重叠的三角形

三个三角形的图 1 也可能安排在堆栈中的有色建设纸片。如果你是要向堆栈中添加更多和更多的三角形,他们将会建立起成一堆,和什么开始作为一个二维表面将收购第三维度。

即使在 2D 图形,还有一个 Z 轴基本概念 — — 一个虚拟的空间正交到二维屏幕或画布。平面的二维对象的分层被受"Z 顺序"的人物。在基于 XAML 的环境中,例如,附加的属性确定元素似乎坐在其他人,但它真的只是 Canvas.ZIndex 的顺序控制元素呈现在屏幕上。

问题是这样的:在 2D 图形,Z 索引总是适用于整个图。不能使用这种类型的 Z 顺序来绘制三个人物像那些在图 2,与在第二个,第二个在第三、 第一次,但第三个在第一次。

Mutually Overlapping Triangles
图 2 相互重叠的三角形

一个三角形的一个角的改变似乎是轻微的但它代表了什么一个不同的世界 !中的图像图 2 可能很容易与建筑纸,但不是那么容易与绘画 — — 无论是在现实生活中,或在 2D 图形编程。这些三角形中之一需要分为两部分,仔细计算的坐标,或使用裁剪的基础的一个其他三角形呈现。

GPU 和影响

呈现的图 2 可以极大地帮助通过借款的三维图形世界中的一些概念。

这些数字不能有统一的 Z 指数 ; 相反,这些数字必须允许有 Z 坐标的变量在它们的整个表面。绘图的过程然后可以保持 Z 坐标 (称为 Z 缓冲区或深度缓冲区) 的集合,包含了每个像素的渲染表面。如呈现每个图,图中每个像素的 Z 坐标相比是相应的 Z 坐标在此深度缓冲区中。如果该像素是在深度缓冲区中的 Z 坐标,像素绘制并存储在深度缓冲区中的新的 Z 坐标。如果不是,像素将被忽略。

这听起来计算代价高昂 — — 不仅为本身的比较而计算的 Z 坐标为每个像素的每个图形的图 — — 这是准确的评价。这就是为什么它是一个理想的工作移交给现代 GPU 的并行计算能力。

计算的 Z 坐标为每个像素的图从概念上讲是相当容易的如果这个数字恰好是一个三角形 — — 并牢记每个多边形可以分解为三角形。一切必要是三维坐标点,给每个三角形的三个顶点和三角形内的任何点然后可以作为的三个顶点坐标的加权平均计算。(这涉及到重心坐标 — — 不唯一在计算机图形学中使用的概念是由德国数学家 8 月斐迪南莫比乌斯.)

同一的插值过程中可以遮阳的三角形。如果每个顶点被分配一个特定的颜色,那么该三角形内的任何像素是加权的平均数的这三种颜色,如中所示图 3

The ThreeTriangles Program Display
图 3 ThreeTriangles 程序显示

颜色渐变的类型也是一个重要特征的三维编程,因为它允许三角形底纹类似于弯曲的表面。但它不是在传统的 2D 编程中常见的渐变类型。

中的图像图 3 由一个叫做 ThreeTriangles 运行在 Windows 8.1 和 Windows Phone 8.1 下的可下载程序创建。(解决方案被创建在 Visual Studio 2013 更新 2 使用的新的通用应用程序模板,允许共享很多的 Windows 8.1 和 Windows Phone 8.1 之间的代码)。

ThreeTriangles 程序中的图形都做完全在 Direct2D,使用的 Direct2D 称为效果或 (当你的代码他们自己) 的一个功能自定义效果。使用自定义的效果你可以得到更接近真实的 3D 编程比否则为可能与 Direct2D。

为 Direct2D 编写自定义效果时, 你获得一种特权通常只限于 3D 程序员:你可以写在 GPU 执行的代码。此代码的形式被称为着色器,你写使用高级着色语言 (HLSL),类似于 C.的小程序这些着色器是由 Visual Studio 编译到已编译的着色器对象 (.cso) 文件正常项目生成期间,然后在 GPU 上运行,当程序执行时。

事实上,Direct2D 效果有时被称为小更比包装为着色器 !自定义效果是你可以使用着色器的 Direct2D 编程范围内实现类似三维的图像的唯一方式。

三种不同类型的着色器是可供 Direct2D 影响使用:

  • 在顶点执行操作的顶点着色器。每个三角形都有三个顶点。顶点总是涉及到一个坐标点,但可能包括其他信息,如颜色。
  • 在这些三角形内的所有像素执行操作的像素着色器。任何提供顶点的信息自动插在表面的像素着色器的准备中的三角形。
  • 计算着色器使用 GPU 来执行繁重的并行处理。我不会讨论这篇文章中的,计算着色器。

用于 Direct2D 效果的着色器有稍有不同的要求比与 Direct3D 编程,相关联的着色器,但许多概念是相同的。

内置的效果和自定义效果

Direct2D 包括约 40 预定义的内置效果,其中在位图上执行各种图像处理操作,如模糊或锐化或各种类型的彩色处理。

每个这些内置效果是由您使用来创建该类型的效果类 ID 标识。例如,假设您想要使用的颜色矩阵影响,允许指定一个可改变位图中的颜色转换。您可能会在你渲染类作为一个私有字段声明一个 ID2D1Effect 类型的对象:

Microsoft::WRL::ComPtr<ID2D1Effect> m_colorMatrixEffect;

在 CreateDeviceDependentResources 方法中,您可以通过引用的记录的类 ID 创建这种效果:

d2dContext->CreateEffect(
  CLSID_D2D1ColorMatrix, &m_colorMatrixEffect);

在这一点上,您可致电 SetInput 要设置位图,位图和 SetValue 来指定一个变换矩阵的作用对象。 你通过调用渲染此颜色转移的位图:

d2dContext->DrawImage(m_colorMatrixEffect.Get());

所有的内置效果涉及位图输入和 Direct2D 效应的特征之一是您可以链接在一起,对位图应用一系列的影响。

如果你感兴趣写您自己的自定义效果,还有宝贵的 Windows 8.1 Visual Studio 解决方案被称为 Direct2D 自定义的图像效果的示例,其中包含三个单独的项目,展示着色器可用于 Direct2D 效应的三种类型。 三个项目都需要位图作为输入。

因此,你会被原谅的假设 Direct2D 效果总是执行操作位图的输入。 但这并不是这样。 ThreeTriangles 程序,创建的图像在图 3 不需要位图的输入。

你也会原谅为假设 Direct2D 影响涉及只是一种类型的着色器。 当然,内置的效果似乎涉及到顶点着色器和像素着色器,但不是能同时。 然而,ThreeTriangles 程序是不同的在这方面,以及:它定义了一个自定义的效果,使用顶点着色器和像素着色器。

注册、 创建、 绘制

因为 Direct2D 影响的设计是为了预先注册和创建的类 ID,自定义效果需要提供那相同的能力。 ThreeTriangles 程序中的自定义效果是命名为 SimpleTriangleEffect,定义为注册了类的静态方法的类。 由 ThreeTrianglesRenderer 类的构造函数调用此方法,但效果可以在程序中的任意位置注册:

SimpleTriangleEffect::RegisterEffectAsync(d2dFactory)

这种注册方法是异步的因为它需要加载在已编译的着色器文件中,并为此目的在 DirectXHelper 类中提供的唯一方法是 ReadDataAsync。

就像当使用内置的效果,ThreeTrianglesRenderer 类作为一个私有字段,在其头文件中声明了一个 ID2D1Effect 对象:

Microsoft::WRL::ComPtr<ID2D1Effect> m_simpleTriangleEffect;

CreateDeviceDependentResources 方法创建自定义效果内置效果相同的方式:

d2dContext->CreateEffect(
   CLSID_SimpleTriangleEffect, &m_simpleTriangleEffect)

早些时候注册的自定义效果与效果相关联的类 ID。

SimpleTriangleEffect 有没有输入。 (那是什么让"简单"的部分 !只是像一个内置效果呈现效果:

d2dContext->DrawImage(m_simpleTriangleEffect.Get());

也许简单使用此自定义效果表明一些效果类本身中的复杂性。 自定义的效果,如 SimpleTriangleEffect 必须实现 ID2D1EffectImpl (效果执行) 接口。 效果可以由多个传递,被称为变换,组成,每一个通常由 ID2D1DrawTransform 的一个实现。 如果一个类用于这两个接口 — — 这是与 SimpleTriangleEffect 的情况 — — 然后它需要实现 IUnknown (三种方法),ID2D1EffectImpl (三种方法),ID2D1TransformNode (一种方法),ID2D1Transform (三种方法),和 ID2D1DrawTransform (1 法)。

这是一个相当大的开销数额,除了一些 XML,标识效果和它的作者当效果是第一次注册。 幸运的是,对于简单的效果 — — 和这一个当然有资格 — — 的许多效果方法可以有相当容易实现。 影响类的最重要的工作涉及到加载和注册已编译的着色器代码 (和着色器关联的 Guid,供以后参考),并定义顶点缓冲区,也必须是一个 GUID 与相关联。

从顶点缓冲区......

顶点缓冲区是加工组装的顶点的集合。 每个顶点总是包括一个 2D 或 3D 的坐标点,但通常以及其他项目。 数据相关联的每个顶点和它如何组织被称为"布局"顶点缓冲区的总体而言,ThreeTriangles 程序定义了三种不同 — — 但相等 — — 数据类型来描述这个顶点的布局。

此顶点数据的第一次表示形式所示图 4。 这是一个简单的结构命名为顶点,其中包括 3D 协调­nate 点和 RGB 颜色。 这些结构的数组定义对程序所显示的三个三角形。 (此数组是硬编码在所需的初始化方法的简单­TriangleEffect 类 ; 在一个真正的程序效果类将允许数组的顶点将被输入到影响。)

图 4 顶点定义在 SimpleTriangleEffect

// Define Vertex for simple initialization
struct Vertex
{
  float x;
  float y;
  float z;
  float r;
  float g;
  float b;
};
// Each triangle has three points and three colors
static Vertex vertices [] =
{
  // Triangle 1
  {    0, -1000, 0.0f, 1, 0, 0 },
  {  985,  -174, 0.5f, 0, 1, 0 },
  {  342,   940, 1.0f, 0, 0, 1 },
  // Triangle 2
  {  866,   500, 0.0f, 1, 0, 0 },
  { -342,   940, 0.5f, 0, 1, 0 },
  { -985,  -174, 1.0f, 0, 0, 1 },
  // Triangle 3
  { -866,   500, 0.0f, 1, 0, 0 },
  { -643,  -766, 0.5f, 0, 1, 0 },
  {  643,  -766, 1.0f, 0, 0, 1 }
};
// Define layout for the effect
static const D2D1_INPUT_ELEMENT_DESC vertexLayout [] =
{
  { "MESH_POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0 },
  { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12 },
};

X 和 y 值基于正弦和余弦的角度增量为 40 度,一个半径为 1000。 然而,请注意,那的 z 坐标是 0 和 1 之间,所有集红色顶点的 z 值为 1 的绿色的顶点都是 0.5,的蓝色的顶点都是 0。 更多关于这晚一点。

之后该数组是另一个小小的数组,但这一定义的顶点信息在更正规的方式所需的创建和注册的顶点缓冲区。

顶点缓冲区和顶点着色器是在 SimpleTriangleEffect 的 SetDrawInfo 方法中引用的。 每一次呈现时的效果,这些九个顶点被传递到顶点着色器。

... 到顶点着色器......

图 5 显示的顶点着色器 SimpleTriangleEffect。 它由三个结构和调用主函数组成。 主要功能被呼吁每个顶点的顶点缓冲区 ; 在这种情况下,这就是只有九个顶点,但往往有很多。

图 5 SimpleTriangleEffectVertexShader.hlsl 文件

// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
  float3 position : MESH_POSITION;
  float3 color : COLOR0;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Information provided for Direct2D vertex shaders
cbuffer ClipSpaceTransforms : register(b0)
{
  float2x1 sceneToOutputX;
  float2x1 sceneToOutputY;
}
// Called for each vertex
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Append a 'w' value of 1 to the 3D input position
  output.sceneSpaceOutput = float4(input.position.xyz, 1);
  // Standard calculations
  output.clipSpaceOutput.x =
    output.sceneSpaceOutput.x * sceneToOutputX[0] +
    output.sceneSpaceOutput.w * sceneToOutputX[1];
  output.clipSpaceOutput.y =
    output.sceneSpaceOutput.y * sceneToOutputY[0] +
    output.sceneSpaceOutput.w * sceneToOutputY[1];
  output.clipSpaceOutput.z = output.sceneSpaceOutput.z;
  output.clipSpaceOutput.w = output.sceneSpaceOutput.w;
  // Transfer the color
  output.color = input.color;
  return output;
}

每个三个结构包含字段标识与 HLSL 数据类型、 成员名称和标识的特定字段的作用的大写形式语义学。

名为 VertexShaderInput 的结构是主要的输入,这是你们刚才看到,顶点缓冲区,但 HLSL 数据类型的三维位置及 RGB 颜色的布局相同。

名为 VertexShaderOutput 的结构定义主要的输出。 前两个字段是必需的 Direct2D 影响。 (第三个必需字段会存在,如果影响涉及输入的位图)。我叫领域 sceneSpaceOutput 基于输入的坐标。 一些影响变化的协调 ; 这种效果并没有,只需进入 4 D w 值为 1 的齐次坐标的 3D 输入的坐标:

output.sceneSpaceOutput = float4(input.position.xyz, 1);

顶点着色器输出还包括一个称为颜色,只需设置从输入的颜色的非必填字段:

output.color = input.color;

我叫必需的输出字段 clipSpaceOutput 描述了每个顶点坐标归一化坐标在 3D 中使用。 这些坐标是从所述的此列的上个月的分期付款的相机投影变换生成的坐标相同。 在这些 clipSpaceOutput 坐标 x 值的范围从 – 1 在屏幕的左边到右边 ; 1 y 值的范围从 – 1 的底部到顶部 ; 1 和 z 值的范围从查看器中最接近的坐标为 0 到 1 的坐标远的那个地方。 顾名思义的字段的名称,这些归一化的坐标都用于剪切到屏幕上的三维场景。

协助您在这些 clipSpaceOutput 坐标,计算第三个结构是自动提供给你我叫 ClipSpaceTransforms。 这些都是基于像素的宽度和高度的屏幕上,四个数字和任何设备上下文转换的有效 DrawImage 呈现效果时。

然而,提供的转换仅是为 x 和 y 坐标,以及那有为什么我 z 坐标定义在原始的顶点缓冲区,要有 0 和 1 之间的值。 另一种方法是使用实际的摄像机投影变换在顶点着色器中 (如在未来的专栏中,我将演示)。

在深度缓冲区中还会自动使用这些这些 z 值,因此,具有较低的 z 坐标的像素模糊具有较高的 z 值的像素。 但是这只发生如果效果类中的 SetDrawInfo 方法调用 SetVertexProcessing 与 D2D1_VERTEX_OPTIONS_USE_DEPTH_BUFFER 标志。 (这发生,也会导致 COM 错误出现在 Visual Studio 的输出窗口中,而该程序正在运行,但也同样发生在微软的示例 Direct2D 效果代码)。

…到像素着色器

每次呈现效果 (和在一般情况下,即在视频显示的帧速率),顶点着色器的调用每个顶点的顶点缓冲区,在这种情况下九倍。

顶点着色器的输出具有相同的像素着色器的输入格式。 正如你可以看到在像素着色器中图 6,PixelShaderInput 结构是在顶点着色器的 VertexShaderOutput 结构相同。

图 6 SimpleTriangleEffectPixelShader.hlsl 文件

// Per-pixel data input to the pixel shader
struct PixelShaderInput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Called for each pixel
float4 main(PixelShaderInput input) : SV_TARGET
{
  // Simply return color with opacity of 1
  return float4(input.color, 1);
}

然而,像素着色器呼吁在三角形中的每个像素和所有字段的结构有一直都插在该三角形的表面。 像素着色器中的主函数必须返回四元件颜色,其中包括不透明度,所以插值的 RGB 颜色简单地修改通过追加不透明度的一个领域。 那种颜色是输出到显示器。

这里是一些有趣的变化为像素着色器:Z 坐标的 sceneSpaceOutput 字段范围从 0 到 1,所以它是可能通过使用这个坐标构建一个灰色的阴影,并返回它的主要方法从可视化的每个三角形深度:

float z = input.sceneSpaceOutput.z;
return float4(z, z, z, 1);

增强功能吗?

SimpleTriangleEffect 一些偷工减料。 而应是更通用的方法包括顶点输入设置。 其他一些功能不伤身:顶点着色器是一个很大的地方来执行矩阵变换 — — 如旋转或照相机变换 — — 因为矩阵乘法在 GPU 上执行。

很少有程序员都能够抗拒诱惑,执行代码增强功能,尤其是那些静态的图像变成一个生气勃勃。

Charles Petzold 是长期贡献 MSDN 杂志和"编程窗口,第 6 版"的作者 (微软出版社,2013年),一本关于编写应用程序的 Windows 8 书。 他的网站是 charlespetzold.com

衷心感谢以下 Microsoft 技术专家对本文的审阅:道格 · 埃里克森
道格 · 埃里克森是铅编程作家为微软的 OSG 开发人员文档团队。 当不编写和开发 DirectX 图形代码和内容,他读像查尔斯的文章,因为那是他喜欢怎样度过他的空闲时间。 好吧,那,和骑摩托车。