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

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

[复制链接]
发表于 2022-1-5 11:38 | 显示全部楼层 |阅读模式
本文为《Unity Shader入门精要》第八章内容《透明效果》的上半部分笔记,主要涉及透明度测试、透明度混合及其代码实践。
本文相关代码,详见:
原书代码,详见原作者github:
0. 初步了解

实时渲染中,通过控制渲染模型的透明通道来实现透明效果。透明通道值在[0, 1]之间,1表示完全不透明,0表示完全透明。
实时渲染中,深度缓冲用于解决可见性的问题,它决定哪个物体的哪些部分会被渲染在前面,哪些部分被其他物体遮挡。
基本思想:根据深度缓冲中的值来判断该片元距离摄像机的距离,当渲染一个片元时,将它的深度值和已存在于深度缓冲中的值进行比较(如果开启深度测试),如果它的值距离摄像机更近,则将它的深度值更新到深度缓冲中,否则不更新深度缓冲,这个面片不被渲染到屏幕上。

Unity中使用透明度测试透明度混合实现透明效果。
透明度测试:只要片元的透明度不满足条件(通常是小于某个阈值),则该片元被舍弃。(要么看不见,要么完全不透明)
透明度混合:使用当前片元透明度作为混合因子,与颜色缓冲中的颜色值进行混合,得到新颜色。(是真正的半透明效果,关闭了深度写入,但深度测试不关闭)

深度写入:往深度缓冲中写入数据;
深度测试:将当前片元的深度值与深度缓冲中的深度值进行比较。

为什么透明度混合要关闭深度写入?
如果不关闭深度写入,一个半透明物体背后的物体不会被看到。因为透明物体在深度测试后,它的颜色和深度会写入缓冲中,导致后面的物体在缓冲中的数据被覆盖,所以拿不到后面物体的颜色值,无法产生半透明的效果。
1. 渲染顺序很重要

进行透明度混合需要关闭深度写入,而关闭深度写入会让物体的渲染顺序变得非常重要,渲染顺序的不同会导致渲染的结果差别很大。
1.1 例子:半透明A和不透明B



A是透明的,它关闭了深度写入,但是深度测试是开启的;
B是不透明的,它是开启了深度写入和深度测试的。
情况1:先渲染B,再渲染A


  • 深度缓冲中没有任何数据;
  • 渲染B,B首先写入颜色缓冲和深度缓冲;
  • 渲染A,A距离相机更近,使用A的透明度来和颜色缓冲中的B颜色进行混合;
  • 得到正常的半透明效果。
情况2:先渲染A,再渲染B


  • 深度缓冲中没有任何数据;
  • 渲染A,A直接写入颜色缓冲,但是无法写入深度缓冲;
  • 渲染B,深度缓冲中没有数据,B写入深度缓冲,同时写入颜色缓冲,B颜色覆盖了A的颜色数据;
  • 得到错误的效果:看起来A被B挡住了。
由此得出结论:
应该先渲染不透明物体,再渲染半透明物体。
1.2 例子:半透明A和半透明B



A是透明的,它关闭了深度写入,但是深度测试是开启的;
B是透明的,它关闭了深度写入,但是深度测试是开启的。
情况1:先渲染B,再渲染A


  • 渲染B,B正常写入颜色缓冲;
  • 渲染A,A和颜色缓冲中的B颜色进行混合;
  • 得到正确的半透明效果。
情况2:先渲染A,再渲染B


  • 渲染A,A正常写入颜色缓冲;
  • 渲染B,B和颜色缓冲中的A颜色进行混合;
  • 得到完全相反的混合结果,看起来好像B在A的前面。
由此得出结论:
半透明物体之间也要符合一定的渲染顺序。

基于上面两个例子,渲染引擎一般会先对物体进行排序,再渲染。
常用的方法是:

  • 先渲染所有不透明物体,并开启它们的深度测试和深度写入;
  • 把透明物体按照它们距离摄像机的远近进行排序;
  • 按照从后往前的顺序渲染这些半透明物体,开启它们的深度测试,但关闭深度写入。
其实依然有一些情况是上述方法无法解决的,但是因为上述方法能解决绝大多数的情况且容易实现,所以大多数游戏引擎都是用了这个方法。
接下来看看两种上述方法无法解决的情况,第一种是多个物体循环重叠,我们没办法以模型物体作为颗粒度进行渲染优先级的排序:


第二种是相比两个物体各自相对应的点(左上点、中心点、右下点),都是A离相机更近,计算会得出A在B的前面,而实际却是B挡住了A:


上述两种情况,我们仅作了解即可,因为在以物体作为颗粒度的排序下,不论怎么排序都会出现问题。所以为了减少类似问题的出现,要尽可能让模型是凸面体,或考虑将复杂的模型拆分成可以独立排序的多个子模块。
2. Unity Shader的渲染顺序

Unity使用渲染队列,通过设置SubShader的Queue标签来决定模型归于哪个渲染队列。Unity在内部使用一系列整数索引来表示每个渲染队列,索引号越小表示越早被渲染。
Unity提前定义的5个渲染队列:

  • Background
队列索引号:1000
这个渲染队列会在任何其他队列之前被渲染,通常使用该队列来渲染那些绘制在背景上的物体。

  • Geometry
队列索引号:2000
默认的渲染队列,半透明物体不建议使用此队列。

  • AlphaTest
队列索引号:2450
需要透明度测试的物体使用这个队列。

  • Transparent
队列索引号:3000
这个队列中的物体会在所有Geometry和AlphaTest物体渲染后,再按从后往前的顺序进行渲染。任何使用了透明度混合(例如:关闭了深度写入的Shader)的物体都应该使用这个队列。

  • Overlay
队列索引号:4000
用于实现一些叠加效果,任何需要在最后渲染的物体都应该使用这个队列。

代码案例:
// 透明度测试
SubShader
{
    Tags { "Queue" = "AlphaTest" }
    Pass {...}
}

// 透明度混合
SubShader
{
    Tags { "Queue" = "Transparent" }
    Pass {
        // 关闭深度写入
        ZWrite Off
        ...
    }
}
3. 透明度测试

3.1 概念

只要一个片元的透明度不满足条件(通常是小于某个阈值),则这个片元就会被舍弃,不进行任何处理,否则按照普通的不透明物体的处理方式进行处理。
通常在片元着色器中使用clip函数进行透明度测试,clip是CG中的一个函数。
// 函数的参数是裁剪时使用的标量或矢量条件
void clip(float4 x);
void clip(float3 x);
void clip(float2 x);
void clip(float1 x);
void clip(float x);

// 如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色
void clip(float4 x)
{
    if (any(x < 0))
        discard;
}
接下来通过实践了解透明度测试。
3.2 实践:使用半透明纹理实现透明度测试

3.2.1 准备工作

完成如下准备工作:

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

打开Chapter8-AlphaTest,删除里面所有代码,编写如下代码,新的代码逻辑已用注释说明:
// 为Shader命名
Shader "Unity Shaders Book/Chapter 8/Alpha Test"
{
    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"
            // 让当前Shader不会受到投射器的影响
            "IgnoreProjector" = "True"
            // 让Unity把当前Shader归入到提前定义的TransparentCutout组
            "RenderType" = "TransparentCutout"
        }

        Pass
        {
            // 设置LightMode为ForwardBase,可以让我们得到
            // 一些正确的Unity内置光照变量,如:_LightColor0
            Tags { "LightMode" = "ForwardBase" }

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

                // 透明度测试:透明度小于_Cutoff的值,则当前片元会被丢弃
                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.rgb * albedo
                        * max(0, dot(worldNormal, worldLightDir));

                return fixed4(ambient + diffuse, 1.0);
            }
            ENDCG
        }
    }

    // 使用内置的VertexLit Shader兜底
    Fallback "Transparent/Cutout/VertexLit"
}
3.2.3 配置材质参数

给材质设置包含不同透明度的纹理贴图,可使用文章开头Git仓库里的素材——transparent_texure.psd,在/Assets/Textures/Chapter8文件夹下:


分别给_Cutoff值设置0.6、0.7、0.8,可以看到纹理中部分区域不再可见。因为纹理中的四块颜色透明度不同,当透明度值达不到_Cutoff定义的阈值,则被丢弃,不会渲染到屏幕上:


4. 透明度混合

4.1 概念

使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合得到新的颜色。但是需要关闭深度写入,所以我们需要非常小心物体的渲染顺序。
Unity为我们提供了混合命令——Blend,用来控制是否开启混合、以怎样的方式混合。
ShaderLab的Blend命令:

  • Blend Off
关闭混合。

  • Blend SrcFactor DstFactor
开启混合。源颜色(当前片元的颜色)x SrcFactor + 目标颜色(颜色缓冲中的颜色) x DstFactor,得到的最终颜色存入颜色缓冲中。

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

  • BlendOp BlendOperation
并非源颜色和目标颜色的简单相加后混合,而使用BlendOperation对它们进行其他操作。

我们使用Blend指令设置混合因子的同时,Unity会自动帮我们开启混合模式。如果我们遇到模型没有任何透明效果时,不妨看一下Pass中有没有忘记使用Blend命令,或是忘记设置混合因子(本质是开启混合模式)。
我们把源颜色的混合因子SrcFactor设为SrcAlpha,目标颜色的混合因子DstFactor设为OneMinusSrcAlpha,那么混合后的新颜色是:


4.2 实践:使用半透明纹理实现透明度混合

4.2.1 准备工作

完成如下准备工作:

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

打开Chapter8-AlphaBlend,删除里面所有的代码,并赋值上一节Chapter8-AlphaTest的代码,做部分修改,修改部分已用注释说明:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
    Properties
    {
        _Color ("Main Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        // 用于在透明纹理的基础上控制整体的透明度
        _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
    }
    SubShader
    {
        // 透明度混合常用的3个标签
        Tags {
            // 将渲染队列修改为透明混合队列
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            // 将当前Shader归入Transparent组
            "RenderType" = "Transparent"
        }

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

            // 关闭深度写入
            ZWrite Off
            // 将源颜色(该片元着色器产生的颜色)的混合因子设为SrcAlpha
            // 将目标颜色(已存在颜色缓冲中的颜色)的混合因子
            // 设为OneMinusSrcAlpha
            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
        }
    }

    // 修改兜底的Shader
    Fallback "Transparent/VertexLit"
}
4.2.3 配置材质参数

使用上一节的纹理赋给当前的材质,并设置_AlphaBlend数值,可得到不同的透明度效果:


原书给出了之前提到的异常透明度混合的情况:就是如果一个物体自身如果存在互相交叉的结构,由于透明度混合关闭了深度写入,会导致我们无法得到正确的半透明遮挡效果。


这里仅作了解回顾。

写在最后

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

本帖子中包含更多资源

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

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

本版积分规则

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

GMT+8, 2024-11-25 12:37 , Processed in 0.092789 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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