返回
Featured image of post Shader入门精要-透明效果

Shader入门精要-透明效果

笔记摘自书籍《Shader入门精要》

前言

Unity中通常使用两种方法来实现透明 :透明度测试(AlphaTest)透明度混合(AlphaBlend)。前者往往无法实现真正的半透明效果。

  • 透明度测试:它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其他不透明物体最大的不同就是它会根据透明度来舍弃一些片元。虽然简单,但是它产生的效果也很极端,要么完全透明,即看不到,要么完全不透明。
  • 透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。需要注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着,当使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲中的深度值,如果它的深度值距离摄像机更远,那么就不会再进行混合操作了。这一点决定了,当一个不透明物体出现在一个透明物体的前面,而我们先渲染了不透明物体,它仍然可以正常地遮挡住透明物体。也就是说,对于透明度混合来说,深度缓冲是只读的。

为什么渲染顺序很重要

前面说过,对于透明度混合技术,需要关闭深度写入,此时我们就需要小心处理透明物体的渲染顺序。

为什么需要关闭深度写入

如果不关闭深度写入,一个半透明物体表面背后的表面本来是可以透过它被我们看到的,但是由于深度测试时判断结果时该半透明度物体表面距离摄像机更近,导致后面的表面将会被剔除,我们也就无法透过半透明物体看到后面的物体了。

关闭深度写入会发生什么?

在关闭深度写入之后,我们先来看一下它可能会出现什么错误。假设我们要渲染两个物体,一个是半透明的物体A,一个是不透明的物体B。我们假设A在B的前面(A离摄像机更近)

  • 第一种情况,我们先渲染B,再渲染A。那么由于不透明物体开启了深度测试和深度检验。然后因为B是先渲染的,所以B的数据会被写入到深度缓冲,然后当我们再渲染A的时候,我们会提取深度缓冲中的数据,然后和A进行透明度混合。这样是正确的~
  • 第二种情况,我们先渲染A,再渲染B。由于我们对半透明的物体关闭了深度写入,因此当我们渲染A的时候,是不会写入深度缓冲的,因此B就会覆盖深度写入。然而B其实应该在A的后面,但是视觉效果来看,B却出现在了A的前面。这就是问题所在!

这是不透明的物体和半透明物体之间的联系。那么两个物体都是半透明物体呢?假设我们有两个物体A和B,A在B的前面(离摄像机更近),并且两者都是半透明物体。

  • 第一种情况,先渲染B,再渲染A,B的颜色会被写入颜色缓冲(和深度缓冲很像,可以理解为当前颜色),这样A会正确的获得颜色缓冲中的B的颜色数据,然后正确的混合。
  • 第二种情况,先渲染A,再渲染B,A的数据会被先写入颜色缓冲,然后渲染B的时候,会提取A的数据,和B进行混合。这样最终结果看起来像是B在A的前面。这就是错误的结果。

渲染引擎如何处理透明度混合技术

  1. 先渲染所有的不透明物体,并开启他们的深度写入和深度测试。
  2. 把半透明物体按他们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启他们的深度测试,但关闭深度写入。

引擎无法处理的问题

在上图中,由于3个物体相互重叠,我们不可能的得到一个正确的排序顺序。这时候,我们可以选择把物体拆分成两个部分,然后再进行正确的排序。

但即便我们通过分割的方法解决了循环覆盖的问题,还是会有其他的情况来捣乱。

一个物体的网格结构往往占据了空间中的某一块区域,也就是说,这个网格上每一个点的深度值可能都不一样,我们选择哪个深度值来作物整个物体的深度值和其他物体进行排序呢?不幸的是基于上图的情况,无论我们选择哪个点,排序的结果都是A物体在B物体前面。这种问题解决方案通常也是分割网格。

UnityShader的渲染顺序

Unity通过一组Queue标签来决定模型归于哪个渲染队列,队列由整数索引表示,索引号越小越先被渲染。

如果我们希望用RenderQueue来控制物体的渲染顺序,我们需要在着色器代码中指出。

SubShader{
    Tags{"Queue"="Transparent"}
    Pass{
        ZWrite Off
        //...other code
    }
}

当然,除了Tags,我们还要使用ZWrite Off指令来关闭了深度写入。

透明度测试

前面我们已经知道了透明度测试的工作原理。只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理啊,也不会对颜色缓冲产生任何影响。

通常我们会在片元着色器中使用clip函数来进行透明度测试。clip是Cg中的一个函数,它的定义如下。

  • 函数:void clip(float4 x);void clip(float3 x);void clip(float2 x);void clip(float x);
  • 参数:裁剪时使用的标量或矢量条件。
  • 描述:如果给定参数任何一个分量是负数,就会舍弃当前像素的输出颜色。

透明度测试使用的纹理如下:

透明度测试代码实现见下文。

材质面板中的Alpha cutoff参数用于调整透明度测试时使用的阈值,随着Alpha cutoff参数的增大,更多的像素由于不满足透明度测试条件而被剔除。

透明度混合

这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,头民古混合需要关闭深度写入,这使我们要非常小心物体的渲染顺序。

为了进行混合,我们需要使用Unity提供的混合命令——Blend。Blend是Unity提供的设置混合模式的命令。

在本节中我们会使用图中第二种语义,即Blend SrcFactor DsFactor来进行混合。我们需要把源颜色的混合因子SrcFactor设置为SrcAlpha,而目标颜色的混合因子DstFactor设为OneMinusSrcAlpha。这样意味着经过混合后新的颜色是:(对于混合命令不理解的不用急,后文有对混合命令详细讲解,现在只要会使用就行)

DstColornew = SrcAlpha * SrcColor + ( 1 - SrcAlpha ) * DstColorold

我们可以通过调节材质面板中的Alpha cutoff参数用于控制物体的整体透明度。代码实现见下文。

开启深度写入的半透明效果

我们前面解释了由于关闭深度写入带来的各种问题,下图给出了由于排序错误而产生的错误的透明效果。这是由于我们关闭了深度写入造成的。

为了解决这一问题,我们给出了一种解决办法,使用两个Pass来渲染模型:

  • 第一个Pass开启深度写入,但是不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲区中,
  • 第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。

这样做的缺陷在于,多使用一个Pass会对性能造成一定的影响。

我们使用同上面透明度混合同样的代码,仅仅增加一个Pass用于深度写入。在该Pass的第一行,我们开启了深度写入;第二行我们使用了一个新的渲染命令——ColorMask。在ShaderLab中,ColorMask用于设置颜色通道的掩码。它的语义如下:

ColorMask RGB | A | 0 | 其他任何R、G、B、A的组合

当ColorMask设置为0时,意味着该Pass不写入仍和颜色通道,即不会输出任何颜色。这正是我们所需要的——该Pass只需要写入深度缓存即可。

具体代码实现见下文。

ShaderLab的混合命令

前面我们已经使用过Blend命令进行透明度混合,下面将详细讲解关于Blend混合命令的细节问题。

混合是逐片元的操作,而且它是不可编程的,但却是高度可配置的。也就是说我们可以设置混合时使用的运算操作、混合因子等来影响混合结果。

混合等式和参数

参数

当片元着色器产生一个颜色的时候,可以选择与颜色缓冲区的颜色进行混合。这样一来,混合就和三个操作数有关:

  • 源颜色(source color)

    我们通常用S表示,指的是由片元着色器产生的颜色值;

  • 目标颜色(destination color)

    我们通常用D表示,指的是从颜色缓冲区中读取到颜色值;

  • 输出颜色

    对于他们输出产生的颜色我们通常使用O表示,它会重新写入到颜色缓冲区中。

等式

现在我们已经知道了操作数,想要得到输出颜色O,就必须使用要给等式来计算。我们把这个等式称之为混合等式

在进行混合时,我们需要使用两个混合等式:一个用于RGB通道,一个用于A通道。由于混合一个通道时需要两个因子**(S与D)**,那么混合两个通道就需要四个因子。下图展示了两种情况下使用的等式:使用同样因子混合RGB通道与A通道,使用不同的因子分别混合RGB通道与A通道。

在默认情况下,混合等式使用的操作都是加操作(我们也可以使用其他操作),下面展示了使用表中第一中方式进行加法混合时使用的混合公式:

Orgb = SrcFactor * Srgb + DstFactor * Drgb

Oa = SrcFactor * Sa + DstFactor * Da

因子

那么这些混合因子可以有哪些值呢,下图给出了ShaderLab中支持的几种混合因子

举个例子

当我们想使用不同的因子分别混合RGB通道与A通道,且想要在混合后,输出的颜色透明度值就是源颜色的透明度,这时候该如何做呢

  1. 选择对应的等式,即第二种等式:Blend SrcFactor DstFactor, SrcFactorA DstFactorA
  2. 选择对应的因子套入等式,即:Blend SrcAlpha OneMinusSrcAlpha, One Zero

混合操作

前面说过默认的混合等式都是使用的加法,我们可以使用Shader Lab的 BlendOp BlendOperation 命令,即混合操作命令,来修改为减法、乘法等。

例如我们想使用减法透明度混合,就可以这样写:

BlendOp Sub
Blend SrcAlpha OneMinusSrcAlpha

混合操作命令通常时与混合因子一起使用的,但使用MinMax混合操作时,混合因子实际上不起任何作用,他们仅会判断原始的原颜色和目的颜色之间的比较结果。

常见的混合类型

通过混合操作和混合因子命令的组合,我们可以得到一些类似于PS软件中混合模式的混合效果:

双面渲染

显示里如果一个物体时透明的,那么我们应该可以透过它看到它内部的结构,这一渲染现象我们称之为双面渲染。如果我们想要得到双面渲染的效果,可以使用 Cull 命令来控制需要剔除哪个面的渲染图元。在Unity中,Cull 指令的语法如下:

Cull Back | Front | Off

如果设置为Back,那么背对摄像机的渲染图元就不会被渲染(这是默认情况的剔除状态)。

如果设置为Front,那么朝向摄像机的渲染图元就不会被渲染。

如果设置为Off,就会关闭剔除功能,那么所有图元都会被渲染,但由于这是需要渲染的图元数量会成倍增加,因此性能大打折扣。

透明度测试双面渲染

这里我们使用了透明度测试使用的代码,增加了一行Cull Off命令用于关闭双面渲染。代码实现见下文。

透明度混合双面渲染

这里我们使用了透明度混合时使用的代码,做了两点改动:

  1. 我们复制了一份原本Pass,并在该Pass中使用 Cull 命令剔除了正面渲染;
  2. 在原有Pass中使用 Cull 命令剔除了背面渲染。

代码实现见下文。

代码实现

透明测试

Shader "Unity Shaders Book/Chapter 8/Alpha Test"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" { }
        // 透明度控制
        _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
    }
    SubShader
    {
        //		渲染队列 = 透明度测试	忽略投影 = Ture				渲染类型 = 透明物体
        Tags { "Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout" }
        
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _Cutoff;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
                float2 uv: TEXCOORD2;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                
                // 透明度测试
                clip(texColor.a - _Cutoff);
                /*
                // 等于
					if ((texColor.a - _Cutoff) < 0.0)
					{
						discard;
					}
                */
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                return fixed4(ambient + diffuse, 1.0);
            }            
            ENDCG            
        }
    }
    FallBack "Transparent/Cutout/VertexLit"
}

透明度混合

Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" { }
		// 透明度控制
        _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
    }
    SubShader
    {
		//		渲染队列 = 透明度混合		忽略投影 = Ture				渲染类型 = 透明物体
        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }
        
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
			// 关闭深度写入
            ZWrite Off
			// 混合因子设置
            Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaScale;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
                float2 uv: TEXCOORD2;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
				// 用属性参数控制透明度
                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
            }
            
            ENDCG
            
        }
    }
    FallBack "Transparent/VertexLit"
}

开启深度写入的半透明效果

        //		渲染队列 = 透明度混合		忽略投影 = Ture				渲染类型 = 透明物体
        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }
        
        // 添加额外的Pass,仅用于渲染到深度缓冲区
        Pass
        {
            // 开启深度写入
            ZWrite On
            // 用于控制Pass不写入任何颜色通道
            ColorMask 0
        }
        
        Pass
        {
        // 与透明度混合同样的Pass
        }

透明度测试双面渲染

        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            
            // 关闭渲染剔除
            Cull Off
            
            //后面代码同透明度测试相同

透明度混合双面渲染

Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" { }
        _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
    }
    SubShader
    {
        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }
        
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            
            // 第一个Pass只渲染背面
            Cull Front
            
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaScale;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
                float2 uv: TEXCOORD2;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
            }
            
            ENDCG
            
        }
        
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            
            // 第一个Pass只渲染前面
            Cull Back
            
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Lighting.cginc"
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaScale;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float3 worldNormal: TEXCOORD0;
                float3 worldPos: TEXCOORD1;
                float2 uv: TEXCOORD2;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed4 texColor = tex2D(_MainTex, i.uv);
                
                fixed3 albedo = texColor.rgb * _Color.rgb;
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
                
                return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
            }
            
            ENDCG
            
        }
    }
    FallBack "Transparent/VertexLit"
}