3.Shader变体和关键字

3.自定义材质面板-自带Shader材质属性绘制类-Shader变体和关键字


3.1 知识点

Shader中的变体是什么

Shader Variant(变体)指的是一个 Shader 的特定配置,通过不同的关键字(Keywords)和设置组合来实现不同的效果。每一种关键字组合或设置都会生成一个独立的 Shader 变体,最终以二进制形式存储在构建文件中供运行时使用。

说人话:Shader 变体就是基于一个 Shader 文件中的代码,编译生成多个版本的 Shader,它基于关键字来生成各种不同的版本。

举例说明:
一个 Shader 中有两个关键字,每个关键字有两种状态(启用或禁用),最终生成的变体数量为 2² = 4

  • 变体 1:没有启用任何关键字(默认)。
  • 变体 2:启用了关键字 1。
  • 变体 3:启用了关键字 2。
  • 变体 4:同时启用了关键字 1 和关键字 2。

运行时,Unity 会根据具体设置选择与之匹配的变体来使用。

Unity 的一些内置功能会隐式生成变体,比如光照模式、雾效、渲染管线等。

局部Shader关键字

局部关键字的声明有两种方式:

  • 使用 #pragma shader_feature 来声明关键字,此方式仅在关键字启用时生成对应的 Shader 变体。
  • 使用 #pragma multi_compile 来声明关键字,此方式无论关键字是否启用都会生成对应的 Shader 变体。

关键词的命名规则通常采用大写字母和下划线分隔(例如:_Test_Keyword)。

两种声明方式的主要区别在于生成 Shader 变体的数量:

  • shader_feature:编译更快,Shader 文件更小,适用于开关功能较少的场景。
  • multi_compile:编译时间较长,Shader 文件更大,适用于需要完整覆盖功能的场景。

在使用时,通过条件编译指令判断关键字的启用状态,例如:

#if defined(关键词名)
    // ...
#elif defined(关键词名)
    // ...
#else
    // ...
#endif

以根据关键字的状态处理不同的逻辑。

示例根据不同编译指令生成变体的Shader代码

Shader "Unlit/Lesson03_Shader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
        }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            // 该编译指令声明的关键字,只有关键词启用时才会生成对应 Shader 变体
            // #pragma shader_feature _TEST_KEYWORD1 _TEST_KEYWORD2
            // 按片元着色器的示例,如果都不启用,则只生成一个变体

            // 该编译指令声明的关键字,不管是否启用都会生成对应 Shader 变体 
            #pragma multi_compile _TEST_KEYWORD1 _TEST_KEYWORD2
            // 按片元着色器的示例,如果都不启用,还是会生成四个变体

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o, o.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 分支判断示例(注释掉的部分为可选逻辑)
                // #if defined(_TEST_KEYWORD1)
                // ...
                // #elif defined(_TEST_KEYWORD2)
                // ...
                // #else
                // ...
                // #endif

                // 逻辑判断:基于不同的关键字可能产生四种不同的变体
                fixed4 col = fixed4(1, 1, 1, 1); // 默认颜色白色

                #if defined(_TEST_KEYWORD1)
                col = tex2D(_MainTex, i.uv); // 取纹理颜色
                #endif

                #if defined(_TEST_KEYWORD2)
                UNITY_APPLY_FOG(i.fogCoord, col); // 应用雾效
                #endif

                return col;
            }
            ENDCG
        }
    }
}

Shader中利用C#代码启用和禁用关键字

Shader 中的关键字是一种用于控制 Shader 变体的机制,允许我们通过启用或禁用特定功能来动态调整 Shader 的行为。

Shader 中的关键字可以分为两种:

  • 全局关键字:Unity 自带的关键字,所有 Shader 中共享。可通过 C# 代码调用
    Shader.EnableKeyword("全局关键字名");
    Shader.DisableKeyword("全局关键字名");
    
    来控制关键字的启用和禁用。
  • 局部关键字:仅在特定 Shader 中有效,不会影响其他 Shader。Unity 推荐使用局部关键字以减少关键字冲突问题。
    可在 C# 代码中通过材质球的方式调用
    material.EnableKeyword("局部关键字名");
    material.DisableKeyword("局部关键字名");
    
    来控制关键字的启用和禁用。

内置关键字




变体和条件分支的区别

条件分支语句会带来逻辑上的执行开销,虽然运行时直接判断,但仍会对性能产生影响。

而 Shader 变体虽然会增加编译时间并占用更多的存储内容,但其性能较高,灵活性强。在静态功能选择时(例如 Shader 在运行时不会动态切换功能的场景),使用变体更优;而在需要动态切换功能的场景下,条件分支语句则更适合。

总结

Shader 变体是在编译时基于关键字生成多个版本的 Shader 代码,在运行时根据关键字状态选择相应的版本来渲染物体。


3.2 知识点代码

Lesson03_Shader.shader

Shader "Unlit/Lesson03_Shader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
        }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            //该编译指令声明的关键字,只有关键词启用时才会生成对应Shader变体
            // #pragma shader_feature _TEST_KEYWORD1 _TEST_KEYWORD2
            //按片元着色器的示例,如果都不启用,则只生成一个变体


            //该编译指令声明的关键字,不管是否启用都会生成对应Shader变体 
            #pragma multi_compile _TEST_KEYWORD1 _TEST_KEYWORD2
            //按片元着色器的示例,如果都不启用,还是会生成四个变体


            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o, o.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 分支判断
                // #if defined(_TEST_KEYWORD1)
                //
                // #elif defined(_TEST_KEYWORD2)
                //
                // #else
                //
                // #endif


                // 逻辑判断 基于不同的关键字可能产生四种不同的变体
                fixed4 col = fixed4(1, 1, 1, 1); // 默认颜色白色

                #if defined(_TEST_KEYWORD1)
                col = tex2D(_MainTex, i.uv); // 取纹理颜色
                #endif


                #if defined(_TEST_KEYWORD2)
                UNITY_APPLY_FOG(i.fogCoord, col);// 应用雾效
                #endif


                // // sample the texture
                // fixed4 col = tex2D(_MainTex, i.uv);
                // // apply fog
                // UNITY_APPLY_FOG(i.fogCoord, col);
                
                 return col;
            }
            ENDCG
        }
    }
}

Lesson03_自定义材质面板_自带Shader材质属性绘制类_Shader变体和关键字.cs

using UnityEngine;

public class Lesson03_自定义材质面板_自带Shader材质属性绘制类_Shader变体和关键字 : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 Shader中的变体是什么?

        //Shader Variant(变体)指的是一个Shader的特定配置
        //通过不同的关键字(Keywords)和 设置组合 来实现不同的效果
        //每一种关键字组合或设置都会生成一个独立的Shader变体
        //最终以二进制形式存储在构建文件中供运行时使用

        //说人话:
        //Shader变体就是基于一个Shader文件当中的代码,编译生成多个版本的Shader
        //它基于关键字来生成各种不同的版本

        //举例说明
        //一个Shader中有两个关键字
        //每个关键字有两种状态(启用或禁用)
        //最终生成的变体数量为2²=4
        //变体 1:没有启用任何关键字(默认)。
        //变体 2:启用了 关键字1。
        //变体 3:启用了 关键字2。
        //变体 4:同时启用了 关键字1 和 关键字2
        //运行时,Unity会根据具体设置选择与之匹配的变体来使用

        //Unity的一些内置功能会隐式的生成变体
        //比如
        //光照模式
        //雾效
        //渲染管线
        //等等

        #endregion

        #region 知识点二 局部Shader关键字

        //1.声明:
        //声明局部关键字有两种方式
        //  #pragma shader_feature 或 #pragma multi_compile 来声明关键词
        //  1-1.#pragma shader_feature 关键词1 关键词2 关键词3....
        //      作用:
        //      该编译指令声明的关键字,只有关键词启用时才会生成对应Shader变体

        //  1-2.#pragma multi_compile 关键词1 关键词2 关键词3.... 
        //      作用:
        //      该编译指令声明的关键字,不管是否启用都会生成对应Shader变体

        //关键词命名规则
        //通常使用大写字母和下划线分隔(_大写单词_大写单词...)
        //比如:_Test_Keyword

        //两种声明方式的区别:
        //主要区别在于 生成Shader变体的数量
        //shader_feature:
        //编译更快,Shader文件更小,适用开关功能较少的场景
        //multi_compile:
        //编译更长,Shader文件更大,适用需要完整覆盖的功能

        //2.使用
        //关键字使用规则:
        //  利用
        //  #if defined(关键词名)
        //      ....
        //  #elif defined(关键词名)
        //      ....
        //  #else
        //      ....
        //  #endif
        //  配合使用,判断关键词启用时处理什么逻辑

        #endregion

        #region 知识点三 Shader中利用C#代码启用和禁用关键字

        //Unity Shader中的关键字是一种用于控制Shader变体的机制
        //允许我们通过启用或者禁用特定功能来动态调整Shader的行为

        //Shader中的关键字可以分为2种
        //1.全局关键字
        //Unity自带的关键字,所有Shader中共享
        //可以在C#代码中通过
        //Shader.EnableKeyword("全局关键字名")
        //Shader.DisableKeyword("全局关键字名")
        //来控制关键字的启用和禁用

        //2.局部关键字
        //只在特定的Shader中有效,不会影响其他Shader
        //Unity更推荐使用局部关键字以减少关键字冲突问题
        //可以在C#代码中通过
        //材质球.EnableKeyword("局部关键字名")
        //材质球.DisableKeyword("局部关键字名")
        //来控制关键字的启用和禁用

        #endregion

        #region 知识点四 变体和条件分支的区别

        //条件分支语句会带来逻辑上执行的开销
        //虽然运行时直接判断但是会带来对性能的影响

        //变体虽然会增加编译时间并占用更多的内容
        //但是它的性能高,灵活性强

        //在静态功能选择时变体更优 比如随着游戏运行这个Shader不会动态切换的
        //在动态功能选择时条件分支语句更适合 比如随着游戏运行这个Shader会随时动态切换的

        #endregion

        #region 总结

        //Shader 变体就是在编译时基于关键字生成多个版本的 Shader 代码,
        //在运行时根据关键字状态选择相应的版本来渲染物体。

        #endregion
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏