DirectX 因素

处理 3D 空间中的三角形

Charles Petzold

下载代码示例

Charles Petzold荷兰的图形艺术家 M。C.埃舍尔一直是程序员、 工程师和其他技术员之间特定收藏。他诙谐的画的可能结构和头脑中的需要强加秩序对视觉信息,尽管他使用的数学上的启发的网状模式似乎表明软件递归技术熟悉玩。

我从 1948 年,从 2D 绘图就本身正在速写的 3D 手里出现的一双 3D 手是特别粉丝埃舍尔的混合的二维和三维影像"绘图手"(见图 1)。但这并列的 2D 和 3D 图像强调 3D 手仅显示有深度细节和底纹。很明显,在绘图中的一切都呈现在平面纸上。

M.C. Escher’s “Drawing Hands”
图 1 M.C.埃舍尔的"绘图手"

我想要做些类似在这篇文章:我想要 2D 图形对象似乎获得深度和身体,遇有从屏幕和漂浮在 3D 空间中,然后撤退回平的屏幕。

这些图形对象但是不会对人类的手描绘。相反,我会坚守也许,最简单的 3D 对象 — — 五个柏拉图。柏拉图是唯一可能的凸多面体面都是相同定期的凸多边形,相同数量的脸在每个顶点举行会议。他们是四面体 (四个三角形)、 八面体 (八个三角形)、 二十面体 (20 三角形)、 多维数据集 (六个正方形) 和十二面体 (12 个五边形)。

柏拉图是普遍的在简陋的 3D 图形编程因为他们通常很容易定义和组装。顶点的公式可以在维基百科,例如发现。

若要使这项工作作为特殊教育学校温顺,我会用 Direct2D,而不是 Direct3D。然而,您需要熟悉一些概念、 数据类型、 函数和结构常用在 Direct3D。

我的策略是定义这些固体物体在 3D 空间中,使用三角形,然后应用 3D 变换旋转它们。转换后的三角形坐标然后拼合到 2D 空间通过忽略的 Z 坐标,在他们用来创建 ID2D1Mesh 对象,然后使用 ID2D1DeviceContext 对象的 FillMesh 方法进行呈现。

正如你所看到的它是不够的只需定义 3D 对象的坐标。底纹应用来模仿光的反射时,只做对象,似乎逃脱平整度的屏幕。

3D 点和转换

这项工作需要 3D 矩阵转换被应用到 3D 点来旋转空间中的对象。最佳的数据类型和函数的这份工作是什么?

有趣的是,Direct2D 是适合于表示 3D 变换矩阵的 D2D1 命名空间中有一个 D2D1_MATRIX_4X4_F 结构和一个 Matrix4x4F 类。但是,这些数据类型设计仅为使用与定义的 ID2D1DeviceContext,DrawBitmap 方法示在此列的 4 月分期付款。尤其是,Matrix4x4F 甚至没有一个名为可以将变换应用于三维点的变换方法。您将需要实现该矩阵乘法与您自己的代码。

查找 3D 数据类型更好的地方是 DirectX 数学库,由 Direct3D 的程序,以及使用的。此库定义超过 500 职能 — — 所有这些都以字母 XM 开头 — — 和几种数据类型。这些是在 DirectXMath.h 头文件中声明的所有,与 DirectX 的命名空间相关联。

DirectX 数学库中的每个单个函数涉及使用的一个名为 XMVECTOR,是一个集合的四个数字的数据类型。XMVECTOR 是适合于表示 2D 或 3D 点 (有或无 W 坐标) 或一种颜色 (带有或不使用 alpha 通道)。这里是你会如何定义一个 XMVECTOR 类型的对象:

XMVECTOR vector;

注意我说的 XMVECTOR 是"四个数字"的集合,而不是"四个浮点值"或"四个整数"。我不能更具体,因为实际的 XMVECTOR 对象中的四个数字的格式是依赖于硬件。

XMVECTOR 不是一个正常的数据类型 ! 它是实际上在处理器芯片上,具体单指令多数据 (SIMD) 寄存器用于 SIMD 流技术扩展 (SSE) 实现并行处理的四个硬件寄存器的代理。 在 x86 硬件这些寄存器是事实上的单精度浮-­点值,但他们在 ARM 处理器 (Windows RT 设备中找到) 中定义为具有小数部分的整数。

为此,你应该尝试直接访问 XMVECTOR 对象的字段,(除非你知道你在做什么)。 相反,DirectX 数学库包括大量的功能,以将该字段设置从整数或浮点值。 在这里是共同的:

XMVECTOR vector = XMVectorSet(x, y, z, w);

函数还存在以获得单个字段的值:

float x = XMVectorGetX(vector);

因为这种数据类型是硬件寄存器的代理,某些限制管理其使用。 读在线 DirectXMath 编程指南 》 (bit.ly/1d4L7Gk) 定义结构类型的成员的 XMVECTOR 和 XMVECTOR 的参数传递给函数的详细信息。

一般情况下,但是,你就会 proba­布莱大多是本地的一种方法的代码中使用 XMVECTOR。 为通用的存储的 3D 点和向量,DirectX 数学库定义了其他数据类型的简单的正常结构,例如 XMFLOAT3 (其中有三个数据成员的浮点类型命名为 x、 y 和 z) 和 XMFLOAT4 (其中有四个数据成员,包括 w)。 尤其是,你就会想要使用 XMFLOAT3 或 XMFLOAT4 存储阵列的点。

它很容易 XMVECTOR 和 XMFLOAT3 或 XMFLOAT4 之间传输。 假设您使用 XMFLOAT3 来存储一个 3D 点:

XMFLOAT3 point;

当你需要使用需要 XMVECTOR 的 DirectX 数学函数之一时,可以加载到使用 XMLoadFloat3 函数 XMVECTOR 的值:

XMVECTOR vector = XMLoadFloat3(&point);

XMVECTOR 中的 w 值初始化为 0。 然后可以在各种 DirectX 数学函数中使用的 XMVECTOR 对象。 要返回的 XMFLOAT3 对象中存储的 XMVECTOR 值,调用:

XMStoreFloat3(&point, vector);

同样,XMLoadFloat4 和 XMStoreFloat4 转让 XMVECTOR 对象和 XMFLOAT4 之间的值的对象,和这些往往是首选如果 W 坐标是重要的。

在一般情况下,您会使用几个 XMVECTOR 对象在同一代码块,其中的一些对应于基础的 XMFLOAT3 或 XMFLOAT4 对象,并且其中一些只是瞬态。 你不久就会看到的例子。

我刚才所说的 DirectX 数学库中的每个函数涉及到 XMVECTOR。 如果你探索过图书馆,你可能会发现一些函数实际上不需要 XMVECTOR,但不要涉及 XMMATRIX 类型的对象。

XMMATRIX 数据类型是一个 4 × 4 矩阵适合 3D 转换,但它实际上四个 XMVECTOR 对象,另一个用于每个行:

struct XMMATRIX
{
  XMVECTOR r[4];
};

所以我说的是正确的因为要求使用 XMMATRIX 对象的所有 DirectX 数学函数真的做都涉及 XMVECTOR 对象,以及,和 XMMATRIX 有 XMVECTOR 相同的限制。

正如 XMFLOAT4 是一个正常的结构,可以使用传送到和从 XMVECTOR 的对象的值,可以使用一个名为 XMFLOAT4X4 的正常结构来存储一个 4 × 4 矩阵和转让的从 XMMATRIX 使用 XMLoadFloat4x4 和 XMStoreFloat4x4 函数。

如果你已经加载一个 3D 点到 XMVECTOR 对象 (命名为向量,例如),并且您已经加载到一个名为矩阵的 XMMATRIX 对象的变换矩阵,可以应用该转换到点使用:

XMVECTOR result = XMVector3Transform(vector, matrix);

或者,您可以使用:

XMVECTOR result = XMVector4Transform(vector, matrix);

唯一的区别是 XMVector4Transform 使用的实际的 w 值的 XMVECTOR,而 XMVector3Transform 假定它为 1,这是正确的执行 3D 平移。

然而,如果您有一个数组 XMFLOAT3 或 XMFLOAT4 的值,并且想要将转换应用于整个阵列,有更好的解决方案:XMVector3TransformStream 和 XMVector4TransformStream 功能将 XMMATRIX 应用到一个数组中的值并将结果存储在 XMFLOAT4 值 (无论是输入的类型) 的数组。

奖金:因为 XMMATRIX 实际上是在实现 SSE CPU 上的 SIMD 寄存器,CPU 可以使用并行处理应用变换的点数组,并加快在 3D 渲染中的最大瓶颈之一。

定义柏拉图

此列的可下载代码是一个名为 PlatonicSolids 的单一 Windows 8.1 项目。 程序使用 Direct2D 呈现五个柏拉图的 3D 图像。

像所有的 3D 数字,这些固体可以描述为在 3D 空间中的三角形的集合。 我就知道我会想要使用 XMVector3­TransformStream 或 XMVector4TransformStream,变换的 3D 三角形数组和我就知道这两个函数的输出数组总是一个 XMFLOAT4 对象数组,所以我决定为输入数组,以及,使用 XMFLOAT4,这就是我如何界定我的 3D 三角形结构:

struct Triangle3D
{
  DirectX::XMFLOAT4 point1;
  DirectX::XMFLOAT4 point2;
  DirectX::XMFLOAT4 point3;
};

图 2 显示所需的描述和渲染 3D 图的一些额外的私人数据结构定义在 PlatonicSolidsRenderer.h 中存储的信息。 每五个柏拉图是 FigureInfo 类型的对象。 缩放后的 srcTriangles 和 dstTriangles 的集合存储原始的"源"三角形和三角形的"目的地"和已应用旋转转换。这两个集合的大小等于 faceCount 和 trianglesPerFace 的产品。 注意到 srcTriangles.data 和 dstTriangles.data 有效地指向 XMFLOAT4 结构的指针,因此可以 XMVector4TransformStream 函数的参数。 正如你所看到的这发生在 PlatonicSolidRenderer 类中的更新方法。

图 2 的数据结构,用于存储 3D 数字

struct RenderInfo
{
  Microsoft::WRL::ComPtr<ID2D1Mesh> mesh;
  Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
};
struct FigureInfo
{
  // Constructor
  FigureInfo()
  {
  }
  // Move constructor
  FigureInfo(FigureInfo && other) :
    srcTriangles(std::move(other.srcTriangles)),
    dstTriangles(std::move(other.dstTriangles)),
    renderInfo(std::move(other.renderInfo))
  {
  }
  int faceCount;
  int trianglesPerFace;
  std::vector<Triangle3D> srcTriangles;
  std::vector<Triangle3D> dstTriangles;
  D2D1_COLOR_F color;
  std::vector<RenderInfo> renderInfo;
};
std::vector<FigureInfo> m_figureInfos;

RenderInfo 字段是图的每个面有一个 RenderInfo 对象的集合。 这种结构的两名成员也已确定期间更新方法,和他们简单地传递给 ID2D1DeviceContext 对象的 FillMesh 方法吃渲染方法的过程。

PlatonicSolidsRenderer 类的构造函数初始化每个五个的 FigureInfo 对象。 图 3 显示最简单的五个,四面体的过程。

图 3 定义四面体

FigureInfo tetrahedron;
tetrahedron.faceCount = 4;
tetrahedron.trianglesPerFace = 1;
tetrahedron.srcTriangles =
{
  Triangle3D { XMFLOAT4(-1,  1, -1, 1),
               XMFLOAT4(-1, -1,  1, 1),
               XMFLOAT4( 1,  1,  1, 1) },
  Triangle3D { XMFLOAT4( 1, -1, -1, 1),
               XMFLOAT4( 1,  1,  1, 1),
               XMFLOAT4(-1, -1,  1, 1) },
  Triangle3D { XMFLOAT4( 1,  1,  1, 1),
               XMFLOAT4( 1, -1, -1, 1),
               XMFLOAT4(-1,  1, -1, 1) },
  Triangle3D { XMFLOAT4(-1, -1,  1, 1),
               XMFLOAT4(-1,  1, -1, 1),
               XMFLOAT4( 1, -1, -1, 1) }
};
tetrahedron.srcTriangles.shrink_to_fit();
tetrahedron.dstTriangles.resize(tetrahedron.srcTriangles.size());
tetrahedron.color = ColorF(ColorF::Magenta);
tetrahedron.renderInfo.resize(tetrahedron.faceCount);
m_figureInfos.at(0) = tetrahedron;

八面体和二十面体的初始化是相似的。 在所有三种情况下,每个面组成的只是一个三角形。 以像素为单位),坐标是非常小的但后来在程序中的代码将它们缩放到适当大小。

然而,多维数据集和十二面体是不同的的。 多维数据集有六个面,每个是正方形,和十二面体是 12 五边形。 为这两个数字,我用一个不同的数据结构来存储的每个面和一个共同的方法,转换成三角形的每个面顶点 — — 为每个脸的多维数据集和每个工作面的十二面体的三个三角形的两个三角形。

为便于在三维坐标转换为二维坐标,我已经将这些数字基于在 X 坐标增加下去的正确和积极 Y 坐标增加哪些积极的坐标系统。 (它是在 3D 编程中,积极的 Y 坐标,以增加去更常见。我也已经假设,积极 Z 坐标来的屏幕。 因此,这是一个左手坐标系。 如果你点你的左手的食指在正面 X 和中指的正面 Y 方向的方向,你的拇指指向正 Z。

在计算机屏幕的查看器被假定位于正面朝向原点的 Z 轴上的一个点。

3D 中的旋转

PlatonicSolidsRenderer 中的更新方法执行的动画,包括几个部分。 当程序开始运行,五时显示柏拉图,但他们似乎是平坦的如中所示图 4

The PlatonicSolids Program As It Begins Running
图 4 作为它在 PlatonicSolids 程序开始运行

这些显然都不是可认识的作为 3D 对象 !

在 2.5 秒内对象开始旋转。 更新方法计算旋转角度和基于屏幕的大小比例因子,然后使使用 DirectX 数学函数。 如 XMMatrixRotationX 函数计算一个 XMMATRIX 对象,表示绕 X 轴旋转的角度。 XMMATRIX 还定义矩阵乘法运算符,因此这些函数的结果可以相乘。

图 5 显示总矩阵变换是如何计算和应用到的每个图中的 Triangle3D 对象的数组。

图 5 旋转数字

// Calculate total matrix
XMMATRIX matrix = XMMatrixScaling(scale, scale, scale) *
                  XMMatrixRotationX(xAngle) *
                  XMMatrixRotationY(yAngle) *
                  XMMatrixRotationZ(zAngle);
// Transform source triangles to destination triangles
for (FigureInfo& figureInfo : m_figureInfos)
{
  XMVector4TransformStream(
    (XMFLOAT4 *) figureInfo.dstTriangles.data(),
    sizeof(XMFLOAT4),
    (XMFLOAT4 *) figureInfo.srcTriangles.data(),
    sizeof(XMFLOAT4),
    3 * figureInfo.srcTriangles.size(),
    matrix);
}

一旦数字开始旋转,然而,他们仍然似乎是平面多边形,即使他们改变了形状。

闭塞和隐藏的表面

3D 图形编程的重要方面之一使确定对象更接近到观众的眼睛晦涩 (或咬合) 对象越来越远的地方。 在复杂的场景,这不是一个微不足道的问题,和一般情况下,这必须在按像素的基础上的图形硬件中执行。

然而,与凸多面体是相对很简单。 考虑一个多维数据集。 多维数据集在空间中旋转,大多是你看到三个面,和有时只是一个或两个。 你从来没有看到四个、 五个或所有的六个面。

旋转的多维数据集的特定脸,怎样才能确定你看到什么面孔和什么面临被隐藏? 想想 (往往可视化作为一个特定的方向的箭头) 的向量垂直于该多维数据集的每个面和指向外部的多维数据集。 这些被称为"表面正常"向量。

只有当表面的法向量有积极的 Z 分量那表面将可见到观测从正 Z 轴对象查看器。

数学上,计算一个正常的一个三角形的表面非常简单:三角形的三个顶点定义两个向量,及两个向量 (V1 和 V2) 在 3D 空间中的定义一个平面,并垂直于该平面从向量中获得跨产品,如中所示图 6

The Vector Cross Product
图 6 向量跨产品

这一矢量的实际方向取决于坐标系的惯用手。 右侧的坐标系统,例如,您可以确定方向的 V1 × V2 跨产品通过弯曲你的右手从 V1 到 V2 的手指。 中的跨产品方向的拇指点。 左侧的坐标系统,使用你的左手。

任何特定的三角,构成了这些数字,第一步是加载到 XMVECTOR 对象的三个顶点:

XMVECTOR point1 = XMLoadFloat4(&triangle3D.point1);
XMVECTOR point2 = XMLoadFloat4(&triangle3D.point2);
XMVECTOR point3 = XMLoadFloat4(&triangle3D.point3);

然后,两个向量表示三角形的两边可以计算减去 point2 和 point3 point1 使用方便 DirectX 数学函数:

XMVECTOR v1 = XMVectorSubtract(point2, point1);
XMVECTOR v2 = XMVectorSubtract(point3, point1);

在此程序中的所有柏拉图是与三角形的三个点按顺时针方向点 1 至安排到 point3 point2 三角形从外,图中查看时都定义的。 可以使用 DirectX 数学函数,用于获取跨产品计算表面正常指向外,图:

XMVECTOR normal = XMVector3Cross(v1, v2);

显示这些数字的程序只是可以选择不显示任何与正常,已为 0 或负数 Z 组件表面的三角形。 PlatonicSolids 程序相反继续显示这些三角形,但用透明的颜色。

它是所有关于底纹

您看到真实世界中的对象,因为它们反映了光。 没有光,什么也看不见。 在很多现实世界环境中,光来自许多不同的方向因为它反弹关闭其他表面,并在空中散开。

在 3D 图形编程中,这被称为"环境"的光,并不是很充足。 如果在多维数据集浮动在 3D 空间中的相同的环境光线罢工所有六个面,所有的六个面将带有相同的颜色和它根本不会看起来像 3D 多维数据集。

场景在 3D,因此,通常需要一些定向光源 — — 从一个或多个方向来的光线。 简单的 3D 场景的一个常用方法是作为一个向量,似乎从查看器的左肩后面来定义方向性光源:

XMVECTOR lightVector = XMVectorSet(2, 3, -1, 0);

从观众的角度来看,这是许多点向右和向下,和从查看器中的负向 Z 轴方向的媒介之一。

在筹备下一份工作,我想要正常化表面法线向量和光向量:

normal = XMVector3Normalize(normal);
lightVector = XMVector3Normalize(lightVector);

XMVector3Normalize 函数计算使用勾股定理的 3D 形式的向量的严重程度,然后除以如此规模的三个坐标。 由此产生的矢量有 1 数量级。

如果法线向量恰好相等的负面的 lightVector,这意味着光触击三角形垂直于它的表面,和那是定向光源可以提供的最大启示。 如果定向光源不相当垂直于三角形表面,照明将更少。

数学上的表面从方向性光源照明等于光矢量和正常的消极面之间的角度的余弦值。 如果这些两个向量有一个规模 1,然后这个关键数字由两个向量的点积:

XMVECTOR dot = XMVector3Dot(normal, -lightVector);

点积是一个标量 — — 一个数字 — — 而不是一个向量,因此从这个函数持有相同的值返回的 XMVECTOR 对象的所有字段。

要使它看起来仿佛旋转柏拉图神奇地承担 3D 深度从平面屏幕出现时,PlatonicSolids 程序叫 lightIntensity 从 0 到 1 的值进行动画处理,然后又回到 0。 0 值是无定向的光阴影和没有 3D 的效果,而 1 的值是最大的 3D。 此 lightIntensity 值点的产物结合用于计算总的光因子:

float totalLight = 0.5f +
  lightIntensity * 0.5f * XMVectorGetX(dot);

在此公式中的第一次 0.5 指环境光线,和第二次 0.5 允许范围为从 0 到 1 的点积不同价值,totalLight。 (从理论上讲,这并不十分正确。 负值的点积应设置为 0 因为它们会导致在小于环境光线的总光)。

此 totalLight 然后用于计算颜色和画笔的每个平面:

renderColor = ColorF(totalLight * baseColor.r,
                     totalLight * baseColor.g,
                     totalLight * baseColor.b);

最大的 3D ishness 的结果显示在图 7

The PlatonicSolids Program with Maximum 3D
图 7 PlatonicSolids Program 与最大 3D

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

衷心感谢以下 Microsoft 技术专家对本文的审阅:道格 · 埃里克森