曲面细分是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,以及有什么问题也能够在这里汇报。