前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >进阶渲染系列(二)——曲面细分(细分三角形)

进阶渲染系列(二)——曲面细分(细分三角形)

作者头像
放牛的星星
发布2020-07-10 17:35:12
4.2K0
发布2020-07-10 17:35:12
举报
文章被收录于专栏:壹种念头

目录

1 Hull 和 Domains1.1 创建一个曲面细分着色器1.2 Hull 着色器1.3 Patch Constant Functions1.4 Domain 着色器1.5 控制点2 细分三角形2.1 细分因子2.2 不同的边和内部因子2.3 变量因子2.4 分数因子3 启发式细分3.1 边因子3.2 边长度3.3 屏幕坐标中的边长度3.4 使用视距3.5 使用正确的内部因子

本文重点: 1、创建hull和domain着色器 2、细分三角形 3、控制如何细分

本教程介绍如何向自定义着色器添加对曲面细分的支持。它以“平面和线框着色 ”教程为基础。

本教程使用Unity 2017.1.0制作。

(如果你没有足够的三角形,就多生成一些)

1 Hull 和 Domains

曲面细分是将事物切成较小部分的艺术。在我们的例子中,我们将细分三角形,因此最终会得到覆盖相同空间的较小三角形。这可以为几何添加更多细节,但在本教程中还是会更多的关注曲面细分过程本身。

GPU能够拆分提供给它的三角形以进行渲染。这样做有多种原因,例如当三角形的一部分最终被裁剪时。我们无法控制,但是还有一个细分阶段可以配置。此阶段位于顶点和片段着色器阶段之间。但这并不像在着色器中添加一个其他程序那样简单。我们将需要一个壳程序和一个域程序。

(曲面细分着色过程)

1.1 创建一个曲面细分着色器

第一步是创建启用了细分的着色器。让我们将需要的代码放在自己的文件MyTessellation.cginc中,并使用自己的包含保护。

为了清楚地看到三角形被细分,我们将使用Flat Wireframe Shader。复制该着色器,将其重命名为Tessellation Shader,然后调整其菜单名称。

使用细分时的最低着色器目标级别为4.6。如果我们不手动设置,Unity将发出警告并自动使用该级别。向前向base、附加以及延迟pass添加细分阶段。在MyFlatWireframe之后,还要在这些通道中包括MyTessellation。

shadows通道呢?

在渲染阴影时也可以使用曲面细分,但是在本教程中我们不会这样做。

创建一个依赖于此着色器的材质,并将四边形添加到使用它的场景中。我将材质设置为灰色,以使其不太亮,就像Flat Wireframe材质一样。

(一个四边形)

我们将使用这个四边形来测试我们的细分着色器。请注意,它由两个等腰直角三角形组成。短边的长度为1,长对角线的长度为√2。

1.2 Hull 着色器

像几何体着色器一样,细分阶段非常灵活,可以处理三角形,四边形或等值线。我们必须告诉它必须使用什么表面并提供必要的数据。这是 hull 程序的工作。为此,将一个程序添加到MyTessellation中,首先从一个无效的void函数开始。

Hull 程序在曲面补丁上运行,该曲面补丁作为参数传递给它。我们必须添加一个InputPatch参数才能实现这一点。

Patch是网格顶点的集合。就像我们对几何函数的stream参数所做的一样,必须指定顶点的数据格式。现在,我们将使用VertexData结构。

它不是InputPatch吗?

由于Hull阶段在顶点阶段之后,因此从逻辑上讲,Hull函数的输入类型必须与顶点函数的输出类型匹配。的确如此,但是我们暂时将忽略这一事实。

在处理三角形时,每个补丁将包含三个顶点。此数量必须指定为InputPatch的第二个模板参数。

Hull程序的工作是将所需的顶点数据传递到细分阶段。尽管向其提供了整个补丁,但该函数一次仅应输出一个顶点。补丁中的每个顶点都会调用一次它,并带有一个附加参数,该参数指定应该使用哪个控制点(顶点)。该参数是具有SV_OutputControlPointID语义的无符号整数。

只需将补丁当作数组索引即可,然后返回所需的元素。

这看起来像是一个功能程序,因此让我们添加一个编译器指令以将其用作Hull着色器。对涉及的所有三个着色器遍历执行此操作。

这会产生一些编译器错误,抱怨我们没有正确配置Hull着色器。像几何函数一样,它需要属性来配置它。首先,我们必须明确地告诉它它正在处理三角形。这是通过UNITY_domain属性(以tri作为参数)完成的。

这还不够。我们还必须明确指定每个补丁输出三个控制点,每个三角形的角点一个。

当GPU创建新三角形时,它需要知道我们是否要按顺时针或逆时针定义它们。像Unity中的所有其他三角形一样,它们应为顺时针方向。这是通过UNITY_outputtopology属性控制的。它的参数应该是triangle_cw。

还需要通过UNITY_partitioning属性告知GPU应该如何分割补丁。有几种不同的分区方法,我们将在以后进行研究。现在,仅使用整数模式。

除了分区方法外,GPU还必须知道应将补丁切成多少部分。这不是一个恒定值,每个补丁可能有所不同。必须提供一个评估此值的函数,称为补丁常数函数(Patch Constant Functions)。假设我们有一个名为MyPatchConstantFunction的函数。

1.3 Patch Constant Functions

Patch的属性是如何细分的。这意味着Patch Constant Function每个Patch仅被调用一次,而不是每个控制点被调用一次。这就是为什么它被称为常量函数,在整个Patch中都是常量的原因。实际上,此功能是与MyHullProgram并行运行的子阶段。

(HULL 着色器内部)

为了确定如何细分三角形,GPU使用了四个细分因子。三角形面片的每个边缘都有一个因数。三角形的内部也有一个因素。三个边缘向量必须作为具有SV_TessFactor语义的float数组传递。内部因素使用SV_InsideTessFactor语义。让我们为此创建一个结构。

面片常数函数将面片作为输入参数,并输出细分因子。现在让我们创建这个缺少的功能。将所有因子设置为1。这会指示细分阶段不细分补丁。

1.4 Domain 着色器

现在,着色器编译器会报错说“a shader cannot have a tessellation control shader without a tessellation evaluation shader”。HUll着色器只是使曲面细分工作所需的一部分。一旦细分阶段确定了应如何细分补丁,则由几何着色器来评估结果并生成最终三角形的顶点。因此,让我们从占位开始为我们的域(Domain)着色器创建函数。

Hull着色器和Domain着色器都作用于相同的域,即三角形。我们通过UNITY_domain属性再次发出信号。

Domain程序将获得使用的细分因子以及原始补丁的信息,原始补丁在这种情况下为OutputPatch类型。

细分阶段确定补丁的细分方式时,不会产生任何新的顶点。相反,它会为这些顶点提供重心坐标。使用这些坐标来导出最终顶点取决于域着色器。为了使之成为可能,每个顶点都会调用一次域函数,并为其提供重心坐标。它们具有SV_DomainLocation语义。

函数里面,我们必须生成最终的顶点数据。

为了找到该顶点的位置,我们必须使用重心坐标在原始三角形范围内进行插值。X,Y和Z坐标确定第一,第二和第三控制点的权重。

以相同的方式插值所有顶点数据。让我们为此定义一个方便的宏,该宏可用于所有矢量大小。

除了位置之外,还可以插入法线,切线和所有UV坐标。

唯一不插值的就是实例ID。由于Unity不同时支持GPU实例化和细分,因此复制该ID毫无意义。为防止编译器错误,请从三个着色器遍历中删除多编译指令。这还将从着色器的GUI中删除实例化选项。

有没有可能同时使用实例化和细分?

目前,不支持。请记住,多次渲染同一对象时,GPU实例化非常有用。由于细分成本很高,而且要添加细节,因此它们通常不是很好的组合。如果要关闭某个对象的许多实例,可以使用LOD组。使LOD 0使用非实例化细分化材质,而所有其他LOD级别均使用实例化的非细分化材质。

现在,我们有了一个新的顶点,该顶点将在此阶段之后发送到几何程序或插值器。但是这些程序需要InterpolatorsVertex数据,而不是VertexData。为了解决这个问题,我们让域着色器接管了原始顶点程序的职责。这是通过调用其中的MyVertexProgram(与其他任何函数一样)并返回其结果来完成的。

现在,我们可以将域着色器添加到我们的三个着色器通道中,但是仍然会出现错误。

1.5 控制点

MyVertexProgram只需要被调用一次,这只是我们更改了发生这种情况的地方。但是我们仍然需要在顶点着色器阶段(位于Hull着色器之前)指定要调用的顶点程序。现在不需要做任何事情,因此我们可以简单地传递未经修改的顶点数据的函数。

从现在开始,让我们的三个着色器通道对其顶点程序使用此功能。

这将产生另一个编译器错误,抱怨位置语义的重用。为了让编译器正常,必须为顶点程序使用替代的输出结构,该结构将INTERNALTESSPOS语义用于顶点位置。该结构的其余部分与VertexData相同,区别在于它从未具有实例ID。由于此顶点数据被用作细分过程的控制点,因此将其命名为TessellationControlPoint。

更改MyTessellationVertexProgram,以便将顶点数据放入控制点结构中并返回该结构。

接下来,MyHullProgram也必须更改,以便它与TessellationControlPoint(而不是VertexData)一起使用。仅其参数类型需要更改。

补丁常数功能也是如此。

域程序的参数类型也必须更改。

至此,我们终于有了一个正确的细分着色器。它应该像以前一样编译并渲染四边形。由于细分因子始终为1.,因此尚未细分。

2 细分三角形

整个细分设置的重点是我们可以细分补丁。让我们可以用较小的三角形集合代替单个三角形。我们现在就这么做。

2.1 细分因子

三角形面片的细分方式由其细分因子控制。我们在MyPatchConstantFunction中确定这些因素。当前,我们将它们全部设置为1,不会产生视觉变化。Hull,细分和域着色器阶段正在运行,但是它们正在传递原始顶点数据,并且不会产生新的东西。要更改此设置,请将所有因子设置为2。

(细分因子为2)

现在,三角形确实可以细分了。它们的所有边均被分成两个子边,从而每个三角形产生三个新顶点。同样,在每个三角形的中心添加了另一个顶点。这样就可以在每个原始边缘生成两个三角形,因此每个原始三角形已被六个较小的三角形替换。由于四边形由两个三角形组成,因此现在总共有十二个三角形。

如果将所有因子设置为3,则每个边将被分为三个子边。这时,将没有中心顶点。而是在原始三角形内添加了三个顶点,从而形成了一个较小的内部三角形。外边缘将通过三角带连接到该内部三角形。

(细分因子为3)

当因子均匀时,会有一个中心顶点。当它们为奇数时,将有一个中心三角形。如果使用较大的因子,则最终会出现多个嵌套三角形。朝向中心的每一步,将三角形细分的数量减少两个,直到最终得到一个或零个子边。

(细分因子4-7)

2.2 不同的边和内部因子

三角形的细分方式由内部细分因子控制。边缘因子可用于覆盖对它们各自的边缘进行细分的数量。这仅影响原始Patch边,不影响生成的内部三角形。为了清楚地看到这一点,请将内部因子设置为7,同时保持边缘因子1。

(内部因子为7 外围因子为1)

有效地,使用因子7对三角形进行细分,然后丢弃三角形的外围。然后使用自己的因子细分每个边,然后生成三角带,将边缘和内部三角形缝合在一起。

边缘因子也可能大于内部因子。例如,将边缘系数设置为7,而将内部系数保持为1。

(内部为1 但是外围为7)

在这种情况下,内部因子将被强制为2,因为否则将不会生成新的三角形。

如何为每个边使用不同的因子?

这是可能的,但是当你对硬编码值执行此操作时,着色器编译器不喜欢。当尝试使用某些值进行着色时,可能会导致着色器编译器错误。我们将在后面看到为什么不同的因子能用。

2.3 变量因子

硬编码的细分因子不是很有用。因此,让我们使其可配置,从一个统一的值开始。

给他添加一个属性到我们的着色器。将其范围设置为1–64。无论我们要使用的因素有多高,硬件每个补丁程序最多只能有64个细分。

为了能够编辑此因子,请向MyLightingShaderGUI中添加DoTessellation方法以在其自己的部分中显示它。

在渲染模式和线框部分之间的OnGUI内部调用此方法。仅当必需属性存在时才这样做。

(可配置的统一因子)

2.4 分数因子

即使我们使用浮点数来设置细分因子,但最终总是会在每个边上得到大量的等效细分。那是因为我们正在使用整数分区模式。虽然这是查看细分工作原理的好模式,但它阻止了我们在细分级别之间平稳过渡。幸运的是,也有分数分割模式。让我们将模式更改为fractional_odd。

(分数模式)

当使用整个奇数因子时,fractional_odd分区模式将产生与整数模式相同的结果。但是,当在奇数因子之间转换时,多余的边缘细分将被分割并增长,缩小或合并。这意味着边缘不再总是分成相等长度的段。这种方法的优势在于,细分级别之间的过渡现在很顺畅。

也可以使用fractional_even模式。除了基于偶数因素外,它的工作方式相同。

(小数均匀分配)

通常使用fractional_odd模式,因为它可以处理1的因数,而fractional_even模式则被迫使用最小级别2。

3 启发式细分

最好的细分因子是什么呢?这是在进行细分时必须问自己的问题。这个问题没有一个客观的答案。通常,你能做的最好的事情就是提出一些指标,该指标可以作为启发式方法,产生良好的效果。在本教程中,我们将支持两种简单的方法。

3.1 边因子

尽管必须为每个边提供细分因子,但是你不用直接在边上建立细分因子。例如,你可以确定每个顶点的因子,然后将每个边的因子平均。甚至因子可以存储在纹理中。在任何情况下,给定边的两个控制点,使用单独的函数来确定因子都是很方便的。创建这样的函数,现在只需返回统一值即可。

将此函数用于MyPatchConstantFunction内部的边因子。

对于内部因素,我们将仅使用边缘因素的平均值。

3.2 边长度

由于边的细分因子控制着我们对原始三角形的边进行细分的程度,因此有必要将这些因子基于这些边的长度生成。例如,我们可以指定所需的三角形边长度。如果最终得到的三角形边长于该长度,则应将它们细分为所需的长度。为此添加一个变量。

也添加一个属性。让我们使用0.1到1的范围,默认值为0.5。这是世界空间单位。

我们需要一个着色器功能,以便可以在均匀和基于边的曲面细分之间进行切换。使用_TESSELLATION_EDGE关键字将所需的指令添加到所有三个过程中。

接下来,向MyLightingShaderGUI中添加一个枚举类型以表示细分模式。

然后调整DoTessellation,使其可以使用枚举弹出窗口在两种模式之间切换。它的工作方式类似于DoSmoothness如何控制平滑度模式。在这种情况下,统一是默认模式,不需要关键字。

(使用边模式)

现在我们必须调整TessellationEdgeFactor。定义_TESSELLATION_UNIFORM后,确定两个点的世界位置,然后计算它们之间的距离。这是世界空间中的边长。边缘系数等于该长度除以所需长度。

(不同的四阶尺度,相同的边长度)

因为我们现在使用边长度来确定边的细分因子,所以最终可以为每个边缘使用不同的因子。你可以看到这种情况发生在四边形上,因为对角线边比其他边长。当对方形使用非均匀比例并将其沿一维拉伸时,也会变得很明显。

(拉伸四边形)

为了使这项工作有效,至关重要的是,共享同一边的补丁最终都使用相同的细分因子进行边化。否则,生成的顶点将沿着该边不匹配,这会在网格中产生可见的间隙。在我们的例子中,我们对所有边使用相同的逻辑。唯一的区别可以是控制点参数的顺序。由于浮点数的限制,从技术上讲,这可能会产生不同的因素,但是这种差异非常小,以至于不会引起注意。

3.3 屏幕坐标中的边长度

尽管我们现在可以控制世界空间中的三角形边长度,但是这与三角形在屏幕空间中的显示方式并不相同。细分的目的是在需要时添加更多三角形。因此,我们不想细分已经看起来很小的三角形。所以,让我们改用屏幕空间的边缘长度。

首先,更改边长属性的范围。代替世界单位,我们将使用像素,因此5-100的范围更有意义。

用等效的屏幕空间替换世界空间计算。为此,必须将点转换为剪辑空间而不是世界空间。然后,使用X和Y坐标除以W坐标将其投影到屏幕上,以2D方式确定其距离。

现在我们有了剪辑空间的结果,它是一个大小为2的均匀立方体,适合显示。要转换为像素,必须按显示尺寸(以像素为单位)进行缩放。实际上,由于显示很少是正方形的,因此要获得最精确的结果,应该在确定距离之前分别缩放X和Y坐标。但是,仅通过按屏幕高度缩放就可以了,看看它的外观就足够了。

(相同的世界尺寸,不同的屏幕尺寸)

现在,基于渲染的三角形边将其细分。相对于相机,位置,旋转和缩放比例都会影响此效果。结果就是,当物体运动时,细分的数量会发生变化。

我们不是应该使用屏幕高度的一半吗?

由于剪辑空间立方体的范围是-1~1,所以两个单位分别对应于显示器的整个高度和宽度。这意味着我们最终得到的是实际大小的两倍,高估了边的大小。结果是,我们有效地瞄准了比预期长一半的边缘长度。至少对于完美的垂直边来说就是这种情况,因为我们始终没有使用确切的屏幕尺寸。使用屏幕高度的要点是使细分取决于显示分辨率。边缘长度是否与滑块的精确值无关紧要。

3.4 使用视距

纯粹依靠边的可视长度的缺点是,在世界空间中较长的边缘最终在屏幕空间中会变得非常小。这可能会导致这些边缘根本无法细分,而其他边缘则细分很多。当使用细分来近距离添加细节或生成复杂轮廓时,这是不希望的。

另一种方法是返回使用世界空间边长度,但是根据视距调整因子。某物距离越远,它在视觉上应显示的越小,因此所需的细分就越少。因此,将边长度除以边与相机之间的距离。我们可以使用边的中点来确定该距离。

通过简单地将屏幕高度纳入其中并保持我们的5-100滑块范围,我们仍然可以保持细分取决于显示尺寸。请注意,这些值不再直接对应于显示像素。当你更改摄像机的视场时,这是非常明显的,它完全不影响细分。因此,这种简单的方法不适用于使用可变视场(例如放大和缩小)的游戏。

(基于边长度 和 视距)

3.5 使用正确的内部因子

尽管此时曲面细分似乎可以正常工作,但内部细分因素仍存在一些奇怪之处。至少在使用OpenGL Core时就是这种情况。使用统一的四边形并不是那么明显,但是当使用变形的立方体时会变得明显。

(不正确内部因子的立方体)

在立方体的情况下,组成一个面的两个三角形各自具有非常不同的内部细分因子。四边形和立方体面之间的唯一区别是三角形顶点的定义顺序。Unity的默认立方体不使用对称的三角形布局,而四边形则使用对称的三角形布局。这表明边的顺序显然会影响内部细分因子。但是,我们仅取边因素的平均值,因此它们的顺序无关紧要。肯定有其他问题。

我们做一些看似荒谬的事情,并在计算内部因素时再次显式调用TessellationEdgeFactors函数。从逻辑上讲,这应该没有什么区别,因为我们最终只执行了两次完全相同的计算。着色器编译器肯定会对其进行优化。

(正确内部因子的立方体)

显然,这确实有改变的,因为两个表面三角形现在最终都使用几乎相同的内部因子。这里发生了什么?

补丁常数函数与其余的hull着色器并行调用。但这实际上会变得更复杂。着色器编译器也能够并行化边因子的计算。MyPatchConstantFunction内部的代码被撕开并部分重复,替换为交叉的过程,该过程并行计算三个边因子。完成所有三个过程后,将它们的结果合并并用于计算内部因子。

编译器是否决定fork进程不应该影响着色器的结果,而仅影响其性能。不幸的是,OpenGL Core的生成代码中存在错误。在计算内部因子时,不使用三个边因子,而仅使用第三个边因子。数据在那里,它只访问索引2、3,而不是索引0、1和2。因此,我们总是以等于第三个边因子的内部因子结束。

对于补丁常数功能,着色器编译器将并行化设置为优先级。它会尽快拆分进程,之后便无法再优化TessellationEdgeFactor的重复调用。我们以三个过程结束,每个过程计算两个点的世界位置,距离和最终因子。然后还有一个计算内部因素的过程,该过程现在还必须计算三个点的世界位置,以及所涉及的所有距离和因子。由于我们现在正在对内部因子进行所有工作,因此对边因子也单独完成部分工作是没有意义的。

事实证明,如果我们首先计算这些点的世界位置,然后分别对边和内部因子计算TessellationEdgeFactor,则着色器编译器将决定不为每个边因子fork单独的过程。我们最终得到了一个可以全部计算的流程。在这种情况下,着色器编译器确实优化了TessellationEdgeFactor的重复调用。

现在,我们可以细分三角形了,但是我们还没有用这种能力做任何事情。表面位移演示了如何使用细分来使表面变形。

下一章我们将介绍表面位移。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
GPU 云服务器
GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于生成式AI,自动驾驶,深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档