Cg Programming/Unity/Transparency透明度

来源:互联网 发布:魔兽争霸3 mac 缩小 编辑:程序博客网 时间:2024/04/29 07:52

本章介绍了在Unity中使用Cg shader进行图元的混合(也就是说对它们进行组合)。本章假设你熟悉在”裁剪“中讨论的正面和背面概念。
特别地是,本教程是关于透明物体的渲染,比如透明的草、塑料、纤维等等。(更严格地说,实际上存在着半透明物体,因为它们不需要完全透明。)透明物体允许我们看透它们;它们的颜色“混合”着在它们之后的颜色。

混合

就像在“可编程图形管线”这章提到的一样,片元着色器计算每一个片元(除非片元被丢弃了)的RGBA颜色(也就是红、绿、蓝以及在片元输出参数中以语义COLOR标识的alpha组件)。片元随即就像在章节“逐片元操作”讨论的一样被处理:其中的一个操作就是混合阶段,它将片元的颜色(在片元输出函数中指定),称为“源颜色”和已经在帧缓存中相应像素的颜色,称为“目标颜色”(因为最终混合颜色的“目标”就是帧缓存)组合在一起。 混合是一个固定函数阶段,也就是说,你可以配置它但不可以对它编程。它配置的方式就是指定一个混合等式。你可以将混合等式作为最终RGBA颜色的定义:
float4 result = SrcFactor * fragment_output + DstFactor * pixel_color;
片元输出就是由片元着色器计算的RGBA颜色,像素颜色就是当前在帧缓存中的RGBA颜色,结果就是混合后的结果,也就是混合阶段的输出。
SrcFactor和DstFactor是可配置的RGBA颜色(float4类型),并且跟片元输出颜色和像素颜色的各个分量相乘。
SrcFactor和DstFactor的值在Unity的ShaderLab中可以用下面语法指定:
Blend {code for SrcFactor} {code for DstFactor}
比较常用的两种混合因子(factors)总结在如下的表格中(更多的代码在Unity的ShaderLab混合参考中被提及):

Code Resulting Factor (SrcFactor or DstFactor)
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;

这里使用片元输出的alpha分量作为不透明度(opacity)。比如说片元输出颜色越不透明,不透明度和它的alpha分量就越大,并且越多的片元输出颜色被混合在结果中,越少的像素颜色被混合在缓存中。一个完美的不透明片元输出颜色(比如有一个值为1的alpha分量)会完全替代像素颜色。

这个混合等式有时被认为是一种“覆盖”运算,也就是说“片元输出在像素颜色上面”,因为它对应地在像素颜色上面放置一层片元输出颜色以及指定的不透明度。(想想在有另外一种颜色的某个物体上面有一层彩色玻璃或者彩色半透明塑料。)

考虑到alpha混合的受欢迎程度,一个颜色的alpha分量也经常被称为不透明度即使alpha混合没有使用,注意在计算机图形中透明度的一般定义就是1-opacity

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” }
// 在所有不透明几何体渲染后渲染
Pass {
ZWrite Off // 不写入深度缓存
// in order not to occlude other objects

     Blend SrcAlpha OneMinusSrcAlpha // 使用alpha混合     CGPROGRAM      #pragma vertex vert      #pragma fragment frag     float4 vert(float4 vertexPos : POSITION) : SV_POSITION      {        return mul(UNITY_MATRIX_MVP, vertexPos);     }     float4 frag(void) : COLOR      {        // 这是半透明的绿色,第4个分量 (alpha) 最重要:         return float4(0.0, 1.0, 0.0, 0.3);                     }      ENDCG    }

}
}

除了以上讨论的混合等式以外,这里只有两行需要额外解释的:Tags{"Queue" = "Transparent"}Zwrite

ZWrite Off使得写入深度缓冲失效。就像在章节“逐片元操作”中解释得一样,尝试缓冲保存了最近片元的深度以及丢弃了任何大于该深度的片元。就透明片元来说,这不是我们想要的,因为我们可以(至少是潜在的)看穿一个透明的片元。于是,透明片元不应该排除其它片元,因此写入深度缓冲是无效的。另请参阅Unity中ShaderLab关于剔除和深度测试的参考。

这行标签指定使用以上subshader的网格在所有不透明网格渲染之后渲染。部分原因是我们使得写入深度缓冲无效:一个推论就是透明片元会被不透明片元遮挡,即使不透明片元离得更远。为了解决这个问题,我们在绘制所有透明网格(Unity的“透明队列”)之前绘制所有不透明网格。一个网格是否是不透明或透明取决于它所在subshader的像如下这行Tags { "Queue" = "Transparent" }指定的标签。关于子着色器标签的更多细节可以查阅Unity ShaderLab关于子着色器标签的参考。
需要注意的是,使写入深度缓冲无效来渲染透明网格的策略并不是总能解决所有问题。如果片元混合的顺序无关紧要,那么这方法就很有效;举例来说,如果片元颜色正好加在了帧缓冲的像素颜色上面,片元混合的顺序就不重要了;可以查阅章节“顺序无关的透明度”。但是,对于其它的混合等式,也就是alpha混合,根据片元混合的顺序,结果也会有所不同。(如果你透过几乎不透明的绿色玻璃看几乎不透明的红色玻璃,你主要看到绿色的;而你将看到红色如果你透过几乎不透明的红色玻璃看几乎不透明的绿色玻璃。类似的,在几乎不透明红色上混合几乎不透明绿色跟在几乎不透明绿色上混合几乎不透明红色是不一样的。)为了避免伪像,建议使用加性混合或者有小阴影的(预乘)alpha混合(在这种情况下,目的因子DstFactor接近1并且因此alpha混合接近加性混合)。

背面包含

前面的着色器其他对象上效果很好,但是它实际上并没有渲染物体的“内部”。然而,既然我们可以从透明物体的外部看穿,我们应该渲染物体内部。就像在章节“裁剪”中讨论的一样,可以通过Cull Off使得裁剪无效来渲染物体内部。但是,如果我们只是使得裁剪无效,就会带来疑惑:如上讨论,经常更重要的是透明片元是以哪种顺序被渲染但又没被剔除,内部和外部层叠的三角形也许会以随机的顺序被渲染,而这个会导致令人讨厌的渲染假象。因此,我们要保证内部在外部之前被渲染。在Unity的ShaderLab中这种效果是通过两个通道来实现的,它们按照定义的顺序在相同的网格上执行:

Shader "Cg shader using blending" {   SubShader {      Tags { "Queue" = "Transparent" }          // 在所有不透明几何体绘制后绘制      Pass {         Cull Front //  第一个通道只渲染背面             // (the "inside")         ZWrite Off // 不写入深度缓冲             // 为了不遮挡其它物体         Blend SrcAlpha OneMinusSrcAlpha // 使用alpha混合         CGPROGRAM          #pragma vertex vert          #pragma fragment frag         float4 vert(float4 vertexPos : POSITION) : SV_POSITION          {            return mul(UNITY_MATRIX_MVP, vertexPos);         }         float4 frag(void) : COLOR          {            return float4(1.0, 0.0, 0.0, 0.3);               // 第四个分量 (alpha) 最重要:                // 这是半透明的红色         }         ENDCG        }      Pass {         Cull Back // 第二个通道只渲染前面             // (the "outside")         ZWrite Off // 不写入深度缓冲            // 为了不遮挡其它物体         Blend SrcAlpha OneMinusSrcAlpha // 使用alpha混合         CGPROGRAM          #pragma vertex vert          #pragma fragment frag         float4 vert(float4 vertexPos : POSITION) : SV_POSITION          {            return mul(UNITY_MATRIX_MVP, vertexPos);         }         float4 frag(void) : COLOR          {            //这是半透明的绿色,第四个分量 (alpha) 最重要:            return float4(0.0, 1.0, 0.0, 0.3);                        }         ENDCG        }   }}

在这个着色器中,第一个通道使用正面剔除(用指令Cull Front)来渲染背面(内部),然后第二个通道使用背面剔除(用指令Cull Back)来渲染
前面(外部)。这个在凸网格(无凹痕闭合网格;比如说球体或立方体)特别有效,并且对于其它网格是一个很好的近似。

总结

恭喜你,你又学完了一章!一件有趣的事是渲染半透明物体并不只是关于混合,它还需要裁剪和深度缓冲的知识。特别是我们已经知道了以下几点:

  • 在Unity中什么是混合以及它是如何工作的。
  • 有着透明和不透明物体的场景是怎么渲染的,以及在Unity里物体是怎么被定义为透明还是不透明的。
  • 如何去渲染一个透明物体的内部和外部,特别是如何在Unity中指定两个通道。

译者注

关于opacity的理解