跳转到内容

Cg 编程/Unity/透明度

来自 Wikibooks,开放世界开放书籍
皮埃尔·奥古斯特·科特 (Pierre Auguste Cot) 的《春天》(Le Printemps),1873 年。请注意透明的服装。

本教程介绍了使用 Unity 中的 Cg 着色器对片段进行混合(即对其进行合成)。它假设您熟悉“切片”一节中讨论的前后面的概念。

更具体地说,本教程是关于渲染透明物体的,例如透明玻璃、塑料、织物等。(更严格地说,这些实际上是半透明物体,因为它们不需要完全透明。)透明物体允许我们透过它们看到;因此,它们的顏色会与它们背后的物体的顏色“混合”。

正如“可编程图形管道”一节中所述,片段着色器为每个片段计算一个 RGBA 颜色(即片段输出参数中的红色、绿色、蓝色和 alpha 分量,语义为 COLOR)(除非片段被丢弃)。然后,按照“每个片段操作”一节中所述处理这些片段。其中一项操作是混合阶段,它将片段的颜色(如片段输出参数中指定的那样)——称为“源颜色”——与帧缓冲区中已有的对应像素的颜色——称为“目标颜色”(因为混合后的颜色的“目标”是帧缓冲区)——结合起来。

混合是一个固定功能阶段,即您可以配置它,但不能对其进行编程。它的配置方式是指定一个混合方程。您可以将混合方程视为对结果 RGBA 颜色的以下定义

float4 result = SrcFactor * fragment_output + DstFactor * pixel_color;

其中 fragment_output 是片段着色器计算的 RGBA 颜色,pixel_color 是当前在帧缓冲区中的 RGBA 颜色,result 是混合后的结果,即混合阶段的输出。SrcFactorDstFactor 是可配置的 RGBA 颜色(类型为 float4),它们与片段输出颜色和像素颜色按分量进行相乘。SrcFactorDstFactor 的值在 Unity 的 ShaderLab 语法中用以下代码行指定

Blend {SrcFactor 的代码} {DstFactor 的代码}

两种因子最常见的代码总结在下表中(更多代码在Unity 的 ShaderLab 关于混合的参考中提到)

代码 结果因子 (SrcFactorDstFactor)
One float4(1.0, 1.0, 1.0, 1.0)
Zero float4(0.0, 0.0, 0.0, 0.0)
SrcColor fragment_output
SrcAlpha fragment_output.aaaa
DstColor pixel_color
DstAlpha pixel_color.aaaa
OneMinusSrcColor float4(1.0, 1.0, 1.0, 1.0) - fragment_output
OneMinusSrcAlpha float4(1.0, 1.0, 1.0, 1.0) - fragment_output.aaaa
OneMinusDstColor float4(1.0, 1.0, 1.0, 1.0) - pixel_color
OneMinusDstAlpha float4(1.0, 1.0, 1.0, 1.0) - pixel_color.aaaa

正如“向量和矩阵运算”一节中所述,pixel_color.aaaa 只是 float4(pixel_color.a, pixel_color.a, pixel_color.a, pixel_color.a) 的简写。还要注意,混合方程中所有颜色和因子的所有分量都在 0 到 1 之间进行钳位。

Alpha 混合

[编辑 | 编辑源代码]

混合方程的一个具体示例称为“Alpha 混合”。在 Unity 中,它以这种方式指定

Blend SrcAlpha OneMinusSrcAlpha

这对应于

float4 result = fragment_output.aaaa * fragment_output + (float4(1.0, 1.0, 1.0, 1.0) - fragment_output.aaaa) * pixel_color;

这使用 fragment_output 的 alpha 分量作为不透明度。也就是说,片段输出颜色越不透明,其不透明度和 alpha 分量就越大,因此结果中混合的片段输出颜色越多,帧缓冲区中混合的像素颜色越少。完全不透明的片段输出颜色(即 alpha 分量为 1)将完全替换像素颜色。

此混合方程有时被称为“覆盖”操作,即“fragment_output 覆盖 pixel_color”,因为它对应于将一层具有特定不透明度的片段输出颜色叠加在像素颜色之上。(想象一下,一层有色玻璃或有色半透明塑料放在另一个颜色的东西上面。)

由于 Alpha 混合的流行,颜色的 alpha 分量通常被称为不透明度,即使没有使用 Alpha 混合。此外,请注意,在计算机图形学中,透明度的常用正式定义是1 − 不透明度

预乘 Alpha 混合

[编辑 | 编辑源代码]

Alpha 混合有一个重要的变体:有时片段输出颜色已将 alpha 分量预乘到颜色分量中。(您可以将它视为已包含增值税的价格。)在这种情况下,alpha 不应再次乘以颜色(对应于增值税不得多次添加的规则),并且正确的混合是

Blend One OneMinusSrcAlpha

这对应于

float4 result = float4(1.0, 1.0, 1.0, 1.0) * fragment_output + (float4(1.0, 1.0, 1.0, 1.0) - fragment_output.aaaa) * pixel_color;

计算机图形学中的两个常见错误是 1) 在预乘颜色上使用标准 Alpha 混合的混合方程,以及 2) 在未预乘的颜色上使用预乘 Alpha 混合的混合方程。由这些错误引起的视觉错误可能非常微妙;通常它们表现为混合物体的变暗或变亮的轮廓。因此,在编程混合方程时,请确保您知道自己是否正在处理预乘颜色。

叠加混合

[编辑 | 编辑源代码]

混合方程的另一个示例是

Blend One One

这对应于

float4 result = float4(1.0, 1.0, 1.0, 1.0) * fragment_output + float4(1.0, 1.0, 1.0, 1.0) * pixel_color;

它只是将片段输出颜色添加到帧缓冲区中的颜色。请注意,alpha 分量根本没有使用;尽管如此,此混合方程对于许多类型的透明效果非常有用;例如,它通常用于粒子系统,当它们代表火焰或其他透明并发出光的东西时。在“独立于顺序的透明度”一节中将更详细地讨论叠加混合。

更多混合方程示例在Unity 的 ShaderLab 关于混合的参考中给出。

着色器代码

[编辑 | 编辑源代码]

这里是一个简单的着色器,它使用 Alpha 混合渲染具有 0.3 不透明度的绿色。

Shader "Cg shader using blending" {
   SubShader {
      Tags { "Queue" = "Transparent" } 
         // draw after all opaque geometry has been drawn
      Pass {
         ZWrite Off // don't write to depth buffer 
            // in order not to occlude other objects

         Blend SrcAlpha OneMinusSrcAlpha // use alpha blending

         CGPROGRAM 
 
         #pragma vertex vert 
         #pragma fragment frag
 
         float4 vert(float4 vertexPos : POSITION) : SV_POSITION 
         {
            return UnityObjectToClipPos(vertexPos);
         }
 
         float4 frag(void) : COLOR 
         {
            return float4(0.0, 1.0, 0.0, 0.3); 
               // the fourth component (alpha) is important: 
               // this is semitransparent green
         }
 
         ENDCG  
      }
   }
}

除了上面讨论过的混合方程外,只有两行需要更多解释:Tags { "Queue" = "Transparent" }ZWrite Off

ZWrite Off 禁用写入深度缓冲区。正如“每个片段操作”一节中所解释的那样,深度缓冲区保存最近片段的深度,并丢弃任何深度更大的片段。然而,在透明片段的情况下,这不是我们想要的,因为我们可以(至少有可能)透过透明片段看到。因此,透明片段不应遮挡其他片段,因此禁用写入深度缓冲区。另请参见Unity 的 ShaderLab 关于剔除和深度测试的参考.

代码行 Tags { "Queue" = "Transparent" } 指定使用此子着色器的网格将在所有不透明网格渲染完成后进行渲染。部分原因是我们禁用了写入深度缓冲区:一个后果是,即使不透明网格更远,透明片段也可能被不透明网格遮挡。为了解决这个问题,我们首先绘制所有不透明网格(在 Unity 的“不透明队列”中),然后绘制所有透明网格(在 Unity 的“透明队列”中)。网格是否被视为不透明或透明取决于其子着色器的标签,如代码行 Tags { "Queue" = "Transparent" } 中指定的那样。有关子着色器标签的更多详细信息,请参阅Unity 的 ShaderLab 关于子着色器标签的参考.

应该提到,这种使用禁用的写入深度缓冲区来渲染透明网格的策略并不总是能解决所有问题。如果片段混合的顺序无关紧要,它可以完美地工作;例如,如果片段颜色只是添加到帧缓冲区中的像素颜色,则片段混合的顺序并不重要;请参见“独立于顺序的透明度”一节。但是,对于其他混合方程,例如 Alpha 混合,结果将根据片段混合的顺序而不同。(如果您透过几乎不透明的绿色玻璃看几乎不透明的红色玻璃,您将主要看到绿色,而如果您透过几乎不透明的红色玻璃看几乎不透明的绿色玻璃,您将主要看到红色。同样,将几乎不透明的绿色混合在几乎不透明的红色之上与将几乎不透明的红色混合在几乎不透明的绿色之上将有所不同。)为了避免出现伪影,建议使用叠加混合或使用小不透明度的(预乘)Alpha 混合(在这种情况下,目标因子 DstFactor 接近 1,因此 Alpha 混合接近叠加混合)。

包含背面

[编辑 | 编辑源代码]

之前的着色器与其他物体配合得很好,但实际上它没有渲染物体的“内部”。但是,由于我们可以透过透明物体的外部,我们也应该渲染内部。如“切口”部分所述,内部可以通过使用Cull Off禁用剔除来渲染。但是,如果我们只是禁用剔除,我们可能会遇到麻烦:如上所述,透明片段的渲染顺序通常很重要,但在没有剔除的情况下,来自内部和外部的重叠三角形可能以随机顺序渲染,这会导致令人讨厌的渲染伪像。因此,我们希望确保内部(通常更远)在外部渲染之前先渲染。在 Unity 的 ShaderLab 中,这是通过指定两个通道来实现的,这两个通道按定义的顺序为同一个网格执行

Shader "Cg shader using blending" {
   SubShader {
      Tags { "Queue" = "Transparent" } 
         // draw after all opaque geometry has been drawn
      Pass {
         Cull Front // first pass renders only back faces 
             // (the "inside")
         ZWrite Off // don't write to depth buffer 
            // in order not to occlude other objects
         Blend SrcAlpha OneMinusSrcAlpha // use alpha blending

         CGPROGRAM 
 
         #pragma vertex vert 
         #pragma fragment frag
 
         float4 vert(float4 vertexPos : POSITION) : SV_POSITION 
         {
            return UnityObjectToClipPos(vertexPos);
         }
 
         float4 frag(void) : COLOR 
         {
            return float4(1.0, 0.0, 0.0, 0.3);
               // the fourth component (alpha) is important: 
               // this is semitransparent red
         }
 
         ENDCG  
      }

      Pass {
         Cull Back // second pass renders only front faces 
             // (the "outside")
         ZWrite Off // don't write to depth buffer 
            // in order not to occlude other objects
         Blend SrcAlpha OneMinusSrcAlpha // use alpha blending

         CGPROGRAM 
 
         #pragma vertex vert 
         #pragma fragment frag
 
         float4 vert(float4 vertexPos : POSITION) : SV_POSITION 
         {
            return UnityObjectToClipPos(vertexPos);
         }
 
         float4 frag(void) : COLOR 
         {
            return float4(0.0, 1.0, 0.0, 0.3);
               // the fourth component (alpha) is important: 
               // this is semitransparent green
         }
 
         ENDCG  
      }
   }
}

在这个着色器中,第一个通道使用正面剔除(使用Cull Front)来优先渲染背面(内部)。之后,第二个通道使用背面剔除(使用Cull Back)来渲染正面(外部)。这对于凸面网格(没有凹陷的封闭网格;例如球体或立方体)非常有效,并且通常是其他网格的良好近似值。

恭喜你,你完成了本教程!渲染透明物体的一个有趣之处在于它不仅仅是关于混合,还需要了解剔除和深度缓冲区。具体来说,我们已经研究了

  • 什么是混合以及如何在 Unity 中指定它。
  • 包含透明和不透明物体的场景是如何渲染的,以及如何在 Unity 中将物体分类为透明或不透明。
  • 如何渲染透明物体的内部和外部,特别是如何在 Unity 中指定两个通道。

进一步阅读

[编辑 | 编辑源代码]

如果你还想了解更多

< Cg 编程/Unity

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