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

Shader入门精要-透明效果

实现透明效果

技术美术——透明效果

Unity 中通常使用两种方法来实现透明:透明度测试 (AlphaTest)透明度混合 (AlphaBlend)

  • 透明度测试:一个片元透明度不满足条件(小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不再进行任何处理,不如写入颜色信息到 color buffer;否则,按照普通的不透明物体的处理方式来处理它,即进行 深度测试深度写入 等。透明度测试不需要关闭深度写入,它与其他不透明物体最大的不同就是它会根据透明度来舍弃一些片元,所以它产生的效果很极端,要么完全透明,要么完全不透明,不能实现半透明效果。
  • 透明度混合:使用当前片元的透明度作为混合因子,与已经存储在 color buffer 中的颜色值进行混合,得到新的颜色。透明度混合需要 关闭深度写入,也就是当前物体的深度信息不会被记录,但是 深度测试是开启的,也就是说当使用透明度混合渲染一个片元时,还是会比较当前物体的深度值与 depth buffer 中的深度值,如果当前物体的深度值距离摄像机更远,那么就不再进行混合操作。这一点决定了,一个不透明物体出现在一个透明物体的前面,先渲染了不透明物体,它可以正常的遮挡住透明物体。归根结底,对于透明度混合,depth buffer 是只可读的

渲染顺序

对于透明度混合技术,我们需要关闭深度写入,但是关闭深度写入,那我们就需要小心处理透明物体的渲染顺序。

为什么需要关闭深度写入?

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

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

假设我们要渲染两个物体,一个是半透明物体 A,一个是不透明物体 B,A 在 b 前面 ( A 离摄像机更近)

  • 一,先渲染 B,再渲染 A。因为不透明物体开启了深度测试和深度写入,所以 B 的数据会写进深度缓存里,当我们渲染 A 的时候,先提取深度缓存中的数据,然后和 A 进行透明度混合,显示结果正确。
  • 二,先渲染 A,再渲染 B。由于半透明物体关闭了深度吸入,A 的深度信息不会写入深度缓存里;当渲染 B 的时候,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 标签来决定模型属于哪个 渲染队列,队列由整数索引表示,值越小越先渲染。

名称队列索引号描述
Background1000最开始渲染的队列,通常使用该队列来渲染那些需要绘制再背景上的物体
Geometry2000默认的渲染队列,大多数物体使用这个队列。不透明物体使用该队列
AlphaTest2450需要透明度测试的物体使用这个队列。
Transparent3000在所有 Geometry 和 AlphaTest 物体渲染后,再按从后往前的顺序进行渲染,任何使用了透明度混合(例如关闭深度写入的 Shader)的物体都应该使用该队列
Overlay4000用于实现一些叠加效果,任何需要再最后渲染的物体都应该使用该队列

如果我们要控制物体的渲染顺序,需要在着色器中指出。

SubShader{
    Tags{"Queue"="Transparent"} // Tansparent 渲染队列
    Pass{
        // 变比深度写入
        ZWrite Off
        //...other code
    }
}

透明度测试

透明度测试简单粗暴,片元透明度不满足条件则被舍弃,且被舍弃的片元不会参与后面的计算,深度写入,颜色写入。再片元着色器中使用 clip 函数来进行透明度测试,定义如下。

  • 函数:void clip(float4 x); void clip(float3 x); void clip(float2 x); void clip(float x);
  • 参数:裁剪时使用的标量或矢量条件;
  • 描述:如果给定参数任何一个分量是负数,就会舍弃当前像素的输出颜色。
Shader "Unlit/AlphaTestOff"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _AlphaClip("AlphaClip",Range(0.0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "IgnoreProjector"="true" }
        // 关闭剔除效果
        Cull Off
        
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _AlphaClip;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

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

            v2f vert (appdata v)
            {
                v2f o;

                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                o.posWorld = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 halfDir = normalize(worldLightDir + worldViewDir);

                half NdotL = max(0.0, dot(worldNormal, worldLightDir));
                half NdotV = max(0.0, dot(worldNormal, halfDir));

                half4 albedo = tex2D(_MainTex, i.uv);
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo.rgb;
                half3 diffuse = _LightColor0.rgb * albedo.rgb * (NdotL * 0.5 +0.5);

                clip(albedo.a - _AlphaClip);         // 简单粗暴裁剪掉片元,被裁减片元不再参与之后的计算
                return half4(ambient + diffuse, 1.0);
            }
            ENDCG
        }
    }
}

透明度混合

可以实现真正的半透明效果。使用当前片元的透明度作为混合因子,与已经存储在颜色缓存中的颜色值进行混合,得到新的颜色。但是,需要关闭深度写入,要非常小心物体的渲染顺序。为了进行混合,还需要使用 Unity 提供的混合命令 - BlendBlend 是 Unity 提供的设置混合模式的命令。

语义描述
Blend Off关闭混合
Blend SrcFactor DstFactor开启混合,设置混合因子。源颜色 (片元颜色) 乘以 SrcFactor,而目标颜色 (color buffer的颜色)乘以 DstFactor,然后把两者相加后再存入颜色缓冲中
Blend SrcFactor DstFactor, SrcFactorA DstFactorA和上面几乎一样,只是使用不同的因子来混合透明通道
BlendOp BlendOperation并非把源颜色与目标颜色简单相加后混合,而是使用 BlendOperation 对他们进行其他操作

这里使用第二种语义,即 Blend SrcFactor DsFactor 来进行混合。我们需要把源颜色的混合因子 SrcFactor 设置为 SrcAlpha,而目标颜色的混合因子 DstFactor 设为 OneMinusSrcAlpha 。这样意味着经过混合后新的颜色是:

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

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

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

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

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

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

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

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

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

Shader "Unlit/AlphaBlendOneSlide"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _Color("Color", Color) = (1,1,1,1)
        _AlphaBlend("AlphaBlend", Range(0.0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" "IngoreProjector"="True" }

        Pass
        {
            // 开启深度写入
            ZWrite On
            // 不写入任何颜色信息到 color buffer
            ColorMask 0
        }

        Pass
        {
            Tags {"LightModel"="ForwardBase"}

            // 关闭深度写入,也就是不将深度值存入深度buffer中
            ZWrite Off
            // 混合方式
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _AlphaBlend;
            float4 _Color;

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

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

            v2f vert (appdata v)
            {
                v2f o;
                o.posWorld = UnityObjectToClipPos(v.vertex); // 顶点从模型空间转到裁剪空间
                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // _MainTex 的缩放和平移

                o.worldNormal = UnityObjectToWorldNormal(v.normal);     // 法线从模型空间转到世界空间
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;    // 顶点位置从模型空间转到世界空间

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);                         // 归一化世界法线
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 归一化主光源方向
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));   // 归一化视角方向
                half3 halfDir = normalize(worldLightDir + worldViewDir);              // 半角方向

                half NdotL = max(0.0, dot(worldNormal, worldLightDir));               // lambert
                half NdotV = max(0.0, dot(worldNormal, halfDir));

                half4 texColor = tex2D(_MainTex, i.uv);                               // 从纹理读取漫反射系数
                
                half3 albedo = texColor.rgb * _Color.rgb;
                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;          // 环境光
                half3 diffuse = _LightColor0.rgb * albedo * (NdotL * 0.5 + 0.5);// half lambert

                return half4(ambient + diffuse, texColor.a * _AlphaBlend);
            }
            ENDCG
        }
    }
}

双面渲染

Shader "Unlit/AlphaBlendTwoSlide"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _Color("Color", Color) = (1,1,1,1)
        _Alpha("Alpha", Range(0.0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }

        Pass
        {
            Tags {"LightModel"="ForwardBase"}

            Cull Front   // 剔除正面
            ZWrite off   // 关闭深度写入
            Blend SrcAlpha OneMinusSrcAlpha // 混合方式

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float _Alpha;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

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

            v2f vert (appdata v)
            {
                v2f o;
                o.posWorld = UnityObjectToClipPos(v.vertex);

                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // _MainTex 的缩放和平移

                o.worldNormal = UnityObjectToWorldNormal(v.normal);     // 法线从模型空间转世界空间
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;    // 顶点位置从模型空间转世界空间

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);              // 归一化法线方向
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 归一化主光源方向
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));   // 归一化视角方向
                half3 halfDir = normalize(worldLightDir + worldViewDir);              // 半角方向

                half3 NdotL = max(0.0, dot(worldNormal, worldLightDir));
                
                half4 texColor = tex2D(_MainTex, i.uv);
                half3 albedo = texColor.rgb * _Color.rgb;

                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                half3 diffuse = _LightColor0.rgb * albedo * (NdotL*0.5 +0.5);

                return half4(ambient + diffuse, texColor.a * _Alpha);

            }
            ENDCG
        }

                Pass
        {
            Tags {"LightModel"="ForwardBase"}

            Cull Back    // 剔除背面
            ZWrite off   // 关闭深度写入
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float _Alpha;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

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

            v2f vert (appdata v)
            {
                v2f o;
                o.posWorld = UnityObjectToClipPos(v.vertex);

                o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // _MainTex 的缩放和平移

                o.worldNormal = UnityObjectToWorldNormal(v.normal);     // 法线从模型空间转世界空间
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;    // 顶点位置从模型空间转世界空间

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldNormal = normalize(i.worldNormal);              // 归一化法线方向
                half3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 归一化主光源方向
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));   // 归一化视角方向
                half3 halfDir = normalize(worldLightDir + worldViewDir);              // 半角方向

                half3 NdotL = max(0.0, dot(worldNormal, worldLightDir));
                
                half4 texColor = tex2D(_MainTex, i.uv);
                half3 albedo = texColor.rgb * _Color.rgb;

                half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                half3 diffuse = _LightColor0.rgb * albedo * (NdotL*0.5 +0.5);

                return half4(ambient + diffuse, texColor.a * _Alpha);

            }
            ENDCG
        }
    }
}

总结

关于半透明物体的渲染解决方法,多添加一个 Pass

单面渲染,第一个 Pass 开启深度写入 ZWrite On 且不要写入任何信息(颜色、透明度) ColorMask 0;第二个 Pass 关闭深度写入 ZWrite Off ,设置好混合方式 Blend SrcAlpha OneMinusSrcAlpha

双面渲染,两个 Pass 同时关闭深度写入 ZWrite On,设置好混合方式 Blend SraAlpha OneMinusSrcAlpha,第一个 Pass 剔除正面 Cull Front,第二个 Pass 剔除背面 Cull Back

调整透明度,从常规纹理那儿读取纹理的 Alpha 值

half4 texColor = tex2D(_MainTex, i.uv);  // 纹理采样      
half3 albedo = texColor.rgb * _Color.rgb;
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;          // 环境光
half3 diffuse = _LightColor0.rgb * albedo * (NdotL * 0.5 + 0.5);// half lambert

return half4(ambient + diffuse, texColor.a * _AlphaBlend);