LCATod0625 发表于 2023-3-25 06:49

Unity 入门精要学习笔记 第八单元 透明效果

《Unity Shader入门精要》源代码如果没有半透明物体,那么直接用 深度缓冲(z-buffer)是没问题的,但是一旦出现透明物体,事情就没那么简单了。
通常使用两种方式来实现透明效果:第一种是 透明度测试(Alpha Test),但该种方法无法得到真正的半透明效果;另一种是 透明度混合(Alpha Blending)。
简单来说两种方法的原理如下:

[*]透明度测试:只要一个片元的透明度不满足条件(通常是小于某个阈值),我们就将其舍去,否则就按不透明物体处理,即进行深度测试、深度写入。 --- 感觉这样做效果很差
[*]透明度混合:该方法能得到真正的半透明效果。它会使用当前片元的透明值作为混合因子和 深度缓冲里的颜色值进行混合,来得到新的颜色。需要注意的是该方法会关闭深度写入,但不会关闭深度测试,也就是说参与融合的半透明物体的深度值必须必深度缓冲里的深度值小。
8.1 渲染顺序

渲染引擎一般都会对物体进行排序,再渲染。常用的方法是:
(1) 先渲染所有不透明物体,并开启它们的深度测试和深度写入。
(2) 把半透明物体按它们距离摄像机的距离进行排序,然后按照从后往前的顺序渲染这些半透明物体,开启它们的深度测试,但关闭深度写入。
还有个问题是上面第二点讲的排序,这并不是一个简单的事情,因为可能出现如下图物体相互重叠、或是遮挡一部分的情况。



Image


这种问题的解决方式一般是分割网格。同时为了减少排序我们尽可能让模型是图面体,并且考虑将复杂模型拆分成可以独立排序的子模型等情况。如果不想那么麻烦,还可以让透明通道更柔和,来使得穿插看起来不是那么明显。
8.2 Unity Shader 的渲染顺序

Unity 使用渲染队列来解决这一问题,用 SubShader 的 Quene 标签来决定改模型归于哪个渲染队列。



Image


想通过透明度测试来实现透明效果的,代码中应包含以下语句:
SubShader
{
    //如果想用透明度混合,这里的 "AlphaTest" 应当改为 "Transparent"
    Tags { "Quene" = "AlphaTest"}
    Pass
    {
      ...
    }
}
8.3 透明度测试

该方法的工作原理已经在前面提到了,通常我们会在片元着色器中使用 Clip(float4 x) 函数来尽兴透明度测试。clip 是 Cg 中的一个函数,其中 形参 x 的类型 还可以更改为 float3、float2、float1、float。它的作用是给定参数的任意分量是负数,就会舍弃当前像素的输出颜色。
首先搭建场景,右键单击 -> 3D Object -> Plane 和 Cube, 新建shader 和 material ,给 material 选择该纹理,并将材质赋给 Cube。 该节所有场景都如此搭建
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unity Shader Study/Chapter8/AlphaTest"
{
    Properties
    {
      _Color ( "Main Tint", Color) = (1, 1, 1, 1)
      _MainTex ("Main Tex", 2D) = "white" {}
      //用于决定我们调用 clip 进行透明度测试时使用的判断条件。
      _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
    }
    SubShader
    {
      Tags { "Quene"="AlphaTest" "IgnoreProjector" = "True" "RenderType"="TransparentCutout"}

[*]Queue 标签设置为 AlphaTest。Unity中透明度测试使用的渲染队列是名为AlphaTest的队列.
[*]RenderType 标签可以让Unity把这个Shader归入到提前定义的组(这里就是 TransparentCutout 组)中,以指明该Shader是一个使用了透明度测试的Shader。RenderType标签通常被用于着色器替换功能。
[*]IgnoreProjector设置为 True,这意味着这个Shader不会受到投影器(Projectors)的影响。通常, 使用了透明度测试的Shader都应该在SubShader中设置这三个标签。
      Pass
      {
            Tags {"LightMode" = "ForwardBase"}

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

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

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

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

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

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 lightDir = UnityWorldSpaceLightDir(i.worldPos);
                fixed3 viewDir = UnityWorldSpaceViewDir(i.worldPos);

                fixed4 texColor = tex2D(_MainTex, i.uv);

                //Alpha Test
                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 * albedo * max(0, dot(lightDir, i.worldNormal));

                return fixed4(ambient + diffuse, 1.0);
            }
            ENDCG
      }
    }
    Fallback "Transparent/Cutout/VertexLit"
}
效果:


Image


8.4 透明度混合

为了进行混合我们使用 Unity 提供的混合命令 -- Blend。Blend 是 Unity提供的设置混合模式的命令,混合时使用的函数由该指令决定。



Image


在本节中我们会使用上表的第二种语义,即 Blend SrcFactor DstFactor,来进行混合,这个命令在设置混合因子的同时会开启混合模式。注意很多时候没有透明效果是因为没开启混合模式(没使用 Blend 命令)
在下例中,把 SrcFactor 设为 SrcAlpha,而目标颜色的混合因子设为 OneMinusSrcAlpha,经混合后的颜色为:
DstColor_{new} = SrcAlpha * SrcColor + (1-SrcAlpha) * DstColor_{old}
(1)属性
Shader "Unity Shader Study/AlphaBlend"
{
    Properties
    {
      _MainTex ("Texture", 2D) = "white" {}
      _Color ("Main Tint", Color) = (1, 1, 1, 1)
      //使用该属性代替 _Cutoff 属性,该属性用于在透明纹理的基础上控制整体透明度
      _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
    }
(2)标签
    SubShader
    {
      Tags { "Quene"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
(3)给Pass 中的透明度混合设置合适的混合状态参数
      Pass
      {
            Tags {"LightMode"="ForwardBase"}

            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
在上面的代码中我们将深度写入(ZWrite)设置为关闭状态。开启混合模式,把将源颜色(片元着色器中的颜色)设为 SrcAlpha,而目标颜色(颜色缓冲中的颜色)的混合因子设为 OneMinusSrcAlpha。
(4) 属性定义、顶点着色器输入输出结构体定义
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;
            fixed _AlphaScale;

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float3 worldPos :TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };
(5)顶点着色器
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
(6)片元着色器
            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 worldNormal = normalize(i.worldNormal);

                fixed4 texColor = tex2D(_MainTex, i.uv);

                fixed3 albedo = texColor.rgb * _Color.rgb;

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0 * albedo * max(0, dot(worldLightDir, worldNormal));

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

效果


Image


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

由于关闭了深度写入,无法对模型进行像素级的排序,虽然可以分割网格,但是在很多情况往往是不切实际的。归根结底是因为关闭了深度写入,一种解决方法我们可以使用两个 Pass 来渲染模型,第一个 Pass 开启深度写入,但不输出颜色,第二个 Pass 进行正常的透明度混合。在上一节代码中加入一下 Pass
Pass
{
    ZWrite On
    ColorMask 0
}
ColorMask 用于设置颜色通道的 写掩码,它的语义如下 ColorMask RGB| A | 0 | 其他 R、G、B、A 的组合,当 ColorMask 设置为 0 时,意味着 Pass 不写入任何颜色通道。
结果:可以看到改模型虽然不会遮挡环境但是会遮挡自身。



Image


8.6 Shader Lab 的混合命令

用 S 表示源颜色,D 表示目标颜色、O 表示输出颜色
8.6.1 混合等式和参数

当我们进行混合时,我们需要使用两个混合等式,一个用于混合 RGB 通道,一个用于混合 A 通道。当设置混合状态时,我们实际上设置的就是混合等式中的操作和因子&。



Image


其中第一条命令相当于:
O_{rab} = SrcFactor * S_{rgb} + DstFactor * D_{rgb}
O_a = SrcFactor * S_{a} + DstFactor * D_{a}
ShaderLab 中的混合因子:


Image


8.6.2 混合操作

在之前混合颜色我们采用的都是加法,如果要使用减法,我们可以使用 ShaderLab 的 BlendOp BlendOperation 命令。
表 8.5 给出了支持的混合操作:


Image




Image


8.6.3 常见的混合类型

//正常(Normal),即透明度混合
Blend SrcAlpha OneMinusSrcAlpha

//柔和相加( Soft Additive)
Blend OneMinusDstColor One

//正片叠底(Multiply),即相乘
Blend DstColor Zero

//两倍相乘(2x Multiply)
Blend DstColor SrcColor

//变暗(Darken)
BlendOp Min
Blend One One

//变亮(Lighten)
BlendOp Max
Blend One One

//滤色(Screen)
Blend OneMinusDstColor One

//等同于
Blend One OneMinusSrcColor

//线性减淡(Linear Dodge)
Blend One One
效果:


Image


8.7 双面渲染的透明效果

在前面无论是透明度测试还是透明度混合,最终得到的效果都无法看到正方体的内部及背面,这是因为默认情况下这些片元被剔除了,我们可以使用 Cull 指令来控制剔除哪个面的渲染片元。
在 Unity 中 Cull 的语法如下:
Cull Back | Front | Off
在一般情况下是不使用 Cull Off 的,除非要表达一些特殊效果,否则渲染的图元加倍。
8.7.1 透明度测试的双面渲染

只需在之前的 AlphaTest 加上一行代码即可:
Pass
{
    Tags {"LightMode" = "ForwardBase"}
            
    Cull Off



Image


可以看到透明物体内部没有光照效果,这是经验模型的局限。
8.7.2 透明度混合的双面渲染

由于关闭了深度写入,我们就无法保证同一个物体的正面和背面图元的渲染顺序。为了解决这个问题,我们使用两个 Pass ,第一个 Pass 只渲染背面,第二个 Pass 只渲染正面,这样就可以保证渲染顺序的正确性。
只需在每个 Pass 的下列位置添加 Cull Back/Front 指令即可。
Pass
{
    Tags {"LightMode" = "ForwardBase"}
            
    Cull ~



Image


本文使用 Zhihu On VSCode 创作并发布
页: [1]
查看完整版本: Unity 入门精要学习笔记 第八单元 透明效果