├── .gitattributes ├── SocoShaderVariantsStripper ├── Images │ ├── 1.开启界面.png │ ├── 10.优先级.png │ ├── 3.全局设置.png │ ├── 6.添加条件.png │ ├── 8.修改条件.png │ ├── 12.剔除检查.png │ ├── 13.添加条件3.png │ ├── 2.创建配置文件.png │ ├── 7.添加条件2.png │ ├── 4.选择Shader1.png │ ├── 5.添加Shader2.png │ ├── 11.读写Json序列化文件.png │ └── 9.当包含keyword时.png ├── ShaderVariantsStripperCondition.True.cs ├── Editor │ ├── ShaderVariantsStripperConditionWindow.cs │ ├── ShaderVariantsStripperConfig.cs │ ├── ShaderVariantsStripperCode.cs │ └── ShaderVariantsStripperWindow.cs ├── ShaderVariantsStripperCondition.PassTypeIs.cs ├── ShaderVariantsStripperCondition.cs ├── ShaderVariantsStripperCondition.PassName.cs ├── ShaderVariantsStripperCondition.ShaderName.cs ├── stats_keywords.py ├── ShaderVariantsData.cs ├── README.md ├── ShaderVariantsStripperCondition.AExistAndBNotExist.cs ├── ShaderVariantsStripperCondition.MultiCondition.cs └── ShaderVariantsStripperCondition.HasKeywordCombination.cs ├── SocoShaderVariantsCollection ├── ..Images │ ├── 1.开启界面.png │ ├── 10.排除变体.png │ ├── 2.功能选择.png │ ├── 3.快速浏览.png │ ├── 6.合并文件.png │ ├── 7.执行器列表.png │ ├── 9.分割文件.png │ ├── 4.材质收集器列表.png │ ├── 5.变体过滤器列表.png │ └── 8.变体声明组合.png ├── Editor │ ├── Executable │ │ ├── IExecutable.cs │ │ ├── InvalidVariantStrip.cs │ │ └── VariantKeywordCombination.cs │ ├── VariantFilter │ │ ├── IVariantFilter.cs │ │ ├── VariantFilter_PassStrip.cs │ │ └── VariantFilter_Shader.cs │ ├── MaterialCollection │ │ ├── IMaterialCollection.cs │ │ ├── MaterialCollection_AssignMaterial.cs │ │ ├── MaterialCollection_SceneDependency.cs │ │ └── MaterialCollection_TotalMaterial.cs │ ├── MaterialFilter │ │ ├── IMaterialFilter.cs │ │ └── TestMaterialFilter.cs │ ├── SerializableShaderVariant.cs │ ├── ShaderVariantCollectionToolConfig.cs │ ├── ShaderVariantCollectionAddVariantWindow.cs │ ├── ShaderVariantCollectionMaterialVariantConverter.cs │ └── ShaderVariantCollectionMapper.cs └── README.md ├── ..ProjectFile ├── README.md └── MaterialCollection_ProjectDependRes.cs ├── ..Associate ├── README.md └── SocoVariantStripAssociate.cs └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/1.开启界面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/1.开启界面.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/10.优先级.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/10.优先级.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/3.全局设置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/3.全局设置.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/6.添加条件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/6.添加条件.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/8.修改条件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/8.修改条件.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/12.剔除检查.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/12.剔除检查.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/13.添加条件3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/13.添加条件3.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/2.创建配置文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/2.创建配置文件.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/7.添加条件2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/7.添加条件2.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/1.开启界面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/1.开启界面.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/10.排除变体.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/10.排除变体.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/2.功能选择.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/2.功能选择.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/3.快速浏览.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/3.快速浏览.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/6.合并文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/6.合并文件.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/7.执行器列表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/7.执行器列表.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/9.分割文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/9.分割文件.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/4.选择Shader1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/4.选择Shader1.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/5.添加Shader2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/5.添加Shader2.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/4.材质收集器列表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/4.材质收集器列表.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/5.变体过滤器列表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/5.变体过滤器列表.png -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/..Images/8.变体声明组合.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsCollection/..Images/8.变体声明组合.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/11.读写Json序列化文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/11.读写Json序列化文件.png -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Images/9.当包含keyword时.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossous/SocoTools/HEAD/SocoShaderVariantsStripper/Images/9.当包含keyword时.png -------------------------------------------------------------------------------- /..ProjectFile/README.md: -------------------------------------------------------------------------------- 1 | # 项目文件 2 | 3 |   这个文件夹内,存放项目依赖相关脚本,直接使用肯定会报错,请按照项目需求修改脚本,或直接删除脚本。
4 |   当前项目依赖组件包括: 5 | > 1. MaterialCollection_ProjectDependRes: 变体收集工具中,获取项目资源表依赖 -------------------------------------------------------------------------------- /..Associate/README.md: -------------------------------------------------------------------------------- 1 | # 联动组件 2 | 3 |   这个文件夹内,存放多个SocoTools工具的联动功能,如果只使用了部分工具,可能会导致这里的脚本报错,请添加被引用的工具,或删除这个脚本。
4 |   当前联动组件包括: 5 | > 1. SocoVariantStripAssociate: 变体收集与剔除工具的联动脚本,可在收集工具中运行变体剔除判断。 6 | 7 | -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/Executable/IExecutable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace Soco.ShaderVariantsCollection 6 | { 7 | public abstract class IExecutable : ScriptableObject 8 | { 9 | public abstract void Execute(ShaderVariantCollectionMapper mapper); 10 | } 11 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/VariantFilter/IVariantFilter.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace Soco.ShaderVariantsCollection 4 | { 5 | public abstract class IVariantFilter : ScriptableObject 6 | { 7 | //return true will save and false will strip 8 | public abstract bool Filter(ShaderVariantCollection.ShaderVariant variant); 9 | } 10 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/MaterialCollection/IMaterialCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace Soco.ShaderVariantsCollection 5 | { 6 | public abstract class IMaterialCollector : ScriptableObject 7 | { 8 | public abstract void AddMaterialBuildDependency(IList buildDependencyList); 9 | } 10 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/MaterialFilter/IMaterialFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace Soco.ShaderVariantsCollection 5 | { 6 | public abstract class IMaterialFilter : ScriptableObject 7 | { 8 | //return true will save and false will strip 9 | public abstract bool Filter(Material material, List collections); 10 | } 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SocoTools 2 | 3 | 个人开发的Unity工具,提供部分需求的通用解决方案 4 | 5 | 当前包括: 6 | 7 | 1. 变体剔除工具[SocoShaderVariantsStripper](https://github.com/crossous/SocoTools/tree/main/SocoShaderVariantsStripper) 8 | 2. 变体收集工具[SocoShaderVariantsCollection](https://github.com/crossous/SocoTools/tree/main/SocoShaderVariantsCollection) 9 | 10 | 除此外,Associate目录包含工具之间的联动组件,这些组件导入工程需保证相关工具已被导入工程; ProjectFile目录包含项目依赖文件,是提供项目依赖逻辑的简单框架,导入项目要根据需要改写或删除脚本。 11 | -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/MaterialFilter/TestMaterialFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace Soco.ShaderVariantsCollection 5 | { 6 | public sealed class TestMaterialFilter : IMaterialFilter 7 | { 8 | public override bool Filter(Material material, List collections) 9 | { 10 | //nothing to do 11 | return true; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/VariantFilter/VariantFilter_PassStrip.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Linq; 3 | using UnityEngine.Rendering; 4 | 5 | namespace Soco.ShaderVariantsCollection 6 | { 7 | public class VariantFilter_PassStrip : IVariantFilter 8 | { 9 | public PassType[] mStripPasses = new PassType[0]; 10 | 11 | public override bool Filter(ShaderVariantCollection.ShaderVariant variant) 12 | { 13 | return !mStripPasses.Contains(variant.passType); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/MaterialCollection/MaterialCollection_AssignMaterial.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | 5 | namespace Soco.ShaderVariantsCollection 6 | { 7 | // 这个是指定具体某些材质收集进来 8 | // 一般用于Debug 9 | public class MaterialCollection_AssignMaterial : IMaterialCollector 10 | { 11 | public Material[] materials = new Material[0]; 12 | 13 | public override void AddMaterialBuildDependency(IList buildDependencyList) 14 | { 15 | materials.ToList().ForEach(buildDependencyList.Add); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/MaterialCollection/MaterialCollection_SceneDependency.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Soco.ShaderVariantsCollection 7 | { 8 | //用于收集所有打包场景依赖的材质 9 | public class MaterialCollection_SceneDependency : IMaterialCollector 10 | { 11 | //是否只收集在EditorBuildSettings中enable的场景 12 | public bool collectOnlyEnable = true; 13 | public override void AddMaterialBuildDependency(IList buildDependencyList) 14 | { 15 | var sceneDependencyMaterials = EditorBuildSettings.scenes 16 | .Where(scene => !collectOnlyEnable || scene.enabled) 17 | .SelectMany(scene => AssetDatabase.GetDependencies(scene.path)) 18 | .Where(dependencyAsset => dependencyAsset.EndsWith(".mat")) 19 | .Distinct() 20 | .Select(matPath => AssetDatabase.LoadAssetAtPath(matPath)); 21 | 22 | foreach (Material m in sceneDependencyMaterials) 23 | buildDependencyList.Add(m); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.True.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Rendering; 3 | using UnityEditor; 4 | using UnityEngine.Scripting.APIUpdating; 5 | 6 | namespace Soco.ShaderVariantsStripper 7 | { 8 | public class ShaderVariantsStripperConditionTrue : ShaderVariantsStripperCondition 9 | { 10 | public bool Completion(Shader shader, ShaderVariantsData data) 11 | { 12 | return true; 13 | } 14 | 15 | public bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData) 16 | { 17 | return true; 18 | } 19 | 20 | #if UNITY_EDITOR 21 | public string Overview() 22 | { 23 | return "恒定满足条件"; 24 | } 25 | 26 | public void OnGUI(ShaderVariantsStripperConditionOnGUIContext context) 27 | { 28 | EditorGUILayout.BeginVertical(); 29 | 30 | EditorGUILayout.LabelField("恒定满足条件"); 31 | 32 | EditorGUILayout.EndVertical(); 33 | } 34 | 35 | public string GetName() 36 | { 37 | return "恒定满足条件"; 38 | } 39 | #endif 40 | } 41 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Editor/ShaderVariantsStripperConditionWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Soco.ShaderVariantsStripper 7 | { 8 | public class ShaderVariantsStripperConditionWindow : EditorWindow 9 | { 10 | private static ShaderVariantsStripperConditionWindow m_window; 11 | public static ShaderVariantsStripperConditionWindow Window 12 | { 13 | get 14 | { 15 | if (m_window == null) 16 | { 17 | m_window = EditorWindow.GetWindow("ShaderVariantsStripperConditionWindow"); 18 | m_window.minSize = new Vector2(480, 320); 19 | } 20 | return m_window; 21 | } 22 | } 23 | 24 | public ShaderVariantsStripperCondition mCondition; 25 | public ShaderVariantsStripperConditionOnGUIContext mContext; 26 | 27 | public void OnGUI() 28 | { 29 | if (mCondition != null) 30 | { 31 | //mContext.window = this; 32 | mCondition.OnGUI(mContext); 33 | } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/SerializableShaderVariant.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.Rendering; 4 | 5 | namespace Soco.ShaderVariantsCollection 6 | { 7 | [Serializable] 8 | public struct SerializableShaderVariant 9 | { 10 | public Shader shader; 11 | public PassType passType; 12 | public string[] keywords; 13 | 14 | public SerializableShaderVariant(ShaderVariantCollection.ShaderVariant variant) 15 | { 16 | shader = variant.shader; 17 | passType = variant.passType; 18 | keywords = variant.keywords; 19 | } 20 | 21 | public ShaderVariantCollection.ShaderVariant Deserialize() 22 | { 23 | //这样初始化的原因是,假如变体无效不会报错 24 | return new ShaderVariantCollection.ShaderVariant() 25 | { 26 | shader = shader, 27 | passType = passType, 28 | keywords = keywords 29 | }; 30 | } 31 | 32 | public bool IsValid() 33 | { 34 | try 35 | { 36 | new ShaderVariantCollection.ShaderVariant(shader, passType, keywords);; 37 | return true; 38 | } 39 | catch (Exception e) 40 | { 41 | return false; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.PassTypeIs.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Rendering; 3 | using UnityEditor; 4 | 5 | namespace Soco.ShaderVariantsStripper 6 | { 7 | public class ShaderVariantsStripperConditionPassTypeIs : ShaderVariantsStripperCondition 8 | { 9 | public PassType passType = PassType.Normal; 10 | 11 | public bool Completion(Shader shader, ShaderVariantsData data) 12 | { 13 | return data.passType == passType; 14 | } 15 | 16 | public bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData) 17 | { 18 | return other.GetType() == typeof(ShaderVariantsStripperConditionPassTypeIs) && 19 | (other as ShaderVariantsStripperConditionPassTypeIs).passType == this.passType; 20 | } 21 | 22 | #if UNITY_EDITOR 23 | public string Overview() 24 | { 25 | return $"当Pass类型是{passType}时"; 26 | } 27 | 28 | public void OnGUI(ShaderVariantsStripperConditionOnGUIContext context) 29 | { 30 | EditorGUILayout.BeginVertical(); 31 | 32 | passType = (PassType)EditorGUILayout.EnumPopup("PassType", passType); 33 | 34 | EditorGUILayout.EndVertical(); 35 | } 36 | 37 | public string GetName() 38 | { 39 | return "PassType是"; 40 | } 41 | #endif 42 | } 43 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/MaterialCollection/MaterialCollection_TotalMaterial.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Soco.ShaderVariantsCollection 7 | { 8 | //这是一个编写示例 9 | //将收集目录下所有材质 10 | //实际项目不建议这样做 11 | public sealed class MaterialCollection_TotalMaterial : IMaterialCollector 12 | { 13 | public enum PathMode 14 | { 15 | Asset, 16 | String 17 | } 18 | //private readonly string[] cIncludePath = { "Assets", "Packages" }; 19 | [Tooltip("路径选择模式,以文件夹资产的方式,或以字符串路径")] 20 | public PathMode pathMode = PathMode.String; 21 | public string[] mIncludePath = { "Assets", "Packages" }; 22 | public DefaultAsset[] mFolders = new DefaultAsset[0]; 23 | 24 | public override void AddMaterialBuildDependency(IList buildDependencyList) 25 | { 26 | string[] includePath = mFolders.Select(f => AssetDatabase.GetAssetPath(f)).ToArray(); 27 | includePath = pathMode == PathMode.Asset ? includePath : mIncludePath; 28 | var materialsGUID = AssetDatabase.FindAssets("t:Material", includePath); 29 | 30 | foreach (Material m in materialsGUID.Select( 31 | guid => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)) 32 | )) 33 | { 34 | buildDependencyList.Add(m); 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Editor/ShaderVariantsStripperConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace Soco.ShaderVariantsStripper 6 | { 7 | [System.Serializable] 8 | public struct ConditionPair 9 | { 10 | [SerializeReference] 11 | public ShaderVariantsStripperCondition condition; 12 | public bool strip; 13 | public uint priority; 14 | } 15 | 16 | [System.Serializable] 17 | public class ShaderVariantsItem 18 | { 19 | public bool applyGlobalConfig; 20 | public List conditionPairs; 21 | } 22 | 23 | //可序列化字典 24 | [System.Serializable] 25 | public class ShaderConditionsDictionary : Dictionary, ISerializationCallbackReceiver 26 | { 27 | [SerializeField] private List _keys = new List(); 28 | [SerializeField] private List _values = new List(); 29 | 30 | public void OnBeforeSerialize() 31 | { 32 | _keys.Clear(); 33 | _values.Clear(); 34 | 35 | foreach (var kvp in this) 36 | { 37 | _keys.Add(kvp.Key); 38 | _values.Add(kvp.Value); 39 | } 40 | } 41 | 42 | public void OnAfterDeserialize() 43 | { 44 | Clear(); 45 | 46 | for (var i = 0; i != Mathf.Min(_keys.Count, _values.Count); i++) 47 | { 48 | Add(_keys[i], _values[i]); 49 | } 50 | } 51 | } 52 | 53 | [CreateAssetMenu(menuName = "Soco/ShaderVariantsStripper/Create Config")] 54 | public class ShaderVariantsStripperConfig : ScriptableObject 55 | { 56 | public bool mEnable = true; 57 | public bool mIsWhiteList = false; 58 | 59 | public List mGlobalConditions = new List(); 60 | public ShaderConditionsDictionary mShaderConditions = new ShaderConditionsDictionary(); 61 | } 62 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.Rendering; 7 | 8 | namespace Soco.ShaderVariantsStripper 9 | { 10 | #if UNITY_EDITOR 11 | public struct ShaderVariantsStripperConditionOnGUIContext 12 | { 13 | public Shader shader; 14 | 15 | 16 | //ShaderVariantsStripperConditionWindow window; 17 | internal string[] globalKeywords; 18 | internal string[] localKeywords; 19 | 20 | public void Init() 21 | { 22 | if (shader != null) 23 | { 24 | InitGetKeywordMethod(); 25 | 26 | globalKeywords = sGetShaderGlobalKeywordsMethod.Invoke(null, new object[] { shader }) as string[]; 27 | localKeywords = sGetShaderLocalKeywordsMethod.Invoke(null, new object[] { shader }) as string[]; 28 | } 29 | } 30 | 31 | private static MethodInfo sGetShaderGlobalKeywordsMethod = null; 32 | private static MethodInfo sGetShaderLocalKeywordsMethod = null; 33 | 34 | private static void InitGetKeywordMethod() 35 | { 36 | if (sGetShaderGlobalKeywordsMethod == null) 37 | sGetShaderGlobalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderGlobalKeywords", 38 | BindingFlags.NonPublic | BindingFlags.Static); 39 | 40 | if (sGetShaderLocalKeywordsMethod == null) 41 | sGetShaderLocalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderLocalKeywords", 42 | BindingFlags.NonPublic | BindingFlags.Static); 43 | } 44 | } 45 | #endif 46 | 47 | public interface ShaderVariantsStripperCondition 48 | { 49 | bool Completion(Shader shader, ShaderVariantsData data); 50 | 51 | bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData); 52 | 53 | #if UNITY_EDITOR 54 | string Overview(); 55 | void OnGUI(ShaderVariantsStripperConditionOnGUIContext context); 56 | string GetName(); 57 | #endif 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/Executable/InvalidVariantStrip.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Soco.ShaderVariantsCollection 7 | { 8 | public class InvalidVariantStrip : IExecutable 9 | { 10 | public override void Execute(ShaderVariantCollectionMapper mapper) 11 | { 12 | var collection = mapper.mCollection; 13 | 14 | SerializedObject serializedObject = new UnityEditor.SerializedObject(collection); 15 | //serializedObject.Update(); 16 | SerializedProperty m_Shaders = serializedObject.FindProperty("m_Shaders"); 17 | 18 | for (int i = 0; i < m_Shaders.arraySize; ++i) 19 | { 20 | SerializedProperty pair = m_Shaders.GetArrayElementAtIndex(i); 21 | 22 | SerializedProperty first = pair.FindPropertyRelative("first"); 23 | SerializedProperty second = pair.FindPropertyRelative("second");//ShaderInfo 24 | 25 | Shader shader = first.objectReferenceValue as Shader; 26 | 27 | if (shader == null) 28 | continue; 29 | 30 | SerializedProperty variants = second.FindPropertyRelative("variants"); 31 | for (var vi = variants.arraySize - 1; vi >= 0 ; --vi) 32 | { 33 | SerializedProperty variantInfo = variants.GetArrayElementAtIndex(vi); 34 | ShaderVariantCollection.ShaderVariant variant = ShaderVariantCollectionMapper.PropToVariantObject(shader, variantInfo); 35 | 36 | if (!IsValid(variant)) 37 | variants.DeleteArrayElementAtIndex(vi); 38 | } 39 | } 40 | 41 | serializedObject.ApplyModifiedProperties(); 42 | mapper.Refresh(); 43 | 44 | } 45 | 46 | private bool IsValid(ShaderVariantCollection.ShaderVariant variant) 47 | { 48 | try 49 | { 50 | new ShaderVariantCollection.ShaderVariant(variant.shader, variant.passType, variant.keywords); 51 | return true; 52 | } 53 | catch 54 | { 55 | return false; 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /..ProjectFile/MaterialCollection_ProjectDependRes.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Assets.Code.Editor.Res; 5 | using Newtonsoft.Json.Utilities; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | public sealed class MaterialCollection_ProjectDependRes : Soco.ShaderVariantsCollection.IMaterialCollector 10 | { 11 | public override void AddMaterialBuildDependency(IList buildDependencyList) 12 | { 13 | //获取资源表中所有资源 14 | var resList = ResConfigFileEnter.GetConfigFile(); 15 | 16 | int resIndex = 0; 17 | foreach (string res in resList) 18 | { 19 | EditorUtility.DisplayProgressBar("材质收集", $"正在收集直接资源", 20 | (float)resIndex++ / (float)resList.Count); 21 | //如果资源本身是材质,则直接添加到列表中 22 | if (res.EndsWith(".mat")) 23 | { 24 | Material mat = AssetDatabase.LoadAssetAtPath(res); 25 | if(mat != null) 26 | buildDependencyList.Add(mat); 27 | } 28 | //如果不是材质,则找到资源所引用的材质添加到列表中 29 | //这样会导致大量申请小数组的GC,因此用下面的API 30 | // else 31 | // { 32 | // foreach (string depRes in AssetDatabase.GetDependencies(res)) 33 | // { 34 | // if (depRes.EndsWith(".mat")) 35 | // { 36 | // Material mat = AssetDatabase.LoadAssetAtPath(depRes); 37 | // if(mat != null) 38 | // buildDependencyList.Add(mat); 39 | // } 40 | // } 41 | // } 42 | } 43 | 44 | //获取资源间接引用的资源 45 | EditorUtility.DisplayProgressBar("材质收集", "正在获取间接资源,这可能需要一段时间", 0); 46 | resIndex = 0; 47 | var indirectDependRes = AssetDatabase.GetDependencies(resList.ToArray()); 48 | 49 | foreach (string res in indirectDependRes) 50 | { 51 | EditorUtility.DisplayProgressBar("材质收集", $"正在收集间接依赖资源", 52 | (float)resIndex++ / (float)indirectDependRes.Length); 53 | if (res.EndsWith(".mat")) 54 | { 55 | Material mat = AssetDatabase.LoadAssetAtPath(res); 56 | if(mat != null) 57 | buildDependencyList.Add(mat); 58 | } 59 | } 60 | 61 | EditorUtility.ClearProgressBar(); 62 | } 63 | } -------------------------------------------------------------------------------- /..Associate/SocoVariantStripAssociate.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using Soco.ShaderVariantsCollection; 4 | using UnityEditor; 5 | using UnityEditor.Rendering; 6 | using UnityEngine.Rendering; 7 | 8 | // 用于联动变体收集 和 变体剔除工具 9 | // 如果没有使用Soco变体剔除工具,此执行器应删除 10 | public class SocoVariantStripAssociate : Soco.ShaderVariantsCollection.IExecutable 11 | { 12 | public override void Execute(ShaderVariantCollectionMapper mapper) 13 | { 14 | var m_ShaderType = typeof(ShaderSnippetData).GetField("m_ShaderType", BindingFlags.Instance | BindingFlags.NonPublic); 15 | var m_PassType = typeof(ShaderSnippetData).GetField("m_PassType", BindingFlags.Instance | BindingFlags.NonPublic); 16 | var m_PassName = typeof(ShaderSnippetData).GetField("m_PassName", BindingFlags.Instance | BindingFlags.NonPublic); 17 | 18 | var stripConfigs = 19 | com.cyou.soco.ShaderVariantsStripper.ShaderVariantsStripperCode.LoadConfigs(); 20 | 21 | foreach (var shader in mapper.shaders) 22 | { 23 | int variantIndex = 0; 24 | var variants = mapper.GetShaderVariants(shader).ToArray(); 25 | foreach (var variant in variants) 26 | { 27 | EditorUtility.DisplayProgressBar("变体剔除", $"正在处理{shader.name} {variantIndex}/{variants.Length}变体", 28 | (float)variantIndex / (float)variants.Length); 29 | variantIndex++; 30 | 31 | ShaderSnippetData snipperData = new ShaderSnippetData(); 32 | 33 | //这里没对RayTracing、Mesh Shader的剔除做相关处理,默认所有Shader Pass都有VertexShader 34 | m_ShaderType.SetValueDirect(__makeref(snipperData), ShaderType.Vertex); 35 | m_PassType.SetValueDirect(__makeref(snipperData), variant.passType); 36 | m_PassName.SetValueDirect(__makeref(snipperData), ""); 37 | 38 | ShaderCompilerData data = new ShaderCompilerData(); 39 | data.shaderKeywordSet = new ShaderKeywordSet(); 40 | 41 | foreach (var keyword in variant.keywords) 42 | { 43 | data.shaderKeywordSet.Enable(new ShaderKeyword(keyword)); 44 | } 45 | 46 | if (com.cyou.soco.ShaderVariantsStripper.ShaderVariantsStripperCode.IsVariantStrip( 47 | shader, snipperData, data, stripConfigs)) 48 | { 49 | mapper.RemoveVariant(variant); 50 | } 51 | } 52 | } 53 | 54 | EditorUtility.ClearProgressBar(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.PassName.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using UnityEngine; 3 | using UnityEngine.Rendering; 4 | using UnityEditor; 5 | using UnityEngine.Scripting.APIUpdating; 6 | 7 | namespace Soco.ShaderVariantsStripper 8 | { 9 | public class ShaderVariantsStripperConditionPassName : ShaderVariantsStripperCondition 10 | { 11 | public enum MatchMode 12 | { 13 | AllMatch, 14 | StartsWith, 15 | EndsWith, 16 | Regex 17 | } 18 | 19 | public MatchMode mode; 20 | public string passName = ""; 21 | public RegexOptions regexOptions; 22 | 23 | public bool Completion(Shader shader, ShaderVariantsData data) 24 | { 25 | switch (mode) 26 | { 27 | case MatchMode.AllMatch: 28 | return data.passName == passName; 29 | case MatchMode.StartsWith: 30 | return data.passName.StartsWith(passName); 31 | case MatchMode.EndsWith: 32 | return data.passName.EndsWith(passName); 33 | case MatchMode.Regex: 34 | return Regex.IsMatch(data.passName, passName, regexOptions); 35 | } 36 | 37 | return false; 38 | } 39 | 40 | public bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData) 41 | { 42 | return other.GetType() == typeof(ShaderVariantsStripperConditionPassName) && 43 | (other as ShaderVariantsStripperConditionPassName).mode == this.mode && 44 | (other as ShaderVariantsStripperConditionPassName).passName == this.passName && 45 | (this.mode != MatchMode.Regex || (other as ShaderVariantsStripperConditionPassName).regexOptions == this.regexOptions); 46 | } 47 | 48 | #if UNITY_EDITOR 49 | 50 | public string Overview() 51 | { 52 | return $"当Pass名称或模式<{passName}>符合条件<{mode}>时"; 53 | } 54 | 55 | public void OnGUI(ShaderVariantsStripperConditionOnGUIContext context) 56 | { 57 | EditorGUILayout.BeginVertical(); 58 | 59 | mode = (MatchMode)EditorGUILayout.EnumPopup("MatchMode", mode); 60 | 61 | string label = mode == MatchMode.Regex ? "pattern" : "pass name"; 62 | passName = EditorGUILayout.TextField(label, passName); 63 | 64 | if (mode == MatchMode.Regex) 65 | regexOptions = (RegexOptions)EditorGUILayout.EnumFlagsField("Regex Options", regexOptions); 66 | 67 | EditorGUILayout.EndVertical(); 68 | } 69 | 70 | public string GetName() 71 | { 72 | return "Pass名称匹配"; 73 | } 74 | #endif 75 | } 76 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.ShaderName.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using UnityEngine; 3 | using UnityEngine.Rendering; 4 | using UnityEditor; 5 | using UnityEngine.Scripting.APIUpdating; 6 | 7 | namespace Soco.ShaderVariantsStripper 8 | { 9 | public class ShaderVariantsStripperConditionShaderName : ShaderVariantsStripperCondition 10 | { 11 | public enum MatchMode 12 | { 13 | AllMatch, 14 | StartsWith, 15 | EndsWith, 16 | Regex 17 | } 18 | 19 | public MatchMode mode; 20 | public string shaderName; 21 | public RegexOptions regexOptions; 22 | 23 | public bool Completion(Shader shader, ShaderVariantsData data) 24 | { 25 | switch (mode) 26 | { 27 | case MatchMode.AllMatch: 28 | return shader.name == shaderName; 29 | case MatchMode.StartsWith: 30 | return shader.name.StartsWith(shaderName); 31 | case MatchMode.EndsWith: 32 | return shader.name.EndsWith(shaderName); 33 | case MatchMode.Regex: 34 | return Regex.IsMatch(shader.name, shaderName); 35 | } 36 | 37 | return false; 38 | } 39 | 40 | public bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData) 41 | { 42 | return other.GetType() == typeof(ShaderVariantsStripperConditionShaderName) && 43 | (other as ShaderVariantsStripperConditionShaderName).mode == this.mode && 44 | (other as ShaderVariantsStripperConditionShaderName).shaderName == this.shaderName && 45 | (this.mode != MatchMode.Regex || (other as ShaderVariantsStripperConditionShaderName).regexOptions == this.regexOptions); 46 | } 47 | 48 | #if UNITY_EDITOR 49 | 50 | public string Overview() 51 | { 52 | return $"当Shader名称或模式<{shaderName}>符合条件<{mode}>时"; 53 | } 54 | 55 | public void OnGUI(ShaderVariantsStripperConditionOnGUIContext context) 56 | { 57 | EditorGUILayout.BeginVertical(); 58 | 59 | mode = (MatchMode)EditorGUILayout.EnumPopup("MatchMode", mode); 60 | 61 | string label = mode == MatchMode.Regex ? "pattern" : "shader name"; 62 | shaderName = EditorGUILayout.TextField(label, shaderName); 63 | 64 | if (mode == MatchMode.Regex) 65 | regexOptions = (RegexOptions)EditorGUILayout.EnumFlagsField("Regex Options", regexOptions); 66 | 67 | EditorGUILayout.EndVertical(); 68 | } 69 | 70 | public string GetName() 71 | { 72 | return "Shader名称匹配"; 73 | } 74 | #endif 75 | } 76 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/ShaderVariantCollectionToolConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Soco.ShaderVariantsCollection 7 | { 8 | [System.Serializable] 9 | public class ToggleObject 10 | { 11 | [SerializeReference] 12 | public ScriptableObject obj; 13 | public bool use; 14 | } 15 | 16 | //解决嵌套list序列化问题 17 | [System.Serializable] 18 | public struct ListWrapper 19 | { 20 | public List list; 21 | public ListWrapper(List list) => this.list = list; 22 | } 23 | 24 | [CreateAssetMenu(menuName = "Soco/ShaderVariantCollectionTools/Create Config")] 25 | public class ShaderVariantCollectionToolConfig : ScriptableObject, ISerializationCallbackReceiver 26 | { 27 | private Dictionary> mToggleObjects = new Dictionary>(); 28 | 29 | [SerializeField] private List mKeys = new List(); 30 | [SerializeField] private List mValues = new List(); 31 | public void OnBeforeSerialize() 32 | { 33 | mKeys.Clear(); 34 | mValues.Clear(); 35 | 36 | foreach (var kvp in mToggleObjects) 37 | { 38 | mKeys.Add(kvp.Key.AssemblyQualifiedName); 39 | mValues.Add(new ListWrapper(kvp.Value)); 40 | } 41 | } 42 | public void OnAfterDeserialize() 43 | { 44 | mToggleObjects.Clear(); 45 | for (int i = 0; i < mKeys.Count; ++i) 46 | { 47 | System.Type type = System.Type.GetType(mKeys[i]); 48 | if (type != null) 49 | { 50 | mToggleObjects.Add(type, mValues[i].list); 51 | } 52 | } 53 | } 54 | 55 | public List GetToggleObjectList(System.Type type) 56 | { 57 | if (!mToggleObjects.TryGetValue(type, out var list)) 58 | { 59 | list = new List(); 60 | mToggleObjects.Add(type, list); 61 | } 62 | 63 | return list; 64 | } 65 | 66 | public void AddToggleObject(ToggleObject obj) 67 | { 68 | GetToggleObjectList(obj.obj.GetType().BaseType).Add(obj); 69 | } 70 | 71 | public void RemoveToggleObject(ToggleObject obj) 72 | { 73 | GetToggleObjectList(obj.obj.GetType().BaseType).Remove(obj); 74 | } 75 | } 76 | 77 | public class ToolDefaultEditor : Editor 78 | { 79 | public override void OnInspectorGUI() 80 | { 81 | base.OnInspectorGUI(); 82 | } 83 | } 84 | 85 | public abstract class ShaderVariantCollectionToolEditor : Editor 86 | { 87 | } 88 | 89 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 90 | public sealed class ShaderVariantCollectionToolEditorAttribute : Attribute 91 | { 92 | public readonly Type componentType; 93 | 94 | public ShaderVariantCollectionToolEditorAttribute(Type componentType) 95 | { 96 | this.componentType = componentType; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/VariantFilter/VariantFilter_Shader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using System.Linq; 4 | using UnityEditor; 5 | 6 | namespace Soco.ShaderVariantsCollection 7 | { 8 | 9 | 10 | public class VariantFilter_Shader : IVariantFilter 11 | { 12 | public enum Mode 13 | { 14 | Strip, 15 | OnlyReserveContains 16 | } 17 | 18 | public Mode mode = Mode.Strip; 19 | public Shader[] mShaders = new Shader[0]; 20 | 21 | public override bool Filter(ShaderVariantCollection.ShaderVariant variant) 22 | { 23 | bool containShader = mShaders.Contains(variant.shader); 24 | return mode == Mode.Strip ? !containShader : containShader; 25 | } 26 | } 27 | 28 | [ShaderVariantCollectionToolEditor(typeof(VariantFilter_Shader))] 29 | class VariantFilter_ShaderEditor : ShaderVariantCollectionToolEditor 30 | { 31 | private bool foldoutValue = false; 32 | private int mOperatorIndex1 = 0; 33 | private int mOperatorIndex2 = 0; 34 | private Shader mOperatorShader = null; 35 | 36 | public override void OnInspectorGUI() 37 | { 38 | base.OnInspectorGUI(); 39 | 40 | foldoutValue = EditorGUILayout.Foldout(foldoutValue, "添加删除交换操作"); 41 | 42 | if (foldoutValue) 43 | { 44 | EditorGUILayout.BeginVertical(); 45 | 46 | EditorGUILayout.BeginHorizontal(); 47 | mOperatorIndex1 = EditorGUILayout.IntField("操作索引1", mOperatorIndex1); 48 | mOperatorIndex2 = EditorGUILayout.IntField("操作索引2", mOperatorIndex2); 49 | EditorGUILayout.EndHorizontal(); 50 | mOperatorShader = EditorGUILayout.ObjectField("添加Shader", mOperatorShader, typeof(Shader)) as Shader; 51 | 52 | var targetObj = target as VariantFilter_Shader; 53 | 54 | EditorGUILayout.BeginHorizontal(); 55 | if (GUILayout.Button("添加至操作索引1")) 56 | { 57 | if (mOperatorIndex1 >= 0 && mOperatorIndex1 <= targetObj.mShaders.Length) 58 | { 59 | Undo.RecordObject(targetObj, "VariantFilter_Shader insert element from list"); 60 | List temp = new List(targetObj.mShaders); 61 | temp.Insert(mOperatorIndex1, mOperatorShader); 62 | targetObj.mShaders = temp.ToArray(); 63 | } 64 | } 65 | 66 | if (GUILayout.Button("删除操作索引1")) 67 | { 68 | if (mOperatorIndex1 >= 0 && mOperatorIndex1 < targetObj.mShaders.Length) 69 | { 70 | Undo.RecordObject(targetObj, "VariantFilter_Shader delete shader from list"); 71 | List temp = new List(targetObj.mShaders); 72 | temp.RemoveAt(mOperatorIndex1); 73 | targetObj.mShaders = temp.ToArray(); 74 | } 75 | } 76 | 77 | if (GUILayout.Button("交换操作索引")) 78 | { 79 | if (mOperatorIndex1 >= 0 && mOperatorIndex1 < targetObj.mShaders.Length 80 | && mOperatorIndex2 >= 0 && mOperatorIndex2 < targetObj.mShaders.Length) 81 | { 82 | Undo.RecordObject(targetObj, "VariantFilter_Shader swap element from list"); 83 | (targetObj.mShaders[mOperatorIndex1], targetObj.mShaders[mOperatorIndex2]) = (targetObj.mShaders[mOperatorIndex2], targetObj.mShaders[mOperatorIndex1]); 84 | } 85 | } 86 | EditorGUILayout.EndHorizontal(); 87 | 88 | EditorGUILayout.EndVertical(); 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/stats_keywords.py: -------------------------------------------------------------------------------- 1 | #用于统计bundle中当前shader的变体 2 | #使用方法: 3 | #1. 用asset studio打开asset bundle 4 | #2. 在asset list中找到shader 5 | #3. 将shader复制保存到文本中 6 | #4. 编写此脚本ShaderVariants.from_file输入文本路径 7 | 8 | import re 9 | 10 | ParseState = {} 11 | ParseState['None'] = 0 12 | ParseState['SubProgram'] = 1 13 | ParseState['Pass'] = 2 14 | ParseState['PassFind'] = 3 15 | ParseState['Keywords'] = 4 16 | ParseState['Local Keywords'] = 5 17 | 18 | class ShaderVariants: 19 | 20 | def __init(self): 21 | pass 22 | 23 | #total_variants: dict key-pass, value-list of variants 24 | 25 | @staticmethod 26 | def from_file(filename): 27 | 28 | sv = ShaderVariants() 29 | sv.total_variants = {} 30 | 31 | with open(filename, 'r') as f: 32 | 33 | current_state = ParseState['None'] 34 | current_keywords = [] 35 | current_pass = "" 36 | 37 | for line in f: 38 | 39 | if current_state == ParseState['None']: 40 | if len(re.findall('Pass \{', line)) > 0: 41 | current_state = ParseState['Pass'] 42 | 43 | elif current_state == ParseState['Pass']: 44 | current_pass = re.findall('Name "(\w+)"', line)[0] 45 | sv.total_variants[current_pass] = set() 46 | current_state = ParseState['PassFind'] 47 | 48 | elif current_state == ParseState['PassFind']: 49 | if len(re.findall('SubProgram "gles3 " \{', line)) > 0: 50 | current_state = ParseState['SubProgram'] 51 | current_keywords = [] 52 | elif len(re.findall('Pass \{', line)) > 0: 53 | current_state = ParseState['Pass'] 54 | 55 | elif current_state == ParseState['SubProgram']: 56 | if line.startswith("Keywords"): 57 | current_state = ParseState['Keywords'] 58 | current_keywords = re.findall('"(\w+)"', line) 59 | elif line.startswith("Local Keywords"): 60 | current_state = ParseState['Local Keywords'] 61 | current_keywords += re.findall('"(\w+)"', line) 62 | else: 63 | current_state = ParseState['PassFind'] 64 | sv.total_variants[current_pass].add(" ".join(current_keywords)) 65 | 66 | elif current_state == ParseState['Keywords']: 67 | if line.startswith("Local Keywords"): 68 | current_state = ParseState['Local Keywords'] 69 | current_keywords += re.findall('"(\w+)"', line) 70 | else: 71 | current_state = ParseState['PassFind'] 72 | sv.total_variants[current_pass].add(" ".join(current_keywords)) 73 | 74 | elif current_state == ParseState['Local Keywords']: 75 | current_state = ParseState['PassFind'] 76 | sv.total_variants[current_pass].add(" ".join(current_keywords)) 77 | 78 | return sv 79 | 80 | # filename = "PFLitTreeOri.shader" 81 | # filename2 = "PFLitTreeSoco.shader" 82 | 83 | # sv1 = ShaderVariants.from_file(filename) 84 | # sv2 = ShaderVariants.from_file(filename2) 85 | 86 | # for k, v in sv1.total_variants.items(): 87 | # print(f'pass:{k}, len:{len(v)}') 88 | 89 | # print('------') 90 | # for k, v in sv2.total_variants.items(): 91 | # print(f'pass:{k}, len:{len(v)}') 92 | 93 | # var1=sv1.total_variants['ForwardLit'] 94 | # var2=sv2.total_variants['ForwardLit'] 95 | 96 | # var1 = dict.fromkeys(var1) 97 | 98 | # for k in var2: 99 | # if var1.get(k, None) == None: 100 | # print(k) 101 | 102 | filename = "PFLitSoco.shader" 103 | sv1 = ShaderVariants.from_file(filename) 104 | 105 | for k, v in sv1.total_variants.items(): 106 | for i in v: 107 | print(i) 108 | 109 | print('---------') -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | using UnityEngine.Rendering; 6 | 7 | #if UNITY_EDITOR 8 | using UnityEditor.Rendering; 9 | #endif 10 | 11 | namespace Soco.ShaderVariantsStripper 12 | { 13 | public enum ShaderVariantsDataShaderType 14 | { 15 | Vertex = 1, 16 | Fragment = 2, 17 | Geometry = 3, 18 | Hull = 4, 19 | Domain = 5, 20 | Surface = 6, 21 | Count = 7, 22 | RayTracing = 7, 23 | } 24 | 25 | public struct ShaderVariantsData 26 | { 27 | //ShaderSnipperData 28 | public ShaderVariantsDataShaderType shaderType; 29 | public UnityEngine.Rendering.PassType passType; 30 | public string passName; 31 | 32 | public bool inStripCallback; 33 | 34 | //ShaderCompilerData 35 | private UnityEngine.Rendering.ShaderKeywordSet shaderKeywordSet; 36 | private UnityEngine.Rendering.PlatformKeywordSet platformKeywordSet; 37 | 38 | private List _shaderKeywordList; 39 | 40 | private List shaderKeywordList 41 | { 42 | get 43 | { 44 | if (_shaderKeywordList == null) 45 | _shaderKeywordList = new List(); 46 | return _shaderKeywordList; 47 | } 48 | } 49 | 50 | #if UNITY_EDITOR 51 | public static ShaderVariantsData GetDefaultShaderVariantsData() 52 | { 53 | return new ShaderVariantsData() 54 | { 55 | inStripCallback = false, 56 | passName = "" 57 | }; 58 | } 59 | 60 | public static ShaderVariantsData GetShaderVariantsData(ShaderSnippetData shaderSnippetData, 61 | ShaderCompilerData data) 62 | { 63 | return new ShaderVariantsData() 64 | { 65 | shaderType = (ShaderVariantsDataShaderType)shaderSnippetData.shaderType, 66 | passType = shaderSnippetData.passType, 67 | passName = shaderSnippetData.passName, 68 | shaderKeywordSet = data.shaderKeywordSet, 69 | platformKeywordSet = data.platformKeywordSet, 70 | inStripCallback = true 71 | }; 72 | } 73 | #endif 74 | 75 | public void EnableKeyword(ShaderKeyword keyword) 76 | { 77 | if (inStripCallback) 78 | shaderKeywordSet.Enable(keyword); 79 | else 80 | { 81 | if (shaderKeywordList.FindIndex(k => k == keyword.GetKeywordName()) < 0) 82 | { 83 | shaderKeywordList.Add(keyword.GetKeywordName()); 84 | shaderKeywordList.Sort(); 85 | } 86 | } 87 | } 88 | 89 | public void DisableKeyword(ShaderKeyword keyword) 90 | { 91 | if (inStripCallback) 92 | shaderKeywordSet.Disable(keyword); 93 | else 94 | shaderKeywordList.Remove(keyword.GetKeywordName()); 95 | } 96 | 97 | public void DisableKeyword(string keyword) 98 | { 99 | if (inStripCallback) 100 | { 101 | shaderKeywordSet.Disable(new ShaderKeyword(keyword)); 102 | } 103 | else 104 | shaderKeywordList.Remove(keyword); 105 | } 106 | 107 | public bool IsKeywordEnabled(ShaderKeyword keyword) 108 | { 109 | if (inStripCallback) 110 | return shaderKeywordSet.IsEnabled(keyword); 111 | else 112 | return shaderKeywordList.FindIndex(k => k == keyword.GetKeywordName()) >= 0; 113 | } 114 | 115 | public bool IsKeywordEnabled(string keyword, Shader shader = null) 116 | { 117 | if (inStripCallback) 118 | return shaderKeywordSet.IsEnabled(new ShaderKeyword(keyword)) || 119 | (shader != null && shaderKeywordSet.IsEnabled(new ShaderKeyword(shader, keyword))); 120 | else 121 | return shaderKeywordList.FindIndex(k => k == keyword) >= 0; 122 | } 123 | 124 | public string[] GetShaderKeywords() 125 | { 126 | if (inStripCallback) 127 | return (from sk in shaderKeywordSet.GetShaderKeywords() select sk.GetKeywordName()).ToArray(); 128 | else 129 | return shaderKeywordList.ToArray(); 130 | } 131 | 132 | public ShaderKeyword[] GetShaderKeywordsObjectArray() 133 | { 134 | if (inStripCallback) 135 | return shaderKeywordSet.GetShaderKeywords(); 136 | else 137 | { 138 | Debug.LogError("ShaderKeywordSet cannot be used outside of IPreprocessShaders"); 139 | return null; 140 | } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/README.md: -------------------------------------------------------------------------------- 1 | # 变体剔除工具 2 | 3 | ### 1. 开启界面 4 | 5 |   菜单栏Tools/Soco/ShaderVariantsStripper/OpenStripperWindow打开窗口。 6 | 7 | ![](Images/1.%E5%BC%80%E5%90%AF%E7%95%8C%E9%9D%A2.png) 8 | 9 | ### 2. 创建配置文件 10 | 11 |   在Project窗口右键Create/Soco/ShaderVariantsStripper/Create Config创建配置文件,根据需要可以创建若干了,例如我创建了Global(对全局应用)、Effect(特效Shader)、Scene(场景Shader)、Role(角色Shader)、General(其他Shader)。 12 | 13 | ![](Images/2.%E5%88%9B%E5%BB%BA%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6.png) 14 | 15 | ### 3. 全局配置 16 | 17 |   选择好配置文件后,左侧Global Setting View和Shader View出现内容。点击Global Setting View中的全局设置按钮,右侧会出现全局设置栏,可添加全局条件。 18 | 19 | ![](Images/3.%E5%85%A8%E5%B1%80%E8%AE%BE%E7%BD%AE.png) 20 | 21 |   此工具的设计中,当前配置文件中Shader View没有Shader时,全局设置应用于项目所有Shader;当前配置文件中Shader View有Shader时,全局设置应用于当前配置文件中Shader View中的Shader。如果需要修改逻辑,可以查看ShaderVariantsStripperCode.cs 22 | 23 | ### 4. Shader配置 24 | 25 |   左侧下方可单独对shader配置,首先添加shader,可以用直接选择Shader,然后点添加按钮。 26 | 27 | ![](Images/4.%E9%80%89%E6%8B%A9Shader1.png) 28 | 29 |   为了方便,也可以通过输入ShaderName添加Shader。 30 | 31 | ![](Images/5.%E6%B7%BB%E5%8A%A0Shader2.png) 32 | 33 |   点击Shader后,右侧会出现当前Shader的剔除or保留条件。 34 | 35 | ### 5. 条件窗口 36 | 37 | ##### 5.1 条件类型 38 | ###### 5.1.1 PassType是 39 | 40 |   条件窗口中可选择和添加条件,例如我们选 __“PassType是”__ 这一条件,然后选择添加: 41 | 42 | ![](Images/6.%E6%B7%BB%E5%8A%A0%E6%9D%A1%E4%BB%B6.png) 43 | ![](Images/7.%E6%B7%BB%E5%8A%A0%E6%9D%A1%E4%BB%B62.png) 44 | 45 |   在新出现的选项中点击绿色的 __“保留”__ 按钮,变成红色的 __“剔除”__ 按钮,然后点击中间最大的 __“当Pass类型是Normal时”__ 按钮,在弹出的新窗口中,将PassType选择为 __“Deferred”__ 。 46 | ![](Images/8.%E4%BF%AE%E6%94%B9%E6%9D%A1%E4%BB%B6.png) 47 | 48 |   现在的含义是,当满足pass是Deferred这一条件时,剔除当前变体。 49 | 50 | ###### 5.1.2 包含Keyword或集合 51 | 52 |   可以选择包含或不包含,也可以限定PassType。 53 | 54 |   除此外还有其他条件,包含Keyword或集合,例如我们明确打包的物体不需要支持Instance,即可添加一个条件,当包含INSTANCING_ON这一keyword时,直接剔除变体: 55 | 56 | ![](Images/9.%E5%BD%93%E5%8C%85%E5%90%ABkeyword%E6%97%B6.png) 57 | 58 |   点击“当前keyword”下方的按钮可删除keyword。 59 | 60 |   此条件可以添加多个Keyword,用于满足同时存在多个Keyword变体的情况,例如如果我们希望“中质量”关键字和“法线纹理”关键字不会同时存在,则将两个关键字都加入到条件中,然后选择剔除。 61 | 62 | ###### 5.1.3 多个条件 63 | 64 | ![](Images/13.%E6%B7%BB%E5%8A%A0%E6%9D%A1%E4%BB%B63.png) 65 | 66 |   同时满足多个条件才会生效的复合条件; 67 | 68 |   比如确定GBufferPass不需要法线压缩,如果将条件分开为:当前Pass是GBufferPass和当前Pass有法线压缩,则无法判断上述情况,所以需要一个复合条件来判断。 69 | 70 |   复合条件可以有多个。 71 | 72 | ##### 5.2 优先级 73 | 74 |   如果按照上面说,将项目中包含instance关键字的变体全部剔除,但对于草的shader又希望保留,则利用优先级功能: 75 | 76 | ![](Images/10.%E4%BC%98%E5%85%88%E7%BA%A7.png) 77 | 78 |   剔除/保留按钮后面的数字就是优先级,默认是0,数字越大,优先级越高,优先级为5的shader设置覆盖了优先级为0的全局设置,使变体得以保留。 79 | 80 |   优先级只会覆盖相同的条件,而剔除时,只要存在有剔除的条件没有被保留覆盖,就会剔除掉变体。 81 | 82 | ### 6. 剔除检查 83 | 84 |   当配置和条件变得繁杂时,可能无法快速判断一个变体是否会被此工具剔除掉,因此提供这个功能。 85 | 86 |   当配置文件没有选中时,配置选择下方会出现 __“剔除检查”__ 按钮,单击后右侧会出现对应选项。 87 | 88 | ![](Images/12.%E5%89%94%E9%99%A4%E6%A3%80%E6%9F%A5.png) 89 | 90 |   选择好Shader、ShaderType、PassType,以及组成变体的Keyword,点击测试剔除,下方会显示所有配置中满足条件的项,点击后可以在工程中定位到Config文件。 91 | 92 | ### 7. 自定义条件 93 | 94 |   此工具的优势就是可拓展性,可以自定义条件。 95 | 96 |   需要继承自`ShaderVariantsStripperCondition`接口,实现以下方法: 97 | 98 |
99 | 100 | `public bool Completion(Shader shader, ShaderVariantsData data)` 101 | 102 |   `Completion`方法用于判断当前变体是否满足条件,传入的`ShaderVariantsData`对象包含当前变体的信息。 103 | 104 | `public bool EqualTo(ShaderVariantsStripperCondition other)` 105 | 106 |   `EqualTo`方法用于判断两个条件是否相同,用于做优先级覆盖。 107 | 108 | `public string Overview()` 109 | 110 |   `Overview`方法用于概览条件信息,也就是条件窗口上按钮上显示的字样。 111 | 112 | `OnGUI(ShaderVariantsStripperConditionOnGUIContext context)` 113 | 114 |   `OnGUI`方法用于点击按钮后出现的窗口如何显示UI。 115 | 116 | `public string GetName()` 117 | 118 |   `GetName`方法用于,当选择添加条件时,出现的字样。 119 | 120 |
121 | 122 |   当实现上述接口后,重新打开窗口,编辑器会自动根据反射信息获取条件,出现在条件选择添加的下拉菜单中。 123 | 124 | ### 8. 类名、名空间、Assembly修改 125 | 126 |   有时开发者希望迁移代码,例如从Assets Package化,可能会修改`ShaderVariantsStripperCondition`及实现类的类名、修改namespace、改变Assembly。由于序列化接口实现对象需要用到`SerializeReference`,当修改这些后,已经编辑好的Config文件可能无法匹配前后的类型(至少Unity2019-2021都是如此),如果在编辑器Project窗口点击Config,甚至会导致编辑器卡死。 127 | 128 |   如果你的项目资产是文本类型,可以直接用文本方式编辑资产中记录的类名、namespace、Assembly,如果项目资产是二进制,工具提供配置导出、导入为Json功能: 129 | 130 | ![](Images/11.%E8%AF%BB%E5%86%99Json%E5%BA%8F%E5%88%97%E5%8C%96%E6%96%87%E4%BB%B6.png) 131 | 132 |   序列化Config会将Json序列化数据保存到Config同目录同名Json文件中。 133 | 134 |   选择读取路径后,下方会显示当前读取路径,选择读取Json会将Json反序列化到当前Config中,这是覆盖保存,所以注意提前备份好Config。 135 | 136 |   对于修改类名等情况,可以提前Json序列化好配置文件,Json中会保存`SerializeReference`对象的类型、名空间、Assembly,可以用修改Json文件中相关名称,然后修改Unity中类型名称,之后从Json文件中反序列化。 137 | 138 | ### 9. 优化注意事项 139 | 140 |   因为找不到Unity Build的事件函数,无法在Build之前只进行一次Config读取,因此每次进入`OnProcessShader`函数时,都会调用`LoadConfigs`,来保证功能不出错;这个方法的逻辑,是从硬盘中读取Config文件,消耗不大,但依旧会对打包时间造成不少影响。 141 | 142 |   如果你们有Build Bundle相关代码,可以将`LoadConfigs`从`OnProcessShader`中注释掉,然后在Build Bundle前,在外部调用`ShaderVariantsStripperCode.LoadConfigs()`方法。 143 | 144 |   需要注意的是,如果注释掉`LoadConfigs`,会导致工程用Unity默认的File>Build Settings>build不走这一套剔除程序,可以在`OnProcessShader`增加一些判断逻辑,比如`sConfigs`为空时LoadConfigs,这样直接Build不会实时更新config文件,但依旧可以正常走剔除。 -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.AExistAndBNotExist.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Rendering; 3 | using UnityEditor; 4 | 5 | namespace Soco.ShaderVariantsStripper 6 | { 7 | public class ShaderVariantsStripperConditionAExistAndBNotExist : ShaderVariantsStripperCondition 8 | { 9 | private enum AccessLevel 10 | { 11 | Global, 12 | Local 13 | } 14 | 15 | public string keywordA; 16 | public string keywordB; 17 | 18 | private AccessLevel _accessLevel = AccessLevel.Global; 19 | private int _selectedKeywordIndex; 20 | private string _inputKeyword; 21 | 22 | public bool Completion(Shader shader, ShaderVariantsData data) 23 | { 24 | bool valid = keywordA != "" && keywordB != ""; 25 | 26 | bool AExist = data.IsKeywordEnabled(keywordA, shader); 27 | 28 | bool BNotExist = !data.IsKeywordEnabled(keywordB, shader); 29 | 30 | return valid && AExist && BNotExist; 31 | } 32 | 33 | public bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData) 34 | { 35 | return other.GetType() == typeof(ShaderVariantsStripperConditionAExistAndBNotExist) && 36 | (other as ShaderVariantsStripperConditionAExistAndBNotExist).keywordA == this.keywordA && 37 | (other as ShaderVariantsStripperConditionAExistAndBNotExist).keywordB == this.keywordB; 38 | } 39 | 40 | #if UNITY_EDITOR 41 | public string Overview() 42 | { 43 | return $"当Keyword<{keywordA}>存在,且<{keywordB}>不存在时"; 44 | } 45 | 46 | public void OnGUI(ShaderVariantsStripperConditionOnGUIContext context) 47 | { 48 | EditorGUILayout.BeginVertical(); 49 | if (context.shader != null) 50 | { 51 | #region 选择添加 52 | EditorGUILayout.BeginHorizontal(); 53 | float width = 54 | EditorGUIUtility.currentViewWidth * 0.25f; 55 | 56 | AccessLevel newAccessLevel = (AccessLevel)EditorGUILayout.Popup((int)_accessLevel, new string[] { "Global", "Local" }, GUILayout.Width(width)); 57 | 58 | if (newAccessLevel != _accessLevel) 59 | { 60 | _selectedKeywordIndex = 0; 61 | _accessLevel = newAccessLevel; 62 | } 63 | 64 | if (_accessLevel == AccessLevel.Global && context.globalKeywords.Length > 0 || 65 | _accessLevel == AccessLevel.Local && context.localKeywords.Length > 0) 66 | { 67 | _selectedKeywordIndex = EditorGUILayout.Popup(_selectedKeywordIndex, 68 | _accessLevel == AccessLevel.Global ? context.globalKeywords : context.localKeywords, GUILayout.Width(width)); 69 | 70 | string selectedKeyword = _accessLevel == AccessLevel.Global ? context.globalKeywords[_selectedKeywordIndex] : context.localKeywords[_selectedKeywordIndex]; 71 | 72 | if (GUILayout.Button("设置为A", GUILayout.Width(width))) 73 | { 74 | keywordA = selectedKeyword; 75 | } 76 | if (GUILayout.Button("设置为B", GUILayout.Width(width))) 77 | { 78 | keywordB = selectedKeyword; 79 | } 80 | } 81 | EditorGUILayout.EndHorizontal(); 82 | #endregion 83 | 84 | EditorGUILayout.Space(20); 85 | } 86 | 87 | #region 输入添加 88 | EditorGUILayout.BeginHorizontal(); 89 | 90 | _inputKeyword = EditorGUILayout.TextField("输入Keyword", _inputKeyword, GUILayout.Width(EditorGUIUtility.currentViewWidth * 0.5f)); 91 | if (GUILayout.Button("设置为A", GUILayout.Width(EditorGUIUtility.currentViewWidth * 0.25f)) && _inputKeyword != null) 92 | { 93 | string[] inputKeywords = _inputKeyword.Split(' '); 94 | 95 | foreach (var keyword in inputKeywords) 96 | { 97 | if (keyword != "" || _inputKeyword == "") 98 | { 99 | keywordA = keyword; 100 | } 101 | } 102 | } 103 | if (GUILayout.Button("设置为B", GUILayout.Width(EditorGUIUtility.currentViewWidth * 0.25f)) && _inputKeyword != null) 104 | { 105 | string[] inputKeywords = _inputKeyword.Split(' '); 106 | 107 | foreach (var keyword in inputKeywords) 108 | { 109 | if (keyword != "" || _inputKeyword == "") 110 | { 111 | keywordB = keyword; 112 | } 113 | } 114 | } 115 | 116 | EditorGUILayout.EndHorizontal(); 117 | #endregion 118 | 119 | EditorGUILayout.Space(20); 120 | 121 | #region 显示Keyword 122 | EditorGUILayout.LabelField($"当包含Keyword<{keywordA}>,却不包含Keyword<{keywordB}>时"); 123 | #endregion 124 | EditorGUILayout.EndVertical(); 125 | } 126 | 127 | public string GetName() 128 | { 129 | return "KeywordA 存在 且 KeywordB不存在"; 130 | } 131 | #endif 132 | } 133 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.MultiCondition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using UnityEngine; 6 | using UnityEngine.Rendering; 7 | using UnityEditor; 8 | using UnityEngine.Scripting.APIUpdating; 9 | 10 | namespace Soco.ShaderVariantsStripper 11 | { 12 | public class ShaderVariantsStripperConditionMultiCondition : ShaderVariantsStripperCondition 13 | { 14 | [System.Serializable] 15 | private struct CondtionWrap 16 | { 17 | public CondtionWrap(ShaderVariantsStripperCondition condition) 18 | { 19 | this.condition = condition; 20 | } 21 | [SerializeReference] public ShaderVariantsStripperCondition condition; 22 | } 23 | 24 | [SerializeField] private List conditionList = new List(); 25 | 26 | public bool Completion(Shader shader, ShaderVariantsData data) 27 | { 28 | foreach (var condition in conditionList) 29 | { 30 | if (!condition.condition.Completion(shader, data)) 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | public bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData) 38 | { 39 | if (other.GetType() != typeof(ShaderVariantsStripperConditionMultiCondition)) 40 | return false; 41 | 42 | ShaderVariantsStripperConditionMultiCondition otherCondition = 43 | other as ShaderVariantsStripperConditionMultiCondition; 44 | 45 | if (otherCondition.conditionList.Count != this.conditionList.Count) 46 | return false; 47 | 48 | for (int i = 0; i < this.conditionList.Count; ++i) 49 | { 50 | if (!otherCondition.conditionList[i].condition.EqualTo(this.conditionList[i].condition, variantData)) 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | 57 | #if UNITY_EDITOR 58 | 59 | public string Overview() 60 | { 61 | if (conditionList.Count == 0) 62 | return "满足多个条件,当前条件为空"; 63 | else if (conditionList.Count == 1) 64 | return $"满足多个条件,当前只有一个条件:{conditionList[0].condition.Overview()}"; 65 | else 66 | return $"同时满足以下条件:{string.Join(", ", (from condition in conditionList select condition.condition.Overview()))}"; 67 | } 68 | 69 | private const float DividerHeight = 2f; 70 | private static Color dividerColor = Color.gray; 71 | private void DrawDivider() 72 | { 73 | Rect rect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(DividerHeight)); 74 | 75 | EditorGUI.DrawRect(rect, dividerColor); 76 | GUILayout.Space(DividerHeight); 77 | } 78 | 79 | private int mSelectedCondition = 0; 80 | private static Type[] _ConditionTypes = new Type[0]; 81 | private static string[] _ConditionNames = null; 82 | 83 | private static Type[] sConditionTypes 84 | { 85 | get 86 | { 87 | if (_ConditionNames == null) 88 | SetupConditionType(); 89 | return _ConditionTypes; 90 | } 91 | } 92 | 93 | private static string[] sConditionNames 94 | { 95 | get 96 | { 97 | if (_ConditionNames == null) 98 | SetupConditionType(); 99 | return _ConditionNames; 100 | } 101 | } 102 | private static void SetupConditionType() 103 | { 104 | _ConditionTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany( 105 | assembly => assembly.GetTypes()).Where( 106 | type => typeof(ShaderVariantsStripperCondition).IsAssignableFrom(type) && !type.IsAbstract 107 | ).ToArray(); 108 | 109 | _ConditionNames = _ConditionTypes.Select(type => (System.Activator.CreateInstance(type) as ShaderVariantsStripperCondition).GetName()).ToArray(); 110 | } 111 | 112 | private Vector2 mScrollPosition = Vector2.zero; 113 | 114 | public void OnGUI(ShaderVariantsStripperConditionOnGUIContext context) 115 | { 116 | EditorGUILayout.BeginVertical(); 117 | 118 | mScrollPosition = EditorGUILayout.BeginScrollView(mScrollPosition); 119 | int removeIndex = -1; 120 | for (int i = 0; i < conditionList.Count; ++i) 121 | { 122 | var condition = conditionList[i]; 123 | 124 | EditorGUILayout.LabelField(condition.condition.Overview()); 125 | condition.condition.OnGUI(context); 126 | if (GUILayout.Button("删除此条件") && EditorUtility.DisplayDialog("Confirm", "确定删除吗?", "Yes", "No")) 127 | { 128 | removeIndex = i; 129 | } 130 | 131 | DrawDivider(); 132 | } 133 | if (removeIndex != -1) 134 | conditionList.RemoveAt(removeIndex); 135 | EditorGUILayout.EndScrollView(); 136 | 137 | mSelectedCondition = EditorGUILayout.Popup(mSelectedCondition, sConditionNames); 138 | if (GUILayout.Button("添加条件")) 139 | { 140 | if (mSelectedCondition >= sConditionTypes.Length) 141 | { 142 | Debug.LogError("程序有改变,请重新打开窗口"); 143 | return; 144 | } 145 | 146 | ShaderVariantsStripperCondition condition = 147 | Activator.CreateInstance(sConditionTypes[mSelectedCondition]) as 148 | ShaderVariantsStripperCondition; 149 | conditionList.Add(new CondtionWrap(condition)); 150 | } 151 | 152 | EditorGUILayout.EndVertical(); 153 | } 154 | 155 | public string GetName() 156 | { 157 | return "多个条件同时满足"; 158 | } 159 | #endif 160 | } 161 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/ShaderVariantCollectionAddVariantWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using UnityEngine.Rendering; 8 | 9 | namespace Soco.ShaderVariantsCollection 10 | { 11 | public class ShaderVariantCollectionAddVariantWindow : EditorWindow 12 | { 13 | private static ShaderVariantCollectionAddVariantWindow m_window; 14 | public static ShaderVariantCollectionAddVariantWindow Window 15 | { 16 | get 17 | { 18 | if (m_window == null) 19 | { 20 | m_window = EditorWindow.GetWindow("AddVariantWindow"); 21 | m_window.minSize = new Vector2(480, 320); 22 | } 23 | return m_window; 24 | } 25 | } 26 | 27 | private Shader mShader; 28 | private PassType mPassType; 29 | private ShaderVariantCollectionMapper mMapper; 30 | 31 | private static MethodInfo sGetShaderGlobalKeywordsMethod = null; 32 | private static MethodInfo sGetShaderLocalKeywordsMethod = null; 33 | 34 | private string[] mShaderKeywords; 35 | private List mSelectedShaderKeywords = new List(); 36 | private int mSelectedShaderKeywordIndex = 0; 37 | 38 | private enum State 39 | { 40 | None, 41 | Success, 42 | Failure 43 | } 44 | 45 | private State mState; 46 | private string mMessage; 47 | 48 | 49 | private static void InitGetKeywordMethod() 50 | { 51 | if (sGetShaderGlobalKeywordsMethod == null) 52 | sGetShaderGlobalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderGlobalKeywords", 53 | BindingFlags.NonPublic | BindingFlags.Static); 54 | 55 | if (sGetShaderLocalKeywordsMethod == null) 56 | sGetShaderLocalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderLocalKeywords", 57 | BindingFlags.NonPublic | BindingFlags.Static); 58 | } 59 | 60 | public void Setup(Shader shader, PassType passType, ShaderVariantCollectionMapper mapper) 61 | { 62 | mShader = shader; 63 | mPassType = passType; 64 | mMapper = mapper; 65 | 66 | InitGetKeywordMethod(); 67 | 68 | string[] globalKeywords = sGetShaderGlobalKeywordsMethod.Invoke(null, new object[] { shader }) as string[]; 69 | string[] localKeywords = sGetShaderLocalKeywordsMethod.Invoke(null, new object[] { shader }) as string[]; 70 | mShaderKeywords = globalKeywords.Concat(localKeywords).ToArray(); 71 | 72 | mSelectedShaderKeywords.Clear(); 73 | mSelectedShaderKeywordIndex = 0; 74 | mState = State.None; 75 | } 76 | 77 | public void OnGUI() 78 | { 79 | EditorGUILayout.BeginVertical(); 80 | 81 | EditorGUILayout.LabelField($"当前Shader: {mShader}"); 82 | mPassType = (PassType)EditorGUILayout.EnumPopup("PassType", mPassType); 83 | 84 | #region 待添加keyword 85 | EditorGUILayout.LabelField($"待添加Keyword:"); 86 | 87 | EditorGUILayout.BeginHorizontal(); 88 | int i = 0; 89 | foreach (string keyword in mShaderKeywords) 90 | { 91 | if (i != 0 && i % 4 == 0) 92 | { 93 | EditorGUILayout.EndHorizontal(); 94 | EditorGUILayout.BeginHorizontal(); 95 | } 96 | 97 | if (!mSelectedShaderKeywords.Contains(keyword)) 98 | { 99 | if (GUILayout.Button(keyword)) 100 | { 101 | mSelectedShaderKeywords.Add(keyword); 102 | } 103 | i++; 104 | } 105 | } 106 | EditorGUILayout.EndHorizontal(); 107 | #endregion 108 | 109 | #region 当前keyword 110 | EditorGUILayout.LabelField($"当前Keyword:"); 111 | 112 | EditorGUILayout.BeginHorizontal(); 113 | for (int keywordIndex = 0; keywordIndex < mSelectedShaderKeywords.Count; ++keywordIndex) 114 | { 115 | if (keywordIndex != 0 && keywordIndex % 4 == 0) 116 | { 117 | EditorGUILayout.EndHorizontal(); 118 | EditorGUILayout.BeginHorizontal(); 119 | } 120 | 121 | if (GUILayout.Button(mSelectedShaderKeywords[keywordIndex])) 122 | { 123 | mSelectedShaderKeywords.RemoveAt(keywordIndex); 124 | } 125 | } 126 | EditorGUILayout.EndHorizontal(); 127 | #endregion 128 | 129 | if (GUILayout.Button("添加变体")) 130 | { 131 | bool newVariantSuccess = false; 132 | ShaderVariantCollection.ShaderVariant newVariant; 133 | string errorMessage = ""; 134 | 135 | try 136 | { 137 | newVariant = new ShaderVariantCollection.ShaderVariant(mShader, mPassType, 138 | mSelectedShaderKeywords.ToArray()); 139 | newVariantSuccess = true; 140 | } 141 | catch (Exception e) 142 | { 143 | newVariantSuccess = false; 144 | errorMessage = e.Message; 145 | } 146 | 147 | string keywordString = string.Join(", ", mSelectedShaderKeywords); 148 | if (newVariantSuccess) 149 | { 150 | newVariant = new ShaderVariantCollection.ShaderVariant(mShader, mPassType, 151 | mSelectedShaderKeywords.ToArray());//不重新new会因为变量可能没初始化导致编译错误 152 | if (mMapper.HasVariant(newVariant)) 153 | { 154 | mState = State.Failure; 155 | mMessage = $"变体<{mPassType}>[{keywordString}]已存在"; 156 | } 157 | else 158 | { 159 | ShaderVariantCollectionToolsWindow.Window.UndoShaderVariantCollectionTool(); 160 | if (!mMapper.AddVariant(newVariant)) 161 | { 162 | mState = State.Failure; 163 | mMessage = $"变体<{mPassType}>[{keywordString}]添加失败"; 164 | } 165 | else 166 | { 167 | mState = State.Success; 168 | mMessage = $"变体<{mPassType}>[{keywordString}]添加成功"; 169 | ShaderVariantCollectionToolsWindow.Window.RefreshPassKeywordMap(mShader); 170 | //ShaderVariantCollectionToolsWindow.Window.CollectPassKeywordMap(mMapper.GetShaderVariants(mShader)); 171 | ShaderVariantCollectionToolsWindow.Window.Repaint(); 172 | } 173 | } 174 | } 175 | else 176 | { 177 | mState = State.Failure; 178 | mMessage = $"变体<{mPassType}>[{keywordString}]创建失败,相关报错为:{errorMessage}"; 179 | } 180 | } 181 | 182 | #region 消息显示 183 | if (mState != State.None) 184 | { 185 | Color oriColor = GUI.color; 186 | GUI.color = mState == State.Success ? Color.green : Color.red; 187 | GUI.color *= 0.5f; 188 | 189 | EditorGUILayout.LabelField(mMessage, EditorStyles.whiteLabel); 190 | 191 | GUI.color = oriColor; 192 | } 193 | #endregion 194 | 195 | EditorGUILayout.EndVertical(); 196 | 197 | 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Editor/ShaderVariantsStripperCode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEditor.Build; 5 | using UnityEditor.Rendering; 6 | using UnityEngine; 7 | using System.Linq; 8 | 9 | namespace Soco.ShaderVariantsStripper 10 | { 11 | public class ShaderVariantsStripperCode : IPreprocessShaders 12 | { 13 | public int callbackOrder { get { return 0; } } 14 | 15 | private static string[] sAllPath = { "Assets", "Packages" }; 16 | private static ShaderVariantsStripperConfig[] sConfigs; 17 | 18 | private List<(ConditionPair conditionPair, ShaderVariantsStripperConfig config)> mConditionList = new List<(ConditionPair condition, ShaderVariantsStripperConfig config)>(); 19 | public static ShaderVariantsStripperConfig[] LoadConfigs() 20 | { 21 | string[] guids = AssetDatabase.FindAssets("t:ShaderVariantsStripperConfig", sAllPath); 22 | 23 | sConfigs = (from guid in guids 24 | select AssetDatabase.LoadAssetAtPath( 25 | AssetDatabase.GUIDToAssetPath(guid))) 26 | .ToArray(); 27 | 28 | return sConfigs; 29 | } 30 | 31 | public static void ClearConfigs() 32 | { 33 | sConfigs = null; 34 | } 35 | 36 | public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList data) 37 | { 38 | LoadConfigs(); 39 | 40 | ShaderCompilerData workaround = data[0]; 41 | 42 | int stripCount = 0; 43 | 44 | for (int i = data.Count - 1; i >= 0 ; --i) 45 | { 46 | mConditionList.Clear(); 47 | 48 | StripVariant(shader, snippet, data[i], sConfigs, mConditionList); 49 | 50 | bool isStrip = false; 51 | foreach (var conditionPair_fromConfig in mConditionList) 52 | { 53 | isStrip |= conditionPair_fromConfig.conditionPair.strip; 54 | if (conditionPair_fromConfig.config.mIsWhiteList) 55 | { 56 | isStrip = false; 57 | break; 58 | } 59 | } 60 | 61 | if (isStrip) 62 | { 63 | data.RemoveAt(i); 64 | stripCount++; 65 | } 66 | } 67 | 68 | Debug.Log($"Shader:{shader.name} Pass:{snippet.passType} 剔除个数:{stripCount}"); 69 | 70 | if (data.Count == 0) 71 | { 72 | Debug.Log($"Shader:{shader.name} Pass:{snippet.passType} 因剔除全部保留变体一个"); 73 | data.Add(workaround); 74 | } 75 | } 76 | 77 | //对外开放接口,用于检查keyword是否需要被剔除 78 | private static List<(ConditionPair conditionPair, ShaderVariantsStripperConfig config)> sConditionList = new List<(ConditionPair condition, ShaderVariantsStripperConfig config)>(); 79 | public static bool IsVariantStrip(Shader shader, ShaderSnippetData snippet, ShaderCompilerData data, ShaderVariantsStripperConfig[] configs) 80 | { 81 | sConditionList.Clear(); 82 | StripVariant(shader, snippet, data, sConfigs, sConditionList); 83 | 84 | return sConditionList.Any(conditionPair_fromConfig => 85 | conditionPair_fromConfig.conditionPair.strip) 86 | && sConditionList.All(conditionPair_fromConfig => 87 | !conditionPair_fromConfig.config.mIsWhiteList); 88 | } 89 | 90 | public static void StripVariant(Shader shader, ShaderSnippetData snippet, ShaderCompilerData data, 91 | ShaderVariantsStripperConfig[] configs, 92 | List<(ConditionPair conditionPair, ShaderVariantsStripperConfig config)> conditionList) 93 | { 94 | StripVariant(shader, ShaderVariantsData.GetShaderVariantsData(snippet, data), configs, conditionList); 95 | } 96 | 97 | public static void StripVariant(Shader shader, ShaderVariantsData variantData, ShaderVariantsStripperConfig[] configs, List<(ConditionPair conditionPair, ShaderVariantsStripperConfig config)> conditionList) 98 | { 99 | int FindConditionEqual(ConditionPair pair, out int index) 100 | { 101 | for (int condList_i = 0; condList_i < conditionList.Count; ++condList_i) 102 | { 103 | if (pair.condition.EqualTo(conditionList[condList_i].conditionPair.condition, variantData)) 104 | { 105 | index = condList_i; 106 | return condList_i; 107 | } 108 | } 109 | 110 | index = -1; 111 | return -1; 112 | } 113 | 114 | foreach (ShaderVariantsStripperConfig config in configs) 115 | { 116 | if (!config.mEnable) 117 | continue; 118 | 119 | bool applyGlobalConfig = true; 120 | 121 | // 如果这个配置文件中能找到当前shader,则应用配置文件中“应用global config选项” 122 | if (config.mShaderConditions.TryGetValue(shader, out ShaderVariantsItem item)) 123 | applyGlobalConfig = item.applyGlobalConfig; 124 | // 如果Shader View中没有Shader,则Global Setting应用于全体Shader 125 | else if (config.mShaderConditions.Count == 0 && !config.mIsWhiteList) 126 | applyGlobalConfig = true; 127 | else 128 | applyGlobalConfig = false; 129 | 130 | //Global condition 131 | if (applyGlobalConfig) 132 | { 133 | foreach (ConditionPair pair in config.mGlobalConditions) 134 | { 135 | if (pair.condition.Completion(shader, variantData)) 136 | { 137 | if (!config.mIsWhiteList) 138 | { 139 | //如果有相同的条件, 140 | if (FindConditionEqual(pair, out int findIndex) != -1) 141 | { 142 | //且优先级更高 143 | if(pair.priority > conditionList[findIndex].conditionPair.priority) 144 | conditionList[findIndex] = (pair, config); 145 | //优先级更低则直接丢弃 146 | } 147 | else//否则加入列表 148 | conditionList.Add((pair, config)); 149 | } 150 | else//白名单变体无需判断优先级和相等条件 151 | { 152 | conditionList.Add((pair, config)); 153 | } 154 | } 155 | } 156 | } 157 | //Shader local condition 158 | if (item != null) 159 | { 160 | foreach (ConditionPair pair in item.conditionPairs) 161 | { 162 | if (pair.condition.Completion(shader, variantData)) 163 | { 164 | if (!config.mIsWhiteList) 165 | { 166 | if (FindConditionEqual(pair, out int findIndex) != -1) 167 | { 168 | if (pair.priority > conditionList[findIndex].conditionPair.priority) 169 | conditionList[findIndex] = (pair, config); 170 | } 171 | else 172 | conditionList.Add((pair, config)); 173 | } 174 | else 175 | { 176 | conditionList.Add((pair, config)); 177 | } 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/ShaderVariantsStripperCondition.HasKeywordCombination.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEngine.Rendering; 4 | using UnityEditor; 5 | 6 | 7 | namespace Soco.ShaderVariantsStripper 8 | { 9 | public class ShaderVariantsStripperConditionHasKeywordCombination 10 | : ShaderVariantsStripperCondition 11 | { 12 | 13 | public bool include = true; 14 | public List keywords = new List(); 15 | 16 | public bool constraintPassType = false; 17 | public PassType passType = PassType.Normal; 18 | 19 | private enum AccessLevel 20 | { 21 | Global, 22 | Local 23 | } 24 | 25 | private AccessLevel _accessLevel = AccessLevel.Global; 26 | private int _selectedKeywordIndex; 27 | private string _inputKeyword; 28 | 29 | public bool Completion(Shader shader, ShaderVariantsData data) 30 | { 31 | if (constraintPassType && passType != data.passType) 32 | return false; 33 | 34 | int combinationValue = 0; 35 | foreach (string keyword in keywords) 36 | { 37 | combinationValue += data.IsKeywordEnabled(keyword, shader) 38 | ? 1 39 | : 0; 40 | } 41 | 42 | return keywords.Count == 0 43 | || (include ? combinationValue == keywords.Count : combinationValue != keywords.Count); 44 | } 45 | 46 | public bool EqualTo(ShaderVariantsStripperCondition other, ShaderVariantsData variantData) 47 | { 48 | if (other.GetType() != typeof(ShaderVariantsStripperConditionHasKeywordCombination)) 49 | { 50 | return false; 51 | } 52 | 53 | ShaderVariantsStripperConditionHasKeywordCombination otherCondition = 54 | other as ShaderVariantsStripperConditionHasKeywordCombination; 55 | 56 | //两个条件都未指定Pass 57 | bool nonConstraintPassType = this.constraintPassType == otherCondition.constraintPassType && 58 | !this.constraintPassType; 59 | 60 | //两个条件都指定Pass,且Pass相等 61 | bool constraintPassTypeAndTypeEqual = this.constraintPassType == otherCondition.constraintPassType && 62 | this.constraintPassType && this.passType == otherCondition.passType; 63 | 64 | //其中一个指定了Pass,另一个未指定,但当前环境下pass符合条件 65 | PassType constraintPassType = this.constraintPassType ? this.passType : otherCondition.passType; 66 | bool constraintPassTypeNotEqualButPassEqual = 67 | this.constraintPassType != otherCondition.constraintPassType && 68 | constraintPassType == variantData.passType; 69 | 70 | //三个条件满足之一就算指定Pass的条件相等 71 | bool passEqualCondtion = nonConstraintPassType || constraintPassTypeAndTypeEqual || 72 | constraintPassTypeNotEqualButPassEqual; 73 | 74 | if (this.keywords.Count != otherCondition.keywords.Count || !passEqualCondtion) 75 | { 76 | return false; 77 | } 78 | 79 | var set1 = new HashSet(this.keywords); 80 | var set2 = new HashSet(otherCondition.keywords); 81 | return set1.SetEquals(set2) && this.include == otherCondition.include; 82 | } 83 | 84 | #if UNITY_EDITOR 85 | public string Overview() 86 | { 87 | string passConstraint = constraintPassType ? $"({passType})" : ""; 88 | string c = include ? "" : "不"; 89 | string s = $"{passConstraint}当{c}包含Keyword<"; 90 | 91 | 92 | for (int i = 0; i < keywords.Count; ++i) 93 | { 94 | s += keywords[i]; 95 | if (i != keywords.Count - 1) 96 | s += ", "; 97 | } 98 | 99 | s += ">"; 100 | if (keywords.Count > 1) 101 | { 102 | 103 | s += $"(共{keywords.Count}个)组合"; 104 | } 105 | 106 | 107 | s += "时"; 108 | 109 | return s; 110 | } 111 | 112 | public void OnGUI(ShaderVariantsStripperConditionOnGUIContext context) 113 | { 114 | EditorGUILayout.BeginVertical(); 115 | 116 | #region 包含选项 117 | EditorGUILayout.BeginHorizontal(); 118 | 119 | string c = include ? "" : "不"; 120 | if (GUILayout.Button($"当{c}包含")) 121 | { 122 | include = !include; 123 | } 124 | EditorGUILayout.LabelField("下列keyword或keyword组合时"); 125 | 126 | EditorGUILayout.EndHorizontal(); 127 | #endregion 128 | EditorGUILayout.Space(20); 129 | 130 | if (context.shader != null) 131 | { 132 | #region 选择添加 133 | EditorGUILayout.BeginHorizontal(); 134 | float width = 135 | EditorGUIUtility.currentViewWidth * 0.33f; 136 | 137 | AccessLevel newAccessLevel = (AccessLevel)EditorGUILayout.Popup((int)_accessLevel, new string[] { "Global", "Local" }, GUILayout.Width(width)); 138 | 139 | if (newAccessLevel != _accessLevel) 140 | { 141 | _selectedKeywordIndex = 0; 142 | _accessLevel = newAccessLevel; 143 | } 144 | 145 | if (_accessLevel == AccessLevel.Global && context.globalKeywords.Length > 0 || 146 | _accessLevel == AccessLevel.Local && context.localKeywords.Length > 0) 147 | { 148 | _selectedKeywordIndex = EditorGUILayout.Popup(_selectedKeywordIndex, 149 | _accessLevel == AccessLevel.Global ? context.globalKeywords : context.localKeywords, GUILayout.Width(width)); 150 | 151 | string selectedKeyword = _accessLevel == AccessLevel.Global ? context.globalKeywords[_selectedKeywordIndex] : context.localKeywords[_selectedKeywordIndex]; 152 | 153 | if (GUILayout.Button("添加", GUILayout.Width(width)) && !keywords.Contains(selectedKeyword)) 154 | { 155 | keywords.Add(selectedKeyword); 156 | } 157 | } 158 | EditorGUILayout.EndHorizontal(); 159 | #endregion 160 | 161 | EditorGUILayout.Space(20); 162 | } 163 | 164 | #region 输入添加 165 | EditorGUILayout.BeginHorizontal(); 166 | 167 | _inputKeyword = EditorGUILayout.TextField("输入Keyword", _inputKeyword, GUILayout.Width(EditorGUIUtility.currentViewWidth * 0.66f)); 168 | if (GUILayout.Button("添加", GUILayout.Width(EditorGUIUtility.currentViewWidth * 0.33f)) && _inputKeyword != null) 169 | { 170 | string[] inputKeywords = _inputKeyword.Split(' '); 171 | 172 | foreach (var keyword in inputKeywords) 173 | { 174 | if(keyword != "" && !keywords.Contains(keyword)) 175 | keywords.Add(keyword); 176 | } 177 | } 178 | 179 | EditorGUILayout.EndHorizontal(); 180 | #endregion 181 | 182 | EditorGUILayout.Space(20); 183 | 184 | #region 显示/删除Keyword 185 | 186 | EditorGUILayout.LabelField("当前Keyword:" + (keywords.Count == 0 ? " 无" : "")); 187 | EditorGUILayout.BeginHorizontal(); 188 | 189 | const float itemWidth = 160.0f; 190 | float accumulationWidth = 0; 191 | 192 | for (int i = 0; i < keywords.Count; ++i) 193 | { 194 | if (accumulationWidth + itemWidth > 195 | EditorGUIUtility.currentViewWidth) 196 | { 197 | accumulationWidth = 0; 198 | EditorGUILayout.EndHorizontal(); 199 | EditorGUILayout.BeginHorizontal(); 200 | } 201 | 202 | if (GUILayout.Button(new GUIContent(keywords[i], keywords[i]), GUILayout.Width(itemWidth))) 203 | { 204 | keywords.RemoveAt(i); 205 | break; 206 | } 207 | 208 | accumulationWidth += itemWidth; 209 | } 210 | 211 | EditorGUILayout.EndHorizontal(); 212 | #endregion 213 | 214 | #region 限定PassType 215 | EditorGUILayout.BeginHorizontal(); 216 | constraintPassType = EditorGUILayout.ToggleLeft("条件限定指定PassType生效", constraintPassType); 217 | if (constraintPassType) 218 | { 219 | passType = (PassType)EditorGUILayout.EnumPopup("PassType", passType); 220 | } 221 | EditorGUILayout.EndHorizontal(); 222 | #endregion 223 | 224 | EditorGUILayout.EndVertical(); 225 | } 226 | 227 | public string GetName() => "包含Keyword或集合"; 228 | #endif 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/README.md: -------------------------------------------------------------------------------- 1 | # 变体收集工具 2 | 3 | ### 1. 开启菜单 4 | 5 |   顶部菜单栏Tools/Soco/ShaderVariantCollectionTools/OpenWindow 开启窗口。 6 | ![](..Images/1.开启界面.png) 7 |   **变体收集文件**:Unity原生的变体收集文件,可以对Shader变体产生引用,使变体打入Bundle,可用于预热变体。在填入当前操作变体收集文件前,将显示红色。
8 |   **工具配置文件**:当前工具的配置文件,方便保留材质收集、变体材质过滤等工具的参数,不需要每次打开都重新编写。工具打开时会自动寻找,且如果找不到时,会自动在工具目录下生成默认配置文件,并选择添加(上图的Default ShaderVariantCollection Tool Config)。 9 | 10 | ### 2. 功能选择 11 | 12 |   当变体收集文件不为空时,功能选择菜单启用。
13 |   当前工具暂时支持三类主要功能:**快速浏览、收集工具、批处理工具**。 14 | 15 | ![](..Images/2.功能选择.png) 16 | 17 |   **快速浏览**:浏览变体收集文件的内容;相比Unity原生的变体收集UI,工具的浏览能更快捷的定位shader、pass所拥有的变体,概览变体个数。
18 |   **项目收集工具**:利用项目中材质对变体的引用,收集变体,在过程中过滤材质或变体。
19 |   **批处理工具**:对收集完的变体收集文件批量处理,例如排列组合添加材质不会收集到的multi_compile变体。 20 | 21 | ### 3. 快速浏览 22 | 23 |   点击快速浏览后,次级功能选项会变成Shader View,可显示当前变体收集文件中包含的Shader。
24 |   通过选择项目Shader并点击添加,以及点击下方Shader名称后的减号,可以添加或删除Shader。通过“过滤”中的字符串,可以筛选Shader。
25 |   点击列表中的Shader名称,可以查看收集文件中,Shader拥有哪些变体。变体在右侧浏览窗口,通过PassType分好组。可以通过加号和减少增减变体。 26 | 27 | ![](..Images/3.快速浏览.png) 28 | 29 | ### 4. 收集工具 30 | 31 | #### 4.1 材质收集器 32 |   点击项目收集工具后,先进入材质收集器列表选项。
33 |   材质收集器是对象,对象的类需要实现`Soco.ShaderVariantsCollection.IMaterialCollection`接口,并实现`AddMaterialBuildDependency`这个方法,将打包所需要的材质添加到`AddMaterialBuildDependency`传入的`List`中。
34 |   下面是我所实现的事例`MaterialCollection_SceneDependency`,作用是获取所有打包场景依赖的材质: 35 | ```C# 36 | namespace Soco.ShaderVariantsCollection 37 | { 38 | //用于收集所有打包场景依赖的材质 39 | public class MaterialCollection_SceneDependency : IMaterialCollection 40 | { 41 | //是否只收集在EditorBuildSettings中enable的场景 42 | public bool collectOnlyEnable = true; 43 | public override void AddMaterialBuildDependency(IList buildDependencyList) 44 | { 45 | var sceneDependencyMaterials = EditorBuildSettings.scenes //所有场景 46 | .Where(scene => !collectOnlyEnable || scene.enabled) //是否enable 47 | .SelectMany(scene => AssetDatabase.GetDependencies(scene.path)) //获取场景依赖的所有资源 48 | .Where(dependencyAsset => dependencyAsset.EndsWith(".mat")) //获取资源中的材质 49 | .Distinct() //去重 50 | .Select(matPath => AssetDatabase.LoadAssetAtPath(matPath)); 51 | 52 | buildDependencyList.AddRange(sceneDependencyMaterials); 53 | } 54 | } 55 | } 56 | ``` 57 |   不同项目可按照需要实现接口,比如某些项目用资源表决定有哪些材质会打入包中,就可以实现一个类,专门读取资源表获取资源,然后获取资源引用的材质,或资源本身就是材质,下边是我们项目的实现:
58 | ```C# 59 | //用于获取资源表所引用的资源 60 | //注意,这个类并不在Soco.ShaderVariantsCollection名空间下,因为理想中,这个类是依赖于工程,而非工具的功能,所以继承时用到了类包含namespace的全名 61 | public sealed class MaterialCollection_DependRes : Soco.ShaderVariantsCollection.IMaterialCollection 62 | { 63 | public override void AddMaterialBuildDependency(IList buildDependencyList) 64 | { 65 | //获取资源表中所有资源 66 | List resList = ResConfigFileEnter.GetConfigFile(); 67 | foreach (string res in resList) 68 | { 69 | //如果资源本身是材质,则直接添加到列表中 70 | if (res.EndsWith(".mat")) 71 | { 72 | Material mat = AssetDatabase.LoadAssetAtPath(res); 73 | if(mat != null) 74 | buildDependencyList.Add(mat); 75 | } 76 | //如果不是材质,则找到资源所引用的材质添加到列表中 77 | else 78 | { 79 | foreach (string depRes in AssetDatabase.GetDependencies(res)) 80 | { 81 | if (depRes.EndsWith(".mat")) 82 | { 83 | Material mat = AssetDatabase.LoadAssetAtPath(depRes); 84 | if(mat != null) 85 | buildDependencyList.Add(mat); 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | ``` 93 |   工具打开时会获取所有实现`IMaterialCollection`接口的类,可以通过点击“添加收集材质回调”按钮,实例化实现类,对象和对象的参数会保存在工具配置文件中。
94 |   “**删除**”按钮可以删除收集器对象,**使用**表示下次点击“**收集材质**”时,是否会用到这个收集器对象。
95 |   下方绘制了实现类的成员,可通过修改数值使对象收集不同的材质,例如我的一个材质收集器实现类,可以收集某些路径下所有材质,成员变量是一个路径,通过修改成员的数值,就可以收集不同路径下的材质。 96 | ![](..Images/4.材质收集器列表.png) 97 | 98 | #### 4.2 收集变体 99 |   搞定好收集器后,先确认是否要覆盖原有文件,还是在原有文件上添加内容,可通过次级功能选项“**Collection View**”下的复选框修改。
100 |   收集变体分为三步:**收集材质、材质变体转化、写入收集文件**。在次级功能菜单**Collection View**中可以单步运行,也可以直接点击“一键变体收集”运行三步。
101 |   **收集材质**是通过4.1描述的材质收集器,在项目中收集材质。
102 |   **材质变体转化**是将材质存储的shader keyword,转换为一个或多个shader变体。
103 |   **写入收集文件**是将转换得到的变体写入变体收集文件。 104 | 105 | #### 4.3 材质、变体过滤 106 | ![](..Images/5.变体过滤器列表.png) 107 |   上述收集器会按照规则收集材质,然后转换变体,但有时会得到我们不希望得到的变体;
108 |   例如一个URP管线的项目,肯定不希望得到`Standard`这个Shader实例化出来的材质,如果按照引用规则收集,很多模型默认的内嵌材质会导致收集到。
109 |   或者一个Forward管线也可能收集到`Deferred`或`Meta`这些Pass的变体。
110 |   理所当然的,可以在收集器的逻辑中,屏蔽掉某个Shader的所有材质,但如果这样,需要在每个收集器的逻辑里添加屏蔽代码,需知材质收集器不止一种,屏蔽需求也不止一个,所以我添加了过滤器这一功能,将收集和过滤分离开来。
111 |   和收集器一样,需要实现接口来实现过滤器。材质过滤器需要继承`IMaterialFilter`接口,并实现`Filter`方法: 112 | ```C# 113 | public abstract class IMaterialFilter : ScriptableObject 114 | { 115 | //return true will save and false will strip 116 | public abstract bool Filter(Material material, List collections); 117 | } 118 | ``` 119 |   方法将传入材质,以及收集到材质的收集器,方法需要返回bool类型参数,返回true这个材质将保留,false则会使材质被剔除。
120 |   类似的,变体的过滤器需要继承`IVariantFilter`接口,并实现`Filter`方法: 121 | ```C# 122 | public abstract class IVariantFilter : ScriptableObject 123 | { 124 | //return true will save and false will strip 125 | public abstract bool Filter(ShaderVariantCollection.ShaderVariant variant); 126 | } 127 | ``` 128 |   工具自带实现了两个变体过滤器:可以剔除或只保留指定Shader的`VariantFilter_Shader`,可以剔除指定Pass的`VariantFilter_PassStrip`。
129 |   材质过滤器将在材质收集阶段后应用,变体过滤器将在材质-变体转换阶段后应用。 130 | 131 |   注: 不需要的变体自然能通过Unity的变体剔除来剔除,这里的做法只是为了保证变体收集文件的整洁性。 132 | 133 | #### 4.4 自带收集器与过滤器使用说明 134 | 材质收集器: 135 | 136 | ① `MaterialCollection_SceneDependency`获取所有在BuildSetting中的场景,所引用的材质。
137 | 成员`collectOnlyEnable`指定是否只收集在BuildSetting中打勾的场景。 138 | 139 | ② `MaterialCollection_TotalMaterial`获取指定目录下的所有材质。
140 | 有两种指定文件夹的形式,利用`pathMode`指定;
141 | 其一是Asset,可以拖动文件夹到`mFolders`数组中,这样的缺陷是,`Assets`和`Packages`目录无法拖动;
142 | 其二是String,可以将路径字符串指定到`mIncludePath`数组中,路径的起始从项目根路径开始,也就是类似`Assets\Res`这种形式。 143 | 144 | ③ `MaterialCollection_AssignMaterial`指定收集某几个材质
145 | 将材质拖到materials数组中即可 146 | 147 | 变体过滤器: 148 | 149 | ① `VariantFilter_Shaderp`剔除或只保留指定shader。
150 | 将需要操作的Shader指定到`mShaders`数组中,mode指定模式。
151 | 当模式为Strip时,将剔除收集到变体中,shader处于`mShaders`数组中的变体。
152 | 当模式为OnlyReserveContains时,将只保留shader处于`mShaders`数组中的变体。
153 | 只保留模式可以用来只收集某一shader的变体,这样不会减少收集时间,但非覆盖模式下,可以只收集几个Shader,应对需要的场景。 154 | 155 | ② `VariantFilter_PassStrip`剔除指定Pass。
156 | 将需要剔除的PassType指定到`mStripPasses`数组中即可 157 | 158 | 材质过滤器因没有需求,暂时没提供默认实现类。 159 | 160 | ### 5. 批处理工具 161 |   收集工具将变体收集、写入到变体文件中后,如果有对变体批量处理的需求,则会用到这一功能。
162 |   通过功能选择的批处理工具按钮会进入到批处理工具页面,当前有三个功能:**批处理执行器列表**、**合并文件** 163 | 164 | #### 5.1 批处理执行器列表 165 |   当需要自定义批处理功能时,可按照接口实现执行器。
166 |   这里举例一个使用场景:前面收集器收集到的是所有材质记录的keywords,这个参数是通过Material.EnableKeyword添加,Material.DisableKeyword去除,多数情况下,需要材质来开启关闭的keyword,用shader_feature声明,而雾效、阴影这些全局效果关键字,用multi_compile声明。
167 |   打包时的规则是,获取引用到的所有变体组合的shader_feature部分(材质和变体收集文件会对变体产生引用),然后和shader的multi_compile keyword排列组合;通过材质获取的keyword组合,能保证打包不会丢变体,但由于没有收集、组合multi_compile keyword,会导致无法正确预热。
168 |   此时就需要用批处理工具,将multi_compile部分与已经收集的变体排列组合,并添加到变体收集文件中,使变体正确预热。 169 | 170 |   和材质收集器、材质变体过滤器类似,批处理执行器需要实现`IExecutable`接口,并实现`Execute`方法。 171 | ```C# 172 | public abstract class IExecutable : ScriptableObject 173 | { 174 | public abstract void Execute(ShaderVariantCollectionMapper mapper); 175 | } 176 | ``` 177 |   方法会将当前正在处理的变体收集文件的包装器传入(因为Unity原生的变体收集文件提供的接口太少了),这个包装器类提供了Shader、变体是否存在,以及添加、删除Shader和变体的接口。 178 | ![](..Images/7.执行器列表.png) 179 |   执行器的列表中,会多出“执行”和“全部执行”按钮;执行会执行当前执行器,不管执行器的“使用”选项是否勾选;而全部执行会执行所有勾选“使用”的执行器。 180 | 181 | #### 5.2 自带执行器使用说明 182 | 183 | ① `VariantKeywordCombination`
184 | 添加keyword声明组,排列组合后,与现有变体再组合,写入到变体收集文件中。
185 | 变体声明组就是Shader中的`#pragma multi_compile _ A B`,这样默认keyword`_`和`A`、`B`就是一个声明组。 186 | Shader参数指定需要组合的Shader。
187 | 添加keyword声明组可以通过最下面的加号`+`,然后会出现新的一行,包含`+`和`-`,`-`会去掉当前变体声明组,`+`会添加keyword。
188 | 添加的keyword会根据选择模式变化,选择模式共3个`Custom`、`Default`、`DeclareStatement`。
189 | 当模式为`Custom`时,点击`+`会将选择模式右侧选择的变体添加到当前声明组中。
190 | 当模式为`Default`时,点击`+`会将默认keyword`_`(代表全下划线keyword,无论shader中用了几个下划线声明都一样)添加到当前声明组中。
191 | 当模式为`DeclareStatement`时,右侧可以输入声明字符串(类似`#pragma multi_compile _ A B`),点击`+`会解析字符串,并将解析出的声明组添加到当前声明组,这一选项方便直接从shader复制语句。
192 | 声明字符串只支持`multi_compile`,无论是否有local,支持instance(`multi_compile_instancing`)、fog、particle这些build_in声明,暂不支持`multi_compile_fwdbase`等其他build_in声明,如需要可改代码。
193 | 当shader不为空时,可以通过“尝试收集声明组”按钮,读取shader文件尝试解析文件内容,获取声明组,这样能加快设置速度,但需要人工排查是否正确收集,否则很可能收集到不想要的声明组。 194 | ![](..Images/8.变体声明组合.png) 195 | 196 | ② `SocoVariantStripAssociate`
197 | 与[Soco变体剔除工具](https://github.com/crossous/SocoTools/tree/main/SocoShaderVariantsStripper)联动,可以利用剔除工具的逻辑,将变体收集文件中,不会打入bundle的变体剔除掉,精简变体收集文件。
198 | 因为依赖于变体剔除工具,所以当变体剔除工具不存在时,可以将这个类去掉。 199 | 200 | #### 5.3 合并文件 201 | ![](..Images/6.合并文件.png) 202 |   作用是将新放入的文件的内容,合并到左侧的变体收集文件中。 203 | 204 | #### 5.4 分割文件 205 | ![](..Images/9.分割文件.png) 206 |   有时候会有需求将现有文件分割成多份,比如低端设备初次进入游戏,会在预热时卡住一段时间,希望能在预热时显示进度条,此时就会希望分割文件。
207 |   首先设置路径,默认是源文件位置。然后设置每个文件最多多少个变体。
208 |   然后选择切割模式,其一是每个文件固定变体数量。
209 |   选框可以决定分割的最小单位,如果可以分割Shader也可以分割Pass,则最小单位就是变体;否则如果可以分割Shader但不能分割Shader中的Pass,则最小分割单位是Pass;最后就是不可分割Shader,则不会分割Shader。
210 |   第二个切割模式是按照固定文件数量切割,变体均匀分布在每个文件中,这样方便配置资源表。 211 | 212 | #### 5.5 排除变体 213 | ![](..Images/10.排除变体.png) 214 |   有时有需求跑变体收集,例如游戏需要先预热新手流程前30分钟的变体。 215 |   但Unity的变体收集会收集到不想要的变体,这时候可以先用标准流程收集到打包变体,然后切换收集到的变体为操作文件,利用排除变体功能,将不在标准收集文件中的变体删除掉,相当于做了一个交集(Intersection)操作,这样可以保证前30min收集到的变体都是干净的。 216 | 217 | 218 | ### 6. 自定义编辑器 219 |   可以发现材质收集器、材质过滤器、变体过滤器、执行器的界面大多相似,因为它们的实现方式都是继承自某个抽象类然后实现抽象方法。
220 |   界面都是绘制各个对象的成员,如果希望自定义编辑器,工具提供了相关抽象。例如上面的执行器自身的定义是这样: 221 | ```C# 222 | public class VariantKeywordCombination : IExecutable 223 | { 224 | //something 225 | } 226 | ``` 227 |   那么自定义编辑器可以这样定义: 228 | ```C# 229 | [ShaderVariantCollectionToolEditor(typeof(VariantKeywordCombination))] 230 | class VariantKeywordCombinationEditor : ShaderVariantCollectionToolEditor 231 | { 232 | public void OnEnable() 233 | { 234 | //something 235 | } 236 | 237 | public override void OnInspectorGUI() 238 | { 239 | //something 240 | } 241 | } 242 | ``` 243 |   `ShaderVariantCollectionToolEditor`这个类继承自Unity的`Editor`类,所以相关的事件方法都可以实现,界面打开时会自动寻找有没有实现当前对象类的编辑器,如果没有就绘制类的成员,如果有则按照实现的编辑器代码绘制。 -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/ShaderVariantCollectionMaterialVariantConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Soco.ShaderVariantsCollection; 6 | using UnityEditor; 7 | using UnityEngine; 8 | using UnityEngine.Rendering; 9 | 10 | public class ShaderVariantCollectionMaterialVariantConverter 11 | { 12 | //Key-材质 13 | //Value-材质被哪个收集器采集 14 | private Dictionary> mMaterials = new Dictionary>(); 15 | 16 | //用于缓存Shader包含哪些keyword 17 | private Dictionary mCachedKeywords = new Dictionary(); 18 | 19 | //存储已经添加的变体 20 | //列表存放的是,去重、排序后组合成的字符串,当字符串加入到列表中,代表这一keyword序列所有可能的变体都被加入到变体列表中 21 | private Dictionary> mInsertedVariants = new Dictionary>(); 22 | 23 | private List mVariants = new List(); 24 | 25 | //获取shader keyword方法,需要反射获取 26 | private static MethodInfo _GetShaderGlobalKeywordsMethod = null; 27 | private static MethodInfo _GetShaderLocalKeywordsMethod = null; 28 | 29 | private static MethodInfo GetShaderGlobalKeywordsMethod 30 | { 31 | get 32 | { 33 | if (_GetShaderGlobalKeywordsMethod == null) 34 | _GetShaderGlobalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderGlobalKeywords", 35 | BindingFlags.NonPublic | BindingFlags.Static); 36 | 37 | return _GetShaderGlobalKeywordsMethod; 38 | } 39 | } 40 | 41 | private static MethodInfo GetShaderLocalKeywordsMethod 42 | { 43 | get 44 | { 45 | if (_GetShaderLocalKeywordsMethod == null) 46 | _GetShaderLocalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderLocalKeywords", 47 | BindingFlags.NonPublic | BindingFlags.Static); 48 | 49 | return _GetShaderLocalKeywordsMethod; 50 | } 51 | } 52 | 53 | private static string[] GetShaderKeywords(Shader shader) 54 | { 55 | string[] globalKeywords = GetShaderGlobalKeywordsMethod.Invoke(null, new object[] { shader }) as string[]; 56 | string[] localKeywords = GetShaderLocalKeywordsMethod.Invoke(null, new object[] { shader }) as string[]; 57 | 58 | return globalKeywords.Concat(localKeywords).ToArray(); 59 | } 60 | 61 | public bool CollectMaterial(IEnumerable collectors) 62 | { 63 | mMaterials.Clear(); 64 | List materials = new List(); 65 | 66 | bool success = false; 67 | 68 | try 69 | { 70 | int collectorIndex = 0; 71 | int collectorListCount = collectors.Count(); 72 | foreach (IMaterialCollector collector in collectors) 73 | { 74 | EditorUtility.DisplayProgressBar("材质收集", $"正在处理第{collectorIndex + 1}/{collectorListCount}个 --{collector.name}", 75 | (float)collectorIndex / (float)collectorListCount); 76 | materials.Clear(); 77 | collector.AddMaterialBuildDependency(materials); 78 | foreach (Material material in materials) 79 | { 80 | if (!mMaterials.TryGetValue(material, out List fromCollect)) 81 | { 82 | fromCollect = new List(); 83 | mMaterials.Add(material, fromCollect); 84 | } 85 | 86 | fromCollect.Add(collector); 87 | } 88 | 89 | collectorIndex++; 90 | } 91 | 92 | Debug.Log($"[材质收集]共收集到{mMaterials}个材质"); 93 | 94 | success = true; 95 | } 96 | finally 97 | { 98 | EditorUtility.ClearProgressBar(); 99 | } 100 | 101 | return success; 102 | } 103 | 104 | public bool FilterMaterial(IEnumerable materialFilters) 105 | { 106 | var deleteList = mMaterials 107 | .Where(kv => materialFilters.Any(mf => !mf.Filter(kv.Key, kv.Value))) 108 | .Select(kv => kv.Key); 109 | 110 | foreach (var deleteMat in deleteList) 111 | mMaterials.Remove(deleteMat); 112 | 113 | return true; 114 | } 115 | 116 | public bool FilterMaterial(IEnumerable variantFilters) 117 | { 118 | mVariants.RemoveAll(variant => variantFilters.Any(vf => !vf.Filter(variant))); 119 | return true; 120 | } 121 | 122 | //查看keyword是否属于shader,用于去除无效keyword 123 | private bool IsKeywordBelongToShader(Shader shader, string keyword) 124 | { 125 | if (!mCachedKeywords.TryGetValue(shader, out string[] keywords)) 126 | { 127 | keywords = GetShaderKeywords(shader); 128 | mCachedKeywords.Add(shader, keywords); 129 | } 130 | 131 | return keywords.Contains(keyword); 132 | } 133 | 134 | private bool AddVariant(ShaderVariantCollection.ShaderVariant variant) 135 | { 136 | int findIndex = mVariants.FindIndex(v => 137 | { 138 | return v.shader == variant.shader 139 | && v.passType == variant.passType 140 | && v.keywords.SequenceEqual(variant.keywords); 141 | }); 142 | 143 | if (findIndex < 0) 144 | { 145 | mVariants.Add(variant); 146 | } 147 | 148 | return findIndex < 0; 149 | } 150 | 151 | private bool IsValidVariant(Shader shader, PassType passType, string[] keywords) 152 | { 153 | try 154 | { 155 | ShaderVariantCollection.ShaderVariant testVariant 156 | = new ShaderVariantCollection.ShaderVariant(shader, passType, keywords); 157 | 158 | return true; 159 | } 160 | catch 161 | { 162 | return false; 163 | } 164 | } 165 | 166 | private readonly string[] mSingleKeywords = new string[1]; 167 | private bool IsKeywordInPass(Shader shader, PassType passType, string keyword) 168 | { 169 | mSingleKeywords[0] = keyword; 170 | return IsValidVariant(shader, passType, mSingleKeywords); 171 | } 172 | 173 | private readonly string[] mEmptyKeywords = new string[0]; 174 | private bool IsPassExist(Shader shader, PassType passType) 175 | { 176 | return IsValidVariant(shader, passType, mEmptyKeywords); 177 | } 178 | 179 | 180 | private List mTempInsertKeywords = new List(); 181 | private List mTempSortedKeywords = new List(); 182 | 183 | private void AddVariantFromKeywords(Shader shader, List validKeywords) 184 | { 185 | //如果keyword为空,直接尝试加入变体 186 | if (validKeywords.Count == 0) 187 | { 188 | //循环尝试所有Pass是否能加入 189 | for (PassType passType = (PassType)Enum.GetValues(typeof(PassType)).Cast().Min(); 190 | passType <= (PassType)Enum.GetValues(typeof(PassType)).Cast().Max(); 191 | ++passType) 192 | { 193 | if (!IsPassExist(shader, passType)) 194 | continue; 195 | 196 | AddVariant(new ShaderVariantCollection.ShaderVariant(shader, passType, mEmptyKeywords)); 197 | } 198 | 199 | return; 200 | } 201 | 202 | 203 | foreach (string keyword in validKeywords) 204 | { 205 | for (PassType passType = (PassType)Enum.GetValues(typeof(PassType)).Cast().Min(); 206 | passType <= (PassType)Enum.GetValues(typeof(PassType)).Cast().Max(); 207 | ++passType) 208 | { 209 | //如果当前pass没有这个keyword,跳过 210 | if (!IsKeywordInPass(shader, passType, keyword)) 211 | continue; 212 | 213 | mTempInsertKeywords.Clear(); 214 | mTempInsertKeywords.Add(keyword); 215 | 216 | //遍历其他keyword,逐个添加进组合,找到对于这个keyword的最长的有效组合 217 | foreach (string currentKeyword in validKeywords) 218 | { 219 | if (currentKeyword == keyword) 220 | continue; 221 | 222 | mTempInsertKeywords.Add(currentKeyword); 223 | 224 | //需要有序来检查Sequence Equal 225 | mTempSortedKeywords.Clear(); 226 | mTempSortedKeywords.AddRange(mTempInsertKeywords); 227 | mTempSortedKeywords.Sort(); 228 | 229 | //如果加入当前keyword后,变体无效,则将当前keyword去掉 230 | if (!IsValidVariant(shader, passType, mTempSortedKeywords.ToArray())) 231 | mTempInsertKeywords.RemoveAt(mTempInsertKeywords.Count - 1); 232 | } 233 | 234 | mTempInsertKeywords.Sort(); 235 | AddVariant(new ShaderVariantCollection.ShaderVariant(shader, passType, mTempInsertKeywords.ToArray())); 236 | } 237 | } 238 | } 239 | 240 | public bool CollectVariant() 241 | { 242 | mCachedKeywords.Clear(); 243 | mInsertedVariants.Clear(); 244 | mVariants.Clear(); 245 | 246 | List validKeywords = new List(); 247 | 248 | int materialIndex = 0; 249 | 250 | bool success = false; 251 | 252 | try 253 | { 254 | foreach (Material material in mMaterials.Keys) 255 | { 256 | EditorUtility.DisplayProgressBar("变体收集-材质与变体转换", 257 | $"正在处理第{materialIndex}/{mMaterials.Keys.Count}个材质 --{material.name}", 258 | (float)materialIndex / (float)mMaterials.Keys.Count); 259 | 260 | Shader shader = material.shader; 261 | 262 | validKeywords.Clear(); 263 | validKeywords.AddRange(material.shaderKeywords.Distinct()); //去重 264 | validKeywords.RemoveAll(keyword => !IsKeywordBelongToShader(shader, keyword)); //去除没有的变体 265 | validKeywords.Sort(); 266 | 267 | //检查keyword序列是否已添加,如果是则跳过 268 | string keywordsKey = string.Join(" ", validKeywords); 269 | if (mInsertedVariants.TryGetValue(shader, out List collectedVariant)) 270 | { 271 | if (collectedVariant.Contains(keywordsKey)) 272 | continue; 273 | } 274 | else 275 | { 276 | collectedVariant = new List(); 277 | mInsertedVariants.Add(shader, collectedVariant); 278 | } 279 | 280 | AddVariantFromKeywords(shader, validKeywords); 281 | materialIndex++; 282 | } 283 | 284 | success = true; 285 | } 286 | catch (Exception e) 287 | { 288 | Debug.LogError( 289 | $"[材质变体转换]正在处理第{materialIndex}个材质{mMaterials.ElementAt(materialIndex).Key.name}\n报错信息为:{e.Message}\nStackTrace:{e.StackTrace}"); 290 | } 291 | finally 292 | { 293 | EditorUtility.ClearProgressBar(); 294 | } 295 | 296 | Debug.Log($"[材质变体转换]总共加入{mVariants.Count}个变体"); 297 | return success; 298 | } 299 | 300 | public IEnumerable GetMaterials() 301 | { 302 | return mMaterials.Keys; 303 | } 304 | 305 | public int GetVariantCount() 306 | { 307 | return mVariants.Count; 308 | } 309 | 310 | public IEnumerable GetMaterialFrom(Material material) 311 | { 312 | if (mMaterials.TryGetValue(material, out var fromList)) 313 | { 314 | return fromList; 315 | } 316 | 317 | return new IMaterialCollector[0]; 318 | } 319 | 320 | public void WriteToShaderVariantCollectionFile(ShaderVariantCollection collection) 321 | { 322 | foreach (var variant in mVariants) 323 | { 324 | collection.Add(variant); 325 | } 326 | } 327 | 328 | public void GetKeywordFromMaterial(string keyword, IList matList, Shader shader = null) 329 | { 330 | foreach (Material mat in GetMaterials()) 331 | { 332 | if (shader != null && mat.shader != shader) 333 | continue; 334 | 335 | if (mat.shaderKeywords.Contains(keyword) && !matList.Contains(mat)) 336 | matList.Add(mat); 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/ShaderVariantCollectionMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEngine; 8 | using UnityEngine.Rendering; 9 | 10 | namespace Soco.ShaderVariantsCollection 11 | { 12 | //用于访问ShaderVariantCollection被隐藏的C++部分 13 | //继承UnityEngine.ScriptableObject, ISerializationCallbackReceiver是为了Undo 14 | public class ShaderVariantCollectionMapper : UnityEngine.ScriptableObject, ISerializationCallbackReceiver 15 | { 16 | [SerializeField] 17 | internal ShaderVariantCollection mCollection; 18 | private Dictionary> mMapper = new Dictionary>(); 19 | 20 | private List mMapperSerializeKeys = new List(); 21 | [System.Serializable] 22 | private struct ListWrapper 23 | { 24 | public List list; 25 | 26 | public ListWrapper(List list) 27 | { 28 | this.list = list; 29 | } 30 | 31 | // public List Deserialize() 32 | // { 33 | // return list.Select(variant => variant.Deserialize()).ToList(); 34 | // } 35 | } 36 | private List mMapperSerializeValues = new List(); 37 | 38 | [NonSerialized] 39 | private bool mSerializedFlag = false; 40 | 41 | private static MethodInfo sAddNewShaderToCollection = null; 42 | 43 | private static void InitShaderUtilMethod() 44 | { 45 | if (sAddNewShaderToCollection == null) 46 | sAddNewShaderToCollection = typeof(ShaderUtil).GetMethod("AddNewShaderToCollection", 47 | BindingFlags.NonPublic | BindingFlags.Static); 48 | } 49 | 50 | //当用CreateInstance时,调用这个方法 51 | public void Init(ShaderVariantCollection collection) 52 | { 53 | mCollection = collection; 54 | 55 | if (mCollection == null) 56 | { 57 | throw new Exception( 58 | "ShaderVariantCollectionMapper constructor get a null ShaderVariantCollection Object"); 59 | } 60 | 61 | ReadFromFile(); 62 | InitShaderUtilMethod(); 63 | } 64 | 65 | public ShaderVariantCollectionMapper(ShaderVariantCollection collection) 66 | { 67 | Init(collection); 68 | } 69 | 70 | internal static ShaderVariantCollection.ShaderVariant PropToVariantObject(Shader shader, SerializedProperty variantInfo) 71 | { 72 | PassType passType = (PassType)variantInfo.FindPropertyRelative("passType").intValue; 73 | string keywords = variantInfo.FindPropertyRelative("keywords").stringValue; 74 | string[] keywordSet = keywords.Split(' '); 75 | keywordSet = (keywordSet.Length == 1 && keywordSet[0] == "") ? new string[0] : keywordSet; 76 | 77 | ShaderVariantCollection.ShaderVariant newVariant = new ShaderVariantCollection.ShaderVariant() 78 | { 79 | shader = shader, 80 | keywords = keywordSet, 81 | passType = passType 82 | }; 83 | 84 | return newVariant; 85 | } 86 | 87 | private void ReadFromFile() 88 | { 89 | mMapper.Clear(); 90 | 91 | SerializedObject serializedObject = new UnityEditor.SerializedObject(mCollection); 92 | //serializedObject.Update(); 93 | SerializedProperty m_Shaders = serializedObject.FindProperty("m_Shaders"); 94 | 95 | for (int i = 0; i < m_Shaders.arraySize; ++i) 96 | { 97 | SerializedProperty pair = m_Shaders.GetArrayElementAtIndex(i); 98 | 99 | SerializedProperty first = pair.FindPropertyRelative("first"); 100 | SerializedProperty second = pair.FindPropertyRelative("second");//ShaderInfo 101 | 102 | Shader shader = first.objectReferenceValue as Shader; 103 | 104 | if (shader == null) 105 | continue; 106 | 107 | mMapper[shader] = new List(); 108 | 109 | SerializedProperty variants = second.FindPropertyRelative("variants"); 110 | for (var vi = 0; vi < variants.arraySize; ++vi) 111 | { 112 | SerializedProperty variantInfo = variants.GetArrayElementAtIndex(vi); 113 | 114 | ShaderVariantCollection.ShaderVariant variant = PropToVariantObject(shader, variantInfo); 115 | mMapper[shader].Add(new SerializableShaderVariant(variant)); 116 | } 117 | } 118 | } 119 | 120 | public Dictionary>.KeyCollection shaders => mMapper.Keys; 121 | 122 | public ReadOnlyCollection GetShaderVariants(Shader shader) => 123 | mMapper[shader].Select(v=>v.Deserialize()).ToList().AsReadOnly(); 124 | 125 | public bool HasShader(Shader shader) 126 | { 127 | bool collectionHasShader = false; 128 | SerializedObject serializedObject = new UnityEditor.SerializedObject(mCollection); 129 | //serializedObject.Update(); 130 | SerializedProperty m_Shaders = serializedObject.FindProperty("m_Shaders"); 131 | 132 | for (int i = 0; i < m_Shaders.arraySize; ++i) 133 | { 134 | SerializedProperty pair = m_Shaders.GetArrayElementAtIndex(i); 135 | 136 | SerializedProperty first = pair.FindPropertyRelative("first"); 137 | SerializedProperty second = pair.FindPropertyRelative("second"); //ShaderInfo 138 | 139 | Shader currentShader = first.objectReferenceValue as Shader; 140 | 141 | if (currentShader == shader) 142 | { 143 | collectionHasShader = true; 144 | break; 145 | } 146 | } 147 | 148 | bool mapperHasShader = mMapper.ContainsKey(shader); 149 | 150 | Debug.Assert(collectionHasShader == mapperHasShader, "map对象与源文件内容不符"); 151 | return mapperHasShader; 152 | } 153 | 154 | public void AddShader(Shader shader) 155 | { 156 | if (!mMapper.ContainsKey(shader)) 157 | { 158 | mMapper.Add(shader, new List()); 159 | } 160 | 161 | sAddNewShaderToCollection.Invoke(null, new object[] { shader, mCollection }); 162 | } 163 | 164 | public void RemoveShader(Shader shader) 165 | { 166 | if (mMapper.ContainsKey(shader)) 167 | { 168 | mMapper.Remove(shader); 169 | } 170 | 171 | SerializedObject serializedObject = new UnityEditor.SerializedObject(mCollection); 172 | //serializedObject.Update(); 173 | SerializedProperty m_Shaders = serializedObject.FindProperty("m_Shaders"); 174 | 175 | for (int shaderIndex = 0; shaderIndex < m_Shaders.arraySize; ++shaderIndex) 176 | { 177 | SerializedProperty pair = m_Shaders.GetArrayElementAtIndex(shaderIndex); 178 | 179 | SerializedProperty first = pair.FindPropertyRelative("first"); 180 | 181 | Shader currentShader = first.objectReferenceValue as Shader; 182 | 183 | if (currentShader == shader) 184 | { 185 | m_Shaders.DeleteArrayElementAtIndex(shaderIndex); 186 | break; 187 | } 188 | } 189 | 190 | serializedObject.ApplyModifiedProperties(); 191 | } 192 | 193 | private bool VariantCompare(SerializableShaderVariant l, SerializableShaderVariant r) 194 | { 195 | if (l.shader != r.shader) 196 | return false; 197 | 198 | if (l.passType != r.passType) 199 | return false; 200 | 201 | if (l.keywords.Length != r.keywords.Length) 202 | return false; 203 | 204 | return l.keywords.OrderBy(s => s).SequenceEqual(r.keywords.OrderBy(t => t)); 205 | } 206 | 207 | public bool HasVariant(ShaderVariantCollection.ShaderVariant variant) 208 | { 209 | bool collectionHasVariant = mCollection.Contains(variant); 210 | 211 | bool mapperHasVariant = 212 | mMapper.ContainsKey(variant.shader) && mMapper[variant.shader].FindIndex(v=>VariantCompare(new SerializableShaderVariant(variant), v)) != -1; 213 | 214 | Debug.Assert(collectionHasVariant == mapperHasVariant, "map对象与源文件内容不符"); 215 | return collectionHasVariant; 216 | } 217 | 218 | public bool AddVariant(ShaderVariantCollection.ShaderVariant variant) 219 | { 220 | if (!mCollection.Add(variant)) 221 | return false; 222 | 223 | if (!mMapper.ContainsKey(variant.shader)) 224 | mMapper.Add(variant.shader, new List()); 225 | 226 | mMapper[variant.shader].Add(new SerializableShaderVariant(variant)); 227 | 228 | return true; 229 | } 230 | 231 | public bool RemoveVariant(ShaderVariantCollection.ShaderVariant variant) 232 | { 233 | if (!mCollection.Remove(variant)) 234 | return false; 235 | 236 | if (mMapper.TryGetValue(variant.shader, out var variantList)) 237 | { 238 | int index = variantList.FindIndex(v => VariantCompare(new SerializableShaderVariant(variant), v)); 239 | Debug.Assert(index != -1, "map对象与源文件内容不符,无法在map对象中找到变体"); 240 | variantList.RemoveAt(index); 241 | 242 | return true; 243 | } 244 | 245 | Debug.Assert(false, "map对象与源文件内容不符,无法在map对象中找到Shader"); 246 | return false; 247 | } 248 | 249 | private ShaderVariantCollection.ShaderVariant PropToObject(Shader shader, SerializedProperty variantInfo) 250 | { 251 | PassType passType = (PassType)variantInfo.FindPropertyRelative("passType").intValue; 252 | string keywords = variantInfo.FindPropertyRelative("keywords").stringValue; 253 | string[] keywordSet = keywords.Split(' '); 254 | ShaderVariantCollection.ShaderVariant newVariant = new ShaderVariantCollection.ShaderVariant() 255 | { 256 | shader = shader, 257 | keywords = keywordSet, 258 | passType = passType 259 | }; 260 | 261 | return newVariant; 262 | } 263 | 264 | public void Merge(ShaderVariantCollection otherFile) 265 | { 266 | if (otherFile == null) 267 | return; 268 | 269 | SerializedObject serializedOther = new UnityEditor.SerializedObject(otherFile); 270 | SerializedProperty m_Shaders = serializedOther.FindProperty("m_Shaders"); 271 | 272 | for (int i = 0; i < m_Shaders.arraySize; ++i) 273 | { 274 | SerializedProperty pair = m_Shaders.GetArrayElementAtIndex(i); 275 | 276 | SerializedProperty first = pair.FindPropertyRelative("first"); 277 | SerializedProperty second = pair.FindPropertyRelative("second");//ShaderInfo 278 | 279 | Shader shader = first.objectReferenceValue as Shader; 280 | 281 | SerializedProperty variants = second.FindPropertyRelative("variants"); 282 | for (var vi = 0; vi < variants.arraySize; ++vi) 283 | { 284 | SerializedProperty variantInfo = variants.GetArrayElementAtIndex(vi); 285 | 286 | ShaderVariantCollection.ShaderVariant newVariant = PropToObject(shader, variantInfo); 287 | if (!mCollection.Contains(newVariant)) 288 | { 289 | mCollection.Add(newVariant); 290 | } 291 | } 292 | } 293 | 294 | ReadFromFile(); 295 | } 296 | 297 | public int Intersection(ShaderVariantCollection otherFile) 298 | { 299 | if (otherFile == null) 300 | return -1; 301 | 302 | ShaderVariantCollectionMapper otherMapper = new ShaderVariantCollectionMapper(otherFile); 303 | List excludeVariants = new List(); 304 | foreach (KeyValuePair> pair in mMapper) 305 | { 306 | foreach (SerializableShaderVariant serializableVariant in pair.Value) 307 | { 308 | ShaderVariantCollection.ShaderVariant variant = serializableVariant.Deserialize(); 309 | if (!otherMapper.HasVariant(variant)) 310 | { 311 | excludeVariants.Add(variant); 312 | } 313 | } 314 | } 315 | 316 | foreach (ShaderVariantCollection.ShaderVariant variant in excludeVariants) 317 | { 318 | RemoveVariant(variant); 319 | } 320 | 321 | List excludeShaders = new List(); 322 | foreach (KeyValuePair> pair in mMapper) 323 | { 324 | if (pair.Value.Count() == 0) 325 | excludeShaders.Add(pair.Key); 326 | } 327 | 328 | foreach (Shader shader in excludeShaders) 329 | { 330 | RemoveShader(shader); 331 | } 332 | 333 | ReadFromFile(); 334 | 335 | return excludeVariants.Count(); 336 | } 337 | 338 | public void Refresh() => ReadFromFile(); 339 | 340 | public void SetSerializeFlag(bool serialize) => mSerializedFlag = serialize; 341 | 342 | public void OnBeforeSerialize() 343 | { 344 | if (!mSerializedFlag) 345 | { 346 | return; 347 | } 348 | 349 | if (mMapper == null) 350 | mMapper = new Dictionary>(); 351 | 352 | mMapperSerializeKeys.Clear(); 353 | mMapperSerializeValues.Clear(); 354 | 355 | foreach (var kvp in mMapper) 356 | { 357 | mMapperSerializeKeys.Add(kvp.Key); 358 | //序列化与反序列化都需要浅拷贝一层引用,否则对map的value的list操作,依旧会改变序列化中的数值 359 | mMapperSerializeValues.Add(new ListWrapper(kvp.Value.ToList())); 360 | } 361 | } 362 | 363 | public void OnAfterDeserialize() 364 | { 365 | if (mMapper == null) 366 | mMapper = new Dictionary>(); 367 | 368 | mMapper.Clear(); 369 | for (int i = 0; i < mMapperSerializeKeys.Count; ++i) 370 | { 371 | mMapper.Add(mMapperSerializeKeys[i], mMapperSerializeValues[i].list.ToList()); 372 | } 373 | } 374 | } 375 | 376 | } -------------------------------------------------------------------------------- /SocoShaderVariantsCollection/Editor/Executable/VariantKeywordCombination.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text.RegularExpressions; 7 | using UnityEditor; 8 | using UnityEngine; 9 | 10 | namespace Soco.ShaderVariantsCollection 11 | { 12 | [System.Serializable] 13 | public struct KeywordDeclareGroup 14 | { 15 | public List keywords; 16 | } 17 | 18 | public class VariantKeywordCombination : IExecutable 19 | { 20 | public Shader mShader; 21 | public List mKeywordDeclareGroups = new List(); 22 | 23 | public override void Execute(ShaderVariantCollectionMapper mapper) 24 | { 25 | if (mShader == null || mKeywordDeclareGroups.Count == 0) 26 | { 27 | Debug.LogError($"执行器{name}无法执行,原因是成员参数不符合规格,可能shader{mShader}为null,或keyword声明组长度为0(当前长度为{mKeywordDeclareGroups.Count})"); 28 | return; 29 | } 30 | 31 | if (!mapper.HasShader(mShader)) 32 | { 33 | Debug.LogError($"执行器{name}无法执行,原因是收集文件中不包含当前shader{mShader}"); 34 | return; 35 | } 36 | 37 | var shaderVariants = mapper.GetShaderVariants(mShader); 38 | List combinationList = new List(); 39 | combinationList.Add(new string[0]); 40 | 41 | foreach (KeywordDeclareGroup declareGroup in mKeywordDeclareGroups) 42 | { 43 | foreach (var keyword in declareGroup.keywords) 44 | { 45 | if (keyword == "_") 46 | { 47 | continue; 48 | } 49 | else 50 | { 51 | int combinationLimit = combinationList.Count; 52 | for (int i = 0; i < combinationLimit; ++i) 53 | { 54 | List combination = combinationList[i].ToList(); 55 | if (!combination.Contains(keyword)) 56 | combination.Add(keyword); 57 | 58 | combinationList.Add(combination.ToArray()); 59 | } 60 | } 61 | } 62 | } 63 | 64 | foreach (ShaderVariantCollection.ShaderVariant variant in shaderVariants.ToArray()) 65 | { 66 | //去除已有变体中包含的multi_compile声明 67 | var removeMultiCompileKeywordList = variant.keywords.Where((string k) => 68 | { 69 | foreach (KeywordDeclareGroup declareGroup in mKeywordDeclareGroups) 70 | { 71 | foreach (var keyword in declareGroup.keywords) 72 | { 73 | if (k == keyword) 74 | return false; 75 | } 76 | } 77 | 78 | return true; 79 | }); 80 | 81 | foreach (string[] combination in combinationList) 82 | { 83 | try 84 | { 85 | ShaderVariantCollection.ShaderVariant newVariant = 86 | new ShaderVariantCollection.ShaderVariant(variant.shader, variant.passType, 87 | removeMultiCompileKeywordList.Concat(combination).ToArray()); 88 | 89 | mapper.AddVariant(newVariant); 90 | } 91 | catch (Exception e) 92 | { 93 | continue; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | [ShaderVariantCollectionToolEditor(typeof(VariantKeywordCombination))] 101 | class VariantKeywordCombinationEditor : ShaderVariantCollectionToolEditor 102 | { 103 | private static MethodInfo _GetShaderGlobalKeywordsMethod = null; 104 | private static MethodInfo _GetShaderLocalKeywordsMethod = null; 105 | 106 | private static MethodInfo GetShaderGlobalKeywordsMethod 107 | { 108 | get 109 | { 110 | if(_GetShaderGlobalKeywordsMethod == null) 111 | _GetShaderGlobalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderGlobalKeywords", 112 | BindingFlags.NonPublic | BindingFlags.Static); 113 | 114 | return _GetShaderGlobalKeywordsMethod; 115 | } 116 | } 117 | 118 | private static MethodInfo GetShaderLocalKeywordsMethod 119 | { 120 | get 121 | { 122 | if(_GetShaderLocalKeywordsMethod == null) 123 | _GetShaderLocalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderLocalKeywords", 124 | BindingFlags.NonPublic | BindingFlags.Static); 125 | 126 | return _GetShaderLocalKeywordsMethod; 127 | } 128 | } 129 | 130 | private string[] mShaderKeywords = new string[0]; 131 | private int mSelectedKeywordIndex = 0; 132 | 133 | private enum KeywordMode 134 | { 135 | CustomKeyword, 136 | DefaultKeyword, 137 | DeclareStatement 138 | } 139 | 140 | private KeywordMode mKeywordMode = KeywordMode.CustomKeyword; 141 | private string mDeclareStatement = ""; 142 | 143 | private const string cDeclarePattern = @"#pragma\s+(multi_compile(?:_\w+)?)+(?:\s+(\w+))*"; 144 | 145 | public void OnEnable() 146 | { 147 | VariantKeywordCombination obj = target as VariantKeywordCombination; 148 | 149 | if (obj.mShader != null) 150 | { 151 | string[] globalKeywords = GetShaderGlobalKeywordsMethod.Invoke(null, new object[] { obj.mShader }) as string[]; 152 | string[] localKeywords = GetShaderLocalKeywordsMethod.Invoke(null, new object[] { obj.mShader }) as string[]; 153 | mShaderKeywords = globalKeywords.Concat(localKeywords).ToArray(); 154 | Array.Sort(mShaderKeywords); 155 | mSelectedKeywordIndex = 0; 156 | } 157 | } 158 | 159 | public override void OnInspectorGUI() 160 | { 161 | VariantKeywordCombination obj = target as VariantKeywordCombination; 162 | 163 | EditorGUILayout.BeginVertical(); 164 | 165 | EditorGUILayout.BeginHorizontal(); 166 | EditorGUI.BeginChangeCheck(); 167 | Shader newShader = EditorGUILayout.ObjectField("Shader", obj.mShader, typeof(Shader)) as Shader; 168 | if (EditorGUI.EndChangeCheck() && newShader != obj.mShader) 169 | { 170 | Undo.RecordObject(obj, $"{obj.GetType().Name} change shader"); 171 | 172 | obj.mShader = newShader; 173 | if (obj.mShader == null) 174 | mShaderKeywords = new string[0]; 175 | else 176 | { 177 | string[] globalKeywords = GetShaderGlobalKeywordsMethod.Invoke(null, new object[] { obj.mShader }) as string[]; 178 | string[] localKeywords = GetShaderLocalKeywordsMethod.Invoke(null, new object[] { obj.mShader }) as string[]; 179 | mShaderKeywords = globalKeywords.Concat(localKeywords).ToArray(); 180 | Array.Sort(mShaderKeywords); 181 | } 182 | obj.mKeywordDeclareGroups.Clear(); 183 | 184 | mSelectedKeywordIndex = 0; 185 | } 186 | 187 | if (obj.mShader != null && GUILayout.Button("尝试收集声明组")) 188 | { 189 | Undo.RecordObject(obj, $"{obj.GetType().Name} add keyword throw declare statement"); 190 | obj.mKeywordDeclareGroups.Clear(); 191 | TryParseShader(obj.mShader, obj.mKeywordDeclareGroups); 192 | 193 | } 194 | EditorGUILayout.EndHorizontal(); 195 | 196 | EditorGUILayout.BeginHorizontal(); 197 | mKeywordMode = (KeywordMode)EditorGUILayout.EnumPopup("选择模式", mKeywordMode); 198 | if (mKeywordMode == KeywordMode.CustomKeyword) 199 | { 200 | mSelectedKeywordIndex = EditorGUILayout.Popup(mSelectedKeywordIndex, mShaderKeywords); 201 | } 202 | else if (mKeywordMode == KeywordMode.DeclareStatement) 203 | { 204 | mDeclareStatement = EditorGUILayout.TextField("声明语句", mDeclareStatement); 205 | } 206 | EditorGUILayout.EndHorizontal(); 207 | 208 | for (int i = 0; i < obj.mKeywordDeclareGroups.Count; ++i) 209 | { 210 | EditorGUILayout.BeginHorizontal(); 211 | 212 | for (int j = 0; j < obj.mKeywordDeclareGroups[i].keywords.Count; ++j) 213 | { 214 | string keyword = obj.mKeywordDeclareGroups[i].keywords[j]; 215 | if (GUILayout.Button(new GUIContent(keyword, keyword))) 216 | { 217 | Undo.RecordObject(obj, $"{obj.GetType().Name} delete declareGroup's keyword"); 218 | obj.mKeywordDeclareGroups[i].keywords.RemoveAt(j); 219 | } 220 | } 221 | 222 | if (GUILayout.Button(new GUIContent("+", "将keyword添加到当前组合中"), GUILayout.Width(20))) 223 | { 224 | if (mKeywordMode == KeywordMode.CustomKeyword) 225 | { 226 | if (!obj.mKeywordDeclareGroups[i].keywords.Contains(mShaderKeywords[mSelectedKeywordIndex])) 227 | { 228 | Undo.RecordObject(obj, $"{obj.GetType().Name} add keyword to declareGroup"); 229 | obj.mKeywordDeclareGroups[i].keywords.Add(mShaderKeywords[mSelectedKeywordIndex]); 230 | } 231 | } 232 | else if(mKeywordMode == KeywordMode.DefaultKeyword) 233 | { 234 | if (!obj.mKeywordDeclareGroups[i].keywords.Contains("_")) 235 | { 236 | Undo.RecordObject(obj, $"{obj.GetType().Name} add default keyword to declareGroup"); 237 | obj.mKeywordDeclareGroups[i].keywords.Add("_"); 238 | } 239 | } 240 | else if (mKeywordMode == KeywordMode.DeclareStatement) 241 | { 242 | Undo.RecordObject(obj, $"{obj.GetType().Name} add keyword throw declare statement"); 243 | foreach (string keyword in ParseDeclareStatement(mDeclareStatement)) 244 | { 245 | if (!obj.mKeywordDeclareGroups[i].keywords.Contains(keyword)) 246 | obj.mKeywordDeclareGroups[i].keywords.Add(keyword); 247 | } 248 | } 249 | } 250 | 251 | if (GUILayout.Button(new GUIContent("-", "将当前变体组删除"), GUILayout.Width(20))) 252 | { 253 | Undo.RecordObject(obj, $"{obj.GetType().Name} delete declareGroup"); 254 | obj.mKeywordDeclareGroups.RemoveAt(i); 255 | EditorGUILayout.EndHorizontal(); 256 | break; 257 | } 258 | 259 | EditorGUILayout.EndHorizontal(); 260 | } 261 | 262 | if (GUILayout.Button(new GUIContent("+", "添加变体声明组"), GUILayout.Width(20))) 263 | { 264 | Undo.RecordObject(obj, $"{obj.GetType().Name} add keywords"); 265 | if (mKeywordMode == KeywordMode.DeclareStatement) 266 | { 267 | obj.mKeywordDeclareGroups.Add(new KeywordDeclareGroup(){keywords = ParseDeclareStatement(mDeclareStatement).ToList()}); 268 | } 269 | else 270 | { 271 | obj.mKeywordDeclareGroups.Add(new KeywordDeclareGroup(){keywords = new List()}); 272 | } 273 | } 274 | 275 | EditorGUILayout.EndVertical(); 276 | } 277 | 278 | private void TryParseShader(Shader shader, List targetList) 279 | { 280 | string path = AssetDatabase.GetAssetPath(shader); 281 | 282 | if (!File.Exists(path)) 283 | { 284 | Debug.LogError($"{shader}所在路径{path}不存在,可能Shader是Unity内置Shader"); 285 | return; 286 | } 287 | 288 | using (var sr = new StreamReader(path)) 289 | { 290 | string content = sr.ReadToEnd(); 291 | //正则前加行首和空字符,这样去除注释的声明 292 | MatchCollection matches = Regex.Matches(content, @"^\s+"+cDeclarePattern, RegexOptions.Multiline); 293 | 294 | foreach (Match match in matches) 295 | { 296 | string[] declareGroup = ParseDeclareStatement(match.Groups[0].Value); 297 | if (declareGroup.Length == 0) 298 | continue; 299 | 300 | bool hasDeclared = targetList.FindIndex( 301 | kdg => kdg.keywords.OrderBy(k => k).SequenceEqual(declareGroup.OrderBy(k => k))) > 0; 302 | 303 | if (!hasDeclared) 304 | targetList.Add(new KeywordDeclareGroup(){keywords = declareGroup.ToList()}); 305 | } 306 | } 307 | } 308 | 309 | private string[] ParseDeclareStatement(string statement) 310 | { 311 | MatchCollection matches = Regex.Matches(statement, cDeclarePattern); 312 | 313 | if (matches.Count == 0) 314 | { 315 | Debug.LogError($"当前声明语句{statement}无法匹配出字段"); 316 | return new string[0]; 317 | } 318 | else 319 | { 320 | string multiCompileDeclare = matches[0].Groups[1].Value; 321 | multiCompileDeclare = multiCompileDeclare.Replace("_vertex", ""); 322 | multiCompileDeclare = multiCompileDeclare.Replace("_fragment", ""); 323 | multiCompileDeclare = multiCompileDeclare.Replace("_hull", ""); 324 | multiCompileDeclare = multiCompileDeclare.Replace("_domain", ""); 325 | multiCompileDeclare = multiCompileDeclare.Replace("_geometry", ""); 326 | multiCompileDeclare = multiCompileDeclare.Replace("_raytracing", ""); 327 | multiCompileDeclare = multiCompileDeclare.Replace("_local", ""); 328 | 329 | if (multiCompileDeclare == "multi_compile") 330 | { 331 | List keywords = new List(); 332 | foreach (Capture capture in matches[0].Groups[2].Captures) 333 | //如果是全下划线 334 | if (capture.Value.All(cr=>cr=='_')) 335 | keywords.Add("_"); 336 | else 337 | keywords.Add(capture.Value); 338 | 339 | if (keywords.Count == 1) 340 | { 341 | if (keywords[0] != "_") 342 | { 343 | keywords.Add("_"); 344 | } 345 | else 346 | { 347 | Debug.LogError($"{statement}只声明了一个全下划线变体"); 348 | return new string[0]; 349 | } 350 | } 351 | 352 | return keywords.ToArray(); 353 | } 354 | else if (multiCompileDeclare == "multi_compile_instancing") 355 | { 356 | return new string[] { "_", "INSTANCING_ON" }; 357 | } 358 | else if (multiCompileDeclare == "multi_compile_particles") 359 | { 360 | return new string[] { "_", "SOFTPARTICLES_ON" }; 361 | } 362 | else if (multiCompileDeclare == "multi_compile_fog") 363 | { 364 | return new string[] { "_", "FOG_LINEAR", "FOG_EXP", "FOG_EXP2" }; 365 | } 366 | //TODO: Other BuildIn keyword declare, eg. multi_compile_fwdbase 367 | else 368 | { 369 | Debug.LogError($"未实现当前{multiCompileDeclare}变体声明,请根据需要到VariantKeywordCombination中实现"); 370 | return new string[0]; 371 | } 372 | } 373 | 374 | } 375 | } 376 | } -------------------------------------------------------------------------------- /SocoShaderVariantsStripper/Editor/ShaderVariantsStripperWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEngine; 8 | using UnityEngine.Rendering; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace Soco.ShaderVariantsStripper 12 | { 13 | public class ShaderVariantsStripperWindow : EditorWindow 14 | { 15 | private enum ContentState 16 | { 17 | None, 18 | StripCheck, 19 | GlobalSetting, 20 | ShaderSetting 21 | } 22 | 23 | private static ShaderVariantsStripperWindow m_window; 24 | private static ShaderVariantsStripperWindow Window 25 | { 26 | get 27 | { 28 | if (m_window == null) 29 | { 30 | m_window = EditorWindow.GetWindow("ShaderVariantsStripper"); 31 | m_window.minSize = cMinWindowSize; 32 | } 33 | return m_window; 34 | } 35 | } 36 | 37 | private GUIStyle mBlackStyle, mItemStyle; 38 | 39 | private ShaderVariantsStripperConfig mConfig; 40 | private string mReadJsonPath = ""; 41 | 42 | //右侧菜单滚动条、状态 43 | private ContentState mCurrentContentState = ContentState.None; 44 | private Vector2 mContentScrollViewPos = Vector2.zero; 45 | 46 | private int mSelectedCondition = 0; 47 | private Type[] mConditionTypes = new Type[0]; 48 | private string[] mConditionNames = null; 49 | 50 | //Shader View变量 51 | private Vector2 mShaderViewScrollViewPos = Vector2.zero; 52 | private Shader mShaderViewSelectedShader = null; 53 | private bool mTextShaderInput = false; 54 | private string mTextShaderInputStr = ""; 55 | private Shader mWillInsertShader = null; 56 | private string mFilterShaderName = ""; 57 | private Dictionary mFilterShaders = new Dictionary(); 58 | 59 | //剔除检查 60 | private Shader mStripCheckShader; 61 | private ShaderVariantsData mStripCheckData; 62 | private static MethodInfo sGetShaderGlobalKeywordsMethod = null; 63 | private static MethodInfo sGetShaderLocalKeywordsMethod = null; 64 | private string[] mStripCheckShaderGlobalKeywords; 65 | private string[] mStripCheckShaderLocalKeywords; 66 | private enum AccessLevel 67 | { 68 | Global, 69 | Local 70 | } 71 | private AccessLevel mStripCheckAccessLevel = AccessLevel.Global; 72 | private int mStripCheckSelectedKeywordIndex; 73 | private List<(ConditionPair conditionPair, ShaderVariantsStripperConfig config)> mStripCheckConditionList = new List<(ConditionPair condition, ShaderVariantsStripperConfig config)>(); 74 | private Vector2 mStripCheckConditionListScrollViewPos = Vector2.zero; 75 | 76 | private static int cBorderWidth = 10; 77 | private static Vector2 cMinWindowSize = new Vector2(1200, 600); 78 | private static int cLeftWidth = 400; 79 | private static int cLeftTopHeight = 100; 80 | private static int cLeftMiddleHeight = 100; 81 | private static int cMiddleWidth = 20; 82 | 83 | [MenuItem("Tools/Soco/ShaderVariantsStripper/OpenStripperWindow", priority = 200)] 84 | public static void OpenWindow() 85 | { 86 | Window.Show(); 87 | } 88 | 89 | public void OnGUI() 90 | { 91 | EditorGUILayout.Space(cBorderWidth); 92 | GUILayout.BeginHorizontal(GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); 93 | 94 | #region 左半部分 95 | EditorGUILayout.BeginVertical(GUILayout.MinWidth(cLeftWidth)); 96 | 97 | #region 上半部分 读取配置文件 98 | EditorGUILayout.BeginVertical(mBlackStyle, GUILayout.MinHeight(cLeftTopHeight)); 99 | EditorGUILayout.LabelField("配置文件:"); 100 | ShaderVariantsStripperConfig newConfig = EditorGUILayout.ObjectField(mConfig, typeof(ShaderVariantsStripperConfig), false) as ShaderVariantsStripperConfig; 101 | 102 | if (newConfig != mConfig) 103 | { 104 | SaveObject(mConfig); 105 | mConfig = newConfig; 106 | } 107 | 108 | if (mConfig != null) 109 | { 110 | EditorGUILayout.BeginHorizontal(); 111 | Color oriColor = GUI.color; 112 | GUI.color = mConfig.mEnable ? Color.green : Color.red; 113 | if (GUILayout.Button(new GUIContent((mConfig.mEnable ? "已启用" : "已禁用"), "启用或禁用当前配置文件"))) 114 | { 115 | mConfig.mEnable = !mConfig.mEnable; 116 | } 117 | 118 | GUI.color = mConfig.mIsWhiteList ? Color.white : Color.grey; 119 | if (GUILayout.Button(new GUIContent((mConfig.mIsWhiteList ? "白名单" : "剔除列表"), "当前配置文件是否为白名单"))) 120 | { 121 | mConfig.mIsWhiteList = !mConfig.mIsWhiteList; 122 | } 123 | GUI.color = oriColor; 124 | EditorGUILayout.EndHorizontal(); 125 | 126 | EditorGUILayout.BeginHorizontal(); 127 | if (GUILayout.Button("序列化Config为Json", GUILayout.Width(cLeftWidth * 0.5f))) 128 | { 129 | string configPath = AssetDatabase.GetAssetPath(mConfig); 130 | string jsonPath = configPath.Replace(".asset", ".json"); 131 | 132 | string json = JsonUtility.ToJson(mConfig); 133 | 134 | using (StreamWriter writer = new StreamWriter(jsonPath, false)) 135 | { 136 | writer.Write(json); 137 | } 138 | 139 | ShowNotification(new GUIContent($"成功保存至{jsonPath}")); 140 | } 141 | 142 | if (GUILayout.Button("选择读取路径", GUILayout.Width(cLeftWidth * 0.25f))) 143 | { 144 | string configPath = AssetDatabase.GetAssetPath(mConfig); 145 | 146 | mReadJsonPath = EditorUtility.OpenFilePanel("选择JSON路径", 147 | (mReadJsonPath == "" ? Path.GetDirectoryName(configPath) : mReadJsonPath), "json"); 148 | } 149 | if (GUILayout.Button("读取JSON", GUILayout.Width(cLeftWidth * 0.25f)) && mReadJsonPath != "" 150 | && EditorUtility.DisplayDialog("注意", "是否读取Json覆盖到当前配置文件,确认后不可恢复", "确认", "停,停!")) 151 | { 152 | if (System.IO.File.Exists(mReadJsonPath)) 153 | { 154 | using (StreamReader reader = new StreamReader(mReadJsonPath)) 155 | { 156 | string json = reader.ReadToEnd(); 157 | JsonUtility.FromJsonOverwrite(json, mConfig); 158 | } 159 | } 160 | else 161 | Debug.LogError($"Json文件{mReadJsonPath}不存在"); 162 | } 163 | EditorGUILayout.EndHorizontal(); 164 | if (mReadJsonPath != "") 165 | { 166 | EditorGUILayout.LabelField(new GUIContent(mReadJsonPath, mReadJsonPath)); 167 | } 168 | } 169 | else//mConfig == null 170 | { 171 | Color oriColor = GUI.color; 172 | GUI.color = mCurrentContentState == ContentState.StripCheck ? Color.green : oriColor; 173 | if (GUILayout.Button(new GUIContent("剔除检查", "给定一组变体和条件,检查该变体是否会被剔除"))) 174 | { 175 | mCurrentContentState = ContentState.StripCheck; 176 | } 177 | GUI.color = oriColor; 178 | } 179 | 180 | EditorGUILayout.EndVertical(); 181 | #endregion 182 | 183 | EditorGUILayout.Space(cBorderWidth); 184 | 185 | #region 中部 全局设置 186 | EditorGUILayout.BeginVertical(mBlackStyle, GUILayout.MinHeight(cLeftMiddleHeight)); 187 | EditorGUILayout.LabelField("Global Setting View"); 188 | if (mConfig != null) 189 | { 190 | string globalText = "全局设置" + 191 | (mConfig.mGlobalConditions.Count == 0 ? " " : $"({mConfig.mGlobalConditions.Count}条)"); 192 | 193 | string applyText = ""; 194 | if (mConfig.mIsWhiteList) 195 | { 196 | applyText = (mConfig.mShaderConditions.Count == 0 ? "(当前无效)" : "(应用于ShaderView中Shader)"); 197 | } 198 | else 199 | { 200 | applyText = (mConfig.mShaderConditions.Count == 0 ? "(应用于全体Shader)" : "(应用于ShaderView中Shader)"); 201 | } 202 | 203 | if (GUILayout.Button(new GUIContent(globalText + applyText, "浏览全局设置"), GUILayout.ExpandHeight(true), 204 | GUILayout.ExpandWidth(true))) 205 | { 206 | mCurrentContentState = ContentState.GlobalSetting; 207 | } 208 | } 209 | EditorGUILayout.EndVertical(); 210 | #endregion 211 | 212 | EditorGUILayout.Space(cBorderWidth); 213 | 214 | #region 下半部分 选择Shader 215 | EditorGUILayout.BeginVertical(mBlackStyle, GUILayout.MinHeight(position.height - cLeftTopHeight - cLeftMiddleHeight - 4 * cBorderWidth)); 216 | EditorGUILayout.LabelField("Shader View"); 217 | 218 | if (mConfig != null) 219 | { 220 | EditorGUILayout.BeginHorizontal(); 221 | if (mTextShaderInput) 222 | { 223 | mTextShaderInputStr = EditorGUILayout.TextField(mTextShaderInputStr); 224 | } 225 | else 226 | { 227 | mWillInsertShader = EditorGUILayout.ObjectField(mWillInsertShader, typeof(Shader)) as Shader; 228 | } 229 | 230 | Color oriColor = GUI.color; 231 | if (mTextShaderInput) 232 | GUI.color = Color.red; 233 | 234 | if (GUILayout.Button("文本输入")) 235 | { 236 | mTextShaderInput = !mTextShaderInput; 237 | } 238 | 239 | GUI.color = oriColor; 240 | 241 | if (GUILayout.Button("添加")) 242 | { 243 | if (mTextShaderInput) 244 | { 245 | mWillInsertShader = Shader.Find(mTextShaderInputStr); 246 | } 247 | 248 | if (mWillInsertShader != null && !mConfig.mShaderConditions.ContainsKey(mWillInsertShader)) 249 | { 250 | var item = new ShaderVariantsItem(); 251 | item.conditionPairs = new List(); 252 | item.applyGlobalConfig = true; 253 | mConfig.mShaderConditions.Add(mWillInsertShader, item); 254 | if (mFilterShaderName != "" && mWillInsertShader.name.IndexOf(mFilterShaderName, StringComparison.OrdinalIgnoreCase) > 0) 255 | { 256 | mFilterShaders.Add(mWillInsertShader, item); 257 | } 258 | } 259 | 260 | } 261 | EditorGUILayout.EndHorizontal(); 262 | 263 | string prevFilterShaderName = mFilterShaderName; 264 | mFilterShaderName = EditorGUILayout.TextField("过滤", mFilterShaderName); 265 | if (mFilterShaderName == "") 266 | { 267 | mFilterShaders.Clear(); 268 | } 269 | else if (prevFilterShaderName != mFilterShaderName) 270 | { 271 | mFilterShaders.Clear(); 272 | foreach (var kvp in mConfig.mShaderConditions) 273 | { 274 | Shader shader = kvp.Key; 275 | if (shader.name.IndexOf(mFilterShaderName, StringComparison.OrdinalIgnoreCase) > 0) 276 | { 277 | mFilterShaders.Add(kvp.Key, kvp.Value); 278 | } 279 | } 280 | } 281 | 282 | mShaderViewScrollViewPos = EditorGUILayout.BeginScrollView(mShaderViewScrollViewPos); 283 | Shader willRemoveInDict = null; 284 | 285 | Dictionary displayList = 286 | mFilterShaderName == "" ? mConfig.mShaderConditions : mFilterShaders; 287 | 288 | foreach (var kvp in displayList) 289 | { 290 | Shader shader = kvp.Key; 291 | ShaderVariantsItem item = kvp.Value; 292 | 293 | Color oriGUIColor = GUI.color; 294 | if (shader == mShaderViewSelectedShader) 295 | GUI.color = Color.green; 296 | 297 | EditorGUILayout.BeginHorizontal(); 298 | if (GUILayout.Button(new GUIContent(shader.name, shader.name), GUILayout.MinWidth(cLeftWidth - 100))) 299 | { 300 | if (mShaderViewSelectedShader == shader)//选中状态下再次选择,在项目中定位 301 | { 302 | Selection.activeObject = shader; 303 | EditorGUIUtility.PingObject(shader); 304 | } 305 | 306 | mShaderViewSelectedShader = shader; 307 | mCurrentContentState = ContentState.ShaderSetting; 308 | } 309 | 310 | GUI.color = item.applyGlobalConfig ? Color.green : Color.red; 311 | if (GUILayout.Button(new GUIContent("G", "应用全局设置"), GUILayout.MinWidth(45))) 312 | { 313 | item.applyGlobalConfig = 314 | !item.applyGlobalConfig; 315 | } 316 | 317 | GUI.color = oriGUIColor; 318 | if (GUILayout.Button("删除", GUILayout.MinWidth(45))) 319 | { 320 | willRemoveInDict = shader; 321 | if (mCurrentContentState == ContentState.ShaderSetting && mShaderViewSelectedShader == shader) 322 | { 323 | mCurrentContentState = ContentState.None; 324 | mShaderViewSelectedShader = null; 325 | } 326 | } 327 | EditorGUILayout.EndHorizontal(); 328 | } 329 | 330 | if (willRemoveInDict != null) 331 | { 332 | Undo.RegisterCompleteObjectUndo(mConfig, "ShaderVariantsStripper config delete shader"); 333 | mConfig.mShaderConditions.Remove(willRemoveInDict); 334 | mFilterShaders.Remove(willRemoveInDict); 335 | } 336 | 337 | 338 | EditorGUILayout.EndScrollView(); 339 | } 340 | 341 | EditorGUILayout.EndVertical(); 342 | #endregion 343 | 344 | EditorGUILayout.EndVertical(); 345 | #endregion 346 | 347 | #region 中间分隔线 348 | EditorGUILayout.BeginVertical(GUILayout.MinWidth(cMiddleWidth)); 349 | EditorGUILayout.Space(10); 350 | EditorGUILayout.EndVertical(); 351 | #endregion 352 | 353 | #region 右半部分 354 | int rightWidth = (int)(position.width - cLeftWidth - cMiddleWidth - 10); 355 | EditorGUILayout.BeginVertical(mBlackStyle, GUILayout.MinWidth(rightWidth), GUILayout.MinHeight(position.height - cBorderWidth * 2)); 356 | if (mConfig != null) 357 | { 358 | #region 标题状态+获取列表 359 | List conditionList = null; 360 | switch (mCurrentContentState) 361 | { 362 | case ContentState.GlobalSetting: 363 | EditorGUILayout.LabelField("全局设置"); 364 | conditionList = mConfig.mGlobalConditions; 365 | break; 366 | case ContentState.ShaderSetting: 367 | EditorGUILayout.LabelField($"Shader<{mShaderViewSelectedShader.name}>设置"); 368 | if (mShaderViewSelectedShader != null && mConfig.mShaderConditions.TryGetValue(mShaderViewSelectedShader, out ShaderVariantsItem svi)) 369 | { 370 | conditionList = svi.conditionPairs; 371 | } 372 | break; 373 | default: 374 | EditorGUILayout.LabelField("未选择,请选择<全局设置>或"); 375 | break; 376 | } 377 | #endregion 378 | 379 | if (conditionList != null) 380 | { 381 | #region 添加条件 382 | EditorGUILayout.BeginHorizontal(); 383 | mSelectedCondition = EditorGUILayout.Popup(mSelectedCondition, mConditionNames, GUILayout.MinWidth(rightWidth * 0.7f)); 384 | if (GUILayout.Button("添加", GUILayout.MinWidth(rightWidth * 0.3f))) 385 | { 386 | if (mSelectedCondition >= mConditionTypes.Length) 387 | { 388 | Debug.LogError("程序有改变,请重新打开窗口"); 389 | return; 390 | } 391 | 392 | ShaderVariantsStripperCondition condition = 393 | Activator.CreateInstance(mConditionTypes[mSelectedCondition]) as 394 | ShaderVariantsStripperCondition; 395 | conditionList.Add(new ConditionPair(){condition = condition, strip = false, priority = 0}); 396 | } 397 | EditorGUILayout.EndHorizontal(); 398 | #endregion 399 | 400 | #region 浏览条件 401 | mContentScrollViewPos = EditorGUILayout.BeginScrollView(mContentScrollViewPos); 402 | 403 | for (int i = 0; i < conditionList.Count; ++i) 404 | { 405 | //var (globalCondition, strip) = conditionList[i]; 406 | ConditionPair conditionPair = conditionList[i]; 407 | // var condition = conditionList[i].condition; 408 | // bool strip = conditionList[i].strip; 409 | // uint priority = conditionList[i].priority; 410 | 411 | EditorGUILayout.BeginHorizontal(mItemStyle, GUILayout.Width(rightWidth), GUILayout.Height(50)); 412 | 413 | if (!mConfig.mIsWhiteList) 414 | { 415 | Color oriGUIColor = GUI.color; 416 | if (conditionPair.strip) 417 | GUI.color = Color.red; 418 | else 419 | GUI.color = Color.green; 420 | if (GUILayout.Button((conditionPair.strip ? "剔除" : "保留"), GUILayout.Width(50), 421 | GUILayout.Height(50))) 422 | { 423 | //conditionList[i] = new ConditionPair(){condition = condition, strip = !strip, priority = priority}; 424 | conditionPair.strip = !conditionPair.strip; 425 | conditionList[i] = conditionPair; 426 | } 427 | GUI.color = oriGUIColor; 428 | 429 | uint newPriority = (uint)Mathf.Max(0, 430 | EditorGUILayout.IntField((int)conditionPair.priority, GUILayout.Width(50), 431 | GUILayout.Height(50))); 432 | if (newPriority != conditionPair.priority) 433 | { 434 | conditionPair.priority = newPriority; 435 | conditionList[i] = conditionPair; 436 | } 437 | } 438 | 439 | if (conditionPair.condition == null) 440 | conditionList.RemoveAt(i); 441 | 442 | if (GUILayout.Button(conditionPair.condition.Overview(), GUILayout.Width(rightWidth - 3 * 50 - 25), 443 | GUILayout.Height(50))) 444 | { 445 | ShaderVariantsStripperConditionWindow.Window.mCondition = conditionPair.condition; 446 | ShaderVariantsStripperConditionOnGUIContext context = new ShaderVariantsStripperConditionOnGUIContext() 447 | { 448 | shader = mCurrentContentState == ContentState.ShaderSetting ? mShaderViewSelectedShader : null 449 | }; 450 | context.Init(); 451 | ShaderVariantsStripperConditionWindow.Window.mContext = context; 452 | 453 | ShaderVariantsStripperConditionWindow.Window.Show(); 454 | ShowNotification(new GUIContent("已打开条件窗口,如未发现请检查窗口是否被当前窗口覆盖")); 455 | } 456 | 457 | if (GUILayout.Button("删除", GUILayout.Width(50), GUILayout.Height(50))) 458 | { 459 | Undo.RegisterCompleteObjectUndo(mConfig, "ShaderVariantsStripper config delete condition"); 460 | conditionList.RemoveAt(i); 461 | } 462 | 463 | EditorGUILayout.BeginVertical(GUILayout.Width(25), GUILayout.Height(50)); 464 | if (GUILayout.Button("↑", GUILayout.Width(25), GUILayout.Height(25)) && i != 0) 465 | (conditionList[i - 1], conditionList[i]) = (conditionList[i], conditionList[i - 1]); 466 | if (GUILayout.Button("↓", GUILayout.Width(25), GUILayout.Height(25)) && i != conditionList.Count - 1) 467 | (conditionList[i + 1], conditionList[i]) = (conditionList[i], conditionList[i + 1]); 468 | 469 | EditorGUILayout.EndVertical(); 470 | 471 | EditorGUILayout.EndHorizontal(); 472 | EditorGUILayout.Space(5); 473 | } 474 | EditorGUILayout.EndScrollView(); 475 | #endregion 476 | } 477 | } 478 | else// mConfig == null 479 | { 480 | #region 剔除检查 481 | if (mCurrentContentState == ContentState.StripCheck) 482 | { 483 | Shader nextStripCheckShader = 484 | EditorGUILayout.ObjectField("剔除检查目标Shader", mStripCheckShader, typeof(Shader)) as Shader; 485 | 486 | if (nextStripCheckShader != mStripCheckShader) 487 | { 488 | mStripCheckShader = nextStripCheckShader; 489 | mStripCheckData = ShaderVariantsData.GetDefaultShaderVariantsData(); 490 | mStripCheckAccessLevel = AccessLevel.Global; 491 | mStripCheckSelectedKeywordIndex = 0; 492 | if (nextStripCheckShader != null) 493 | { 494 | mStripCheckShaderGlobalKeywords = sGetShaderGlobalKeywordsMethod.Invoke(null, new object[] { nextStripCheckShader }) as string[]; 495 | mStripCheckShaderLocalKeywords = sGetShaderLocalKeywordsMethod.Invoke(null, new object[] { nextStripCheckShader }) as string[]; 496 | } 497 | } 498 | 499 | if (mStripCheckShader != null) 500 | { 501 | EditorGUILayout.LabelField("变体条件"); 502 | mStripCheckData.shaderType = (ShaderVariantsDataShaderType) EditorGUILayout.EnumPopup("ShaderType", mStripCheckData.shaderType); 503 | mStripCheckData.shaderType = (ShaderVariantsDataShaderType)Mathf.Clamp((int)mStripCheckData.shaderType, 504 | (int)ShaderVariantsDataShaderType.Vertex, (int)ShaderVariantsDataShaderType.RayTracing + 1); 505 | mStripCheckData.passType = (UnityEngine.Rendering.PassType) EditorGUILayout.EnumPopup("PassType", mStripCheckData.passType); 506 | mStripCheckData.passName = EditorGUILayout.TextField("PassName", mStripCheckData.passName); 507 | 508 | string[] keywords = mStripCheckData.GetShaderKeywords(); 509 | 510 | EditorGUILayout.LabelField($"Keyword,共{keywords.Length}个"); 511 | 512 | EditorGUILayout.BeginHorizontal(); 513 | AccessLevel newAccessLevel = (AccessLevel)EditorGUILayout.Popup((int) mStripCheckAccessLevel, new string[] { "Global", "Local" }, GUILayout.Width(rightWidth * 0.3f)); 514 | if (newAccessLevel != mStripCheckAccessLevel) 515 | { 516 | mStripCheckSelectedKeywordIndex = 0; 517 | mStripCheckAccessLevel = newAccessLevel; 518 | } 519 | 520 | if (mStripCheckAccessLevel == AccessLevel.Global && mStripCheckShaderGlobalKeywords.Length > 0 || 521 | mStripCheckAccessLevel == AccessLevel.Local && mStripCheckShaderLocalKeywords.Length > 0) 522 | { 523 | string[] keywordList = mStripCheckAccessLevel == AccessLevel.Global 524 | ? mStripCheckShaderGlobalKeywords 525 | : mStripCheckShaderLocalKeywords; 526 | mStripCheckSelectedKeywordIndex = EditorGUILayout.Popup(mStripCheckSelectedKeywordIndex, 527 | keywordList, GUILayout.Width(rightWidth * 0.3f)); 528 | 529 | string selectedKeyword = keywordList[mStripCheckSelectedKeywordIndex]; 530 | UnityEngine.Rendering.ShaderKeyword newKeyword = 531 | mStripCheckAccessLevel == AccessLevel.Global 532 | ? new ShaderKeyword(selectedKeyword) 533 | : new ShaderKeyword(mStripCheckShader, selectedKeyword); 534 | 535 | if (GUILayout.Button("添加", GUILayout.Width(rightWidth * 0.3f)) && !mStripCheckData.IsKeywordEnabled(newKeyword)) 536 | { 537 | mStripCheckData.EnableKeyword(newKeyword); 538 | } 539 | } 540 | EditorGUILayout.EndHorizontal(); 541 | 542 | EditorGUILayout.BeginHorizontal(); 543 | int keywordCount = 0; 544 | foreach (var keyword in keywords) 545 | { 546 | if (GUILayout.Button(new GUIContent(keyword, keyword), GUILayout.Width(rightWidth * 0.2f))) 547 | { 548 | mStripCheckData.DisableKeyword(keyword); 549 | } 550 | keywordCount++; 551 | if (keywordCount % 4 == 0) 552 | { 553 | EditorGUILayout.EndHorizontal(); 554 | EditorGUILayout.BeginHorizontal(); 555 | } 556 | else 557 | EditorGUILayout.Space(); 558 | } 559 | EditorGUILayout.EndHorizontal(); 560 | 561 | EditorGUILayout.BeginHorizontal(); 562 | if (GUILayout.Button("测试剔除")) 563 | { 564 | mStripCheckConditionList.Clear(); 565 | 566 | string[] guids = AssetDatabase.FindAssets("t:ShaderVariantsStripperConfig", new string[]{"Assets", "Packages"}); 567 | 568 | ShaderVariantsStripperConfig[] allConfigs = (from guid in guids 569 | select AssetDatabase.LoadAssetAtPath( 570 | AssetDatabase.GUIDToAssetPath(guid))) 571 | .ToArray(); 572 | ShaderVariantsStripperCode.StripVariant(mStripCheckShader, mStripCheckData, allConfigs, mStripCheckConditionList); 573 | 574 | bool strip = mStripCheckConditionList.Any(conditionPair_fromConfig => 575 | conditionPair_fromConfig.conditionPair.strip); 576 | bool hasWhiteList = mStripCheckConditionList.Any(conditionPair_fromConfig => 577 | conditionPair_fromConfig.config.mIsWhiteList); 578 | string res = hasWhiteList ? "白名单" : (strip ? "剔除" : "保留"); 579 | ShowNotification(new GUIContent("检测结果为:" + res)); 580 | mStripCheckConditionListScrollViewPos = Vector2.zero; 581 | } 582 | 583 | if (mStripCheckConditionList.Count != 0 && GUILayout.Button("清空结果")) 584 | { 585 | mStripCheckConditionList.Clear(); 586 | } 587 | EditorGUILayout.EndHorizontal(); 588 | 589 | if (mStripCheckConditionList.Count != 0) 590 | { 591 | bool strip = mStripCheckConditionList.Any(conditionPair_fromConfig => 592 | conditionPair_fromConfig.conditionPair.strip); 593 | 594 | bool hasWhiteList = mStripCheckConditionList.Any(conditionPair_fromConfig => 595 | conditionPair_fromConfig.config.mIsWhiteList); 596 | 597 | string res = hasWhiteList ? "白名单" : (strip ? "剔除" : "保留"); 598 | EditorGUILayout.LabelField("检测结果为:" + res); 599 | 600 | mStripCheckConditionListScrollViewPos = 601 | EditorGUILayout.BeginScrollView(mStripCheckConditionListScrollViewPos); 602 | 603 | foreach (var conditionPair_fromConfig in mStripCheckConditionList) 604 | { 605 | string conditionRes = ""; 606 | if (conditionPair_fromConfig.config.mIsWhiteList) 607 | { 608 | conditionRes = " 白名单"; 609 | } 610 | else 611 | { 612 | conditionRes = (conditionPair_fromConfig.conditionPair.strip ? " 剔除" : " 保留") + $" 优先级:{conditionPair_fromConfig.conditionPair.priority}"; 613 | } 614 | 615 | if (GUILayout.Button(conditionPair_fromConfig.config.name + conditionRes + $" 条件:{conditionPair_fromConfig.conditionPair.condition.Overview()}")) 616 | { 617 | Selection.activeObject = conditionPair_fromConfig.config; 618 | EditorGUIUtility.PingObject(conditionPair_fromConfig.config); 619 | } 620 | } 621 | EditorGUILayout.EndScrollView(); 622 | } 623 | } 624 | } 625 | #endregion 626 | } 627 | EditorGUILayout.EndVertical(); 628 | #endregion 629 | 630 | GUILayout.EndHorizontal(); 631 | } 632 | 633 | void SaveObject(Object obj) 634 | { 635 | if (obj != null) 636 | { 637 | EditorUtility.SetDirty(obj); 638 | AssetDatabase.SaveAssets(); 639 | AssetDatabase.Refresh(); 640 | } 641 | } 642 | 643 | Texture2D MakeTex(int width, int height, Color col) 644 | { 645 | Color[] pix = new Color[width * height]; 646 | for (int i = 0; i < pix.Length; i++) 647 | pix[i] = col; 648 | Texture2D result = new Texture2D(width, height); 649 | result.SetPixels(pix); 650 | result.Apply(); 651 | return result; 652 | } 653 | 654 | private void SetupStyle() 655 | { 656 | if (mBlackStyle == null) 657 | { 658 | Color backColor = EditorGUIUtility.isProSkin ? new Color(0.18f, 0.18f, 0.18f) : new Color(0.7f, 0.7f, 0.7f); 659 | Texture2D _blackTexture; 660 | _blackTexture = MakeTex(4, 4, backColor); 661 | _blackTexture.hideFlags = HideFlags.DontSave; 662 | mBlackStyle = new GUIStyle(); 663 | mBlackStyle.normal.background = _blackTexture; 664 | } 665 | 666 | if (mItemStyle == null) 667 | { 668 | Color itemColor = EditorGUIUtility.isProSkin ? new Color(0.3f, 0.3f, 0.3f) : new Color(0.9f, 0.9f, 0.9f); 669 | Texture2D _itemColorTexture; 670 | _itemColorTexture = MakeTex(4, 4, itemColor); 671 | _itemColorTexture.hideFlags = HideFlags.DontSave; 672 | mItemStyle = new GUIStyle(); 673 | mItemStyle.normal.background = _itemColorTexture; 674 | } 675 | } 676 | 677 | private void SetupConditionType() 678 | { 679 | mConditionTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany( 680 | assembly => assembly.GetTypes()).Where( 681 | type => typeof(ShaderVariantsStripperCondition).IsAssignableFrom(type) && !type.IsAbstract 682 | ).ToArray(); 683 | 684 | mConditionNames = mConditionTypes.Select(type => (System.Activator.CreateInstance(type) as ShaderVariantsStripperCondition).GetName()).ToArray(); 685 | } 686 | 687 | private static void InitGetKeywordMethod() 688 | { 689 | if (sGetShaderGlobalKeywordsMethod == null) 690 | sGetShaderGlobalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderGlobalKeywords", 691 | BindingFlags.NonPublic | BindingFlags.Static); 692 | 693 | if (sGetShaderLocalKeywordsMethod == null) 694 | sGetShaderLocalKeywordsMethod = typeof(ShaderUtil).GetMethod("GetShaderLocalKeywords", 695 | BindingFlags.NonPublic | BindingFlags.Static); 696 | } 697 | 698 | public void Awake() 699 | { 700 | SetupStyle(); 701 | SetupConditionType(); 702 | InitGetKeywordMethod(); 703 | } 704 | 705 | public void OnDisable() 706 | { 707 | SaveObject(mConfig); 708 | ShaderVariantsStripperConditionWindow.Window.Close(); 709 | } 710 | } 711 | } --------------------------------------------------------------------------------