DirectX11 With Windows SDK--33 曲面细分阶段(Tessellation)

前言

曲面细分是Direct3D 11带来的其中一项重要的新功能。它引入了两个可编程着色器阶段以及一个固定的镶嵌处理过程。简单来讲,曲面细分技术能够将几何体细分为更小的三角形,并以某种方式把这些新生成的顶点偏移到合适的位置,从而以增长三角形数量的方式丰富网格细节。但为何不在建立网格之初就直接赋予它高模(high-poly,高面数多边形)的细节呢?如下是使用曲面细分的3个理由:html

  1. 基于GPU实现动态LOD(Level of Detail,细节级别)。能够根据网格与摄像机的距离或依据其余因素来调整其细节。好比说,若网格离摄像机过远,则按高模的规格对它进行渲染将是一种浪费,由于在那个距离咱们根本看不清网格的全部细节。随着物体与摄像机之间距离的拉紧,咱们就能连续地对它镶嵌细分,以增长物体的细节。
  2. 物理模拟与动画特效。咱们能够在低模(low-poly,低面数多边形)网格上执行物理模拟与动画特效的相关计算,再以镶嵌画处理手段来获取细节上更加丰富的网格。这种下降物理模拟与动画特效计算量的作法可以节省很多的计算资源。
  3. 节约内存。咱们能够在各类存储器(磁盘、RAM、VRAM)中保存低模网格,再根据需求用GPU动态地对网格进行镶嵌细分。

曲面细分技术涉及到的三个阶段都是可选的,但若是要使用曲面细分,这三个阶段都是必需要经历的。git

学习目标:程序员

  1. 了解曲面细分所用的面片图元类型。
  2. 理解曲面细分阶段中的每一个步骤都作了什么,它们所需的输入及输出又分别是哪一种数据
  3. 经过编写外壳着色器与域着色器程序来对几何图形进行镶嵌化细分
  4. 熟悉不一样的细分策略,以便于在镶嵌化处理的时候选择出最适当的方案。除此以外,还须要了解硬件曲面细分的性能
  5. 学习贝塞尔曲线与贝塞尔曲面的数学描述,并在曲面细分阶段将它们予以实现

DirectX11 With Windows SDK完整目录github

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_TessFactorSV_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)进行镶嵌化处理的过程由两个构成:

  1. 4个边缘曲面细分因子控制着对应边缘镶嵌后的份数
  2. 两个内部曲面细分因子指示了如何来对该四边形面片的内部进行镶嵌化处理(其中一个针对四边形的横向维度,另外一个则做用于四边形的纵向维度)

三角形面片(tri)进行镶嵌化处理的过程一样分为两部分:

  1. 3个边缘曲面细分因子控制着对应边缘镶嵌后的份数
  2. 一个内部曲面细分因子指示着三角形面片内部的镶嵌份数。

等值线(isoline)进行镶嵌化处理的过程以下:

  1. 2个边缘细分因子控制着等值线如何进行镶嵌。第一个值暂时不知道做用(忽略),第二个用于控制两个相邻控制点之间分红多少段。

Direct3D 11硬件所支持的最大曲面细分因子为64(D3D11_TESSELLATOR_MAX_TESSELLATION_FACTOR).若是把全部的曲面细分因子都设置为0,则该面片会被后续的处理阶段所丢弃。这就使得咱们可以以每一个面片为基准来实现如视锥体剔除与背面剔除这类优化。

  1. 若是面片根本没有出如今视锥体范围内,那么就能将它从后续的处理中丢弃(假若已经对该面片进行了镶嵌化处理,那么其细分后的各三角形将在三角形裁剪期间被抛弃)
  2. 若是面片是背面朝向的,那么就能将其从后面的处理过程当中丢弃(若是该面片已通过了镶嵌化处理,则其细分后的全部三角形会在光栅化阶段的背面剔除过程当中被抛弃)

一个问题天然而然地复现出来:到底应该执行几回镶嵌化处理才合适?前面提到,曲面细分的基本想法就是为了丰富网格的细节。可是,若是用户对此无感,咱们就不须要对它增添细节了。如下是一些肯定镶嵌次数的经常使用衡量标准。

  1. 根据与摄像机之间的距离:物体与摄像机的距离越远,能分辨的细节就越少。所以,咱们在二者距离较远的时候渲染物体的低模版本,并随着二者逐渐接近而逐步对物体进行更加细致的镶嵌化细分。
  2. 根据占用屏幕的范围:能够先估算出物体覆盖屏幕的像素个数。若是数量比较少,则渲染物体的低模版本。随着物体占用屏幕范围的增长,咱们即可以逐渐增大镶嵌化细分因子。
  3. 根据三角形的朝向:三角形相对于观察者的朝向也被归入考虑的范畴之中。位于物体轮廓边线上的三角形势必比其余位置的三角形拥有更多的细节。
  4. 根据粗糙程度:粗糙不平的表面较光滑的表面须要进行更为细致的曲面细分处理。经过对表面纹理进行检测能够预算出相应的粗糙度数据,继而来决定镶嵌化处理的次数。

[Story10(可点击)]给出了如下几点关于性能的建议。

  1. 若是曲面细分因子为1(这个数字意味着该面片没必要细分),那么就考虑在渲染此面片时不对它进行细分处理;不然,便会在曲面细分阶段白白浪费GPU资源,由于在此阶段并不对其执行任何操做。
  2. 考虑到性能又涉及GPU对曲面细分的具体实现,因此不要对小于8个像素这种太小的三角形进行镶嵌化处理。
  3. 使用曲面细分技术时要采用批绘制调用(batch draw call,即尽可能将曲面细分任务集中执行)(在绘制调用之间往复开启、关闭曲面细分功能的代价极其高昂)。

控制点外壳着色器

控制点外壳着色器以大量的控制点做为输入与输出,顶点着色器每输出一个控制点,此着色器都会被调用一次。控制点外壳着色器的应用之一是改变曲面的表示方式,好比把一个普通的三角形(向渲染管线提交的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个控制点所衍生。

上面的控制点外壳着色器还用到了如下几种属性。

  1. domain:面片的类型。可选用的参数有tri(三角形面片)、quad(四边形面片)或isoline(等值线)

  2. partioning:指定了曲面细分的细分模式。

    • integer:新顶点的添加或移除依据的是上取整的函数。例如咱们将细分值设为3.25f时,实际上它将会细分为4份。这样一来,在网格随着曲面细分级别而改变时,会容易发生明显的跃变。
    • 非整型曲面细分(fractional_even/fractional_odd):新顶点的增长或移除取决于曲面细分因子的整数部分,可是细微的渐变“过渡”调整就要根据细分因子的小数部分。当咱们但愿将粗糙的网格经曲面细分而平滑地过渡到具备更加细节的网格时,该参数就派上用场了。
    • pow2:目前测试的时候行为和integer一致,不知道什么缘由。这里暂时不讲述。
  3. outputtopology:经过细分所创的三角形的绕序

    • triangle_cw:顺时针方向的绕序
    • triangle_ccw:逆时针方向的绕序
    • line:针对线段的曲面细分
  4. outputcontrolpoints:外壳着色器执行的次数,每次执行都输出1个控制点。系统值SV_OutputControlPointID给出的索引标明了当前正在工做的外壳着色器所输出的控制点。

  5. patchconstantfunc:指定常量外壳着色器函数名称的字符串

  6. maxtessfactor:告知驱动程序,用户在着色器中所用的曲面细分因子的最大值。若是硬件知道了此上限,就能够了解曲面细分所需的资源,继而在后台对此进行优化。Direct3D 11硬件支持的曲面细分因子最大值为64

镶嵌器阶段

程序员没法对镶嵌器这一阶段进行任何控制,由于这一步的操做全权交给硬件处理。此环节会基于常量外壳着色器程序所输出的曲面细分因子,对面片进行镶嵌化处理。

四边形面片的曲面细分示例

  1. integer模式

  1. fractional_odd模式:

  1. 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);
}

贝塞尔曲线(Bézier Curves)

这部分借用GAMES101来讲明。而且这里讲的贝塞尔曲线所用的算法是由Pierre Bézier和Paul de Casteljau所提出的。

如今咱们先从二阶贝塞尔曲线开始,下面有3个非共线的控制点b0b1b2

接下来咱们用线性插值的方式,从b0b1方向的线段使用参数t来肯定其中一点,记为\(\mathbf{b_{0}^{1}}\)

而后从b1b2方向的线段使用一样的参数t来肯定另外一点,记为\(\mathbf{b_{1}^{1}}\)

\(b_{0}^{1}\)\(b_{1}^{1}\)链接起来,问题降级为一阶贝塞尔曲线(直线)。咱们对其再使用一次参数t的线性插值便可获得在参数t下该贝塞尔曲线的对应一点\(\mathbf{b_{2}^{0}}\)

咱们将t[0, 1]的全部状况都求出来,就能够获得一条光滑的贝塞尔曲线。

三阶的贝塞尔曲线须要用到4个控制点,但作法也是相似的。首先求出b0b1b1b2b2到b3的线段在t时刻下的插值点\(\mathbf{b_{0}^{1}}, \mathbf{b_{1}^{1}}, \mathbf{b_{2}^{1}}\),此时问题就被转化成了这三个控制点下的二阶贝塞尔曲线。不断降阶最终算出目标点便可。

能够看到,三阶贝塞尔曲线须要进行6次插值运算,二阶贝塞尔曲线则须要进行3次插值运算。以此类推,咱们能够知道n阶贝塞尔曲线须要n(n+1)/2次运算

计算过程

下图阐述了二阶贝塞尔曲线的计算过程

观察b0b1b2的各项系数,能够发现它们知足二项式定理。咱们也能够用下面的一个金字塔来描述

从底端的这些控制点选取其中一个而后不断往上走,最终走到顶端点的过程当中,若是走过一段朝着右上方向的路径,则给该控制点乘上因子(1-t),而朝着左上方向的路径则给该控制点乘上因子t。好比从b0走到顶端的项为\(t^3 b_{0}\)。咱们将这全部8条路径都加起来就能够获得最后的结果:

\[\mathbf{b_0^3} = (1-t)^3\mathbf{b_0} + 3t(1-t)^2\mathbf{b_1} + 3t^2 (1-t)\mathbf{b_2} + t^3\mathbf{b_3} \]

更抽象的,咱们能够用伯恩斯坦函数的形式来表示:

\[\mathbf{b^n}(t)=\sum_{i=0}^{n} C_{n}^{i}t^{i}(1-t)^{n-i}\cdot\mathbf{b_i} \]

对于三阶贝塞尔曲线而言,曲线端点为:

\[\mathbf{b^3}(0)=\mathbf{b_0};\;\;\mathbf{b^3}(1)=\mathbf{b_3} \]

而三次伯恩斯坦函数的导数为:

\[\begin{align} {B_{0}^{3}}'(t)&=-3(1-t)^2\\ {B_{1}^{3}}'(t)&=3(1-t)^2-6t(1-t)\\ {B_{2}^{3}}'(t)&=6t(1-t)-3t^2\\ {B_{3}^{3}}'(t)&=3t^2\\ \end{align} \]

所以,对3次贝塞尔曲线求导的结果为:

\[\mathbf{{b^3}'(t)}=-3(1-t)^2\mathbf{b_0}+[3(1-t)^2-6t(1-t)]\mathbf{b_1}+[6t(1-t)-3t^2]\mathbf{b_2}+3t^2\mathbf{b_3} \]

经过这些导数就能够很方便地计算出曲线上某点处的切向量。

连续性

对于复杂曲线,若是咱们使用高阶曲线的话,观察伯恩斯坦函数形式的顶点式会发现计算量呈平方级别增加。为此咱们能够考虑将该曲线分段,而后这些曲线都使用较低阶的贝塞尔曲线去拟合,以此来减小计算量。

假设咱们如今有两条三阶贝塞尔曲线ab,那么当曲线a的最后一个控制点与曲线b的第一个控制点位置相同时,咱们称这两条曲线知足C0连续。下图的红点为控制点,观察中间的控制点处显然知足这一性质,可是能够看到左右两端曲线的过渡并非平缓的。

而若是在知足C0连续的基础上,曲线a在最后一个控制点处的导数还与曲线b在第一个控制点处的导数相等,则此时咱们说这两条曲线知足C1连续。

而知足C1连续的控制点必然知足:

\[\mathbf{a_n}=\mathbf{b_0}=\frac{1}{2}(\mathbf{a_{n-1}+b_{1}}) \]

即曲线ab的链接点为曲线a倒数第二个控制点与曲线b第二个控制点的中点。

固然还有更高级别的C2连续,即知足二阶导数相等,这里再也不深刻讨论。

HLSL代码实现

绘制贝塞尔曲线的外壳着色器和域着色器代码以下:

// 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代入就能够求得最终在曲面上的一点。

HLSL代码实现

贝塞尔曲面的着色器代码实现以下:

// 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)
};

注意:这里并无严格地限定控制点必定要按等距排列为均匀的网格。

贝塞尔曲面示例

下图展现了贝塞尔曲面

练习题

  1. 参考16章,只用6个顶点构成的八面体,并根据其与观察点的距离关系将其镶嵌细分为一个球体(距离越近,镶嵌程度越大)
  2. 尝试修改本演示程序中的控制点来改变其中的贝塞尔曲面
  3. 修改本演示程序,利用光照使得其中的贝塞尔曲面表现出明暗变化。为此,咱们须要在域着色器中计算顶点法线。而位于顶点处的法线能够用此曲面点处坐标的偏导数的叉积求得。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。

相关文章
相关标签/搜索