关于Shader KeyWord的整理

关于Shader KeyWord的整理

关于Shader KeyWord的整理。源自于挺久之前做的Demo,今天翻出来整理一下。



前言

关于Shader KeyWord的整理。源自于挺久之前做的Demo,今天翻出来整理一下。


一、KeyWord

在shader 编写中,我们常常会遇到这样的问题。某些显示功能我们想通过代码实现手动控制打开关闭。由此 变体应运而生。Unity shader 的变体定义有
multi_compile
shader_feature
shader_feature_local unity 2019之后才有
以上都可以实现功能的开关功能。但是他们的作用域是不一样的。 multi_compile作用于全局也可作用于局部,可以使用Shader.EnableKeyword(“XXXX”),将作用于全局,使用meshRender.material.EnableKeyword (“XXXX”)则作用于局部。shader_feature也同理,但在打包上差别就很大,以下会说明。
在定义上multi_compile A 将只定义一个变体A,而且默认打开,无法关闭。所以我们需要添加下划线,代表关闭状态
multi_compile _ A。
而定义 shader_feature A 将会默认定义变体_ 以及变体A。
Local keywords:
shader_feature和multi_compile的主要缺点是,定义的所有关键字都限制了Unity的全局关键字数量(256个全局关键字,加上64个本地关键字)。为了避免这个问题,我们可以使用不同的着色器变体指令:shader_feature_local和multi_compile_local。

shader_feature_local: 与 shader_feature类似, 但是仅限本shader使用
multi_compile_local: 与multi_compile类似, 但是限本shader使用

二、KeyWord查看

我们可以点击这里切换Debug模式,查看材质球上缓存的变体。以及其他数据。
在这里插入图片描述
在这里插入图片描述
选择shader文件,点击keyword,可以显示shader定义的变体。
在这里插入图片描述
点击Compile And Show Code的箭头可以查看变体组合
在这里插入图片描述
在这里插入图片描述
在frame debugger上,我们也能看到当前生效的变体。
在这里插入图片描述

三、KeyWordDemo

我们先直接上我们的demo代码。首先定义一个shader 代码如下:

Shader "Unlit/NewUnlitShader"
{

   SubShader {
		Pass {
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#pragma multi_compile MY_multi_1 
		#pragma multi_compile MY_multi_2
		#include "UnityCG.cginc"
		struct vertOut {
			 float4 pos : POSITION;
            };
		vertOut vert(appdata_base v)
		{
			vertOut o;
			o.pos = UnityObjectToClipPos(v.vertex);
			return o;
		}
		float4 frag(vertOut i):COLOR
		{
			float4 c = float4(0, 0, 0, 0);
			#if defined (MY_multi_1)
			c = float4(0, 1, 0, 0);//输出绿色
			#endif
			#if defined (MY_multi_2)
			c = float4(0, 0, 1, 0);//输出蓝色
			#endif
			return c;
		}
		ENDCG
		}	
	} 
}

代码如上,我们定义两个multi_compile全局变体 MY_multi_1, MY_multi_2。MY_multi_2的优先级高于MY_multi_1。当MY_multi_2 成立时,我们输出蓝色,当MY_multi_1 成立,并且MY_multi_2不成立时,我们输出绿色。当MY_multi_1, MY_multi_2都不生效时,我们直接输出黑色。
我们添加CS脚本,控制变体开关。

public class TestKeyWorld : MonoBehaviour
{
    public bool multi_1;
    public MeshRenderer meshRender;

    public void OnChangeJJJJ()
    {
        multi_1 = !multi_1;
       if (multi_1) {
            Shader.EnableKeyword("MY_multi_1");
            Shader.DisableKeyword("MY_multi_2");
            //meshRender.material.EnableKeyword ("MY_multi_1");
            //meshRender.material.DisableKeyword ("MY_multi_2");
        } else {
            Shader.EnableKeyword("MY_multi_2");
            Shader.DisableKeyword("MY_multi_1");
            //meshRender.material.EnableKeyword ("MY_multi_2");
            //meshRender.material.DisableKeyword ("MY_multi_1");
        }
    }
}

我们定义函数OnChangeJJJJ来控制变体的开关。
场景上我们定义两个面片A,B。 以及按钮。按钮的点击事件绑定我们的函数OnChangeJJJJ()
场景如下:
在这里插入图片描述

1.multi_compile

我们可以看到面片已经显示蓝色了。这是因为我们变体定义#pragma multi_compile MY_multi_1,不含下划线。那他的默认值就是开启状态。并且函数OnChangeJJJJ()控制不了变体的开关。
这时候我们修改变体定义,添加关闭选项”_“
#pragma multi_compile _ MY_multi_1
//#pragma multi_compile _ MY_multi_2 (注释该变体)
在这里插入图片描述
可以看到他显示黑色,即MY_multi_1并没有开启。我们点击按钮就可以控制变体的开启与关闭了。
在这里插入图片描述
我们可以看到时两个面板一起变绿,因为我们用的是multi_compile全局变体。变体控制我们用的是 Shader.EnableKeyword(“MY_multi_1”);
如果我们想使用multi_compile控制单个材质球变体开关,我们可以使用meshRender.material.EnableKeyword (“MY_multi_1”)。我们修改代码shader

#pragma multi_compile _ MY_multi_1
#pragma multi_compile _ MY_multi_2

修改函数OnChangeJJJJ

 public void OnChangeJJJJ()
    {
        multi_1 = !multi_1;
       if (multi_1) {
            //Shader.EnableKeyword("MY_multi_1");
            //Shader.DisableKeyword("MY_multi_2");
            meshRender.material.EnableKeyword("MY_multi_1");
            meshRender.material.DisableKeyword("MY_multi_2");
        } else {
            //Shader.EnableKeyword("MY_multi_2");
            //Shader.DisableKeyword("MY_multi_1");
            meshRender.material.EnableKeyword("MY_multi_2");
            meshRender.material.DisableKeyword("MY_multi_1");
        }
    }

再次运行游戏。我们可以看到右边的面板还是绿色,实际上他应该时黑色,应该时材质球的缓存导致的,当我们打包出来后或者重启Unity就会变成黑色的。左边面板功能正常。
在这里插入图片描述
我们打包试exe一下
在这里插入图片描述
可以看到功能正常。我们这里用的是multi_compile,打包时候会生成所有变体,无论当前没有用到。但是就是因为这东西会生成所有变体组合,当变体定义数量多时,变体组合成指数增长,内存会爆炸的。所以我们需要适当使用。

2.shader_feature

我们尝试使用 shader_feature 实现以上效果。修改shader 文件

#pragma shader_feature MY_multi_1
#pragma shader_feature MY_multi_2

修改cs函数

 public void OnChangeJJJJ()
    {
        multi_1 = !multi_1;
       if (multi_1) {
            //Shader.EnableKeyword("MY_multi_1");
            //Shader.DisableKeyword("MY_multi_2");
            meshRender.material.EnableKeyword("MY_multi_1");
            meshRender.material.DisableKeyword("MY_multi_2");
        } else {
            //Shader.EnableKeyword("MY_multi_2");
            //Shader.DisableKeyword("MY_multi_1");
            meshRender.material.EnableKeyword("MY_multi_2");
            meshRender.material.DisableKeyword("MY_multi_1");
        }
}

变体定义 #pragma shader_feature MY_multi_1 不需要携带_下划线。他会默认定义。
运行由此,我们发现没什么异常。
在这里插入图片描述
我们打个包看看
在这里插入图片描述
我们可以看到他默认显示黑色,并且按钮没有反应。因为shader_feature 变体打包时候只会打进已编译的变体。shader_feature 的默认值是”_”,默认是不开启的。
为解决以上问题ShaderVariants 变体收集器,应运而生。
在这里插入图片描述
在这里插入图片描述
Demo中选择后
在这里插入图片描述
放到这里预加载
在这里插入图片描述
我们再打一次包试试
在这里插入图片描述
功能正常了。
我们已经明白了使用变体收集器ShaderVariants配合使用shader_feature可以很好的控制变体组合生成,排除不需要的变体。

四、变体收集器自动生成

变体生成规则如下:
shader_feature A
shader_feature B
变体Group如下
A,B,AB
直接贴代码。

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Reflection;
using System;
using UnityEngine.Rendering;
using System.Linq;

public class ShaderCollection : EditorWindow
{
    static Dictionary<string, List<ShaderVariantCollection.ShaderVariant>> ShaderVariantDict = new Dictionary<string, List<ShaderVariantCollection.ShaderVariant>>();
    public static List<string> GetAllRuntimeDirects()
    {
        //搜索所有资源
        List<string> directories = new List<string>();
        directories.Add("Assets");
        return directories;
    }
    private ShaderVariantCollection svc;
    readonly public static string ALL_SHADER_VARAINT_PATH = "Assets/AllShaders.shadervariants";

    static List<string> allShaderNameList = new List<string>();

    [MenuItem("ShaderTool/AutoShaderVariants")]
    public static void GenShaderVariant()
    {
        ShaderVariantDict = new Dictionary<string, List<ShaderVariantCollection.ShaderVariant>>();
        //先搜集所有keyword到工具类SVC
        toolSVC = new ShaderVariantCollection();
        var shaders = AssetDatabase.FindAssets("t:Shader", new string[] { "Assets", "Packages" }).ToList();
        foreach (var shader in shaders)
        {
            ShaderVariantCollection.ShaderVariant sv = new ShaderVariantCollection.ShaderVariant();
            var shaderPath = AssetDatabase.GUIDToAssetPath(shader);
            sv.shader = AssetDatabase.LoadAssetAtPath<Shader>(shaderPath);
            toolSVC.Add(sv);
            //
            allShaderNameList.Add(shaderPath);
        }

        var toolsSVCpath = "Assets/Tools.shadervariants";
        //防空

        File.WriteAllText(toolsSVCpath, "");
        AssetDatabase.DeleteAsset(toolsSVCpath);
        AssetDatabase.CreateAsset(toolSVC, toolsSVCpath);

        //搜索所有Mat
        var paths = GetAllRuntimeDirects().ToArray();
        var assets = AssetDatabase.FindAssets("t:Prefab", paths).ToList();
        var assets2 = AssetDatabase.FindAssets("t:Material", paths);
        assets.AddRange(assets2);
        List<string> allMats = new List<string>();

        //GUID to assetPath
        for (int i = 0; i < assets.Count; i++)
        {
            var p = AssetDatabase.GUIDToAssetPath(assets[i]);
            //获取依赖中的mat
            var dependenciesPath = AssetDatabase.GetDependencies(p, true);
            var mats = dependenciesPath.ToList().FindAll((dp) => dp.EndsWith(".mat"));
            allMats.AddRange(mats);
        }

        //处理所有的 material
        allMats = allMats.Distinct().ToList();

        float count = 1;
        foreach (var mat in allMats)
        {
            var obj = AssetDatabase.LoadMainAssetAtPath(mat);
            if (obj is Material)
            {
                var _mat = obj as Material;
                EditorUtility.DisplayProgressBar("处理mat", string.Format("处理:{0} - {1}", Path.GetFileName(mat), _mat.shader.name), count / allMats.Count);
                AddToDict(_mat);
            }

            count++;
        }

        EditorUtility.ClearProgressBar();
        //所有的svc
        ShaderVariantCollection svc = new ShaderVariantCollection();
        foreach (var item in ShaderVariantDict)
        {
            foreach (var _sv in item.Value)
            {
                svc.Add(_sv);
            }
        }

        AssetDatabase.DeleteAsset(ALL_SHADER_VARAINT_PATH);
        AssetDatabase.CreateAsset(svc, ALL_SHADER_VARAINT_PATH);
        AssetDatabase.Refresh();

    }
    public class ShaderData
    {
        public int[] PassTypes = new int[] { };
        public string[][] KeyWords = new string[][] { };
        public string[] ReMainingKeyWords = new string[] { };
    }

    //shader数据的缓存
    static Dictionary<string, ShaderData> ShaderDataDict = new Dictionary<string, ShaderData>();

   

    //添加Material计算
    static List<string> passShaderList = new List<string>();

    /// <summary>
    /// 添加到Dictionary
    /// </summary>
    /// <param name="curMat"></param>
    static void AddToDict(Material curMat)
    {
        if (!curMat || !curMat.shader) return;

        var path = AssetDatabase.GetAssetPath(curMat.shader);
        if (!allShaderNameList.Contains(path))
        {
            Debug.LogError("不存在shader:" + curMat.shader.name);
            Debug.Log(path);
            return;
        }

        ShaderData sd = null;
        ShaderDataDict.TryGetValue(curMat.shader.name, out sd);
        if (sd == null)
        {
            //一次性取出所有的 passtypes 和  keywords
            sd = GetShaderKeywords(curMat.shader);
            ShaderDataDict[curMat.shader.name] = sd;
        }

        var kwCount = sd.PassTypes.Length;
        if (kwCount > 2000)
        {
            if (!passShaderList.Contains(curMat.shader.name))
            {
                Debug.LogFormat("Shader【{0}】,变体数量:{1},不建议继续分析,后续也会跳过!", curMat.shader.name, kwCount);
                passShaderList.Add(curMat.shader.name);
            }
            else
            {
                Debug.LogFormat("mat:{0} , shader:{1} ,keywordCount:{2}", curMat.name, curMat.shader.name, kwCount);
            }

            return;
        }

      
        List<ShaderVariantCollection.ShaderVariant> svlist = null;
        if (!ShaderVariantDict.TryGetValue(curMat.shader.name, out svlist))
        {
            svlist = new List<ShaderVariantCollection.ShaderVariant>();
            ShaderVariantDict[curMat.shader.name] = svlist;
        }

        //求所有mat的kw
        for (int i = 0; i < sd.PassTypes.Length; i++)
        {
            //
            var pt = (PassType)sd.PassTypes[i];
            ShaderVariantCollection.ShaderVariant? sv = null;
            try
            {
                string[] key_worlds = sd.KeyWords[i];

                //变体交集 大于0 ,添加到 svcList
                sv = new ShaderVariantCollection.ShaderVariant(curMat.shader, pt, key_worlds);
                SetShaderVariantKeyWorld(svlist, sv);
            }
            catch (Exception e)
            {
                Debug.LogErrorFormat("{0}-当前shader不存在变体(可以无视):{1}-{2}", curMat.name, pt, curMat.shaderKeywords.ToString());
                continue;
            }

 
        }
    }

    static void SetShaderVariantKeyWorld(List<ShaderVariantCollection.ShaderVariant> svlist, ShaderVariantCollection.ShaderVariant? sv)
    {
        //判断sv 是否存在,不存在则添加
        if (sv != null)
        {
            bool isContain = false;
            var _sv = (ShaderVariantCollection.ShaderVariant)sv;
            foreach (var val in svlist)
            {
                if (val.passType == _sv.passType && System.Linq.Enumerable.SequenceEqual(val.keywords, _sv.keywords))
                {
                    isContain = true;
                    break;
                }
            }

            if (!isContain)
            {
                svlist.Add(_sv);
            }
        }
    }


    static MethodInfo GetShaderVariantEntries = null;

    static ShaderVariantCollection toolSVC = null;

    //获取shader的 keywords
    public static ShaderData GetShaderKeywords(Shader shader)
    {
        ShaderData sd = new ShaderData();
        GetShaderVariantEntriesFiltered(shader, new string[] { }, out sd.PassTypes, out sd.KeyWords, out sd.ReMainingKeyWords);
        return sd;
    }

    /// <summary>
    /// 获取keyword
    /// </summary>
    /// <param name="shader"></param>
    /// <param name="filterKeywords"></param>
    /// <param name="passTypes"></param>
    /// <param name="keywordLists"></param>
    /// <param name="remainingKeywords"></param>
    static void GetShaderVariantEntriesFiltered(Shader shader, string[] filterKeywords, out int[] passTypes, out string[][] keywordLists, out string[] remainingKeywords)
    {
        //2019.3接口
        //            internal static void GetShaderVariantEntriesFiltered(
        //                Shader                  shader,                     0
        //                int                     maxEntries,                 1
        //                string[]                filterKeywords,             2
        //                ShaderVariantCollection excludeCollection,          3
        //                out int[]               passTypes,                  4
        //                out string[]            keywordLists,               5
        //                out string[]            remainingKeywords)          6
        if (GetShaderVariantEntries == null)
        {
            GetShaderVariantEntries = typeof(ShaderUtil).GetMethod("GetShaderVariantEntriesFiltered", BindingFlags.NonPublic | BindingFlags.Static);
        }

        passTypes = new int[] { };
        keywordLists = new string[][] { };
        remainingKeywords = new string[] { };
        if (toolSVC != null)
        {
            var _passtypes = new int[] { };
            var _keywords = new string[] { };
            var _remainingKeywords = new string[] { };
            object[] args = new object[] { shader, 256, filterKeywords, toolSVC, _passtypes, _keywords, _remainingKeywords };
            GetShaderVariantEntries.Invoke(null, args);

            var passtypes = args[4] as int[];
            passTypes = passtypes;
            //key word
            keywordLists = new string[passtypes.Length][];
            var kws = args[5] as string[];
            for (int i = 0; i < passtypes.Length; i++)
            {
                keywordLists[i] = kws[i].Split(' ');
            }

            //Remaning key word
            var rnkws = args[6] as string[];
            remainingKeywords = rnkws;
        }
    }
}

在这里插入图片描述
点击这里自动收集变体组合。
我们还想在打包时候输出shader信息
通过实现接口IPreprocessShaders完成
直接贴代码

using System.Collections;
using System.Collections.Generic;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;

public class MyCustomBuildProcessor : IPreprocessShaders
{
    ShaderKeyword m_Blue;

    public MyCustomBuildProcessor()
    {
        m_Blue = new ShaderKeyword("_BLUE");
    }

    public int callbackOrder { get { return 0; } }

    public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        sb.AppendFormat("shader={3}, passType={0}, passName={1}, shaderType={2}n",
            snippet.passType, snippet.passName, snippet.shaderType, shader.name);

        for (int i = 0; i < data.Count; ++i)
        {
            var pdata = data[i];
            sb.AppendFormat("{0}.{1},{2}: ", i, pdata.graphicsTier, pdata.shaderCompilerPlatform);
            var ks = pdata.shaderKeywordSet.GetShaderKeywords();
            foreach (var k in ks)
            {
                sb.AppendFormat("{0}, ", k.ToString());
            }
            sb.Append("n");
        }
        Debug.Log(sb.ToString());
    }
}

总结

以上就是今天要讲的内容,如有错误欢迎指出。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>