Bootstrap

Unity Shader学习3:透明效果

透明效果原理

Unity中的透明效果由透明通道控制(RGBA中的A),其值为0是完全透明,为1时完全不透明。有两种方法可以实现透明效果:透明度测试(Alpha Test)透明度混合(Alpha Blend)

透明度测试(Alpha Test)

透明度测试是指通过特定的条件(通常是Alpha通道的值是否超过某个阈值)来判断片元是否透明,只有完全透明和完全不透明两种状态。完全透明时,该片元直接被舍弃,不会对颜色缓冲产生任何影响,完全不透明时,就会被当做普通的不透明物体进行处理(进行深度测试和深度写入等)。

透明度混合(Alpha Blend)

透明度混合可以得到真正的半透明效果。它会使用当前片元的透明度做为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。

透明度混合需要关闭深度写入。如果不关闭深度写入,在渲染一个半透明物体的片元时,如果片元后面有不透明物体,那么写入深度缓存后,不透明物体的片元就会被剔除,此时透过半透明物体无法看到不透明物体,这样效果就出错了。

关闭深度写入后,渲染顺序会变得尤为重要。
在这里插入图片描述
如图8.1中,若先渲染A,由于关闭了半透明物体的深度写入,此时深度缓冲中不会写入A的深度值,再渲染B的时候,由于B是不透明物体,且深度缓冲中没有值,那么B的片元则会通过深度测试,写入深度值,同时覆盖颜色缓冲中A之前渲染得到的颜色值,效果就变成了A被B遮挡。

如图8.2中,若先渲染A, 那么A的颜色会先写入颜色缓冲,再渲染B,B片元的颜色值和颜色缓冲的值混合时,混合后的效果会反过来,看起来像B覆盖在A上。

可以看出,渲染含有半透明物体的场景分为两个步骤:
a. 先渲染所有不透明物体,开启深度测试和深度写入。
b. 将半透明物体按照他们距离摄像机的远近进行排序,并按照从后往前的顺序渲染这些半透明物体,开启深度测试,但关闭深度写入。

这里有个隐患:按照离摄像机的远近排序物品不能完全保证渲染片元的顺序,比如如下图所示的互相交叠的情况。
在这里插入图片描述
为了减少错误的排序,我们可以尽可能让模型是凸面体,或者将复杂的模型拆分成可以独立排序的多个子模型等。如果不想分隔网格,可以试着让透明通道更加柔和(?这啥意思),使穿插看起来不是那么明显。

Unity Shader 的渲染顺序

Unity 为了解决渲染顺序的问题提供了渲染队列(render queue) 这一解决方案。SubShade 的 Queue 标签来决定模型将归于哪个渲染队列。队列索引号越小越先渲染。
队列索引号越小越先渲染

Unity Shader 实现透明度测试

透明度测试中重点的代码如下,其含义是若参数值小于零,则该片元被舍弃,不对颜色缓冲产生任何影响。

clip(nAlpha - nCutOff)

完整代码如下:

// 透明度测试:判断片元的透明度是否满足条件(比如小于某个阈值),若不满足则舍弃,该片元不会对颜色缓冲产生任何影响,若满足,则按照不透明物体的处理方式来处理(进行深度测试,深度写入等),所以透明度测试不需要关闭深度写入
// 透明度混合:它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合得到新的颜色。透明度混合关闭了深度写入,但没有关闭深度测试,深度缓冲的值是只读的,保证能够遮挡出于不透明物体后面的透明物体。
Shader "Custom/Chapter8-AlphaTest"
{
    Properties{
        _Color("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex("Main Tex", 2D) = "white" {}
        _Cutoff("Alpha Cutoff", Range(0, 1)) = 0.5  // 决定片元是否显示的透明度阈值
    }

    SubShader{
        
        // AlphaTest是Unity中透明度测试使用的渲染队列(在所有不透明物体(渲染队列为Geometry)渲染之后)
        // IgnoreProjector设为true可以让该shader不受投影器(Projector)的影响
        // RenderType标签可以让Unity把这个shader归入到提前定义的组(这里就是TransparentCutout组中),以指明改shader是一个实用了透明度测试的shader
        // 以上太长不看:通常使用了透明度测试的shader都应该在SubShader中设置这三个标签
        Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}

        Pass{
            Tags {"LightMode" = "ForwardBase"}

            CGPROGRAM
            
            #include "Lighting.cginc"

            #pragma vertex vert 
            #pragma fragment frag 

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Cutoff;

            struct a2v {
                float4 pos : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            v2f vert(a2v i)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(i.pos);
                o.worldPos = mul(unity_ObjectToWorld, i.pos).xyz;
                o.worldNormal = normalize(UnityObjectToWorldNormal(i.normal));
                o.uv = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float3 worldLight = normalize(UnityWorldSpaceLightDir(i.pos));
                fixed4 texColor = tex2D(_MainTex, i.uv);

                // if (texColor.a < _Cutoff)
                // {
                //     discard;
                // }
                // or 
                clip(texColor.a - _Cutoff);     // 参数值小于0,则该片元被舍弃

                fixed3 albedo = texColor.rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldLight, i.worldNormal));

                return fixed4((ambient + diffuse), 1);
            }

            ENDCG
        }
    }

    // 这里如果设置成diffuse,透明处会有阴影,为何?
    Fallback "Transparent/Cutout/VertexLit"
}

效果如下:
请添加图片描述
透明度测试得到的效果很极端——要么完全透明,要么完全不透明。如果想要更柔滑的透明效果,可以使用透明度混合。

Unity Shader 实现透明度混合

为了进行透明度混合,我们需要使用 Unity 提供的混合命令:Blend
在这里插入图片描述
混合命令进行的操作是,将片元的颜色值和颜色缓冲中的颜色值分别乘以不同的混合因子后再相加,得到新的颜色值写入颜色缓冲。
D s t C o l o r n e w = S r c F a c t o r ⋅ S r c C o l o r + D s t F a c t o r ⋅ D s t C o l o r o l d DstColor_{new} = SrcFactor · SrcColor + DstFactor · DstColor_{old} DstColornew=SrcFactorSrcColor+DstFactorDstColorold

具体代码如下:

Shader "Custom/Chapter8-AlphaBlend"
{
    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"}

            ZWrite Off // 关闭深度写入后无法进行像素级别的深度排序,在模型网格之间有相互交叉的结构是不能得到正确的半透明效果
            Blend SrcAlpha OneMinusSrcAlpha     // 开启混合并设置混合因子

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag 
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            fixed4 _MainTex_ST;
            float _AlphaScale;

            struct a2v {
                float4 pos : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            v2f vert(a2v i)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(i.pos);
                o.worldPos = mul(unity_ObjectToWorld, i.pos).xyz;
                o.worldNormal = normalize(UnityObjectToWorldNormal(i.normal));
                o.uv = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target 
            {
                fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.pos));
                fixed4 texColor = tex2D(_MainTex, i.uv);
                fixed3 albedo = texColor.rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldLight, i.worldNormal));

                return fixed4((ambient + diffuse), texColor.a * _AlphaScale);   // 只有打开了混合,这里设置alpha通道才有意义,否则透明度值不会对片元的透明效果有任何影响
            }


            ENDCG

        }
    }

    Fallback "Transparent/VertexLit"
}

最后效果如下:
请添加图片描述

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

上述的 AlphaBlend 有一个问题,由于关闭了深度写入,缺少像素级的深度缓冲值,物体自身发生交叠时,前后的片元颜色会混合在一起。在这里插入图片描述
一种解决方式是使用两个 Pass来渲染模型。第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中。第二个Pass进行正常的透明度混合。由于上一个Pass已经得到了逐像素的深度信息,第二个Pass就可以按照像素级别的深度排序结果进行透明渲染。这种做法的缺点是额外的Pass会对性能造成一定的影响,但可以让最终效果的模型内部之间不会有半透明效果。

代码的修改与之前的 Alpha Blend 几乎一样,只需要在原来的 Pass 之前增加一个 Pass:

        Pass{
            ZWrite On   // 写入深度数据
            ColorMask 0 // 用于设置颜色通道的写掩码,设为0即不输出任何颜色
        }

效果如下:
在这里插入图片描述

双面渲染的透明效果

在现实生活中,如果一个物体是透明的,意味着我们可以透过物体表面看到它的内部结构,但上述的效果没能做到这一点,这是因为默认情况下渲染引擎提出了物体背面的渲染图元。要得到双面渲染的透明效果,我们需要使用 Cull 指令来控制剔除哪个面的渲染图元

Cull Back | Front | Off

设置为Back,那么背对摄像机的图元不会被渲染,设置为Front,正对摄像机的图元不会被渲染,设置为Off,所有的图元都会被渲染。

透明度测试的双面渲染

Shader的修改在AlphaTest的基础上只在Pass中增加了一行:

        Pass{
            Tags{"LightMode" = "ForwardBase"}

            Cull off    // 比起AlphaTest只添加了这一行代码实现双面渲染

效果如下:
在这里插入图片描述

透明度混合的双面渲染

由于透明度混合需要关闭深度写入,所以要保证背向摄像机的图元永远在面向摄像机的图元之前被渲染。实现这种效果的方式是设置两个Pass,第一个Pass剔除正面,第二个Pass剔除背面。

具体代码如下:

Shader "Custom/Chapter8-AlphaBlendBothSided"
{
    Properties{
        _Color("Color Tint", Color) = (1, 1, 1, 1)
        _AlphaScale("Alpha Scale", Range(0, 1)) = 0.5
        _MainTex("Main Tex", 2D) = "white" {}
    }

    SubShader{
        Tags{"RenderType" = "TransParent" "Queue" = "Transparent" "IgnoreProjector" = "True"}

        Pass{
            Tags{"LightMode" = "ForwardBase"}

            ZWrite Off 
            Blend SrcAlpha OneMinusSrcAlpha

            Cull Front 

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag 
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            fixed4 _MainTex_ST;
            float _AlphaScale;

            struct a2v {
                float4 pos : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            v2f vert(a2v i)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(i.pos);
                o.worldPos = mul(unity_ObjectToWorld, i.pos).xyz;
                o.worldNormal = normalize(UnityObjectToWorldNormal(i.normal));
                o.uv = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target 
            {
                fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.pos));
                fixed4 texColor = tex2D(_MainTex, i.uv);
                fixed3 albedo = texColor.rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldLight, i.worldNormal));

                return fixed4((ambient + diffuse), texColor.a * _AlphaScale);
            }
            ENDCG
        }

        Pass{
            Tags{"LightMode" = "ForwardBase"}

            ZWrite Off 
            Blend SrcAlpha OneMinusSrcAlpha

            Cull Back 

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag 
            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            fixed4 _MainTex_ST;
            float _AlphaScale;

            struct a2v {
                float4 pos : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

            v2f vert(a2v i)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(i.pos);
                o.worldPos = mul(unity_ObjectToWorld, i.pos).xyz;
                o.worldNormal = normalize(UnityObjectToWorldNormal(i.normal));
                o.uv = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target 
            {
                fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.pos));
                fixed4 texColor = tex2D(_MainTex, i.uv);
                fixed3 albedo = texColor.rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(worldLight, i.worldNormal));

                return fixed4((ambient + diffuse), texColor.a * _AlphaScale);
            }
            ENDCG
        }
    }
    Fallback "Transparent/VertexLit"
}

效果如下:
在这里插入图片描述

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;