跳转到内容

Cg 编程/Unity/切面

来自维基教科书,自由的教科书
菲利波·布鲁内莱斯基于 1414-36 年绘制的佛罗伦萨大教堂圆顶切面图。

本教程涵盖了 **丢弃片段** 和 **正面和背面剔除**。本教程假设您熟悉顶点输出参数,如 “RGB 立方体”部分 中所述。

本教程的主要主题是切除三角形或片段,即使它们是正在渲染的网格的一部分。主要有两个原因:我们想透过三角形或片段(就像左边图中的屋顶,它只是部分切除),或者我们知道三角形无论如何都是不可见的;因此,我们可以通过不处理它来节省一些性能。GPU 以多种方式支持这些情况;我们将讨论其中的两种。

非常简单的切面

[编辑 | 编辑源代码]

以下着色器是一种非常简单的方法,可以切除网格的某些部分:所有具有正 坐标(即在它被建模的坐标系中;有关坐标系的详细信息,请参阅 “顶点变换”部分)的片段将被切除。

以下是代码

Shader "Cg shader using discard" {
   SubShader {
      Pass {
         Cull Off // turn off triangle culling, alternatives are:
         // Cull Back (or nothing): cull only back faces 
         // Cull Front : cull only front faces
 
         CGPROGRAM 
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         struct vertexInput {
            float4 vertex : POSITION;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posInObjectCoords : TEXCOORD0;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            output.pos = UnityObjectToClipPos(input.vertex);
            output.posInObjectCoords = input.vertex; 
 
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR 
         {
            if (input.posInObjectCoords.y > 0.0) 
            {
               discard; // drop the fragment if y coordinate > 0
            }
            return float4(0.0, 1.0, 0.0, 1.0); // green
         }
 
         ENDCG  
      }
   }
}

当您将此着色器应用于任何默认对象时,着色器将切除它们的一半。这是一种非常简单的方法,可以生成半球或开口圆柱体。

丢弃片段

[编辑 | 编辑源代码]

首先让我们关注片段着色器中的 `discard` 指令。此指令基本上只是丢弃已处理的片段。(这在早期着色语言中被称为片段“kill”;我可以理解片段更喜欢术语“discard”。)根据硬件,这可能是一种相当昂贵的技术,因为一旦有一个着色器包含 `discard` 指令,渲染性能可能会大幅下降(无论实际丢弃了多少片段,只要指令存在,就会导致一些重要优化的停用)。因此,您应尽可能避免使用此指令,特别是在遇到性能问题时。

还有一点需要注意:片段 `discard` 的条件只包含一个对象坐标。结果是,您可以以任何方式旋转和移动对象,而切除部分始终会与对象一起旋转和移动。您可能想检查一下世界空间中的切除是什么样的:更改顶点和片段着色器,以便世界坐标 用于片段 `discard` 的条件中。提示:请参阅 “在世界空间中着色”部分,了解如何将顶点转换为世界空间。

更好的切面

[编辑 | 编辑源代码]

如果您不熟悉 Unity 中的脚本编写,可以尝试以下想法来改进着色器:更改它,以便如果 坐标大于某个阈值变量,则丢弃片段。然后引入一个着色器属性,允许用户控制此阈值。提示:请参阅 “在世界空间中着色”部分,了解有关着色器属性的讨论。

如果您熟悉 Unity 中的脚本编写,可以尝试以下想法:为一个对象编写一个脚本,该脚本引用另一个球体对象,并将该球体对象的逆模型矩阵(`GetComponent(Renderer).worldToLocalMatrix`)分配(使用 `GetComponent(Renderer).sharedMaterial.SetMatrix()`)给着色器的一个 `float4x4` 统一参数。在着色器中,计算片段在世界坐标系中的位置,并将另一个球体对象的逆模型矩阵应用于片段位置。现在您有了片段在另一个球体对象局部坐标系中的位置;在这里,很容易测试片段是在球体内部还是外部,因为在这个坐标系中,默认的 Unity 球体以半径为 0.5 围绕原点居中。如果片段在另一个球体对象内部,则丢弃它。生成的脚本和着色器可以借助一个切割球体来切除任何对象表面的点,该球体可以在编辑器中像任何其他球体一样交互地操作。

正面或背面的剔除

[编辑 | 编辑源代码]

最后,着色器(更具体地说是着色器通道)包含一行 `Cull Off`。此行必须出现在 `CGPROGRAM` 之前,因为它不在 Cg 中。事实上,它是 Unity 的 ShaderLab 的一个命令,用于关闭任何三角形剔除。这是必要的,因为默认情况下,背面会像指定了 `Cull Back` 行一样被剔除。您也可以通过 `Cull Front` 指定正面剔除。背面剔除默认情况下处于活动状态的原因是,对象的内部通常是不可见的;因此,背面剔除可以通过避免光栅化这些三角形来节省相当多的性能,如下所述。当然,我们能够看到内部是因为我们已经丢弃了一些片段;因此,我们应该停用背面剔除。

剔除是如何工作的?三角形和顶点像往常一样被处理。但是,在将顶点转换为屏幕坐标的视口变换之后(请参阅 “顶点变换”部分),图形处理器会确定三角形的顶点是在屏幕上以逆时针顺序出现还是以顺时针顺序出现。根据此测试,每个三角形都被视为正面三角形或背面三角形。如果它是正面三角形,并且正面三角形的剔除处于活动状态,它将被丢弃,即它的处理停止,它不会被光栅化。类似地,如果它是背面三角形,并且背面三角形的剔除处于活动状态。否则,该三角形将像往常一样被处理。

我们可以将剔除用于什么?一个应用是为正面和背面使用不同的着色器,即为对象的外部和内部使用不同的着色器。以下着色器使用两个通道。在第一个通道中,只剔除正面,剩余的正面以红色渲染(如果片段没有被丢弃)。第二个通道只剔除背面,并以绿色渲染剩余的背面。

Shader "Cg shader with two passes using discard" {
   SubShader {

      // first pass (is executed before the second pass)
      Pass {
         Cull Front // cull only front faces
 
         CGPROGRAM 
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         struct vertexInput {
            float4 vertex : POSITION;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posInObjectCoords : TEXCOORD0;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            output.pos = UnityObjectToClipPos(input.vertex);
            output.posInObjectCoords = input.vertex; 
 
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR 
         {
            if (input.posInObjectCoords.y > 0.0) 
            {
               discard; // drop the fragment if y coordinate > 0
            }
            return float4(1.0, 0.0, 0.0, 1.0); // red
         }
 
         ENDCG  
      }

      // second pass (is executed after the first pass)
      Pass {
         Cull Back // cull only back faces

         CGPROGRAM 
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         struct vertexInput {
            float4 vertex : POSITION;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posInObjectCoords : TEXCOORD0;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            output.pos = UnityObjectToClipPos(input.vertex);
            output.posInObjectCoords = input.vertex; 
 
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR 
         {
            if (input.posInObjectCoords.y > 0.0) 
            {
               discard; // drop the fragment if y coordinate > 0
            }
            return float4(0.0, 1.0, 0.0, 1.0); // green
         }
 
         ENDCG  
      }
   }
}

请记住,Unity 着色器中只执行一个子着色器(取决于哪个子着色器最适合 GPU 的功能),但该子着色器的所有通道都会被执行。

在许多 GPU 上,使用具有语义 `VFACE` 的片段输入参数在 Cg 中区分正面和背面的一种更有效的方法;请参阅 Unity 的着色器语义文档。但是,并非所有 GPU 都支持此功能。

恭喜您完成了另一个教程。(如果您尝试过其中一项作业:干得好!我还没有。)我们已经了解了

  • 如何丢弃片段。
  • 如何指定正面和背面的剔除。
  • 如何使用剔除和两个通道来为网格的内部和外部使用不同的着色器。

进一步阅读

[编辑 | 编辑源代码]

如果您想了解更多

< Cg 编程/Unity

除非另有说明,本页面所有示例源代码均属于公有领域。
华夏公益教科书