Files
AIC-Project/Client/Assets/Editor/ShaderVariantCollector.cs
2026-07-10 10:24:29 +08:00

254 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace XGameEditor
{
/// <summary>
/// 收集 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。
/// </summary>
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<Camera>();
if (cam == null)
cam = new GameObject("Capture Camera", typeof(Camera)).GetComponent<Camera>();
Light light = Object.FindObjectOfType<Light>();
if (light == null)
light = new GameObject("Directional Light", typeof(Light)).GetComponent<Light>();
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<Material>(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<Collider>();
if (collider != null) Object.DestroyImmediate(collider);
cube.transform.position = new Vector3(col * spacing - half, row * spacing - half, 0f);
cube.GetComponent<MeshRenderer>().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<ShaderVariantCollection>(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<ShaderVariantCollection>(OutputPath);
if (asset != null) EditorGUIUtility.PingObject(asset);
}
[MenuItem("Tools/Shader/清空已跟踪变体")]
public static void ClearTrackedVariants()
{
ClearCollection();
Debug.Log("[ShaderVariantCollector] 已清空当前跟踪的 shader 变体,可重新进入 Play 重新采集。");
}
}
}