using System; using System.IO; using System.Reflection; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace XGameEditor { /// /// 收集 shader 变体(ShaderVariantCollection)并保存到 Assets/Game/Shader/Game.shadervariants, /// 该文件会被活跃打包流程打进 game_shader.unity3d,运行时由 Game.Init() 加载并 WarmUp。 /// /// 关键作用:被包含进构建的 SVC 所引用的变体不会被 URP 的 "Strip Unused Variants" 剔除, /// 从而修复"从 AssetBundle 加载的预设(如 Main)在真机上平行光/光照不起效"的问题。 /// /// 提供两个入口: /// - Tools/Shader/自动收集Art材质变体 :自动遍历 Assets/Game/Art 下所有材质, /// 在临时场景里逐个建 cube 渲染一遍并收集(推荐)。 /// - Tools/Shader/收集当前Shader变体到SVC:手动版,收集"当前编辑器会话已渲染过"的变体。 /// /// 说明:用到的 4 个 ShaderUtil 方法在 2022.3 里是 internal,公开 API 不可见, /// 因此通过反射调用(参见 UnityCsReference/Editor/Mono/ShaderUtil.bindings.cs)。 /// 光照相关变体只有在材质被"实际渲染"时才会由 URP 设置并被跟踪,所以必须渲染 cube。 /// public static class ShaderVariantCollector { private const string ArtDir = "Assets/Game/Art"; // 放在 Assets/Game/Shader 顶层:活跃打包流程 HybridCLRBuild.BuildHotFix 里 // AddBuildMap("game_shader.unity3d","*.*","Assets/Game/Shader") 会(非递归地)把顶层文件打进 game_shader.unity3d。 private const string OutputDir = "Assets/Game/Shader"; private const string OutputPath = OutputDir + "/Game.shadervariants"; private const string TempScenePath = "Assets/Scenes/__temp_shadervariant_collect.unity"; // ---------------- 反射封装的 internal ShaderUtil 接口 ---------------- private static MethodInfo GetMethod(string name) { var m = typeof(ShaderUtil).GetMethod( name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (m == null) Debug.LogError($"[ShaderVariantCollector] 反射找不到 ShaderUtil.{name}," + "当前 Unity 版本可能改了该内部接口。"); return m; } private static void ClearCollection() { GetMethod("ClearCurrentShaderVariantCollection")?.Invoke(null, null); } private static bool SaveCollection(string path) { var m = GetMethod("SaveCurrentShaderVariantCollection"); if (m == null) return false; m.Invoke(null, new object[] { path }); return true; } private static int GetTrackedShaderCount() { var m = GetMethod("GetCurrentShaderVariantCollectionShaderCount"); return m != null ? (int)m.Invoke(null, null) : -1; } private static int GetTrackedVariantCount() { var m = GetMethod("GetCurrentShaderVariantCollectionVariantCount"); return m != null ? (int)m.Invoke(null, null) : -1; } // ---------------- 自动收集 Assets/Game/Art 下所有材质 ---------------- [MenuItem("Tools/Shader/自动收集Art材质变体")] public static void AutoCollectArtMaterials() { if (!Directory.Exists(ArtDir)) { EditorUtility.DisplayDialog("目录不存在", $"找不到目录:\n{ArtDir}", "好"); return; } // 先让用户保存当前场景,避免丢失未保存内容 if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return; // 1. 遍历材质 string[] guids = AssetDatabase.FindAssets("t:Material", new[] { ArtDir }); if (guids.Length == 0) { EditorUtility.DisplayDialog("没有材质", $"{ArtDir} 下没有找到任何材质。", "好"); return; } if (guids.Length > 3000 && !EditorUtility.DisplayDialog("材质较多", $"共找到 {guids.Length} 个材质,将创建同等数量的 cube 渲染,可能较慢且占用内存。是否继续?", "继续", "取消")) return; // 2. 新建带默认相机+平行光的临时场景并保存到 Assets/Scenes Scene scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single); string sceneDir = Path.GetDirectoryName(TempScenePath); if (!Directory.Exists(sceneDir)) Directory.CreateDirectory(sceneDir); Camera cam = Object.FindObjectOfType(); if (cam == null) cam = new GameObject("Capture Camera", typeof(Camera)).GetComponent(); Light light = Object.FindObjectOfType(); if (light == null) light = new GameObject("Directional Light", typeof(Light)).GetComponent(); light.type = LightType.Directional; light.intensity = 1f; light.shadows = LightShadows.Soft; light.transform.rotation = Quaternion.Euler(50f, -30f, 0f); RenderTexture rt = null; try { // 3. 网格排布,逐个材质创建 cube 并赋上去 const float spacing = 2.2f; int cols = Mathf.CeilToInt(Mathf.Sqrt(guids.Length)); float half = (cols - 1) * spacing * 0.5f; int created = 0; for (int i = 0; i < guids.Length; i++) { string path = AssetDatabase.GUIDToAssetPath(guids[i]); var mat = AssetDatabase.LoadAssetAtPath(path); if (mat == null || mat.shader == null) continue; if (i % 50 == 0) EditorUtility.DisplayProgressBar("收集Shader变体", $"创建 cube ({i + 1}/{guids.Length})", (float)i / guids.Length); int col = i % cols; int row = i / cols; var cube = GameObject.CreatePrimitive(PrimitiveType.Cube); cube.name = mat.name; var collider = cube.GetComponent(); if (collider != null) Object.DestroyImmediate(collider); cube.transform.position = new Vector3(col * spacing - half, row * spacing - half, 0f); cube.GetComponent().sharedMaterial = mat; created++; } // 4. 正交相机正对网格,渲染到 RT,保证全部进视锥被实际渲染 float gridExtent = cols * spacing * 0.5f + spacing; cam.orthographic = true; cam.orthographicSize = gridExtent; cam.aspect = 1f; cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = Color.gray; cam.transform.position = new Vector3(0f, 0f, -20f); cam.transform.rotation = Quaternion.identity; cam.nearClipPlane = 0.1f; cam.farClipPlane = 100f; EditorSceneManager.SaveScene(scene, TempScenePath); rt = new RenderTexture(2048, 2048, 24); cam.targetTexture = rt; // 采集前清空,确保结果只包含本次渲染的变体 ClearCollection(); EditorUtility.DisplayProgressBar("收集Shader变体", "渲染中...", 0.9f); // 多渲染几帧,确保各变体都被编译并跟踪 for (int k = 0; k < 4; k++) cam.Render(); cam.targetTexture = null; int variantCount = GetTrackedVariantCount(); int shaderCount = GetTrackedShaderCount(); // 5. 保存 SVC if (!Directory.Exists(OutputDir)) Directory.CreateDirectory(OutputDir); bool ok = SaveCollection(OutputPath); AssetDatabase.ImportAsset(OutputPath, ImportAssetOptions.ForceUpdate); AssetDatabase.Refresh(); Debug.Log($"[ShaderVariantCollector] 自动收集完成: {OutputPath}\n" + $" 材质: {guids.Length}, 创建cube: {created}, " + $"shader数: {shaderCount}, 变体数: {variantCount}"); if (!ok || variantCount <= 0) { EditorUtility.DisplayDialog("收集异常", $"变体数={variantCount}。\n若为 0,可能是 URP 下编辑器 Camera.Render 未触发跟踪," + "可改用 Play 模式 + Tools/Shader/收集当前Shader变体到SVC 兜底。", "好"); } else { EditorUtility.DisplayDialog("收集完成", $"已写入: {OutputPath}\n材质: {guids.Length}\nshader数: {shaderCount}\n变体数: {variantCount}\n\n现在可以重新打包构建。", "好"); var asset = AssetDatabase.LoadAssetAtPath(OutputPath); if (asset != null) EditorGUIUtility.PingObject(asset); } } finally { EditorUtility.ClearProgressBar(); if (rt != null) { if (cam != null) cam.targetTexture = null; rt.Release(); Object.DestroyImmediate(rt); } } } // ---------------- 手动收集(当前会话已渲染过的变体) ---------------- [MenuItem("Tools/Shader/收集当前Shader变体到SVC")] public static void SaveCurrentVariants() { if (GetMethod("SaveCurrentShaderVariantCollection") == null) return; int variantCount = GetTrackedVariantCount(); int shaderCount = GetTrackedShaderCount(); if (variantCount == 0 && !EditorUtility.DisplayDialog("没有可收集的变体", "当前跟踪到的 shader 变体为 0。\n\n请先进入 Play 模式并显示出问题界面/模型,让相关 shader 实际渲染一次,再执行本命令。", "我知道了", "取消")) return; if (!Directory.Exists(OutputDir)) Directory.CreateDirectory(OutputDir); if (!SaveCollection(OutputPath)) return; AssetDatabase.ImportAsset(OutputPath, ImportAssetOptions.ForceUpdate); AssetDatabase.Refresh(); Debug.Log($"[ShaderVariantCollector] 已保存变体集合: {OutputPath}\n" + $" shader 数: {shaderCount}, 变体数: {variantCount}"); EditorUtility.DisplayDialog("收集完成", $"已写入: {OutputPath}\nshader 数: {shaderCount}\n变体数: {variantCount}\n\n现在可以重新打包构建。", "好"); var asset = AssetDatabase.LoadAssetAtPath(OutputPath); if (asset != null) EditorGUIUtility.PingObject(asset); } [MenuItem("Tools/Shader/清空已跟踪变体")] public static void ClearTrackedVariants() { ClearCollection(); Debug.Log("[ShaderVariantCollector] 已清空当前跟踪的 shader 变体,可重新进入 Play 重新采集。"); } } }