DirectX 因素

三角形和曲面细分

Charles Petzold

下载的代码项目

Charles Petzold三角形是最基本的二维图。它是什么都不超过三个点由三条线相连,如果您尝试使任何简单,它将折叠为单个维度。另一方面,任何其他类型的多边形可以分解成三角形的集合。

即使在三个维度,一个三角形始终是平的。事实上,在 3D 空间中定义一个平面的一种方法是与非共线三点,这就是一个三角形。在3D 空间中的一个广场并不保证是平坦的因为第四个点不可能在其他三个相同的平面。但那广场可以划分成两个三角形,其中每个是平的虽然不一定在同一平面。

在 3D 图形编程中,三角形形成固体的数字,开始用最简单的所有三维数字、 三角金字塔或四面体的表面。从三角"构建基块"组装看似坚固的图是在 3D 计算机图形学的最基本过程。当然,经常弯曲表面的现实世界的对象,但如果你使三角形的足够小,他们可以近似曲面到足以愚弄人眼的程度。

通过利用三角形的另一个特点增强曲率的错觉:如果一个三角形的三个顶点是与三个不同的值相关联 — — 例如,三个不同的颜色或三个不同的几何向量 — — 可以在三角形的表面插值计算出这些值,并将其用于颜色那表面。这是光的三角形如何着色来模仿真实世界对象中见过的反射。

在 Direct2D 三角形

三角形是 3D 计算机图形学中无处不在。很多现代图形处理单元 (GPU) 所执行的工作涉及呈现三角形,所以课程的 Direct3D 编程涉及使用三角形来定义固体的数字。

相比之下,三角形是不是在所有发现大多数 2D 图形编程的接口,直线、 曲线、 矩形和椭圆的最常见的二维基元在哪里。所以它是有点令人惊讶在 Direct2D 相当晦涩角弹出的三角形。或者,也许是真的不足为奇:因为 Direct2D 构建 Direct3D,似乎合理的 Direct2D 采取利用在 Direct3D 和 GPU 的三角形支持。

在 Direct2D 中定义的三角形结构很简单:

struct D2D1_TRIANGLE
{
  D2D1_POINT_2F point1;
  D2D1_POINT_2F point2;
  D2D1_POINT_2F point3;
};

只要我可以确定,这种结构用于 Direct2D 只因"网"是 ID2D1Mesh 类型的对象中存储的三角形的集合。 (从哪个 ID2D1DeviceContext 派生) 的 ID2D1RenderTarget 支持一个名为 CreateMesh,创建一个对象,这种方法:

ID2D1Mesh * mesh;
deviceContext->CreateMesh(&mesh);

(为了简单起见,我这里没有显示使用 ComPtr 或检查这些简短的代码示例中的 HRESULT 值)。ID2D1Mesh 接口定义一个名为打开的单个方法。 此方法返回一个 ID2D1TessellationSink 类型的对象:

ID2D1TessellationSink * tessellationSink;
mesh->Open(&tessellationSink);

一般情况下,"镶嵌"指的是进程的覆盖表面用马赛克图案,但词在 Direct2D 和 Direct3D 编程使用方式略有不同。 在 Direct2D,镶嵌是一个二维区域分解成三角形的过程。

ID2D1TessellationSink 接口有的只是两种方法:添加­三角形 (其中向集合中添加一个 D2D1_TRIANGLE 对象的集合) 和关闭,使网格对象不可变。

虽然您的程序可以调用 AddTriangles 本身,往往它将 ID2D1TessellationSink 对象向 Tessellate 方法传递由 ID2D1Geometry 接口定义:

geometry->Tessellate(IdentityMatrix(), tessellationSink);
tessellationSink->Close();

Tessellate 方法生成包括括在几何领域的三角形。 调用 Close 方法后,可以丢弃,接收器和你离开一个 ID2D1Mesh 对象。 生成使用 ID2D1TessellationSink ID2D1Mesh 对象的内容的过程是类似的定义 ID2D1Path­使用 ID2D1GeometrySink 的几何。

然后,你可以呈现此使用 FillMesh 方法的 ID2D1RenderTarget 的 ID2D1Mesh 对象。 画笔管辖色网格的如何:

deviceContext->FillMesh(mesh, brush);

请记住这些网格三角形定义区域和不区域的轮廓。 没有 DrawMesh 的方法。

FillMesh 有一定的局限性:当调用 FillMesh 时,无法启用抗锯齿。 在 FillMesh 之前对 SetAntialiasMode 的调用:

deviceContext->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);

您可能想知道:点是什么? 为什么不干脆叫 FillGeometry 的原始几何对象上呢? 视觉效果应该是一样 (除了抗锯齿)。 但实际上深刻差异由如何创建这两个对象显示出来的 ID2D1Geometry 和 ID2D1Mesh 对象。

几何形状的大多只是收藏的坐标点,所以几何图形是独立于设备的对象。 可以通过调用定义的 ID2D1Factory 方法来创建各种类型的几何形状。

网格是三角形,都只是三胞胎的坐标点,所以网格布应独立于设备对象以及的集合。 但通过调用定义的 ID2D1RenderTarget 方法创建一个 ID2D1Mesh 对象。 这意味着网格是一种依赖于设备对象,像一个画笔。

这会告诉你的三角形,包括网格存储依赖于设备的方式,最有可能适合于由 GPU,或实际上在 GPU 上处理窗体中。 而这意味着 FillMesh 应执行比 FillGeometry 为等效图快得多。

我们须测试这一假设呢?

这篇文章的可下载代码之间一个程序命名为 MeshTest,创建路径几何图形为 201 点的明星,并慢慢地将它旋转同时计算和显示的帧速率。 时编译该程序,在调试模式下 x 86 和我 Surface Pro 上的运行,得到小于 30 每秒帧数 (FPS) 呈现网格时呈现路径几何图形 (即使几何是概述,消除重叠区域和夷为平地,消除曲线),但帧速率飞跃时达 60 FPS 的帧速率。

结论:对于复杂的几何,将它们转换为网格为呈现是有意义的。 如果需要禁用抗锯齿,来呈现该滤网是一个交易断路器,您可能想要查阅 ID2D1GeometryRealization,介绍了在 Windows 8.1。 这将 ID2D1Mesh 的性能结合在一起,但允许抗锯齿。 保持在记住网格和几何的认识必须重新创建如果显示设备将重新创建,只是与其他设备相关资源,如画笔一样。

检查三角形

我很好奇镶嵌过程生成的三角形。 他们实际上前者能够将吗? ID2D1Mesh 对象不允许您访问的三角形,包括网格,但它是可以编写您自己的类,实现 ID2D1TessellationSink 接口,并将这类的一个实例传递给 Tessellate 方法。

我打电话给我 ID2D1TessellationSink 执行 Interrogable­TessellationSink,和它原来是困窘简单。 它用于存储三角形对象包含私有数据成员:

std::vector<D2D1_TRIANGLE> m_triangles;

大部分的代码被致力于实现的 IUnknown 接口。 图 1 显示实现的两个 ID2D1TessellationSink 方法和获得三角形的结果所需的代码。

图 1 InterrogableTessellationSink 相关的代码

// ID2D1TessellationSink methods
void InterrogableTessellationSink::AddTriangles(_In_ const D2D1_TRIANGLE *triangles,
                                          UINT trianglesCount)
{
  for (UINT i = 0; i < trianglesCount; i++)
  {
    m_triangles.push_back(triangles[i]);
  }
}
HRESULT InterrogableTessellationSink::Close()
{
  // Assume the class accessing the tessellation sink knows what it's doing
  return S_OK;
}
// Method for this implementation
std::vector<D2D1_TRIANGLE> InterrogableTessellationSink::GetTriangles()
{
  return m_triangles;
}

我纳入此类在项目中命名镶嵌­可视化。 该程序创建的各种几何形状 — — 一切从几何形状的文本的字形从生成到一个简单的矩形几何 — — 和使用 InterrogableTessellationSink 来获取由 Tessellate 方法创建的三角形的集合。 每个三角形然后转换成组成的三条直线的 ID2D1PathGeometry 对象。 然后使用 DrawGeometry 呈现这些路径几何图形。

正如你可能期望 ID2D1RectangleGeometry 是镶嵌化成只是两个三角形,但其他几何图形更有趣。 图 2 显示的三角形,包括 ID2D1Rounded­RectangleGeometry。

A Rounded Rectangle Decomposed into Triangles
图 2 圆角矩形分解为三角形

这不是一个人会 tessellate 的圆角的矩形的方法。 一个人可能会将圆角的矩形划分为五个矩形和四个四分之一圆和 tessellate 每个这些数字分别。 尤其是,人类将饼图楔入切片四季度-圈子。

换句话说,一个人会在内部要援助中镶嵌的几何图形的定义几个更多的积分。 但几何对象所定义的镶嵌算法并不使用所创建的拼合的几何形状之外的任何点。

图 3 显示使用佩斯卡德罗字体呈现的两个字符分解成三角形。

Text Decomposed into Triangles
图 3 文本分解成三角形

我也是好奇这些三角形被生成,并通过单击在左下角窗口中的渐变填充选项的顺序,可以找出。 选中此选项后,程序将调用 FillGeometry 为每个三角形几何形状。 纯色画笔传递给 FillGeometry,但颜色取决于该三角形的集合中的索引。

你会发现是 FillGeometry 选项呈现某种自上而下渐变画笔,这意味着三角形都存储在集合中可视的自上而下顺序。 它出现镶嵌算法试图最大限度地在三角形的水平扫描线的宽度,大概最大化呈现性能。

虽然我清楚地认识到这种方法的智慧,但我必须承认我是有点失望。 我希望扩阔后的贝塞尔曲线 (举例来说) 可能 tessellated 线的另一端开始到另在继续,所以三角形的可以用来呈现渐变从一端到另,这不是一种常见的 DirectX 程序中的渐变的 ! 但这是不能。

有趣的是,我需要关闭抗锯齿,FillGeometry 在 TessellationVisualization 调用或呈现三角形之间的模糊线条出现之前。 这些微弱的线产生的抗锯齿算法,涉及不变得不透明时重叠的部分透明的像素。 这使我怀疑使用带有 FillMesh 的抗锯齿不是硬件或软件的限制,但要避免视觉异常的授权限制。

在 2D 和 3D 的三角形

工作之后只是有点与 ID2D1Mesh 对象时,我开始可视化所有二维区域作为三角形的马赛克。 这种心态是正常的当在做 3D 节目,但我从来没有延长了对 2D 世界这种以三角为中心的远景。

Tessellate 方法的文件指示生成的三角形是"顺时针旋转伤口,"这意味着,point1,point2 和 point3 的 D2D1_TRIANGLE 结构的成员进行排序按顺时针方向。 这不是非常有用的信息时使用这些三角形在 2D 图形编程中,但在 3D 世界中,凡订购中有一个三角形的点通常指示前面或后面,图它变得相当重要。

当然,我很有兴趣使用这些二维镶嵌的三角形突破到第三层面,三角形在哪里最舒服地在家里。 但我不想要那么急忽视了探索镶嵌三角形在两个维度中一些有趣的效果。

唯一着色三角形

对我来说,在图形编程中的最大刺激我从来没有见过,一种在计算机屏幕上创建图像,以及我不觉得我见过镶嵌成三角形以随机的方式更改其颜色的文本。 发生这种情况在程序中我叫 SparklingText。

请牢记 FillGeometry 和 FillMesh 涉及只有一个笔刷,所以如果你需要呈现数百个三角形用不同的颜色,你需要数以百计的 FillGeometry 或 FillMesh 调用,每个呈现一个单一的三角形。 这是更有效率? 要呈现包含三直行 ID2D1PathGeometry FillGeometry 调用吗? 或致电 FillMesh 与 ID2D1Mesh 包含一个单一的三角形吗?

我假定 FillMesh 会比 FillGeometry 更有效率只有网格包含多个三角形,并要为一个三角形,速度较慢,所以我最初写的程序从镶嵌三角形生成路径几何图形。 后来才做了添加一个复选框标记为"使用网格的每个三角形而不是 PathGeometry"并纳入这一逻辑。

SparklingText 的 SparklingTextRenderer 类中的策略是使用 ID2D1FontFace 的 GetGlyphRunOutline 方法来获取字符轮廓路径的形状。 该程序然后调用 Tessellate 方法对此几何与 InterrogableGeometrySink 来获取 D2D1_TRIANGLE 对象的集合。 然后,这些是转换为路径几何图形或网格 (根据复选框的值),存储在一个名为 m_triangleGeometries 和 m_triangleMeshes 两个矢量集合。

图 4 显示一块相关的呈现三角形的结果的呈现方法和 Tessellate 方法填充这些集合。 像往常一样,已删除的 HRESULT 检查来简化代码清单。

图 4 镶嵌和呈现 SparklingTextRenderer 中的代码

void SparklingTextRenderer::Tessellate()
{
  ...
// Tessellate geometry into triangles
  ComPtr<InterrogableTessellationSink> tessellationSink =
    new InterrogableTessellationSink();
  pathGeometry->Tessellate(IdentityMatrix(), tessellationSink.Get());
  std::vector<D2D1_TRIANGLE> triangles = tessellationSink->GetTriangles();
  if (m_useMeshesNotGeometries)
  {
    // Generate a separate mesh from each triangle
    ID2D1DeviceContext* context = m_deviceResources->GetD2DDeviceContext();
    for (D2D1_TRIANGLE triangle : triangles)
    {
      ComPtr<ID2D1Mesh> triangleMesh;
      context->CreateMesh(&triangleMesh);
      ComPtr<ID2D1TessellationSink> sink;
      triangleMesh->Open(&sink);
      sink->AddTriangles(&triangle, 1);
      sink->Close();
      m_triangleMeshes.push_back(triangleMesh);
    }
  }
  else
  {
    // Generate a path geometry from each triangle
    for (D2D1_TRIANGLE triangle : triangles)
    {
      ComPtr<ID2D1PathGeometry> triangleGeometry;
      d2dFactory->CreatePathGeometry(&triangleGeometry);
      ComPtr<ID2D1GeometrySink> geometrySink;
      triangleGeometry->Open(&geometrySink);
      geometrySink->BeginFigure(triangle.point1, D2D1_FIGURE_BEGIN_FILLED);
      geometrySink->AddLine(triangle.point2);
      geometrySink->AddLine(triangle.point3);
      geometrySink->EndFigure(D2D1_FIGURE_END_CLOSED);
      geometrySink->Close();
      m_triangleGeometries.push_back(triangleGeometry);
    }
  }
}
void SparklingTextRenderer::Render()
{
  ...
Matrix3x2F centerMatrix = D2D1::Matrix3x2F::Translation(
    (logicalSize.Width - (m_geometryBounds.right + m_geometryBounds.left)) / 2,
    (logicalSize.Height - (m_geometryBounds.bottom + m_geometryBounds.top)) / 2);
  context->SetTransform(centerMatrix *
    m_deviceResources->GetOrientationTransform2D());
  context->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
  if (m_useMeshesNotGeometries)
  {
    for (ComPtr<ID2D1Mesh>& triangleMesh : m_triangleMeshes)
    {
      float gray = (rand() % 1000) * 0.001f;
      m_solidBrush->SetColor(ColorF(gray, gray, gray));
      context->FillMesh(triangleMesh.Get(), m_solidBrush.Get());
    }
  }
  else
  {
    for (ComPtr<ID2D1PathGeometry>& triangleGeometry : m_triangleGeometries)
    {
      float gray = (rand() % 1000) * 0.001f;
      m_solidBrush->SetColor(ColorF(gray, gray, gray));
      context->FillGeometry(triangleGeometry.Get(), m_solidBrush.Get());
    }
  }
  ...
}

基于视频的帧速率 (其中,程序将显示),我 Surface Pro 呈现网格比路径几何图形,尽管每个网格包含只是一个单一的三角形的事实更快。

颜色的动画是令人不安的闪烁的偏头痛光环,让人想起的所以您可能想要查看它时,小心一些。 图 5 显示静止图像从程序,应该是安全得多。

The SparklingText Display
图 5 SparklingText 显示

移动的镶嵌的三角形

在剩余的两个程序使用类似于 SparklingText 的战略来生成为窗体字形轮廓,三角形的集合,但然后移动屏幕周围的小三角形。

为 OutThereAndBackAgain,我设想会撕裂成其复合的三角形,然后回去再形成文本的文本。 图 6 演示这一过程在 3%到飞行除了动画。

OutThereAndBackAgainRenderer 类中的 CreateWindowSizeDependentResources 方法将汇编的信息结构中每个三角形关于我打电话给 TriangleInfo。 此结构包含一个单三角形 ID2D1Mesh 对象,以及需采取该三角形上的旅程向外,然后再返回的信息。 这趟旅程需要利用该功能您可以使用独立渲染的几何图形。 ID2D1Geometry 中的 ComputeLength 方法返回总长度的几何形状,而 ComputePointAtLength 返回的点上,曲线和切线到在任何长度的曲线。 从信息可以派生平移和旋转矩阵。

正如你可以看到在图 6,我需要将渐变画笔用于文本,这样,稍有不同颜色的三角形将交叉路径和有点混合。 即使我使用只有一个画笔,所需的效果需要调用 SetTransform 和 FillMesh 为每单三角形网格的呈现方法。 好像在转换前的原始位置网格应用渐变画笔。

A Still from the OutThereAndBackAgain Program
图 6 仍然从 OutThereAndBackAgain 的程序

我想知道,是否它会有效的更新方法变换"手动"与对方法调用将 TransformPoint 的 Matrix3x2F 类的所有个别的三角形,并巩固这些在一个单一的 ID2D1Mesh 对象,然后会呈现一次的 FillMesh 调用。 因此,添加了一个选项,果然,速度更快。 我不想像得到在更新的每个调用中创建的 ID2D1Mesh 就非常好,但它不会。 视觉效果略有不同,但是:所以有没有混杂的颜色渐变画笔应用于转换三角形网格中。

文字变形吗?

假设你 tessellate 字形轮廓几何形状的两个文本字符串的 — — 例如,"DirectX"和"因素"组成单词的此列的名称 — — 和配对插值为三角形。 动画然后可以定义将一个单词转换成其他。 这不完全是一个变形的作用,但我不知道还能叫它什么。

图 7 显示效果中途岛这两个词,并与小小的想象,您几乎可以出"DirectX"或"因子"在图像中。

The TextMorphing Display
图 7 TextMorphing 显示

以最佳的方式,王者三角形的每一对应该是空间上密切,但像货郎担问题最小化的三角形的所有对之间的距离是。 我通过排序的三角中心的 X 坐标由三角形的两个集合,然后分离成组表示的 X 坐标,范围集合了相对更简单的方法,排序那些由 Y 坐标。 当然,两个三角形集合是不同的大小,所以一些三角形中"的因素"一词对应于在"DirectX。"一词中的两个三角形

图 8 显示插值逻辑中的更新和呈现器中的呈现逻辑。

图 8 更新,并在 TextMorphing 中的呈现

void TextMorphingRenderer::Update(DX::StepTimer const& timer)
{
  ...
// Calculate an interpolation factor
  float t = (float)fmod(timer.GetTotalSeconds(), 10) / 10;
  t = std::cos(t * 2 * 3.14159f);     // 1 to 0 to -1 to 0 to 1
  t = (1 - t) / 2;                    // 0 to 1 to 0
  // Two functions for interpolation
  std::function<D2D1_POINT_2F(D2D1_POINT_2F, D2D1_POINT_2F, float)>
    InterpolatePoint =
      [](D2D1_POINT_2F pt0, D2D1_POINT_2F pt1, float t)
  {
    return Point2F((1 - t) * pt0.x + t * pt1.x,
      (1 - t) * pt0.y + t * pt1.y);
  };
  std::function<D2D1_TRIANGLE(D2D1_TRIANGLE, D2D1_TRIANGLE, float)>  
    InterpolateTriangle =
      [InterpolatePoint](D2D1_TRIANGLE tri0, D2D1_TRIANGLE tri1, float t)
  {
    D2D1_TRIANGLE triangle;
    triangle.point1 = InterpolatePoint(tri0.point1, tri1.point1, t);
    triangle.point2 = InterpolatePoint(tri0.point2, tri1.point2, t);
    triangle.point3 = InterpolatePoint(tri0.point3, tri1.point3, t);
    return triangle;
  };
  // Interpolate the triangles
  int count = m_triangleInfos.size();
  std::vector<D2D1_TRIANGLE> triangles(count);
  for (int index = 0; index < count; index++)
  {
    triangles.at(index) =
      InterpolateTriangle(m_triangleInfos.at(index).triangle[0],
                          m_triangleInfos.at(index).triangle[1], t);
  }
  // Create a mesh with the interpolated triangles
  m_deviceResources->GetD2DDeviceContext()->CreateMesh(&m_textMesh);
  ComPtr<ID2D1TessellationSink> tessellationSink;
  m_textMesh->Open(&tessellationSink);
  tessellationSink->AddTriangles(triangles.data(), triangles.size());
  tessellationSink->Close();
}
// Renders a frame to the screen
void TextMorphingRenderer::Render()
{
  ...
if (m_textMesh != nullptr)
  {
    Matrix3x2F centerMatrix = D2D1::Matrix3x2F::Translation(
      (logicalSize.Width - (m_geometryBounds.right + m_geometryBounds.left)) / 2,
      (logicalSize.Height - (m_geometryBounds.bottom + m_geometryBounds.top)) / 2);
    context->SetTransform(centerMatrix *
      m_deviceResources->GetOrientationTransform2D());
    context->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
    context->FillMesh(m_textMesh.Get(), m_blueBrush.Get());
  }
  ...
}

这一点,我认为我已经满足我对 2D 三角形的好奇心,我准备好要给这些三角形第三个层面。

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

衷心感谢以下 Microsoft 技术专家对本文的审阅:JimGalasyn 和迈克财富