跳转到内容

GLSL 编程/Unity/透明度

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

本教程介绍了使用 Unity 中的 GLSL 着色器对片段进行混合(即合成它们)。它假定您熟悉前面和后面的概念,如“剖视图”部分中所述。

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

“OpenGL ES 2.0 管道”部分所述,片段着色器为每个片段计算一个 RGBA 颜色(即在gl_FragColor中为红色、绿色、蓝色和 alpha 分量)(除非片段被丢弃)。然后,如“每个片段的操作”部分中所述,对这些片段进行处理。其中一个操作是混合阶段,它将片段的颜色(如gl_FragColor中指定的)与已在帧缓冲区中的相应像素的颜色组合起来,称为“源颜色”(因为混合后的颜色的“目标”是帧缓冲区),称为“目标颜色”。

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

vec4 result = SrcFactor * gl_FragColor + DstFactor * pixel_color;

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

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

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

代码 结果因子 (SrcFactorDstFactor)
One vec4(1.0)
Zero vec4(0.0)
SrcColor gl_FragColor
SrcAlpha vec4(gl_FragColor.a)
DstColor pixel_color
DstAlpha vec4(pixel_color.a)
OneMinusSrcColor vec4(1.0) - gl_FragColor
OneMinusSrcAlpha vec4(1.0 - gl_FragColor.a)
OneMinusDstColor vec4(1.0) - pixel_color
OneMinusDstAlpha vec4(1.0 - pixel_color.a)

“向量和矩阵操作”部分中所述,vec4(1.0)只是vec4(1.0, 1.0, 1.0, 1.0)的简写。另外请注意,混合方程式中所有颜色和因子的所有分量都在 0 到 1 之间。

Alpha 混合

[编辑 | 编辑源代码]

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

Blend SrcAlpha OneMinusSrcAlpha

对应于

vec4 result = vec4(gl_FragColor.a) * gl_FragColor + vec4(1.0 - gl_FragColor.a) * pixel_color;

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

此混合方程式有时被称为“over”操作,即“gl_FragColor over pixel_color”,因为它对应于在像素颜色之上放置具有特定不透明度的片段颜色层。(想想一层彩色玻璃或彩色半透明塑料放在另一颜色的东西之上。)

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

预乘 Alpha 混合

[编辑 | 编辑源代码]

alpha 混合有一个重要的变体:有时片段颜色已将其 alpha 分量预乘到颜色分量中。(您可能会将其视为已包含增值税的价格。)在这种情况下,alpha 不应再次相乘(增值税不应再次添加),正确的混合是

Blend One OneMinusSrcAlpha

对应于

vec4 result = vec4(1.0) * gl_FragColor + vec4(1.0 - gl_FragColor.a) * pixel_color;

叠加混合

[编辑 | 编辑源代码]

混合方程式的另一个示例是

Blend One One

这对应于

vec4 result = vec4(1.0) * gl_FragColor + vec4(1.0) * pixel_color;

这只是将片段颜色添加到帧缓冲区中的颜色。请注意,alpha 分量根本没有使用;尽管如此,此混合方程式对于许多类型的透明效果非常有用;例如,它通常用于粒子系统,当它们表示火或其他透明且发光的物体时。在“与顺序无关的透明度”部分中更详细地讨论了叠加混合。

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

着色器代码

[编辑 | 编辑源代码]

这是一个简单的着色器,它使用 alpha 混合来渲染不透明度为 0.3 的绿色。

Shader "GLSL 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

         GLSLPROGRAM
               
         #ifdef VERTEX
         
         void main()
         {
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif

         #ifdef FRAGMENT
         
         void main()
         {
            gl_FragColor = vec4(0.0, 1.0, 0.0, 0.3); 
               // the fourth component (alpha) is important: 
               // this is semitransparent green
         }
         
         #endif

         ENDGLSL
      }
   }
}

除了上面讨论的混合方程式之外,只有两行需要更多解释: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 "GLSL shader using blending (including back faces)" {
   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

         GLSLPROGRAM
               
         #ifdef VERTEX
         
         void main()
         {
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif


         #ifdef FRAGMENT
         
         void main()
         {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 0.3);
               // the fourth component (alpha) is important: 
               // this is semitransparent red
         }
         
         #endif

         ENDGLSL
      }

      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 
            // standard blend equation "source over destination"

         GLSLPROGRAM
               
         #ifdef VERTEX
         
         void main()
         {
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif


         #ifdef FRAGMENT
         
         void main()
         {
            gl_FragColor = vec4(0.0, 1.0, 0.0, 0.3);
               // fourth component (alpha) is important: 
               // this is semitransparent green
         }
         
         #endif

         ENDGLSL
      }
   }
}

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

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

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

进一步阅读

[编辑 | 编辑源代码]

如果你还想了解更多

  • 关于 OpenGL 管道,你应该阅读“OpenGL ES 2.0 管道”部分。
  • 关于 OpenGL 管道中的每个片段操作(例如混合和深度测试),你应该阅读“每个片段操作”部分。
  • 关于正面剔除和背面剔除,你应该阅读“剖面图”部分。
  • 关于如何在 Unity 中指定剔除和深度缓冲区功能,你应该阅读 Unity 的 ShaderLab 关于剔除和深度测试的参考。
  • 关于如何在 Unity 中指定混合,你应该阅读 Unity 的 ShaderLab 关于混合的参考。
  • 关于 Unity 中的渲染队列,你应该阅读 Unity 的 ShaderLab 关于子着色器标签的参考。


< GLSL 编程/Unity

除非另有说明,否则本页上的所有示例源代码均归属公共领域。
华夏公益教科书