曲面細分是Direct3D 11帶來的其中一項重要的新功能。它引入了兩個可編程着色器階段以及一個固定的鑲嵌處理過程。簡單來講,曲面細分技術能夠將幾何體細分爲更小的三角形,並以某種方式把這些新生成的頂點偏移到合適的位置,從而以增長三角形數量的方式豐富網格細節。但爲何不在建立網格之初就直接賦予它高模(high-poly,高面數多邊形)的細節呢?如下是使用曲面細分的3個理由:html
曲面細分技術涉及到的三個階段都是可選的,但若是要使用曲面細分,這三個階段都是必需要經歷的。git
學習目標:程序員
DirectX11 With Windows SDK完整目錄github
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。編程
在進行曲面細分時,咱們並不向IA(輸入裝配)階段提交三角形,而是提交具備若干控制點的面片。Direct3D支持具備1~32個控制點的面片,並如下列圖元類型進行描述:數組
D3D_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST = 33, D3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST = 34, D3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST = 35, ... D3D_PRIMITIVE_TOPOLOGY_31_CONTROL_POINT_PATCHLIST = 63, D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64,
因爲能夠將三角形看做是擁有3個控制點的三角形面片(D3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST
),因此咱們依然能夠提交須要鑲嵌化處理的普通三角形網格。對於簡單的四邊形面片而言,則只須要提交4個控制點的面片(D3D_PRIMITIVE_4_CONTROL_POINT_PATCH)便可。這些面片最終也會在曲面細分階段通過鑲嵌化處理而分解爲多個三角形。dom
注意:
D3D_PRIMITIVE_TOPOLOGY
枚舉項描述輸入裝配階段中的頂點類型,而D3D_PRIMITIVE
枚舉項則描述的是外殼着色器的輸入圖元類型。ide
那麼,具備更多控制點的面片又有什麼用處呢?控制點的概念來自於特定種類數學角度上特定曲線或曲面的構造過程。若是在相似於Adobe Illustrator這樣的繪圖程序中使用過貝塞爾曲線工具,那讀者必定會知道要經過控制點才能描繪出曲線形狀。在數學上,能夠利用貝塞爾曲線來生成貝塞爾曲面。舉個例子,咱們能夠用9個控制點或16個控制點來建立一個貝塞爾四邊形面片,所用的控制點越多,咱們對面片形狀的控制也就越爲所欲爲。所以,這一切圖元控制類型都是爲了給這些不一樣種類的曲線、曲面的繪製提供支持。函數
在咱們向渲染管線提交了面片的控制點後,它們就會被推送至頂點着色器。這樣一來,在開始曲面細分的時候,頂點着色器就完全淪爲「處理控制點的着色器」。正由於如此,咱們還能在曲面細分開始以前,對控制點進行一些調整。通常來講,動畫與物理模擬的計算工做都會在對幾何體進行鑲嵌化處理以前的頂點着色器中以較低的頻次進行(鑲嵌化處理以後,頂點增多,處理的頻次也將隨之增長)。
外殼着色器是由兩種着色器共同組成的:常量外殼着色器(Constant Hull Shader)和控制點外殼着色器(Control Point Hull Shader)
常量外殼着色器會針對每一個面片統一進行處理(即每處理一個面片就被調用一次)。它的任務是輸出當前網格的曲面細分因子,並且必需要輸出。曲面細分因子指示了在曲面細分階段中將面片鑲嵌處理後的份數,以及怎麼進行細分。它由兩個輸出系統值所表示:SV_TessFactor
和SV_InsideTessFactor
,這兩個系統值屬於float或float數組的類型,具體取決於輸入裝配階段定義的圖元類型。常量外殼着色器的輸出被限制在128個標量(如32個4D單精度浮點向量),這意味着除了系統值,你還能夠額外添加輸出信息供每一個面片所使用。下面是一個具備3個控制點的四邊形面片示例,咱們經過常量緩衝區來爲其設置各個方面的細分程度:
struct QuadPatchTess { float EdgeTess[4] : SV_TessFactor; float InsideTess[2] : SV_InsideTessFactor; // 能夠在下面爲每一個面片附加所需的額外信息 }; QuadPatchTess QuadConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID) { QuadPatchTess pt; pt.EdgeTess[0] = g_QuadEdgeTess[0]; // 四邊形面片的左側邊緣 pt.EdgeTess[1] = g_QuadEdgeTess[1]; // 四邊形面片的上側邊緣 pt.EdgeTess[2] = g_QuadEdgeTess[2]; // 四邊形面片的右側邊緣 pt.EdgeTess[3] = g_QuadEdgeTess[3]; // 四邊形面片的下冊邊緣 pt.InsideTess[0] = g_QuadInsideTess[0]; // u軸(四邊形內部細分的列數) pt.InsideTess[1] = g_QuadInsideTess[1]; // v軸(四邊形內部細分的行數) return pt; }
其中InputPatch<VertexOut, 4>
定義了控制點的數目和信息。前面提到,控制點首先會傳至頂點着色器,所以它們的類型由頂點着色器的輸出類型VertexOut
來肯定。在此例中,咱們的面片擁有4個控制點,因此就將InputPatch
模板第二個參數指定爲4。系統還經過SV_PrimitiveID
語義提供了面片的ID值,此ID惟一地標識了繪製調用過程當中的各個面片,咱們能夠根據具體的需求來運用它。
但按左上右下的順序來控制邊緣細分是創建在使用下面的頂點擺放順序而言的:
XMFLOAT3 quadVertices[4] = { XMFLOAT3(-0.54f, 0.72f, 0.0f), // 左上角 XMFLOAT3(0.54f, 0.72f, 0.0f), // 右上角 XMFLOAT3(-0.54f, -0.72f, 0.0f), // 左下角 XMFLOAT3(0.54f, -0.72f, 0.0f) // 右下角 };
對四邊形面片(quad)進行鑲嵌化處理的過程由兩個構成:
對三角形面片(tri)進行鑲嵌化處理的過程一樣分爲兩部分:
對等值線(isoline)進行鑲嵌化處理的過程以下:
Direct3D 11硬件所支持的最大麴面細分因子爲64(D3D11_TESSELLATOR_MAX_TESSELLATION_FACTOR
).若是把全部的曲面細分因子都設置爲0,則該面片會被後續的處理階段所丟棄。這就使得咱們可以以每一個面片爲基準來實現如視錐體剔除與背面剔除這類優化。
一個問題天然而然地復現出來:到底應該執行幾回鑲嵌化處理才合適?前面提到,曲面細分的基本想法就是爲了豐富網格的細節。可是,若是用戶對此無感,咱們就不須要對它增添細節了。如下是一些肯定鑲嵌次數的經常使用衡量標準。
[Story10(可點擊)]給出瞭如下幾點關於性能的建議。
控制點外殼着色器以大量的控制點做爲輸入與輸出,頂點着色器每輸出一個控制點,此着色器都會被調用一次。控制點外殼着色器的應用之一是改變曲面的表示方式,好比把一個普通的三角形(向渲染管線提交的3個控制點)轉換爲3次貝塞爾三角形面片。例如,假設咱們像日常那樣利用三角形對網格進行建模,就能夠經過控制點外殼着色器,把這些三角形轉換爲具備10個控制點的高階三次貝塞爾三角形面片。新增的控制點不只會帶來更加豐富的細節,並且能將三角形面片鑲嵌細分爲用戶所指望的份數。這一策略被稱之爲N-patches方法(法線—面片方法,normal-patches scheme)或PN三角形方法(即(曲面)點—法線三角形方法,point-normal triangles,簡寫爲PN triangles scheme)[Vlachos]。因爲這種方案只需用曲面細分技術來改進存在的三角形網格,且無需改動美術製做流程,因此實現起來比較方便。對於本章前面兩個演示案例來講,控制點外殼着色器僅充當一個簡單的傳遞着色器,它不會對控制點進行任何的修改。
注意:驅動程序可能會對傳遞着色器進行檢測與優化。
struct VertexOut { float3 PosL : POSITION; }; typedef VertexOut HullOut; // Tessellation_Quad_Integer_HS.hlsl [domain("quad")] [partitioning("integer")] [outputtopology("triangle_cw")] [outputcontrolpoints(4)] [patchconstantfunc("QuadConstantHS")] [maxtessfactor(64.0f)] float3 HS(InputPatch<VertexOut, 4> patch, uint i : SV_OutputControlPointID, uint patchId : SV_PrimitiveID) : POSITION { return patch[i].PosL; }
經過InputPatch
參數能夠將面片的全部控制點都傳至外殼着色器中。系統值SV_OutputControlPointID
索引的是正在被外殼着色器處理的輸出控制點。值得注意的是,輸入的控制點數量與輸出的控制點數量未必相同。例如,輸入的面片可能僅含有4個控制點,而輸出的面片卻可以擁有16個控制點;這些多出來的控制點能夠由輸入的4個控制點所衍生。
上面的控制點外殼着色器還用到了如下幾種屬性。
domain
:面片的類型。可選用的參數有tri
(三角形面片)、quad
(四邊形面片)或isoline
(等值線)
partioning
:指定了曲面細分的細分模式。
integer
:新頂點的添加或移除依據的是上取整的函數。例如咱們將細分值設爲3.25f時,實際上它將會細分爲4份。這樣一來,在網格隨着曲面細分級別而改變時,會容易發生明顯的躍變。fractional_even
/fractional_odd
):新頂點的增長或移除取決於曲面細分因子的整數部分,可是細微的漸變「過渡」調整就要根據細分因子的小數部分。當咱們但願將粗糙的網格經曲面細分而平滑地過渡到具備更加細節的網格時,該參數就派上用場了。pow2
:目前測試的時候行爲和integer
一致,不知道什麼緣由。這裏暫時不講述。outputtopology
:經過細分所創的三角形的繞序
triangle_cw
:順時針方向的繞序triangle_ccw
:逆時針方向的繞序line
:針對線段的曲面細分outputcontrolpoints
:外殼着色器執行的次數,每次執行都輸出1個控制點。系統值SV_OutputControlPointID
給出的索引標明瞭當前正在工做的外殼着色器所輸出的控制點。
patchconstantfunc
:指定常量外殼着色器函數名稱的字符串
maxtessfactor
:告知驅動程序,用戶在着色器中所用的曲面細分因子的最大值。若是硬件知道了此上限,就能夠了解曲面細分所需的資源,繼而在後臺對此進行優化。Direct3D 11硬件支持的曲面細分因子最大值爲64
程序員沒法對鑲嵌器這一階段進行任何控制,由於這一步的操做全權交給硬件處理。此環節會基於常量外殼着色器程序所輸出的曲面細分因子,對面片進行鑲嵌化處理。
integer
模式fractional_odd
模式:fractional_even
模式:鑲嵌器階段會輸出新建的全部頂點與三角形,在此階段所建立的頂點,都會逐一調用域着色器進行後續處理。隨着曲面細分功能的開啓,頂點着色器便化身爲「處理每一個控制點的頂點着色器」,而外殼着色器的本質則爲「針對已經鑲嵌化的面片進行處理的頂點着色器」。特別是,咱們能夠在此將通過鑲嵌化處理的面片頂點投射到齊次裁剪空間。
首先是三角形面片,域着色器以曲面細分因子(還有一些來自常量外殼着色器所輸出的每一個面片的附加信息)、控制點外殼着色器所輸出的全部面片控制點、鑲嵌化處理後的頂點位置參數(以重心座標系(alpha, beta, gamma)
的形式表示)做爲輸入。注意,域着色器給出的並非鑲嵌化處理後的實際頂點位置,而是這些點位於面片域空間內的參數座標。是否利用這些參數座標及控制點來求取真正的3D頂點位置,徹底取決於用戶本身。下面展現了前面的例子顯示的三角形所用到的域着色器代碼:
struct VertexOut { float3 PosL : POSITION; }; typedef VertexOut HullOut; // Tessellation_Triangle_DS.hlsl [domain("tri")] float4 DS(TriPatchTess patchTess, float3 weights : SV_DomainLocation, const OutputPatch<HullOut, 3> tri) : SV_POSITION { // 重心座標系插值 float3 pos = tri[0].PosL * weights[0] + tri[1].PosL * weights[1] + tri[2].PosL * weights[2]; return float4(pos, 1.0f); }
將三角形面片以重心座標系做爲輸出的緣由,極可能是由於貝塞爾三角形面片都是用重心座標來定義所致使的。
而四邊形面片的頂點位置參數以(u, v)
的形式表示,前面例子的四邊形所用的域着色器代碼以下:
struct VertexOut { float3 PosL : POSITION; }; typedef VertexOut HullOut; // Tessellation_Quad_DS.hlsl [domain("quad")] float4 DS(QuadPatchTess patchTess, float2 uv : SV_DomainLocation, const OutputPatch<HullOut, 4> quad) : SV_POSITION { // 雙線性插值 float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x); float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x); float3 p = lerp(v1, v2, uv.y); return float4(p, 1.0f); }
這部分借用GAMES101來講明。而且這裏講的貝塞爾曲線所用的算法是由Pierre Bézier和Paul de Casteljau所提出的。
如今咱們先從二階貝塞爾曲線開始,下面有3個非共線的控制點b0、b1和b2。
接下來咱們用線性插值的方式,從b0到b1方向的線段使用參數t來肯定其中一點,記爲\(\mathbf{b_{0}^{1}}\)。
而後從b1到b2方向的線段使用一樣的參數t來肯定另外一點,記爲\(\mathbf{b_{1}^{1}}\)。
將\(b_{0}^{1}\)和\(b_{1}^{1}\)鏈接起來,問題降級爲一階貝塞爾曲線(直線)。咱們對其再使用一次參數t的線性插值便可獲得在參數t下該貝塞爾曲線的對應一點\(\mathbf{b_{2}^{0}}\)。
咱們將t在[0, 1]
的全部狀況都求出來,就能夠獲得一條光滑的貝塞爾曲線。
三階的貝塞爾曲線須要用到4個控制點,但作法也是相似的。首先求出b0到b1,b1到b2,b2到b3的線段在t時刻下的插值點\(\mathbf{b_{0}^{1}}, \mathbf{b_{1}^{1}}, \mathbf{b_{2}^{1}}\),此時問題就被轉化成了這三個控制點下的二階貝塞爾曲線。不斷降階最終算出目標點便可。
能夠看到,三階貝塞爾曲線須要進行6次插值運算,二階貝塞爾曲線則須要進行3次插值運算。以此類推,咱們能夠知道n階貝塞爾曲線須要n(n+1)/2次運算
下圖闡述了二階貝塞爾曲線的計算過程
觀察b0、b1和b2的各項係數,能夠發現它們知足二項式定理。咱們也能夠用下面的一個金字塔來描述
從底端的這些控制點選取其中一個而後不斷往上走,最終走到頂端點的過程當中,若是走過一段朝着右上方向的路徑,則給該控制點乘上因子(1-t),而朝着左上方向的路徑則給該控制點乘上因子t。好比從b0走到頂端的項爲\(t^3 b_{0}\)。咱們將這全部8條路徑都加起來就能夠獲得最後的結果:
更抽象的,咱們能夠用伯恩斯坦函數的形式來表示:
對於三階貝塞爾曲線而言,曲線端點爲:
而三次伯恩斯坦函數的導數爲:
所以,對3次貝塞爾曲線求導的結果爲:
經過這些導數就能夠很方便地計算出曲線上某點處的切向量。
對於複雜曲線,若是咱們使用高階曲線的話,觀察伯恩斯坦函數形式的頂點式會發現計算量呈平方級別增加。爲此咱們能夠考慮將該曲線分段,而後這些曲線都使用較低階的貝塞爾曲線去擬合,以此來減小計算量。
假設咱們如今有兩條三階貝塞爾曲線a和b,那麼當曲線a的最後一個控制點與曲線b的第一個控制點位置相同時,咱們稱這兩條曲線知足C0連續。下圖的紅點爲控制點,觀察中間的控制點處顯然知足這一性質,可是能夠看到左右兩端曲線的過渡並非平緩的。
而若是在知足C0連續的基礎上,曲線a在最後一個控制點處的導數還與曲線b在第一個控制點處的導數相等,則此時咱們說這兩條曲線知足C1連續。
而知足C1連續的控制點必然知足:
即曲線a和b的鏈接點爲曲線a倒數第二個控制點與曲線b第二個控制點的中點。
固然還有更高級別的C2連續,即知足二階導數相等,這裏再也不深刻討論。
繪製貝塞爾曲線的外殼着色器和域着色器代碼以下:
// Tessellation.hlsli float4 BernsteinBasis(float t) { float invT = 1.0f - t; return float4( invT * invT * invT, // B_{0}^{3}(t)= (1-t)^3 3.0f * t * invT * invT, // B_{1}^{3}(t)= 3t(1-t)^2 3.0f * t * t * invT, // B_{2}^{3}(t)= 3t^2(1-t) t * t * t); // B_{3}^{3}(t)= t^3 } float4 dBernsteinBasis(float t) { float invT = 1.0f - t; return float4( -3 * invT * invT, // B_{0}^{3}'(t)= -3(1-t)^2 3.0f * invT * invT - 6 * t * invT, // B_{1}^{3}'(t)= 3(1-t)^2 - 6t(1-t) 6 * t * invT - 3 * t * t, // B_{2}^{3}'(t)= 6t(1-t) - 3t^2 3 * t * t); // B_{3}^{3}'(t)= 3t^2 }
// Tessellation_Isoline_HS.hlsl IsolinePatchTess IsolineConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID) { IsolinePatchTess pt; pt.EdgeTess[0] = g_IsolineEdgeTess[0]; // 未知 pt.EdgeTess[1] = g_IsolineEdgeTess[1]; // 段數 return pt; } [domain("isoline")] [partitioning("integer")] [outputtopology("line")] [outputcontrolpoints(4)] [patchconstantfunc("IsolineConstantHS")] [maxtessfactor(64.0f)] float3 HS(InputPatch<VertexOut, 4> patch, uint i : SV_OutputControlPointID, uint patchId : SV_PrimitiveID) : POSITION { return patch[i].PosL; }
// Tessellation_BezierCurve_DS.hlsl #include "Tessellation.hlsli" [domain("isoline")] float4 DS(IsolinePatchTess patchTess, float t : SV_DomainLocation, const OutputPatch<HullOut, 4> bezPatch) : SV_POSITION { float4 basisU = BernsteinBasis(t); // 貝塞爾曲線插值 float3 sum = basisU.x * bezPatch[0].PosL + basisU.y * bezPatch[1].PosL + basisU.z * bezPatch[2].PosL + basisU.w * bezPatch[3].PosL; float4 posH = mul(float4(sum, 1.0f), g_WorldViewProj); return posH; }
下面的動圖展現了貝塞爾曲線的控制和細分
三階貝塞爾曲線有4個控制點,而三階貝塞爾曲面天然就有4x4控制點了。咱們能夠將其看作4條三次貝塞爾曲線。
這裏就再也不列出複雜的公式來暈人了。在一維狀況下,貝塞爾曲線使用範圍在[0, 1]
的參數t來表示曲線上一點。那麼在二維狀況下,咱們可使用範圍在[0, 1]
的參數(u, v)來表示曲面上一點。
首先對於這4條橫向的貝塞爾曲線,咱們分別使用參數u代入來求得四個點,這四個頂點按列順序構成新的控制點,而後問題就轉化成了在這四個控制點構成的貝塞爾曲線中求其中一點。而後咱們再用參數v代入就能夠求得最終在曲面上的一點。
貝塞爾曲面的着色器代碼實現以下:
// Tessellation.hlsli // 計算以4x4控制點爲基礎的三階貝塞爾曲面在(u, v)下的一點 float3 CubicBezierSum(const OutputPatch<HullOut, 16> bezPatch, float4 basisU, float4 basisV) { float3 sum = float3(0.0f, 0.0f, 0.0f); sum = basisV.x * (basisU.x * bezPatch[0].PosL + basisU.y * bezPatch[1].PosL + basisU.z * bezPatch[2].PosL + basisU.w * bezPatch[3].PosL); sum += basisV.y * (basisU.x * bezPatch[4].PosL + basisU.y * bezPatch[5].PosL + basisU.z * bezPatch[6].PosL + basisU.w * bezPatch[7].PosL); sum += basisV.z * (basisU.x * bezPatch[8].PosL + basisU.y * bezPatch[9].PosL + basisU.z * bezPatch[10].PosL + basisU.w * bezPatch[11].PosL); sum += basisV.w * (basisU.x * bezPatch[12].PosL + basisU.y * bezPatch[13].PosL + basisU.z * bezPatch[14].PosL + basisU.w * bezPatch[15].PosL); return sum; }
上面的函數不只能用來計算\(\mathbf{p}(u, v)\),還可以求它的偏導數:
float4 basisU = BernsteinBasis(uv.x); float4 basisV = BernsteinBasis(uv.y); // p(u, v) float3 p = CubicBezierSum(bezPatch, basisU, basisV); float4 dBasisU = dBernsteinBasis(uv.x); float4 dBasisV = dBernsteinBasis(uv.y); // p(u, v)對u的偏導 float3 dpdu = CubicBezierSum(bezPatch, dbasisU, basisV); // p(u, v)對v的偏導 float3 dpdv = CubicBezierSum(bezPatch, basisU, dbasisV);
注意:能夠發現,咱們把基函數的計算結果傳入了
CubicBezierSum
函數。因爲p(u, v)
與其偏導數的求和形式相同,僅基函數不一樣,所以CubicBezierSum
函數不只能用來計算p(u, v)
,還能用於求其偏導數。
// Tessellation_BezierSurface_HS.hlsl #include "Tessellation.hlsli" [domain("quad")] [partitioning("integer")] [outputtopology("triangle_cw")] [outputcontrolpoints(16)] [patchconstantfunc("QuadPatchConstantHS")] [maxtessfactor(64.0f)] float3 HS(InputPatch<VertexOut, 16> patch, uint i : SV_OutputControlPointID, uint patchId : SV_PrimitiveID) : POSITION { return patch[i].PosL; }
// Tessellation_BezierSurface_DS.hlsl #include "Tessellation.hlsli" [domain("quad")] float4 DS(QuadPatchTess patchTess, float2 uv : SV_DomainLocation, const OutputPatch<HullOut, 16> bezPatch) : SV_POSITION { float4 basisU = BernsteinBasis(uv.x); float4 basisV = BernsteinBasis(uv.y); // 貝塞爾曲面插值 float3 p = CubicBezierSum(bezPatch, basisU, basisV); float4 posH = mul(float4(p, 1.0f), g_WorldViewProj); return posH; }
下面的代碼定義了16個控制點:
XMFLOAT3 surfaceVertices[16] = { // 行 0 XMFLOAT3(-10.0f, -10.0f, +15.0f), XMFLOAT3(-5.0f, 0.0f, +15.0f), XMFLOAT3(+5.0f, 0.0f, +15.0f), XMFLOAT3(+10.0f, 0.0f, +15.0f), // 行 1 XMFLOAT3(-15.0f, 0.0f, +5.0f), XMFLOAT3(-5.0f, 0.0f, +5.0f), XMFLOAT3(+5.0f, 20.0f, +5.0f), XMFLOAT3(+15.0f, 0.0f, +5.0f), // 行 2 XMFLOAT3(-15.0f, 0.0f, -5.0f), XMFLOAT3(-5.0f, 0.0f, -5.0f), XMFLOAT3(+5.0f, 0.0f, -5.0f), XMFLOAT3(+15.0f, 0.0f, -5.0f), // 行 3 XMFLOAT3(-10.0f, 10.0f, -15.0f), XMFLOAT3(-5.0f, 0.0f, -15.0f), XMFLOAT3(+5.0f, 0.0f, -15.0f), XMFLOAT3(+25.0f, 10.0f, -15.0f) };
注意:這裏並無嚴格地限定控制點必定要按等距排列爲均勻的網格。
下圖展現了貝塞爾曲面
DirectX11 With Windows SDK完整目錄
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。