DirectX 因子

2D 门户网站向 3D 世界的转变

Charles Petzold

下载代码示例

Charles Petzold如果你精通 2D 图形,您可能认为 3D 是相似,但是额外的维度。不完全正确 !已涉足 3D 图形编程的人都知道它是多么的困难。3D 图形编程都需要你之外什么都在传统的二维世界中遇到主新和异国情调的概念。许多准备工作需要在屏幕上,让只是小 3D 和甚至然后轻微的误判可以呈现不可见。因此,对学习图形编程这么重要的视觉反馈延迟,直到所有的编程块是在的地方,在和谐工作为止。

DirectX 承认与 Direct2D 和 Direct3D 之间司的 2D 和 3D 图形编程之间的深刻差异。虽然您可以混合使用 2D 和 3D 内容相同的输出设备上,这些都是非常独特和不同的编程接口,并且有没有中间地带。DirectX 不允许你做一点小小的国家,有点摇滚。

不是吗?

有趣的是,Direct2D 包括的一些概念和起源于 3D 编程宇宙的设施。通过几何镶嵌 (分解复杂的几何形状成三角形) 和二维效果图使用着色器 (其中包括特殊图形处理单元或 GPU 运行的代码) 等功能,有可能利用一些功能强大的 3D 概念却仍能 Direct2D 的范围内进行。

此外,可以遇到,渐渐地,探讨这些 3D 概念,你得到的实际上在屏幕上看到结果满意。您可以获得 3D 脚湿在 Direct2D 所以 Direct3D 编程以后陷入是少一点令人震惊。

我猜应该不那么令人惊讶的是 Direct2D 包含了一些 3D 功能。在结构上,Direct2D 是建在 Direct3D,允许 Direct2D 还利用硬件加速的 GPU。Direct2D 和 Direct3D 之间的关系变得更明显,当您开始探索虚空地区的 Direct2D。

我会开始这个勘探与审查的三维坐标和坐标系统。

向外大的飞跃

如果你一直跟踪此列在最近几个月中,你知道它是可以调用 GetGlyphRunOutline 方法的对象的实现 IDWriteFontFace 接口,以获得一个 ID2D1PathGeometry 实例,它描述的文本字符的直线和贝塞尔曲线轮廓。然后,您可以操作这些直线和曲线坐标来扭曲中的各种方法的文本字符。

它也是可能的路径几何图形的 2D 坐标转换成三维坐标,然后 manip­乌拉特前将它们转换回 2D 通常显示的路径几何图形这些三维坐标。这听起来像好玩吗?

在二维空间中的坐标来表示作为数对 (X,Y) 对应到 ; 在屏幕上的位置 3D 坐标是 (X,Y,Z) 的形式,并在概念上,Z 轴正交屏幕。除非你在处理一个全息显示或 3D 打印机,这些 Z 坐标不是几乎一样的真实作为 X 和 Y 坐标。

有其他 2D 和 3D 坐标系之间的差异。传统的 2D 的起源 — 点 (0,0) — 是显示设备的左上角。X 坐标向右增加,Y 坐标增加下去。在 3D 的起源很多时候是在屏幕的中心和它是一个标准的笛卡尔坐标系:X 坐标仍增加往右,但是 Y 坐标增加上去了,也有消极的坐标以及。(当然,起源、 缩放和这些轴的方向可以改变与矩阵变换,和通常是)。

在概念上,积极的 Z 轴可以的屏幕点或点到屏幕。这两项公约被称为"右"和"左"的坐标系统,指的一种技术来区分它们:与右侧的坐标系统中,如果你的 X 轴和中指的正面 Y 方向的积极方向点你的右手的食指拇指指向正 Z。另外,如果你的曲线的右手从 X 轴正向 Y 轴向、 你的拇指点正面积极 Z 到手指。左手坐标系统中,它是一样的只使用左手。

我在这里的目标是获得 2D 路径几何图形的简短的文本字符串,并把然后它扭曲围绕原点成 3D 的戒指所以开始会见结束时,与图中所示相似图 1。因为我就会转换 2D 到 3D 的坐标坐标,然后返回到 2D,我选择使用一个三维的坐标系统与 Y 坐标增加下去,就像在 2D。积极的 Z 轴来的屏幕,但它真的是一个左手坐标系统。


图 1 为这篇文章中的程序使用的坐标系

若要使此整个作业尽可能方便,我使用存储程序的资源,作为一个字体文件,创建一个 IDWriteFontFile 对象,用于获得的 IDWriteFontFace 对象。或者,您可以通过从系统字体集合更多迂回方法获取 IDWriteFontFace。

从 GetGlyphRunOutline 方法生成的 ID2D1PathGeometry 对象然后通过使用 D2D1_GEOMETRY_SIMPLIFICATION_OPTION_LINES 参数来拼合所有贝塞尔曲线组成的短行序列的简化方法。简化的几何传递到自定义的 ID2D1GeometrySink 实现命名为 FlattenedGeometrySink,进一步分解成更短的直线之间的直线。结果就是仅由组成的线路完全玛钢几何。

易于操纵的这些坐标 FlattenedGeometry­接收器生成多边形对象的集合。图 2 显示多边形结构的定义。它基本上是只连接 2D 点的集合。多边形的每个对象对应于路径几何图形中的闭合图。在路径几何图形中的并不是所有数字都已都关闭,但那些在文本字形始终都闭合的所以这种结构是细为此目的。某些字符 (如 C、 E 和 X) 是由一个多边形 ; 描述 一些 (A、 D 和 O) 包含的两个多边形对象的内部和外部 ; 一些 (B,例如) 组成的 3 个 ; 和一些符号字符可能更多。

图 2 为存储的闭合的路径的多边形类数字

 

struct Polygon
{
  // Constructors
  Polygon()
  {
  }
  Polygon(size_t pointCount)
  {
    Points = std::vector<D2D1_POINT_2F>(pointCount);
  }
  // Move constructor
  Polygon(Polygon && other) : Points(std::move(other.Points))
  {
  }
  std::vector<D2D1_POINT_2F> Points;
  static HRESULT CreateGeometry(ID2D1Factory* factory,
                                const std::vector<Polygon>& polygons,
                                ID2D1PathGeometry** pathGeometry);
};
HRESULT Polygon::CreateGeometry(ID2D1Factory* factory,
                                const std::vector<Polygon>& polygons,
                                ID2D1PathGeometry** pathGeometry)
{
  HRESULT hr;
  if (FAILED(hr = factory->CreatePathGeometry(pathGeometry)))
    return hr;
  Microsoft::WRL::ComPtr<ID2D1GeometrySink> geometrySink;
  if (FAILED(hr = (*pathGeometry)->Open(&geometrySink)))
    return hr;
  for (const Polygon& polygon : polygons)
  {
    if (polygon.Points.size() > 0)
    {
      geometrySink->BeginFigure(polygon.Points[0],
                                D2D1_FIGURE_BEGIN_FILLED);
      if (polygon.Points.size() > 1)
      {
        geometrySink->AddLines(polygon.Points.data() + 1,
                               polygon.Points.size() - 1);
      }
      geometrySink->EndFigure(D2D1_FIGURE_END_CLOSED);
    }
  }
  return geometrySink->Close();
}

此列的可下载代码之间 Windows 应用商店的程序被命名为 CircularText,它创建一个基于文本"文本在无限的圈子,"结束了旨在连接回围成一圈开始的多边形对象的集合。 文本字符串是实际的程序中指定作为"ext T 无限圈"要避免空间在开始或结束,将会消失时从字形生成路径几何图形。

在项目中包含的两个方法对象的 CircularText 中的 CircularTextRenderer 类类型称为 m_srcPolygons (原始多边形生成的对象从路径几何图形) 和 m_dstPolygons (用于生成呈现的路径几何图形的多边形对象) 的多边形。 图 3 显示的方法将源多边形转换为基于屏幕的大小目标多边形的 CreateWindowSizeDependentResources。

图 3 从 2D 到 3D 到 2D CircularText Program 中

void CircularTextRenderer::CreateWindowSizeDependentResources()
{
  // Get window size and geometry size
  Windows::Foundation::Size logicalSize = m_deviceResources->GetLogicalSize();
  float geometryWidth = m_geometryBounds.right - m_geometryBounds.left;
  float geometryHeight = m_geometryBounds.bottom - m_geometryBounds.top;
  // Calculate a few factors for converting 2D to 3D
  float radius = logicalSize.Width / 2 - 50;
  float circumference = 2 * 3.14159f * radius;
  float scale = circumference / geometryWidth;
  float height = scale * geometryHeight;
  for (size_t polygonIndex = 0; polygonIndex < m_srcPolygons.size(); polygonIndex++)
  {
    const Polygon& srcPolygon = m_srcPolygons.at(polygonIndex);
    Polygon& dstPolygon = m_dstPolygons.at(polygonIndex);
    for (size_t pointIndex = 0; pointIndex < srcPolygon.Points.size(); pointIndex++)
    {
      const D2D1_POINT_2F pt = srcPolygon.Points.at(pointIndex);
      float radians = 2 * 3.14159f * (pt.x - m_geometryBounds.left) / geometryWidth;
      float x = radius * sin(radians);
      float z = radius * cos(radians);
      float y = height * ((pt.y - m_geometryBounds.top) / geometryHeight - 0.5f);
      dstPolygon.Points.at(pointIndex) = Point2F(x, y);
    }
  }
  // Create path geometry from Polygon collection
  DX::ThrowIfFailed(
    Polygon::CreateGeometry(m_deviceResources->GetD2DFactory(),
                            m_dstPolygons,
                            &m_pathGeometry)
    );
}

在内部循环,你会看到 x、 y 和 z 值计算。 这是一个 3D 的坐标,但它甚至不保存。 相反,它是立即折叠回 2D 通过简单地忽略的 z 值。 到得­的晚了这些三维坐标,代码首先转换水平位置上向以弧度表示的角度从 0 到 2 π 的原始路径几何图形。 Sin 和 cos 函数计算在 XZ 平面上的单位圆上的位置。 Y 值是从垂直坐标的原始路径几何图形的更直接转换。

CreateWindowSizeDependentResources 方法的结论通过从多边形集合目标获得一个新的 ID2D1PathGeometry 对象。 Render 方法然后设置矩阵变换原点放在屏幕的中心和两个填充和概述此路径几何图形中所示,结果图 4

The CircularText Display
图 4 CircularText 显示

该程序工作,吗? 很难告诉 ! 仔细看,你可以看到在中心一些宽字符和窄字符在左和右。 但最大的问题是我开始与路径几何图形与不相交的线,现在回上本身不填补这些重叠区域,结果显示几何。 这种效果是典型的几何形状,和它发生是否由多边形结构创建的路径几何图形的填充模式为备用或缠绕。

获取一些观点

三维图形编程不是只是约坐标点。 视觉提示是必要的查看器来解释代表在 3D 空间中的对象作为一个 2D 屏幕上的图像。 在现实世界中,你很少查看对象从一个恒定的了望点。 你会得到更好的中的 3D 文本视图图 4 如果你可以倾斜它有些让它看起来更像是在中环图 1

若要获得关于三维文字的一些观点,坐标需要在空间中旋转。 正如你所知,Direct2D 支持命名为 D2D1_MATRIX_3x2_F,你可以使用来定义 2D 转换,你可以通过第一次调用 ID2D1RenderTarget 的 SetTransform 方法将应用于您的 2D 图形输出的矩阵变换结构。

最常见的是您将使用来自 D2D1 命名空间命名 Matrix3x2F,为此目的的一类。 此类从 D2D1_MATRIX_3x2F_F 派生和提供方法用于定义各种类型的标准翻译、 缩放、 旋转和倾斜。

Matrix3x2F 类还定义了一个名为 TransformPoint,它允许您将转换应用于个别 D2D1_POINT_2F 对象的"手动"方法。 这是对于操纵点,他们在呈现之前有用。

你可能觉得我需要一个 3D 旋转矩阵倾斜显示的文本。 我当然会探索 3D 矩阵变换在未来的列,但现在可以让做与 2D 旋转。 想象你自己在某个地方位于负 X 轴上的图 1,请向原点方向看。 就像 X 坐落的积极的 Z 和 Y 轴,在一个常规的 2D 中的 Y 轴坐标系统,所以它看似合理的将 2D 旋转矩阵应用于的 Z 和 Y 值,我可以旋转围绕三-的所有坐标­三维 X 轴。

你可以尝试用这个 CircularText 程序。 在 CreateWindowSizeDependent 中创建一个二维旋转矩阵­资源方法什么时候之前操纵的多边形的坐标:

Matrix3x2F tiltMatrix = Matrix3x2F::Rotation(-8);

这是旋转-8 度,负号表示逆时针旋转。 在内部循环后 x、 y 和 z 已计算出,该转换适用于的 z 和 y 值,好像他们是 x 和 y 的值:

 

D2D1_POINT_2F tiltedPoint =
     tiltMatrix.TransformPoint(Point2F(z, y));
z = tiltedPoint.x;
y = tiltedPoint.y;

图 5 显示你会看到的。

The Tilted CircularText Display
图 5 倾斜的 CircularText 显示

这是更好,但它仍然有问题。 丑陋的事情发生时几何重叠,并没有任何迹象表明哪部分的几何形状是接近你,哪个是进一步走。 盯着它,和您可能会遇到一些观点转变。

应用于此对象的 3D 转换的能力仍然,表明它可能也很容易将对象绕 Y 轴旋转 — 它是。 如果你想象查看起源从积极的 Y 轴,你就会看到 X 和 Z 轴方向相同的方式在一个二维坐标系中的 X 和 Y 轴。

SpinningCircularText 项目实现两个旋转变换旋转文本和倾斜。 以前在 CreateWindowSizeDependentResources 中的所有计算逻辑已被移入的更新方法。 3D 点被旋转两次:一次绕 X 轴基于经过的时间,然后围绕 Y 轴根据扫一根手指在屏幕上的用户。 此更新方法所示图 6

图 6 SpinningCircularText 的更新方法

void SpinningCircularTextRenderer::Update(DX::StepTimer const& timer)
{
  // Get window size and geometry size
  Windows::Foundation::Size logicalSize = m_deviceResources->GetLogicalSize();
  float geometryWidth = m_geometryBounds.right - m_geometryBounds.left;
  float geometryHeight = m_geometryBounds.bottom - m_geometryBounds.top;
  // Calculate a few factors for converting 2D to 3D
  float radius = logicalSize.Width / 2 - 50;
  float circumference = 2 * 3.14159f * radius;
  float scale = circumference / geometryWidth;
  float height = scale * geometryHeight;
  // Calculate rotation matrix
  float rotateAngle = -360 * float(fmod(timer.GetTotalSeconds(), 10)) / 10;
  Matrix3x2F rotateMatrix = Matrix3x2F::Rotation(rotateAngle);
  // Calculate tilt matrix
  Matrix3x2F tiltMatrix = Matrix3x2F::Rotation(m_tiltAngle);
  for (size_t polygonIndex = 0; polygonIndex < m_srcPolygons.size(); polygonIndex++)
  {
    const Polygon& srcPolygon = m_srcPolygons.at(polygonIndex);
    Polygon& dstPolygon = m_dstPolygons.at(polygonIndex);
    for (size_t pointIndex = 0; pointIndex < srcPolygon.Points.size(); pointIndex++)
    {
      const D2D1_POINT_2F pt = srcPolygon.Points.at(pointIndex);
      float radians = 2 * 3.14159f * (pt.x - m_geometryBounds.left) / geometryWidth;
      float x = radius * sin(radians);
      float z = radius * cos(radians);
      float y = height * ((pt.y - m_geometryBounds.top) / geometryHeight - 0.5f);
      // Apply rotation to X and Z
      D2D1_POINT_2F rotatedPoint = rotateMatrix.TransformPoint(Point2F(x, z));
      x = rotatedPoint.x;
      z = rotatedPoint.y;
      // Apply tilt to Y and Z
      D2D1_POINT_2F tiltedPoint = tiltMatrix.TransformPoint(Point2F(y, z));
      y = tiltedPoint.x;
      z = tiltedPoint.y;
      dstPolygon.Points.at(pointIndex) = Point2F(x, y);
    }
  }
  // Create path geometry from Polygon collection
  DX::ThrowIfFailed(
    Polygon::CreateGeometry(m_deviceResources->GetD2DFactory(),
    m_dstPolygons,
    &m_pathGeometry)
    );
    // Update FPS display text
    uint32 fps = timer.GetFramesPerSecond();
    m_text = (fps > 0) ? std::to_wstring(fps) + L" FPS" : L" - FPS";
}

它是知名的复合矩阵变换相当于矩阵乘法和因为矩阵乘法不是交换,也不是复合变换。 请尝试切换周围的倾斜应用和旋转变换的不同的效果 (您可能其实喜欢的)。

在创建时的 SpinningCircularText 程序,我改编由 Visual Studio 模板创建 SpinningCircularTextRenderer 类中,创建的 SampleFpsTextRenderer 类,但离开了呈现速率的显示。 这使您可以看到性能有多糟糕。 在我 Surface Pro,我看到每 25 在调试模式下,指示代码不跟视频显示器的刷新率 (FPS) 第二个图的帧。

如果你不喜欢的表演,我恐怕有一些坏消息:我要让它甚至更糟。

从背景中分离前景色

3D 的路径几何方法的最大问题是重叠区域的影响。 是它可以避免那些重叠吗? 此程序正试图绘制的图像不是那么复杂的。 在任何时间,有的文本的一部分和其余文本,背视图前视图和前视图应始终显示的背视图之上。 如果它是可能的路径几何图形分成两个路径几何图形 — 一为背景和前景的一个 — 你可以呈现单独的 FillGeometry 调用这些路径几何图形,因此前景将背景之上。 这些两个路径几何图形甚至可以使用不同的画笔呈现。

考虑通过 GetGlyphRunOutline 方法创建的原始路径几何图形。 这就是只是平面的二维路径几何图形运动占领的矩形区域。 最终,那几何的一半显示在前景,而另一半显示在背景中。 但获得的多边形对象的时候,就太晚,使那什么像计算易用性的拆分。

相反,原始路径几何图形需要之前获得的多边形对象的一半被打碎。 这个断裂是依赖的旋转角度,这意味着更多的逻辑必须将移入的更新方法。

原始路径几何图形可以拆分在一半两个调用 CombineWithGeometry 方法。 此方法将以各种方式使第三几何两个几何图形结合在一起。 组合在一起的两个几何图形的描述文本轮廓的原始路径几何图形和矩形几何形状,它定义的路径几何图形的一个子集。 此子集将出现在前景色或背景,旋转角度。

例如,如果旋转角度为 0,使矩形几何形状必须覆盖的文本轮廓路径几何图形的中央一半。 这是出现在前台的原始几何的一部分。 在 D2D1_COMBINE_MODE_INTERSECT 模式下调用 CombineWithGeometry 返回仅由组成的该中心的地区,虽然调用 CombineWithGeometry 与 D2D1_COMBINE_MODE_EXCLUDE 获取路径的其余部分几何形状的路径几何图形 — 左侧和右侧的部分。 这些两个路径几何图形然后可以转换为多边形对象分别为坐标,其次是转换回呈现为单独的路径几何图形的操纵。

这种逻辑是一个名为 OccludedCircularText,通过使用不同的画笔填充两个几何图形实现 Render 方法,如中所示的项目的一部分图 7

The OccludedCircularText Display
图 7 OccludedCircularText 显示

现在更加明显的是,什么是在前景和背景是什么。 然而,这么多计算已被移动到更新方法性能是很差。

在传统 2D 编程环境中,我将已经用尽我掌握了所有 2D 编程的工具,现在被困这可怕的性能。 Direct2D,然而,提供一种方法来呈现几何简化逻辑并极大地提高了性能。 此解决方案使使用的最基本的 2D 多边形 — 这是一个还在 3D 编程中发挥着重要作用的多边形。

当然,我说的卑微的三角形。

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

衷心感谢以下技术专家对本文的审阅: Jim Galasyn (Microsoft)