前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >进阶渲染系列(一)——平坦和线框着色(导数和几何体)

进阶渲染系列(一)——平坦和线框着色(导数和几何体)

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

目录

1 平面着色1.1 导数指令1.2 几何着色1.3 逐三角形修改顶点法线2 渲染线框2.1 重心坐标2.2 定义额外的插值器2.3 分割 My Lighting2.4 重写反照率2.5 创建线框2.6 修复线宽度2.7 配置线

本文重点: 1、使用屏幕空间导数查找三角形法线 2、通过几何着色器找出三角形法线 3、使用生成的重心坐标创建线框 4、是线框固定宽度并且可配置

本教程介绍如何添加对平面着色的支持以及如何显示网格的线框。它使用了高级渲染技术,并假定您熟悉“渲染”系列中介绍的材质。

本教程使用Unity 2017.1.0制作。

(展示三角形)

1 平面着色

网格由三角形组成,根据定义,它们是平坦的。我们使用表面法线向量添加曲率幻觉。这样就可以创建看似平滑表面的网格。但是,有时你可能想显示实际上的平面三角形,以用于样式或更好地查看网格的拓扑。

为了使三角形看起来像它们实际一样平坦,我们必须使用实际三角形的表面法线。它将使网格具有多面外观,称为平面着色。这可以通过让三角形的三个顶点的法线向量等于三角形的法线向量来完成。这会导致在三角形之间不能共享顶点,因为那样它们也将共享法线。因此,我们最终得到了更多的网格数据。如果我们可以一直共享顶点将很方便。同样,如果我们可以使用具有任何网格的平面着色材质,并覆盖其原始法线(如果有),那将是更好的。

除了平面着色,显示网格的线框也可能有用或看起来时尚。这使得网格的拓扑更加明显。理想情况下,可以使用自定义材质 在一个单一的pass下,对任何网格进行平面着色和线框渲染。要创建这种材质,需要一个新的着色器。我们将使用“渲染”系列第20部分中的最终着色器作为基础。复制“My First Lighting Shader”,并将其名称更改为“Flat Wireframe”。

不是能在编辑器中看到线框吗?

实际上,我们可以在场景视图中看到线框,但是在游戏视图中却不能在构建中看到。因此,如果要在场景视图之外查看线框,则必须使用自定义解决方案。同样,无论着色器是否渲染其他东西,场景视图都仅显示原始网格的线框。因此,它不适用于细分的顶点位移。

1.1 导数指令

由于三角形是平坦的,所以其表面法线在其表面上的每个点都相同。因此,为三角形渲染的每个片段应使用相同的法线向量。但是我们目前不知道这个向量是什么。在顶点程序中,我们只能访问单独的存储在网格中的顶点数据。除非它们有明确设计过,用来表示三角形的法线,否则此处存储的法线向量对我们没有用。在片段程序中,我们只能访问插值的顶点法线。

为了确定表面法线,我们需要知道三角形在世界空间中的方向。这可以通过三角形顶点的位置来确定。假设三角形不退化,则其法线向量等于三角形两个边缘的归一化叉积。如果它是退化的,则无论如何都不会渲染。因此,按逆时针方向给出三角形的顶点a ,b和c,其法线向量为n =(c-a)×(b-a)。通过归一化,可以得到最终的单位法向矢量。

(推导三角形的法线)

实际上,我们不需要使用三角形的顶点。只要位于三角形平面内的任何三个点也可以,只要这些点也形成三角形即可。具体来说,只要两个向量不平行且大于零,就只需要它们位于三角形平面内即可。

另外一种可能性是使用与渲染片段的世界位置相对应的点。例如,当前正在渲染的片段的世界位置,片段在其右侧的位置以及片段在屏幕空间中的位置。

(使用片段的世界位置)

如果我们可以访问相邻片段的世界位置,那么这可以实现。实际上,着色器并不能直接访问相邻片段的数据,但是我们可以访问此数据的屏幕空间导数类。这是通过特殊指令完成的,该指令告诉我们屏幕空间X或Y维度中任何数据片段在片段之间的变化率。

例如,我们当前片段的世界位置是 p 0 。屏幕空间X维度中下一个片段的位置是 p X 像素。因此,这两个片段之间在X维度上的世界位置变化率是。

这是屏幕空间X维度中世界位置的偏导数。我们可以通过ddx函数在片段程序中检索此数据,方法是向其提供世界位置。在My Lighting.cginc中InitializeFragmentNormal函数的开始处执行此操作。

对屏幕空间Y维度执行相同的操作,调用ddy函数用世界坐标位置,找出

由于这些值表示片段世界位置之间的差异,因此就如同它们定义了三角形的两个边。我们实际上并不知道该三角形的确切形状,但是可以确保它位于原始三角形的平面内,这很重要。因此,最终的法向向量是这些向量的归一化叉积。使用此向量覆盖原始法线。

ddx和ddy如何工作?

GPU在采样纹理时需要知道纹理坐标的屏幕空间导数,以确定要使用的mipmap级别。它通过比较相邻片段的坐标来解决这一问题。屏幕空间导数指令是对它的扩展,使此功能可用于所有片段程序及其使用的任何数据。

为了能够比较片段,GPU以2×2的块进行处理。对于每个块,它为两个2×1片段对确定X维度上的两个导数,对于两个1×2片段对确定Y维度上的两个导数。一对中的两个片段使用相同的导数数据。这意味着导数仅在每个块中更改,每两个像素一次,而不是每个像素更改。结果,这些导数是一个近似值,当用于每个片段非线性变化的数据时,它们将显得块状化。因为三角形是平坦的,所以这种近似不会影响我们得出的法线向量。

(块状的倒数对)

GPU始终以2×2块处理片段,因此沿着三角形的边缘的片段最终的处理结果会在三角形之外。这些无效的片段会被丢弃,但仍需要进行处理以确定导数。在三角形之外,片段的插值数据会推到顶点所定义的范围之外。

创建一个使用我们的Flat Wireframe着色器的新材质。使用此材质的任何网格均应使用平面着色渲染。它们看起来是多面的,如果你同时使用法线贴图时可能很难看清。所以, 在本教程的截图中,会使用标准的胶囊网格,材质为灰色。

(光滑和平坦着色)

从远处看,它看起来像是由四边形制成的胶囊,但这些四边形分别由两个三角形组成。

(四边形由三角形组成)

在执行此操作的同时,我们实际上已更改了所有依赖“My Lighting”包含文件的着色器的行为。因此,删除我们刚刚添加的代码。

1.2 几何着色

除了使用导数指令之外,还有另一种方法可以确定三角形的法线。使用实际的三角形顶点来计算法线向量。这需要使用每个三角形而不是每个单独的顶点或片段来完成工作。这就是几何着色器的领域。

几何着色器阶段位于顶点和片段阶段之间。它被提供给顶点程序的输出,每个primitive一组。几何程序可以在插入和用于渲染片段之前修改该数据。

(逐三角形处理顶点)

几何着色器的附加价值是每个图元都将顶点反馈给它,因此在本例中每个三角形三个。网格三角形是否共享顶点无关紧要,因为几何程序会输出新的顶点数据。这使我们能够导出三角形的法线向量并将其用作所有三个顶点的法线。

让我们将几何着色器的代码放在自己的包含文件MyFlatWireframe.cginc中。让此文件包含My Lighting.cginc并定义MyGeometryProgram函数。从一个空的void函数开始。

仅当目标着色器模型为4.0或更高版本时才支持几何着色器。如果将目标定义得较低,Unity会自动将其增加到该级别,但让我们对其进行明确说明。要实际使用几何着色器,我们必须添加#pragma geometry指令,就像顶点和片段函数一样。最后,必须包括MyFlatWireframe而不是“My Lighting”。将这些更改应用到我们的Flat Wireframe着色器的基础,附加和延迟的pass中。

这将导致着色器编译器错误,因为我们尚未正确定义几何函数。必须声明它将输出多少个顶点。此数字可能有所不同,因此我们需要提供一个最大值。因为我们正在处理三角形,所以每次调用总是输出三个顶点。通过将maxvertexcount属性添加到我们的函数中(以3作为参数)来指定。

下一步是定义输入。当我们在插值之前使用顶点程序的输出时,数据类型为InterpolatorsVertex。因此,在这种情况下,类型名称在技术上并不正确,但是在命名它时并未考虑几何着色器。

还需要声明我们正在处理的原始类型,在我们的例子中为三角形。必须在输入类型之前指定。另外,由于三角形每个都有三个顶点,因此我们正在研究三个结构的数组。必须明确定义它。

由于几何着色器可以输出的顶点数量各不相同,因此我们没有统一的返回类型。相反,几何着色器将写入图元流。在我们的例子中,它是一个TriangleStream,必须将其指定为inout参数。

TriangleStream的工作方式类似于C#中的泛型类型。它需要知道我们要提供的顶点数据的类型,它仍然是InterpolatorsVertex。

现在函数的参数已经正确了,我们必须将顶点数据放入流中。这是通过按每个顶点调用流的Append函数的顺序来完成的(按照我们收到它们的顺序)。

此时,我们的着色器将再次起作用。添加了一个自定义几何阶段,该阶段仅通过顶点程序的输出,而未修改。

为什么几何程序看起来如此不同?

Unity的着色器语法是CG和HLSL代码的混合体。通常看起来像CG,但现在,它类似于HLSL。

1.3 逐三角形修改顶点法线

要找到三角形的法线向量,请先提取其三个顶点的世界位置。

现在,执行标准化的叉积,每个三角形一次。

用该三角形法线替换顶点法线。

(第二种方式实现 平坦着色)

虽然最终得到与以前相同的结果,但是现在使用的是几何着色器阶段,而不是依赖于屏幕空间导数指令。

哪种方法更好?

如果仅需要平面着色,则屏幕空间派生工具是实现该效果的最便宜的方法。然后,你还可以从网格数据中删除法线(Unity可以自动执行此操作),并且还可以删除法线插值器数据。通常,如果你不想使用自定义几何图形阶段,可以这样做。不过,我们将继续使用几何方法,因为线框渲染也将需要它。

2 渲染线框

处理完平面着色后,我们继续渲染网格的线框。不需要创建新的几何图形,也不会使用额外的PASS来绘制线条。我们将通过在三角形内部沿其边缘添加线效果来创建线框视觉效果。尽管定义形状轮廓的线看起来将比内部线的厚度粗一半,但这可以创建令人信服的线框。因为差异不是很明显,因此我们通常会接受这种不一致的情况。

(具有更细轮廓线的线效果)

2.1 重心坐标

要向三角形边缘添加线条效果,我们需要知道片段到最近边缘的距离。这意味着有关三角形的拓扑信息需要在片段程序中可用。这可以通过将三角形的重心坐标添加到插值数据中来完成。

什么是重心坐标?

三角形具有三个分量的坐标。每个分量沿一个边为0,在与该边相对的顶点为1,在这两个边之间线性过渡。这些坐标也用于插值顶点数据。

(三角形内的重心坐标)

向三角形添加重心坐标的一种方法是使用网格的顶点颜色存储它们。每个三角形的第一个顶点变为红色,第二个顶点变为绿色,第三个顶点变为蓝色。但是,这将需要具有以此方式分配的顶点颜色的网格,并且无法共享顶点。我们想要一种适用于任何网格的解决方案。幸运的是,我们可以使用我们的几何程序添加所需的坐标。

由于网格不提供重心坐标,因此顶点程序不了解它们。所以,它们不属于InterpolatorsVertex结构。要使几何程序输出它们,我们必须定义一个新结构。首先在MyGeometryProgram上方定义InterpolatorsGeometry。它应包含与InterpolatorsVertex相同的数据,因此使用它作为其内容。

调整MyGeometryProgram的流数据类型,使其使用新结构。在函数内部定义此类型的变量,将输入数据分配给它们,然后将其附加到流中,而不是直接将输入传递给它们。

现在,我们可以向InterpolatorsGeometry添加其他数据。使用第十个插值器语义为它提供一个float3 barycentricCoordinators向量。

给每个顶点一个重心坐标。哪个顶点获得什么坐标都没有关系,只要它们是有效的即可。

请注意,重心坐标总是加起来为1。因此,只要传递两个就足够了,通过从1中减去其他两个来推导第三个坐标。这意味着我们必须内插一个较小的数字,让我们进行更改。

现在是否已使用重心坐标插补了我们的重心坐标?

是。但是,我们还不能直接使用用于插值顶点数据的重心坐标。由于各种原因,GPU可以决定在最终进入顶点程序之前将三角形拆分为较小的三角形。所以,GPU用于最终插值的坐标可能与预期的不同。

2.2 定义额外的插值器

至此,我们将重心坐标传递给片段程序,但程序尚不了解它们。必须将它们添加到“My Lighting”中“Interpolators ”的定义中。但是我们不能简单地假设此数据可用。至少对于Flat Wireframe着色器来说是这样。因此,让使用My Lighting的任何人都可以通过CUSTOM_GEOMETRY_INTERPOLATORS宏定义通过几何着色器提供的自己的插值器数据。为此,将宏插入到插值器中。

现在我们可以在MyFlatWireframe中定义此宏。在包含“My Lighting”之前,必须这样做。我们也可以在InterpolatorsGeometry中使用它,因此只需要写一次代码。

为什么会出现转换编译错误?

如果你使用的是Rendering 20中的package,那是因为教程错误。“My Lighting”中的ComputeVertexLightColor函数应将InterpolatorsVertex用作其参数类型,但错误地使用Interpolators。修复此错误,错误就会消失了。如果你使用自己的代码,则在某个地方使用错误的插值器结构类型时,可能会遇到类似的错误。

2.3 分割 My Lighting

我们将如何使用重心坐标来可视化线框呢?但是,无论如何, My Lighting都不应参与。相反,通过在代码中插入我们自己的函数,可以通过另一个文件重新连接其功能。

要覆盖My Lighting的功能,必须在包含文件之前定义新代码。但是如果这样做了的话,我们又需要访问在“My Lighting”中定义的插值器,因此需要首先将其包括在内。要解决此问题,我们需要将“My Lighting”分成两个文件。使用包括语句,插值器结构和所有Get函数,在My Lighting的开头复制代码。将此代码放在新的My Lighting Input.cginc文件中。给文件自己的包含保护定义,MY_LIGHTING_INPUT_INCLUDED。

从“My Lighting”中删除相同的代码。为了使现有的着色器正常工作,请改为包括“My Lighting Input”。

现在可以在包含“My Lighting”之前包含“My Lighting Input”。它的包含保护将确保防止重复包含。在MyFlatWireframe中也这样写。

2.4 重写反照率

让我们通过调整材质的反照率来添加线框效果。这要求我们替换“My Lighting”的默认反照率功能。与自定义几何插值器一样,我们将通过宏ALBEDO_FUNCTION进行此操作。在确定已包含输入之后,在“My Lighting Input”中,检查是否已定义此宏。如果不是,请将其定义为GetAlbedo函数,使其成为默认值。

在MyFragmentProgram函数中,用宏替换GetAlbedo的调用。

现在,在包含“My Lighting Input”之后,我们可以在MyFlatWireframe中创建自己的反照率函数。它需要具有与原始GetAlbedo函数相同的形式。首先简单地传递原始函数的结果。之后,使用我们自己的函数名称定义ALBEDO_FUNCTION宏,然后包含“My Lighting”。

为了验证我们确实可以控制片段的反照率,请直接使用重心坐标作为反照率。

(重心坐标作为反照率)

2.5 创建线框

要创建线框效果,我们需要知道片段与最近的三角形边缘的接近程度。可以通过获取最小的重心坐标来找到它。在重心域中,这为我们提供了到边缘的最小距离。让我们直接将其用作反照率。

(最小重心坐标)

看起来有点像白色网格顶部的黑色线框,但是太模糊了。这是因为到最近的边的距离从边的零到三角形中心的⅓。为了使它看起来更像细线,我们必须更快地淡化为白色,例如通过在0到0.1之间从黑色过渡到白色。为了使过渡平滑,让我们为此使用smoothstep函数。

什么是smoothstep函数?

它是一个标准函数,可在两个值之间产生平滑的曲线过渡,而不是线性插值。定义为 其中 t从0到1。

(smoothstep VS线性 过渡)

Smoothstep函数具有三个参数a,b和c。前两个参数a和b定义了过渡应该覆盖的范围,而c是要平滑的值。这导致,在使用前将其钳位为0-1。

(调整过渡)

2.6 修复线宽度

线框效果开始看起来不错,但仅适用于边长大致相同的三角形。此外,这些线还受视距的影响,因为它们是三角形的一部分。理想地,线具有固定的视觉厚度。

为了使线厚度在屏幕空间中保持恒定,我们必须调整用于smoothstep功能的范围。该范围取决于到边缘的距离可视化变化的速度。可以使用屏幕空间导数指令来解决这个问题。

两个屏幕空间尺寸的变化率可能不同。我们应该使用哪个呢?可以同时使用它们,只需添加它们即可。另外,由于变化可能是正的或负的,因此我们应使用其绝对值。通过直接使用结果作为范围,我们最终得到的线条大致覆盖了两个片段。

该公式也可以用作方便的fwidth函数,因此让我们使用它。

(固定宽度的线)

产生的线可能看起来太细。可以通过将过渡点从边缘稍微移开来解决此问题,例如,将过渡范围设为与混合范围相同的值。

(较宽的宽度,但有失真现象)

这样可以产生更清晰的线条,但也会在三角形拐角附近的线条中显示出锯齿失真现象。出现失真的原因是最近的边缘在那些区域中突然改变,从而导致不连续的导数。为了解决这个问题,必须使用各个重心坐标的导数,分别混合它们,然后获取最小值。

(线框 没有失真)

2.7 配置线

现在已经具有实用的线框效果,但你可能需要使用其他线宽,混合区域或颜色。也许你想对每种材质使用不同的设置。因此,让我们使其可配置。为此,向“Flat Wireframe ”着色器添加三个属性。首先是线框颜色,默认为黑色。第二是线框平滑,它控制过渡范围。从0到10的范围应该足够,默认值为1,代表宽度测量的倍数。第三是线框厚度,其设置与平滑相同。

将相应的变量添加到MyFlatWireframe中,并在GetAlbedoWithWireframe中使用它们。根据平滑的最小值,通过在线框颜色和原始反照率之间进行插值来确定最终的反照率。

现在可以配置着色器,但是属性尚未出现在我们的自定义着色器GUI中。我们可以为Flat Wireframe创建一个新的GUI,但是让我们使用快捷方式并将属性直接添加到MyLightingShaderGUI。给它一个新的DoWireframe方法来为线框创建一个小部分。

若要使MyLightingShaderGUI支持带线框和不带线框的两种着色器,如果着色器具有_WireframeColor属性,则仅在其OnGUI方法中调用DoWireframe。我们简单地假设,如果该属性可用,则它具有所有这三个属性。

(配置线框)

现在,你可以使用平面着色器和可配置的线框渲染网格。它将在下一个高级渲染教程Tessellation中派上用场。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档