DirectX 12 曲面細分着色器筆記

1. 背景

曲面細分建模(Tessellation)是一種通過多邊形分解來提升幾何逼真度的方法。曲面細分會導致圖元和頂點的增加,其主要實現依據是對頂點法向量進行插值,達到幾何面中用更細膩的多圖元模擬表達曲面圖元的效果。曲面細分的好處是:節省原始模型的數據空間,提高模型表達的質量、性能和可靠性
注意,對面片(patch)頂點法向量均一致的平面圖元而言,曲面細分並不能提高逼真度,只有針對相鄰面片有不同法向量的三角形來描述的模型,曲面細分才能提高逼真度。

曲面細分是渲染管線的一個可選項,DirectX 12中採用了三個部件來協同從流水線(rendering pipeline)上接管流數據進行曲面細分。它們分別是:Hull Shader 外殼着色器, Tessellator 曲面細分器, Domain Shader 域着色器。其中,Hull Shader和Domain Shader可編程,Tessellator是固定階段不可編程。
曲面細分在渲染流水線中的位置
Fig1. Pipeline with hardware tessellation

2. 曲面細分工作機制

曲面細分三個部分的作用分別爲:
1)Hull Shader: 生成細分輸出面片的頂點,更新所有逐頂點或逐面片的屬性值; 設置細分層次因數,以控制生成圖元的屬性值。
2)Tessellator: 對整個圖元幾何區域創建採樣模式,並根據採樣模式生成細分的面片圖元。
3)Domain Shader: 對每個域採樣計算生成的頂點數據,從而使得細分圖元能夠接入到流水線的下一步。
曲面細分機制

2.1 Hull Shader(外殼着色器)

以下圖爲例說明曲面細分中的Hull Shader技術手段。圖中黑色粗線表示的是原始模型的面片圖元(patch)及其鄰接的拓撲頂點,共有5個模型圖元。Hull Shader會分別接收每一個圖元及其拓撲結構,內部根據法向量等因素生成Bezier曲面的控制點(control points)與曲面細分參數(tess factor, TF)。
曲面細分建模
Hull Shader完成的工作包括兩部分:

  • 對每個輸入的圖元進行運算,轉換初始圖元的頂點,生成Bezier擬合曲面的控制點;
  • 爲Tessellator和Domain Shader準備需要用到的轉換參數,即圖元的細分因子參數,相當於圖元的預處理過程。

Hull Shader實際上是通過兩個並行的過程來實現計算任務的:控制點生成過程圖元細分因子參數生成過程

  • Hull Shader依據傳入的圖元頂點生成新的控制點,並通過產生的系統值SV_OutputControlPointID給生成的頂點賦索引值,這個就是控制點生成過程。
  • 圖元細分因子參數生成過程也是對每個圖元運行一次。細分因子包括:edgeTessFactor[4] (分別指代left edge, right edge, top edge, bottom edge細分程度)、insideTessFactor[2] (分別指代內部u,v座標軸細分程度)。

其中,生成的控制點數據和TF將直接傳給Domain shader,同時TF也傳給tessellator。即TF決定了圖元的細分過程,只是這個過程在Tessellator中完成,如果TF值無效,則該圖元將被忽略,不進行細分優化建模。

生成控制點數據涉及到的參數有:(也稱爲control points hull shader參數)

參數名稱 含義 可能的取值
domain 細分發生的區域 isoline(等值線)、 tri(三角形)、quad(四邊形) 注:不同的domain類型,細分的方式有區別
partitioning 細分模式 integer, fractional_even, fractional_odd, pow2
outputtopology tessellator輸出圖元的拓撲結構 triangle_cw(順時針環繞),triangle_ccw(逆時針環繞)
outputcontrolpoints hull shader每個線程生成控制點的數目 不一定與輸入數量相同,可以新增控制點
max_tessfactor 最大細分度 告知驅動程序shader用到的細分度,硬件可能會針對這個做出優化。DirectX 11和OpenGL core都支持64
patchconstantfunc 指定生成圖元細分因子的函數 如下文中的SetHullConstantsHS函數

圖元細分參數包括:(也稱爲const hull shader參數)

參數名稱 含義
edgeTess[0] lefe edge細分程度
edge Tess[1] top edge細分程度
edge Tess[2] right edge細分程度
edge Tess[3] bottom edge細分程度
inside Tess[0] 內部細分程度,u-axis
insde Tess[1] 內部細分程度,v-axis

外側細分因子負責控制細分區域的周長,內側細分因子控制細分區域的內部劃分方式(區域內水平和垂直方向上各有多少區域存在)。

值得注意的是,
當面片爲quad時,edgeTess的長度爲4,insideTess的長度爲2,分別指定四條邊各被分成多少段、內部在橫向和縱向各被分爲多少段;
當面片爲tri時,edgeTess的長度爲3,insideTess的長度爲2,分別指定三條邊各被分成多少段、內部有多少個點;
當面片爲isoline時,edgeTess的長度爲2,第0個元素指定線段的個數,第1個元素指定線段被分爲多少段,insideTess會被忽略。

2.2 Tessellator(曲面細分器)

Tessellator的主要功能是將圖元區域分成若干小的圖元對象,對每個圖元僅運行一次。它首先將輸入圖元歸化爲標量化的規範域([0,1]空間),然後根據hull shader提供的細分因子進行曲面細分。它輸出uvw標量座標和新面片的幾何拓撲類型到domain shader中。
輸入:patch, TF
輸出:uvw或uv標量座標,patch topology

quad: 頂點以uv座標的形式傳給Domain Shader.
quad uv座標在這裏插入圖片描述
tri: 頂點以uvw重心座標的形式傳給Domain Shader.
tri uvw座標在這裏插入圖片描述
isoline:頂點以uv座標的形式傳給Domain Shader.
isoline uv座標在這裏插入圖片描述三角形的內側細分因子t,如果t是一個偶數,那麼三角形域的中心(重心座標)將定位於(1/2, 1/2, 1/2),然後再中心點和周長之間生成(t/2)-1個同心三角形。反之,如果t是一個奇數,那麼到周長爲止將生成(t/2)-1個同心三角形,但中心點
(重心座標)不再是一個細分座標。

曲面細分器內部存在兩個階段:
1.對細分因子進行優化處理,如對細分因子進行四捨五入,過濾非常小的因子,減少或合併因子,使用32bit浮點運行等。
2. 根據細分因子進行曲面細分,它使用16bit定點小數進行運算,這樣即可採用硬件加速,也可保障運算精度。

2.3 Domain Shader(域細分器)

域着色器用於計算細分圖元的頂點位置,它對Tessellator的每個輸出點運行一次,能夠只讀訪問其輸出的uvw標量座標,同時使用hull shader的兩項輸出數據。所有的頂點信息都會在這裏計算,會涉及到大量運算。
需要注意的是,經過domain shader處理後的數據流會丟失圖元的鄰接拓撲關係,geometry shader將無法從這種經過曲面細分後的數據流中正確提取圖元的拓撲關係。因此,在geometry shader需要使用圖元拓撲結構的情況下,geometry shader與曲面細分兩個過程不能同時使用。
域着手器的輸入輸出
DS輸出的數據,可能會先傳給GS(Geometry Shader)進行進一步的計算(增加頂點、修改頂點位置,計算頂點屬性)。也可以直接(當然首先要進行裁剪和光柵化)傳給FS(Fragment Shader)進行片元着色。最後進入輸出合併階段,完成整個渲染管線。

3. 編程開發

3.1 hull shader編程開發

1. 設計輸入/輸出控制點格式

初始的輸入控制點來源於數據流的模型頂點,通常根據細分面片的需要,除了頂點的座標位置,描述面片方向的頂點法向量、切向量和同樣需要關聯細分的紋理座標都需要作爲頂點輸入數據傳給外殼着色器。相關代碼如下:

struct HS_POINT
{
  float3 vPosition:WORLDPOS; //頂點座標
  float2 vUV: TEXCOORD0; //紋理座標
  float3 vNormal: NORMAL; //法向量
}

經過hull shader運算處理後,輸出的數據依然是控制點,數據流的性質沒有發生改變。

hull shader還需要面片圖元的細分因子參數格式,可採用下面的數據結構:

struct HS_CONSTANT_FACTOR
{
  float Edges[4]: SV_TessFactor; //邊緣細分因子
  float Inside[2]: SV_InsideTessFactor; //內部細分因子
}

2. 設計生成面片常數參量數據的函數

在對每個面片進行細分之前,需要先對每個面片生成面片描述常量數據。實現這一功能的函數定義如下:

HS_CONSTANT_FACTOR SetHullConstantsHS(InputPatch<HS_POINTS,3> ip, unit PatchID: SV_PrimitiveID)
{
  HS_CONSTANT_FACTOR output;
  output.edges[0] = 4;
  output.edges[1] = 4;
  output.edges[2] = 4;
  output.inside = 4;
  return output;
}

常量生成函數的參數包括以下幾個:
1)一個通過SV_PrimitiveID定義的區別標識面片的唯一ID變量PatchID;
2) 輸入控制點ip,同上面定義的輸入控制點結構一樣,如通過ip[0]訪問圖元的第1個控制點;
3) 最基本的函數實現必須返回後一步Tessellator計算每個面片的TF,如HS_CONSTANT_FACTOR結構。本例中, 將所有的因子都賦值爲常量4是最簡單的情形,也只可以通過控制點來動態計算這些值。

3. 設計生成控制點的主函數

hull shader的主入口函數除了負責生成控制點外,還負責對整個曲面細分過程進行參數化配置,這主要是通過HLSL修飾關鍵詞來描述。

[domain("tri")] [partitioning("integer")] [outputtopology("triangle_cw")]
[outputcontrolpoints(3)] [patchconstantfunc("SetHullConstantsHS")]
HS_POINT HullShaderMain(InputPatch<HS_POINT, 3> ip, unit i:SV_OutputControlPointID, uint PatchID: SV_PrimitiveID)
{
  HS_POINT output;
  output.vPosition = ip[PatchID].vPosition;
  output.vNormal = ip[PatchID].vNormal;
  output.vUV = ip[PatchID].vUV;
  return output;
}

3.2 domain shader編程開發

domain shader的任務是結合原始圖元的頂點座標xyz,將tessellator內部標題化的細分圖元頂點座標uvw反算到世界座標空間中,保障細分圖元的空間座標與原始圖元的一致,與周邊其他圖元的邊進行無縫對接。
domain shader的輸入參數包括hull shader輸出的控制點數據、TF和區域位置數據。控制點與常量數據的結構定義均應在hull shader中指定並保持一致,如上段代碼中的HS_POINT結構定義。
domain shader的輸出爲細分後頂點,由於domain shader後數據將回歸到rendering pipeline的正常數據流上,所以可以根據需要另行指定輸出頂點格式,或直接採用頂點着色器的輸出頂點格式。

區域位置相當於記載圖元在模型上定義的原點位置,以便反算出細分圖元的模型座標。對quad, isoline圖元而言,採用float2類型的變量聲明就可以,對於tri類型的圖元,則要通過float3類型的變量來表示其重心座標。定位原點可以是圖元的頂點或其他特徵點,如重心、中點、垂心,但由於在hull shader內部有約束,只需要按其指定的規則聲明,聲明的系統值變量應以SV_DomainLocation語義關鍵詞來描述,代碼如下:
float2 UV: SV_DomainLocation
在主函數前一定要加上對圖元類型的域描述,其定義與hull shader一致。如有下面一個主函數:

[domain("tri")]
HS_POINT Domain_Shader(HS_CONSTANT_FACTOR input, 
                       float3 uvw: SV_DomainLocation, 
                       const outputPatch<HS_POINT, 3> patch)
{
  HS_POINT output;
  float3 pos;
  //基於重心座標的控制頂點生成
  pos = uvw.x * patch[0].vPosition + uvw.y * patch[1].vPosition +
        uvw.z * patch[2].vPosition;
  //反算新產生的細分頂點在投影空間中的位置
  output.vPosition = mul(float4(pos, 1.0f), worldViewProjectMatrix);   
  //統計圖元三個頂點實現的紋理座標填充
  output.vUV = uvw.x * patch[0].vUV + uvw.y * patch[1].vUV + 
               uvw.z * patch[2].vUV;
  return output;
}

或者quad類型示例:

struct DomainOut{float4 PosH : SV_POSITION;};
// The domain shader is called for every vertex created by the tessellator.
// It is like the vertex shader after tessellation.
[domain(“quad”)]
DomainOut DS(PatchTess patchTess, //patchTess:細分參數
             float2 uv : SV_DomainLocation, //uv:曲面細分階段傳入的頂點位置信息
             const OutputPatch<HullOut, 4> quad) //quad:HS傳入的patch數據,尖括號的第二個參數與HS的outputcontrolpoints對應
{  
  DomainOut dout;
  // Bilinear interpolation. 先求頂點座標,再轉換到投影空間
  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);
  float4 posW = mul(float4(p, 1.0f), gWorld);
  dout.PosH = mul(posW, gViewProj);
  return dout;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章