找回密码
 立即注册
查看: 351|回复: 0

《Unity Shader入门精要》笔记(十)

[复制链接]
发表于 2022-1-27 19:21 | 显示全部楼层 |阅读模式
本文为《Unity Shader入门精要》第八章内容《透明效果》的下半部分笔记,主要涉及深度写入的透明度效果、混合命令、双面渲染。
本文相关代码,详见:
原书代码,详见原作者github:
<hr/>1. 开启深度写入的半透明效果

1.1 概念

前面学习过,关闭深度写入会造成错误排序。


针对这种情况我们可以使用两个Pass来渲染模型:

  • 第一个Pass
开启深度写入,但不输出颜色,仅仅是为了把模型的深度值写入深度缓冲;

  • 第二个Pass
进行正常的透明度混合,基于上一个Pass,按照像素级别的深度排序进行透明渲染。

但这种方法有2个缺点:

  • 多个Pass会对性能造成影响;
  • 对于内部存在不同深度结构情况下,模型内部还是实现不了半透明度效果。


1.2 实践:使用两个Pass实现半透明效果

接下来实践一下开启深度写入的半透明效果。
1.2.1 准备工作

完成如下准备工作:

  • 新建名为Scene_8_5的场景,并关闭天空盒子;
  • 新建名为AlphaBlendZWriteMat材质;
  • 新建名为Chapter8-AlphaBlendZWrite的Shader,并赋给上一步创建的材质;
  • 在场景中创建一个立方体,并将第二步创建的材质赋给它;
  • 保存场景。
详细操作详见《<Unity Shader入门精要>笔记(四)》里的案例常用操作说明。
1.2.2 编写Shader代码

打开Chapter8-AlphaBlendZWrite,删除里面所有代码,粘贴复制上一文笔记中写过的Chapter8-AlphaBlend代码,做部分修改,新代码已用注释说明:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 8/Alpha Blend ZWrite"
{
    Properties
    {
        _Color ("Main 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就好
        Pass
        {
            ZWrite On
            // ColorMask渲染指令,用于设置颜色通道的写掩码(write mask)
            // ColorMask RGB | A | 0 | 其他任何R、G、B、A的组合
            // 0表示该Pass不写入任何颜色通道
            ColorMask 0
        }

        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
        }
    }

    // 使用内置的Diffuse shader作为Fallback
    Fallback "Diffuse"
}
保存代码,可得到如下的效果:


为了方便对比上一篇笔记中没有深度写入的效果,我们拖动两个Knot模型(在Assets/Models/Chap8/里面)场景中,一个赋上有深度值写入的材质,另一个附上没有深度值写入的材质,以立方体模型作为背景,看两者的区别:


可以看出有深度写入的模型内部有遮挡关系,而没有深度写入的模型看不出前后遮挡关系。但前者物体的内部没有半透明颜色的融合效果。
2. ShaderLab的混合命令

源颜色(S):由片元着色器产生的颜色值;
目的颜色(D):从颜色缓冲中读取到的颜色值
输出颜色(O):进行混合后得到的输出颜色,重新写入颜色缓冲中。
颜色混合,包含RGBA四个通道的值,而非仅仅是RGB通道。
在Unity中我们使用Blend(Blend Off命令除外)命令时,除了设置混合状态外,Unity内部帮我们开启了混合。但在其他图形API中,我们还是需要手动开启混合模式的。
2.1 混合等式和参数

混合是逐片元操作,不可编程,但是可高度配置。
混合等式:已知源颜色S、目的颜色D,通过计算得到输出颜色S的公式。
当进行混合时,需要使用2个等式:

  • 一个用于混合RGB通道;
  • 另一个用于混合A通道。
当设置混合状态时,实际操作的是混合等式中的操作和因子。默认情况下,混合等式使用加法操作,我们只需要设置混合因子即可。

2个等式,每个等式2个因子,一共4个因子:

  • SrcFactor:RGB通道源颜色因子
  • DstFactor:RGB通道目的颜色因子
  • SrcFactorA:A通道源颜色因子
  • DstFactorA:A通道目的颜色因子

ShaderLab中设置混合因子的命令:

  • Blend SrcFactor DstFactor
开启混合,并设置混合因子。源颜色(该片元产生的颜色)会乘以SrcFactor,而目的颜色(已经存在于颜色缓冲的颜色)会乘以DstFactor,然后把两者相加后再存入颜色缓冲中。

  • Blend SrcFactor DstFactor, SrcFactorA DstFactorA
和上面几乎一样,只是使用不同的因子来混合透明通道。

对应混合公式:





ShaderLab中的混合因子

  • One:混合因子为1
  • Zero:混合因子为0
  • SrcColor:因子为源颜色值。当用于混合RGB的混合等式时,使用SrcColor的RGB分量作为混合因子;当用于混合A的混合等式时,使用SrcFactor的A分量作为混合因子
  • SrcAlpha:因子为源颜色的透明度值(A通道)
  • DstColor:因子为源颜色值。当用于混合RGB通道的混合等式时,使用DstColor的RGB分量作为混合因子;当用于混合A通道的混合等式时,使用DstColor的A分量作为混合因子
  • DstAlpha:因子为目的颜色的透明度值(A通道)
  • OneMinusSrcColor:因子为(1 - 源颜色)。当用于混合RGB的混合等式时,使用结果的RGB分量作为混合因子;当用于混合A的混合等式时,使用结果的A分量作为混合因子
  • OneMinusSrcAlpha:因子为(1 - 源颜色的透明度值)
  • OneMinusDstColor:因子为(1 - 目的颜色)。当用于混合RGB的混合等式时,使用结果的RGB分量作为混合因子;当用于混合A的混合等式时,使用结果的A分量作为混合因子
  • OneMinusDstAlpha:因子为(1 - 目的颜色的透明度值)
2.2 混合操作

ShaderLab中的混合操作:

  • Add:将混合后的源颜色和目的颜色相加。默认的混合操作。混合等式:





  • Sub:用混合后的源颜色减去混合后的目的颜色。混合等式:





  • RevSub:用混合后的目的颜色减去混合后的源颜色。混合等式:





  • Min:使用源颜色和目的颜色中较小的值,逐分量比较。混合等式:



  • Max:使用源颜色和目的颜色中较大的值,是逐分量比较的。混合等式:



  • 其他逻辑操作:仅在DirectX 11.1中支持

混合操作命令通常是和混合因子命令一起工作的,但需要注意的是,当使用Min或Max混合操作时,混合因子实际上是不起任何作用的,它们仅会判断原始的源颜色和目的颜色之间的比较结果。
2.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)
Blend Max
Blend One One

// 滤色(Screen)
Blend OneMinusDstColor One
// 等同于
Blend One OneMinusSrcColor

// 线性减淡(Linear Dodge)
Blend One One1
对应的效果:


注意:

  • 虽然我们使用Min和Max的混合操作设置了混合因子,但是他们不会对结果产生任何影响,因为Min和Max混合操作会忽略混合因子。
  • 没有显式设置混合操作的种类时,默认使用加法操作,即:BlendOp Add。
3. 双面渲染的透明效果

Unity默认剔除了物体背面的渲染图元,只渲染正面。我们客户以使用Cull指令控制渲染哪个面的渲染图元。
Cull Back | Front | Off

  • Back:背对相机的图元被剔除,不会被渲染。
  • Front:正对相机的图元被剔除,不会被渲染。
  • Off:关闭剔除,正面、背面都会被渲染,但是会导致要渲染的图元数量成倍增加,导致更大的性能消耗,除非有特殊需要,否则不建议执行此指令。
3.1 实践:透明度测试的双面渲染

基于之前透明度测试的实践,实现透明度测试的双面渲染。
3.1.1 准备工作

完成如下准备工作:

  • 新建名为Scene_8_7_1的场景,并去掉天空盒子;
  • 新建名为AlphaTestBothSidedMat的材质;
  • 新建名为Chapter8-AlphaTestBothSided的Shader,赋给上一步创建的材质;
  • 在场景中创建一个立方体,将第二步的材质赋给它;
  • 保存场景。
3.1.2 编写Shader代码

打开Chapter8-AlphaTestBothSided,删除里面代码并粘贴之前写的Chapter8-AlphaTest代码,在Pass中增加一行关闭剔除的代码即可:
// 为Shader命名
Shader "Unity Shaders Book/Chapter 8/Alpha Test Both Sided"
{
    Properties
    {
        _Color ("Main Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags {
            "Queue" = "AlphaTest"
            "IgnoreProjector" = "True"
            "RenderType" = "TransparentCutout"
        }

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

            // 关闭剔除功能
            Cull Off

            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);

                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"
}
3.1.3 配置材质参数

配置好立方体材质的贴图和透明度剔除数值:


可得到如下效果:


可以看到透过立方体透明表面,可以看到面片背面也被渲染了,我们对比下开启剔除的效果:


3.2 实践:透明度混合的双面渲染

透明度混合的双面渲染更复杂一些,因为透明度混合关闭了深度写入,我们要非常小心渲染顺序。如果我们直接关闭剔除功能,就无法保证同一个物体的正面和背面图元的渲染顺序,所以这里我们需要用两个Pass,第一个处理背面,第二个处理正面。因为Unity会按照Pass的顺序执行,所以先渲染背面再渲染正面可以保证正确的渲染关系。
3.2.1 准备工作

完成如下准备工作:

  • 新建名为Scene_8_7_2的场景;
  • 新建名为AlphaBlendBothSidedMat的材质;
  • 新建名为Chapter8-AlphaBlendBothSided的shader,并赋给上一步创建的材质;
  • 在场景中创建一个立方体,将第二步创建的材质赋给它;
  • 保存场景。
3.2.2 编写Shader代码

打开Chapter8-AlphaBlendBothSided,删除里面所有代码并粘贴Chapter8-AlphaBlend代码,并做部分修改,修改部分的代码已用注释说明:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 8/Alpha Blend Both Sided"
{
    Properties
    {
        _Color ("Main 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处理背面
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }

            // 剔除正面,做背面图元的渲染
            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" }

            // 剔除背面,做正面图元的渲染
            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"
}
3.2.3 配置材质参数

设置如下材质参数:


得到如下效果:


对比剔除了背面渲染的效果:


4. 后续预告

至此基础篇的内容已结束,接下来进入中级篇。下一篇笔记我们会学到更复杂的光照。

写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 20:20 , Processed in 0.203322 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表