透明渲染顺序及透明效果的处理

不透明物体的渲染顺序

alt 在Unity中为避免被遮挡看不见的部分重复Overdraw浪费性能,需要进行深度测试来剔除隐藏面(消隐)。为此不透明物体采用由近到远的绘制顺序一边写入更新深度缓存一边做深度测试(近的物体深度缓存中的值小),在绘制较远物体时,如果某个片段比已有深度缓存中的值大,说明前面有物体遮挡,需要丢弃这个被遮挡的片段。

alt

半透明物体的渲染顺序

如果同样采用不透明物体的渲染方式会产生第一个问题:半透明和不透明物体的渲染顺序

  • 半透明物体在不透明物体前,由不透明物体的重叠区域会被当作是被遮挡的无效区域进行剔除,正常应该保留进行混合。

alt

  • 不透明物体在半透明物体前,不透明物体后的透明物体会被剔除,正常。

alt

第一种情况的本质原因是不透明物体的重叠区域因为深度测试到前方有透明物体于是把自己剔除了,解决错误剔除有几种可能的办法可以解决:

  • 可以关闭不透明物体B的深度测试或者让其深度测试总为通过,但这种方式仅适用于B前面没有别的不透明物体,否则B会遮盖住前方的物体,不可取,那么是否可以让B检测前面的物体是不透明就不关闭深度测试呢,实际上不可以,只有在渲染的最后Blending阶段才能知道一个物体是不是半透明的,在深度测试阶段无法知道。
  • 正确的方式应该是从半透明物体A入手,不将半透明物体B的深度值写入深度缓存,这样B也就不会知道自己前方存在A,而并不干扰B对其他不透明物体的正常深度测试。

这里需要注意,半透明物体关闭了深度写入但并没有关闭深度测试,当其被不透明物体遮住时还是要正常剔除。

alt

  • 现在关闭了半透明物体A的深度写入,B的重叠部分不会被剔除了,但如果还是从近到远的绘制顺序一起绘制不透明和半透明物体,如图不透明物体B会把A盖住(B不知道自己前面有A),这时如果由远到近进行绘制,就可以解决半透明物体在不透明物体前面的情况。

到这里透明物体A在前,不透明B在后的情况已经正确,但回过头来看不透明B在前的情况:

alt

在对A进行深度测试的时候,B还没有写入深度缓存,所以A不知道自己前面有B,自己不会被剔除,虽然最后效果是正确的,但本该剔除的片元错误的保留会造成浪费。所以还是应该从近到远进行绘制,但这就造成了没有一个统一的绘制顺序。

实际上是可以归为一种顺序的,Unity指定所有不透明物体绘制完毕后,再绘制半透明物体(深度测试信息不受顺序影响)。这样就确保了透明物体A在不透明物体B之前时,由远到近正常叠加,相反时由近到远正常剔除。

alt

另一方面,对于多个半透明物体重叠绘制时,先近后远会导致近处的不透明物体被远处的盖在上面叠加。因此对于半透明物体的渲染顺序还是要指定由远到近进行渲染。

有关不透明之间和半透明物体之间绘制顺序相反可以理解为:不透明物体相当于剔除,要从近到远扣除重叠部分,而透明物体是叠加,要从远到近进行一层一层的叠加,符合正常画画的逻辑。

总结来说,半透明物体之间渲染从远往近,不透明物体之间从近往远,半透明和不透明物体之间先不透明后透明。具体流程如下:

1、先由近到远渲染所有不透明物体,并开启它们的深度测试和写入。

2、把半透明物体按它们离摄像机远近排序,然后按照从后往前的顺序渲染,开启深度测试,关闭深度写入。

透明效果的处理

目前处理透明效果的常见方式有透明测试(适用于完全透明/不透明,非0即1)和透明混合(适用于颜色的混合叠加)

透明测试

和深度测试的思路类似,剔除某些小于透明阈值的像素,实现某些位置完全透明,我们设定一个alpha阈值,当偏于不满足条件(通常是小于阈值)就直接舍弃当作全透明,满足条件的片元就直接当作不透明物体进行处理。所以注意AlphaTest并没有实现真正的半透明效果。 一般透明测试的Shader包含以下指令:

    SubShader
    {   
        //设置渲染标签
        Tags
        {
            "Queue" = "AlphaTest" ////透明测试的渲染队列
            "RenderType" = "TransparentCutout" 
            "IgnoreProjector" = "True" //透明物体不接受投影仪的投射
        }
        Pass
        {
            CGPROGRAM
            clip(textureColor.a - alphaCutoffValue);
            ENDCG

        }
    }

实例Shader(就与Lambert光照模型)

Shader "Custom/AlphaTest Transparent"
{
    Properties
    {   
        _MainTex("MainTex",2D) = "white"{}
        _AlphaTest("Alpha Test",Range(0, 1)) = 0
    }
    SubShader
    {   
        //设置渲染标签
        Tags
        {
            "Queue" = "AlphaTest"
            "RenderType" = "TrannsparentCutout"
            "IgnoreProjector" = "True"
        }

        //渲染背面
        Pass
        {   
            Tags{"LightMode" = "ForwardBase"}

            //关闭几何体剔除
            Cull Off
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _AlphaTest;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = normalize(worldNormal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {   
                //灯光法线 等同于_WorldSpaceLightPos0
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                //按照公式计算漫反射
                fixed ndotl = dot(i.worldNormal,worldLight);
                fixed4 color = tex2D(_MainTex, i.texcoord);

                //开启AlphaTest
                clip(color.a - _AlphaTest);//丢弃小于阈值的像素
                color.rgb *= _LightColor0 * saturate(ndotl);
                color.rgb += unity_AmbientSky;

                return color;
                
            }
            ENDCG
        }
       
    }

}

效果:

alt

开启透明测试:

alt

混合透明效果

将新渲染出来的图像和已存在的进行合并。

常用的混合模式有:

  • Blend Off 关闭混合处理。
  • Blend SrcFactor DstFactor 开启混合处理,自定义混合模式,将目标图像(新渲染出的图像)和源图像(已存在的图像)按照混合系数进行rgba四通道的加权混合。
  • Blend SrcFactor DstFactor,SrcFactorA DstFactorA 与上条类似,只不过分别计算rgb通道和alpha通道的混合。
  • BlendOp Op:使用逻辑操作指令指定加权后的图像像素值具体如何混合,比如Add,Sub等(默认不写为Add)。
  • BlendOp OpColor,OpAlpha: 使用逻辑操作,分别计算rgb通道和alpha通道。

注意,先用BlendOp声明逻辑操作类型,在用Blend声明混合系数。

常用混合逻辑操作:

名称 说明
Add 相加
Sub 源图像-目标图像
RevSub 目标图像 - 源图像
Min 取最小值
Max 取最大值

混合系数:

名称 说明
Zero 数值为0,目标图像或源图像完全不混合
One 数值为1,目标图像或源图像完全混合
SrcColor 用源图像的像素颜色做混合系数
DstColor 用目标图像的像素颜色做混合系数
SrcAlpha 用源图像的Alpha值做混合系数
DstAlpha 用目标图像的Alpha值做混合系数
OneMinusSrcColor 用源图像的像素颜色反相(1-)之后做混合系数
OneMinusDstColor 用目标图像的像素颜色反相(1-)之后做混合系数
OneMinusSrcAlpha 用源图像的Alpha值反相(1-)之后做混合系数
OneMinusDstAlpha 用目标图像的Alpha值反相(1-)之后做混合系数

常用混合指令(操作为Add)

名称 说明
Blend SrcAlpha OneMinusSrcAlpha 普通的透明叠加
Blend One OneMinusSrcAlpha 预乘透明
Blend One One 相加
Blend OneMinusDstColor One 柔和相加
Blend DstColor Zero 相乘
Blend DstColor SrcColor 2倍相乘

混合透明的使用办法

一般混合透明Shader包含以下指令:

SubShader
{
  Tags
  {
  	"Queue" = "Transparent" // 透明物体的单独渲染队列,使得透明物体在不透明物体之后被渲染
    "RenderType" = "Transparent" //透明物体的渲染类型,使得透明物体在不透明物体之后被渲染
    "IgnoreProjector" = "True" //透明物体不接受投影仪的投射
  }
  Pass
  {
    ZwriteOff //关闭透明物体深度写入
    Blend SrcAlpha OneMinusSrcAlpha //透明叠加
  }
}

混合透明实例

对Lambert光照模型的代码进行修改。

Shader "Custom/Blending Transparent"
{
    Properties
    {   
        _MainTex("MainTex",2D) = "white"{}
        _MainColor("MainColor",Color) = (1, 1, 1, 1)
    }
    SubShader
    {   
        //设置渲染标签
        Tags
        {
            "Queue" = "Transparent" // 透明物体的单独渲染队列,使得透明物体在不透明物体之后被渲染
            "RenderType" = "Transparent" //透明物体的渲染类型,使得透明物体在不透明物体之后被渲染
            "IgnoreProjector" = "True" //透明物体不接受投影仪的投射
        }
        Pass
        {   
            Tags{"LightMode" = "ForwardBase"}
            //设置渲染状态
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = normalize(worldNormal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {   
                //灯光法线 等同于_WorldSpaceLightPos0
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                //按照公式计算漫反射
                fixed ndotl = dot(i.worldNormal,worldLight);
                fixed4 color;
                color.rgb = _LightColor0 * tex2D(_MainTex, i.texcoord) * _MainColor.rgb * saturate(ndotl);
                color.rgb += unity_AmbientSky;

                //通过_MainColor属性的a分量控制透明度
                color.a = _MainColor.a;

                return color;
                
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

效果

alt

半透明物体的双面渲染

上节实现的透明混合效果实际上是没有背面(物体的内表面,背朝摄像机)的,这是因为渲染状态默认是Cull Back,将背面进行了剔除。

alt

可以使用Cull Off关闭背面剔除,但由于关闭了深度写入,深度缓存中没有物体的深度信息,导致背面和正面绘制顺序错误,出现了背面的图像叠加到正面的现象(背面的图片反而更亮,经过了一次透明叠加,而正面反而经过了两次)。

alt

为此,需要分两个Pass,先渲染背面再渲染正面。

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

            //开启正面剔除
            Cull front
            //设置渲染状态
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = normalize(worldNormal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {   
                //灯光法线 等同于_WorldSpaceLightPos0
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                //按照公式计算漫反射
                fixed ndotl = dot(i.worldNormal,worldLight);
                fixed4 color;
                color.rgb = _LightColor0 * tex2D(_MainTex, i.texcoord) * _MainColor.rgb * saturate(ndotl);
                color.rgb += unity_AmbientSky;

                //通过_MainColor属性的a分量控制透明度
                color.a = _MainColor.a;

                return color;
                
            }
            ENDCG
        }
        //渲染正面
        Pass
        {   
            Tags{"LightMode" = "ForwardBase"}

            //开启背面剔除
            Cull Back
            //设置渲染状态
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = normalize(worldNormal);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {   
                //灯光法线 等同于_WorldSpaceLightPos0
                float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
                worldLight = normalize(worldLight);

                //按照公式计算漫反射
                fixed ndotl = dot(i.worldNormal,worldLight);
                fixed4 color;
                color.rgb = _LightColor0 * tex2D(_MainTex, i.texcoord) * _MainColor.rgb * saturate(ndotl);
                color.rgb += unity_AmbientSky;

                //通过_MainColor属性的a分量控制透明度
                color.a = _MainColor.a;

                return color;
                
            }

正常的双面渲染:

alt

半透明物体的交叉重叠问题也可以用类似的想法解决,用两个Pass,第一个Pass写入深度信息不做渲染,第二个Pass再做渲染。