Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
@@ -0,0 +1,253 @@
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 重新采集。");
}
}
}