254 lines
12 KiB
C#
254 lines
12 KiB
C#
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 重新采集。");
|
||
}
|
||
}
|
||
}
|