[Shader] 我对着色器的看法:几何着色器 您所在的位置:网站首页 着色器1152 [Shader] 我对着色器的看法:几何着色器

[Shader] 我对着色器的看法:几何着色器

2024-07-05 12:32| 来源: 网络整理| 查看: 265

英文原文:https://halisavakis.com/my-take-on-shaders-geometry-shaders/

文章目录 引言概述基础几何着色器几何着色器本身不起眼的片段着色器挤压金字塔在几何着色器之前几何着色器片段着色器总结

在这里插入图片描述

引言

Unity 着色器的资源已经够少了,但有一种非常特殊的 Unity 着色器资源却非常罕见(至少在撰写本文时是如此): 几何着色器。我的推测是,人们并不真正关心这些着色器,因为它们可能效率低下,而且在某些情况下,它们可以被更快的计算着色器取代。不过,对它们的研究仍然非常有趣,而且可以用它们创建一些非常酷的效果。最明显的例子(我们将在下一个(可能的)教程中研究)就是草地着色器。在某些情况下,生成带有草纹理的草叶或四边形比在所有地方实例化草对象更好,此外,几何着色器还能在优化和自定义方面为我们提供更多灵活性。

我将在这里链接一些帮助我开始学习几何着色器的教程和资源,因此我绝对推荐将它们作为补充材料:

https://jayjingyuliu.wordpress.com/2018/01/24/unity3d-intro-to-geometry-shader/https://roystan.net/articles/grass-shader.html(这也恰好是带有几何着色器的草的权威教程)shaderslab.com 上的几何着色器 概述

在进入代码之前,让我们先来了解一下什么是几何着色器。为了帮助我说明几何着色器的作用,下面是一个简化的实时渲染管道视图:

在这里插入图片描述 这篇文章介绍的是 Vulkan 中的流水线,但其概念与其他流水线非常相似。黄色方框是流水线的实际阶段,我们可以用着色器进行编程/覆盖。值得注意的是,"Tessellation "阶段实际上分为 “Hull Shader 阶段”、"Tessellator 阶段 "和 “Domain Shader 阶段”。Hull着色器和Domain着色器阶段也可以受到相应着色器的影响(如顶点和片段阶段),但 "tessellator 着色器 "阶段则无法修改(即在 Unity 着色器中)。

核心要点是几何着色器是顶点着色器和片段着色器之间的步骤,这也是您在着色器示例中不断看到的概念。它们的主要目的是通过顶点着色器从单独的顶点获取信息,组装将形成对象的图元(通常是三角形)并将最终信息发送到片段着色器以进行着色、照明等。

然而,关于几何着色器,最酷的事情是它们采用三个顶点形成一个三角形(例如),它们不仅可以定义该三角形会发生什么,而且还可以生成新的顶点,从而生成新的三角形,然后获取所有新生成的几何体并将其连枷起来,就像在顶点着色器中替换顶点时所做的那样。例如,这实际上可以让你用三角形制作粒子,就像 VFX Mike 所演示的那样。

最后,虽然他们的代码一开始看起来很吓人,但一旦你掌握了一些窍门,使用它们就会变得非常直观,而且你实际上可以更好地理解 UV 和对象空间坐标等东西是如何工作的。而且,如果你像我一样,你会发现自己经常说:“哦,该死,我没想到这还能用!”。

基础几何着色器

我想先从一个非常基本的几何着色器开始,不带任何附加功能,只是为了把 "文书 "工作做完。这也可以作为你想制作新几何着色器的整洁模板,所以我想把它放在手边的某个地方。

让我们看看代码:

Shader "Geometry/NewGeometryShader" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma geometry geom // 第 16 行 #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2g { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; struct g2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2g vert (appdata v) { v2g o; o.vertex = v.vertex; // 第 48 行 o.uv = v.uv; return o; } [maxvertexcount(3)]// 第 53 行 void geom(triangle v2g IN[3], inout TriangleStream triStream) // 第 54 行 { g2f o; // 第 56 行 for(int i = 0; i // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }

首先,我们必须让整个着色器知道我们将使用几何着色器。因此,我们需要在第 16 行中添加另一条 #pragma 语句,在其中说明将有一个名为 "geom "的方法与几何着色器相对应,我们稍后将定义该方法。这就是 "#pragma geometry geom "的作用,就像顶点着色器和片段着色器一样。

在这种情况下,我们不仅将数据从顶点传递到片段着色器,而且中间还有几何着色器。因此,我们将需要三个数据结构,而不是通常的两个:

" appdata " 结构用于像往常一样将对象的属性传递给顶点着色器。" v2g " 结构用于将数据从顶点着色器传递给几何着色器" g2f " 结构用于将数据从几何着色器传递到片段着色器。

接着来看着色器,首先是顶点着色器。它现在返回的不是 v2f 对象,而是 v2g 对象,如上所述,它将数据传递给几何着色器。在这个简单的实例中,顶点只是传递数据,没有做任何修改。

在第 48 行中,我仅按原样传递对象空间中的顶点位置,并且在第 49 行中对顶点的 UV 坐标也执行了相同的操作。请注意,我没有进行对象到剪辑位置的转换,也没有对 UV 使用“TRANSFORM_TEX”宏,因为这些操作将在将数据传输到片段着色器时在几何着色器中完成。

请记住,我们实际上可以在顶点着色器中进行对象到剪辑位置的转换和 UV 计算,而且它甚至可能会更好,因为它会执行更少的次数。但是,将原始数据传递给几何着色器可以让我们更灵活地处理它们。

几何着色器本身

这里的情况就比较棘手了。首先,你会注意到几何着色器的语法有很大不同。不过,我们会一步步来。

在第 53 行中,着色器上方有一个奇怪的属性。顾名思义,"maxvertexcount "可以让着色器知道这个几何着色器要追加的最大顶点数。由于我们只是添加对象的实际三角形,因此将只输出 3 个顶点。

然后,第 54 行是方法声明,其中有几个奇怪的参数:

triangle v2g IN[3] : 这是一个由三个 v2g 对象组成的数组,每个对象对应当前检查的三角形的一个顶点。三角形 "标签会告知几何着色器,它需要一个三角形作为输入。你可以输入一条线(因此需要一个大小为 2 的 v2g 对象数组)或一个点(因此需要一个大小为 1 的 v2g 对象数组)。inout TriangleStream triStream : 如果你没有注意到,几何着色器返回的是 “void”,所以我们实际上并没有像顶点着色器那样返回一个对象。几何着色器实际上是将每个三角形追加到一个 TriangleStream 列表中,该列表中的对象类型为 “g2f”。如果你想输出线,你可以把它变成 " inout LineStream lineStream ",如果你想输出点,那就是 " inout PointStream pointStream "。

现在让我们来看看实际功能。我们希望这个几何着色器能从顶点着色器中获取数据,然后将三角形组合起来,再由片段着色器转换为片段并着色。

首先,在第 56 行,我们创建了一个 g2f 对象,用来不断修改其字段,然后将其追加到数据流中。

然后,我们要做一个简单的循环,将三个输入顶点追加到数据流中,创建对象的三角形。由于这些数据将进入片段着色器,因此我在这里做了所有我想做的修改。

首先,在第 60 行中,我使用 " UnityObjectToClipPos " 方法将输入的顶点位置从对象空间转换到剪辑空间。然后,我使用 " UNITY_TRANSFER_FOG " 宏来传输有关 Unity 烟雾的所有信息。在第 62 行,我还使用 " TRANSFORM_TEX " 宏,根据"_MainTex "的缩放和平铺信息修改 UV 坐标。所有这些都是针对三角形的三个顶点进行的,就像我们在没有几何着色器的情况下在顶点着色器中所做的一样。

最后,我们使用 "triStream.Append(o); "将修改后的 "g2f "对象添加到三角数据流中。在循环之后还有一个 "RestartStrip "方法,位于第 66 行。这是为了让数据流知道,它将在之后追加一个单独的三角形。在这里,由于我们不会添加任何新的三角形,所以并不真正需要它,但我添加了它,以便在以后扩展着色器时,在需要时可以轻松复制它。

不起眼的片段着色器

最后,在第 69-76 行我们有片段着色器。您可能会注意到,它与新的无光照着色器中的默认片段着色器完全没有变化,只是它获取“g2f”作为参数,而不是“v2f”。

挤压金字塔

现在我们有了几何着色器的基础,我们实际上可以制作一个可以做某事的着色器。具体来说,这个将从每个三角形中挤出金字塔,基本上是在每个三角形的中心添加一个点,并将金字塔朝着面部的法线向量挤出。

我们将对每个三角形执行的操作的直观示例如下:

在这里插入图片描述 我们看一下代码:

Shader "Geometry/TrianglePyramidExtrusion" { Properties { _MainTex ("Texture", 2D) = "white" {} _ExtrusionFactor("Extrusion factor", float) = 0 } SubShader { Tags { "RenderType"="Opaque" } Cull Off // 第 11 行 LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma geometry geom #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2g { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct g2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; float4 color : COLOR; }; sampler2D _MainTex; float4 _MainTex_ST; float _ExtrusionFactor; v2g vert (appdata v) { v2g o; o.vertex = v.vertex; o.uv = v.uv; o.normal = v.normal; return o; } [maxvertexcount(12)] // 第 60 行 void geom(triangle v2g IN[3], inout TriangleStream triStream) { g2f o; float4 barycenter = (IN[0].vertex + IN[1].vertex + IN[2].vertex) / 3; float3 normal = (IN[0].normal + IN[1].normal + IN[2].normal) / 3; for(int i = 0; i o.vertex = UnityObjectToClipPos(IN[i].vertex); UNITY_TRANSFER_FOG(o,o.vertex); o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex); o.color = fixed4(0.0, 0.0, 0.0, 1.0); triStream.Append(o); } triStream.RestartStrip(); } fixed4 frag (g2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv) * i.color; // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } } 在几何着色器之前

在这个着色器中,我添加了一个名为"_ExtrusionFactor "的新属性,它将决定金字塔的挤出程度。此外,由于某些三角形的绘制顺序不正确,为了避免剔除某些面,我在第 11 行添加了 "Cull Off(关闭剔除)"标签。最好的方法是正确地绘制面,但现在我们还是保持这种方式,以便更容易使用循环。

数据结构也做了一些改动。为了获取每个面的法向量,我们首先需要获取每个顶点的法向量,因此我们必须将其添加到 "appdata "结构和 "v2g "结构中。此外,我还在 "g2f "结构中添加了一个颜色字段,这将有助于更好地呈现效果。

顶点着色器几乎和以前一样,我只是将法线向量作为附加信息传递给 "v2g "数据。

几何着色器

这一次,神奇的事情发生了!首先,你可能会注意到,在第 60 行中,我提到在这个着色器中最多输出 12 个顶点。不过,你可能会认为这并不正确,因为我们只是为金字塔的顶端增加了一个额外的顶点。至少这一点最初让我感到困惑。问题在于,既然我们要添加新的三角形,就必须添加构成新三角形的每个新顶点。因此,由于我们要为每一条边添加一个新三角形,我们就会有 3 个新三角形,每个三角形有 3 个顶点,再加上原来网格三角形的 3 个顶点。因此,3 * 3 + 3 = 12 个新顶点。现在你可以看到,这个数字很快就会失控。

对于实际的挤压,我们需要每个三角形的中心及其法线向量。因此,在第 65 行中,我通过获取三角形点的平均位置来计算三角形的重心位置,在第 66 行中,我还通过平均每个三角形点的法向量来获取法向量。

然后,我继续进行金字塔生成。算法是这样的:

对于三角形的每个点 获取下一个点的索引 在当前点的位置添加一个顶点 在三角形的侧中心添加一个顶点,并沿面的法向量挤出,挤出量等于"_ExtrusionFactor"。 在下一点的位置添加一个顶点

这正是第一个 for 循环中发生的事情。在第 69 行中,我通过将 i 增加 1 并使用模 3 来获取下一个点的索引,这样就不会超出数组的范围。然后,在第 71-75 行中,我添加了一个新的顶点,与上面的示例完全相同,只是这次我将该顶点的颜色值设置为黑色(第 74 行)。

在第 77-81 行中,我对金字塔的点做了同样的处理,但在其位置上,我使用了计算出的原心,并在此基础上添加了法向量乘以"_ExtrusionAmount(挤出量)",从而将其向上移动。三角形顶端使用的颜色是白色,这样挤出效果会更加明显。

然后,在第 83-87 行中,我对下一个点做了完全相同的处理,同样将其涂成黑色。

由于这里我们实际上在每个循环中添加一个新的三角形,所以我添加了“triStream.RestartStrip(); ”在循环的底部。

最后,在循环之后的第 92-99 行中,我添加了原始三角形的顶点,并将其涂成黑色,仅此而已!

使用这样的着色器后,你可能会得到一个从每个三角形中挤出金字塔的网格,就像这样:

在这里插入图片描述

片段着色器

片段着色器保持不变,但我将颜色与 "i.color "相乘,这样我们分配的顶点颜色就可以看到了。

总结

我希望这是对几何着色器世界足够温和的介绍!一开始,几何着色器肯定会让人望而生畏,但在了解了它们的原理之后,一切都会变得更加清晰和有趣!我强烈建议大家多玩玩 Shaderslab 中的示例,或者摆弄一下文章中的这两个着色器,看看能实现什么效果!



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有