本文章是由機器翻譯。

DirectX 要素

在 3D 空間中操作三角形

Charles Petzold

下載代碼示例

荷蘭的圖形演出者 M。C.埃舍爾一直是程式師、 工程師和其他技術員之間特定收藏。他詼諧的畫的可能結構和頭腦中的需要強加秩序對視覺資訊,儘管他使用的數學上的啟發的網狀模式似乎表明軟體遞迴技術熟悉玩。

我從 1948 年,從 2D 繪圖就本身正在速寫的 3D 手裡出現的一雙 3D 手是特別粉絲埃舍爾的混合的二維和三維影像"繪圖手"(見圖 1)。但這並列的 2D 和 3D 圖像強調 3D 手僅顯示有深度細節和網底。很明顯,在繪圖中的一切都呈現在平面紙上。


圖 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


圖 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


圖 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


圖 7 PlatonicSolids Program 與最大 3D

CharlesPetzold 是 MSDN 雜誌和作者的"程式設計視窗,第 6 版"長期貢獻 (微軟出版社,2013年),一本關於編寫應用程式的 Windows 8 書。 他的網站是 charlespetzold.com

感謝以下 Microsoft 技術專家對本文的審閱:Doug· 埃裡克森