Files
PrinceOfGlory/Assets/SoraTools/SceneAnalyzer/Editor/SceneAnalyzer.cs
2026-03-03 18:24:17 +08:00

640 lines
27 KiB
C#
Raw 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.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;
using System.Linq;
using UnityEngine.Profiling;
using System.Text;
namespace SoraTools
{
public class SceneAnalyzer : EditorWindow
{
private enum TargetDevice
{
Android_8G,
Android_12G
}
private TargetDevice currentTarget = TargetDevice.Android_8G;
private Vector2 scrollPos;
// 分析结果缓存
private bool hasAnalyzed = false;
private AnalysisData currentData;
// GUI Styles
private GUIStyle titleStyle;
private GUIStyle headerStyle;
private GUIStyle boxStyle;
private GUIStyle resultGoodStyle;
private GUIStyle resultWarnStyle;
private GUIStyle resultBadStyle;
private GUIStyle labelStyle;
private GUIStyle linkStyle;
[MenuItem("SoraTools/Scene Analyzer %&r")]
public static void ShowWindow()
{
var window = GetWindow<SceneAnalyzer>("Scene Analyzer");
window.minSize = new Vector2(400, 600);
window.Show();
}
private void OnEnable()
{
// 初始化样式
InitStyles();
}
private void InitStyles()
{
titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 18,
alignment = TextAnchor.MiddleCenter,
margin = new RectOffset(0, 0, 10, 10)
};
headerStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14,
margin = new RectOffset(0, 0, 10, 5)
};
boxStyle = new GUIStyle(EditorStyles.helpBox)
{
padding = new RectOffset(10, 10, 10, 10),
margin = new RectOffset(5, 5, 5, 5)
};
labelStyle = new GUIStyle(EditorStyles.label)
{
fontSize = 12,
richText = true
};
resultGoodStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.2f, 0.8f, 0.2f) }, fontStyle = FontStyle.Bold };
resultWarnStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.9f, 0.7f, 0.0f) }, fontStyle = FontStyle.Bold };
resultBadStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.9f, 0.3f, 0.3f) }, fontStyle = FontStyle.Bold };
linkStyle = new GUIStyle(EditorStyles.label) {
normal = { textColor = new Color(0.3f, 0.6f, 1.0f) },
hover = { textColor = new Color(0.4f, 0.7f, 1.0f) },
fontStyle = FontStyle.Italic
};
}
private void OnGUI()
{
if (titleStyle == null) InitStyles();
EditorGUILayout.LabelField("场景性能分析工具", titleStyle);
EditorGUILayout.Space();
// 设置区域
EditorGUILayout.BeginVertical(boxStyle);
EditorGUILayout.LabelField("目标设备设置", headerStyle);
currentTarget = (TargetDevice)EditorGUILayout.EnumPopup("目标设备内存", currentTarget);
EditorGUILayout.Space();
if (GUILayout.Button("开始分析当前场景", GUILayout.Height(40)))
{
AnalyzeScene();
}
EditorGUILayout.EndVertical();
if (!hasAnalyzed || currentData == null)
{
EditorGUILayout.HelpBox("请点击上方按钮开始分析当前场景。", MessageType.Info);
return;
}
// 结果展示区域
EditorGUILayout.Space();
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
DrawSection("场景概览", () =>
{
DrawStatItem("GameObjects 总数", currentData.objectCount,
GetThreshold(MetricType.ObjectCount).warning,
GetThreshold(MetricType.ObjectCount).limit);
DrawStatItem("总顶点数 (Vertices)", currentData.totalVertices,
GetThreshold(MetricType.Vertices).warning,
GetThreshold(MetricType.Vertices).limit, FormatNumber);
DrawStatItem("总面数 (Triangles)", currentData.totalTriangles,
GetThreshold(MetricType.Triangles).warning,
GetThreshold(MetricType.Triangles).limit, FormatNumber);
DrawStatItem("非 Static 物体", currentData.nonStaticCount,
500, 1000, null,
"点击选中非Static物体", currentData.nonStaticObjects);
DrawStatItem("Missing Scripts", currentData.missingScriptCount,
0, 0, null,
"点击选中Missing Script物体", currentData.missingScriptObjects);
});
DrawSection("灯光与渲染 (Lighting & Rendering)", () =>
{
DrawStatItem("实时光源 (Realtime Lights)", currentData.realtimeLightCount,
GetThreshold(MetricType.RealtimeLights).warning,
GetThreshold(MetricType.RealtimeLights).limit, null,
"点击选中实时光源", currentData.realtimeLights);
DrawStatItem("阴影投射光源 (Shadow Casters)", currentData.shadowCastingLights,
GetThreshold(MetricType.ShadowLights).warning,
GetThreshold(MetricType.ShadowLights).limit, null,
"点击选中阴影光源", currentData.shadowLights);
DrawStatItem("总光源数量 (All Lights)", currentData.totalLightCount,
GetThreshold(MetricType.TotalLights).warning,
GetThreshold(MetricType.TotalLights).limit);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("是否开启 Fog", GUILayout.Width(200));
EditorGUILayout.LabelField(currentData.hasFog ? "开启 (可能有性能消耗)" : "关闭",
currentData.hasFog ? resultWarnStyle : resultGoodStyle);
EditorGUILayout.EndHorizontal();
DrawStatItem("半透明物体 (Overdraw风险)", currentData.transparentObjectCount,
GetThreshold(MetricType.TransparentObjects).warning,
GetThreshold(MetricType.TransparentObjects).limit, null,
"点击选中半透明物体", currentData.transparentObjects);
});
DrawSection("动画与特效 (Animation & FX)", () =>
{
DrawStatItem("蒙皮网格 (Skinned Mesh)", currentData.skinnedMeshCount,
GetThreshold(MetricType.SkinnedMesh).warning,
GetThreshold(MetricType.SkinnedMesh).limit, null,
"点击选中蒙皮网格", currentData.skinnedMeshes);
DrawStatItem("粒子系统 (Particle Systems)", currentData.particleSystemCount,
GetThreshold(MetricType.ParticleSystems).warning,
GetThreshold(MetricType.ParticleSystems).limit, null,
"点击选中粒子系统", currentData.particleSystems);
});
DrawSection("物理 (Physics)", () =>
{
DrawStatItem("复杂 MeshCollider", currentData.complexMeshColliderCount,
0, 10, null,
"点击选中复杂碰撞体", currentData.complexMeshColliders);
});
DrawSection("内存占用 (估算)", () =>
{
DrawStatItem("纹理内存 (Texture)", currentData.textureMemory,
GetThreshold(MetricType.TextureMem).warning,
GetThreshold(MetricType.TextureMem).limit, FormatBytes,
"查看 Top 10 纹理", null, () => ShowTopResourcesWindow("Top 10 Textures", currentData.topTextures));
DrawStatItem("网格内存 (Mesh)", currentData.meshMemory,
GetThreshold(MetricType.MeshMem).warning,
GetThreshold(MetricType.MeshMem).limit, FormatBytes,
"查看 Top 10 网格", null, () => ShowTopResourcesWindow("Top 10 Meshes", currentData.topMeshes));
DrawStatItem("音频内存 (Audio)", currentData.audioMemory,
GetThreshold(MetricType.AudioMem).warning,
GetThreshold(MetricType.AudioMem).limit, FormatBytes);
});
DrawSection("资源详情", () =>
{
DrawStatItem("独立纹理数量", currentData.uniqueTextureCount, 1000, 2000);
DrawStatItem("独立材质数量", currentData.uniqueMaterialCount, 200, 500);
DrawStatItem("独立网格数量", currentData.uniqueMeshCount, 500, 1000);
});
EditorGUILayout.EndScrollView();
}
private void AnalyzeScene()
{
EditorUtility.DisplayProgressBar("场景分析中", "正在收集场景对象...", 0.1f);
try
{
currentData = new AnalysisData();
var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
var allRenderers = new List<Renderer>();
var allColliders = new List<Collider>();
// 递归收集所有对象
int totalRoots = rootObjects.Length;
for (int i = 0; i < totalRoots; i++)
{
var root = rootObjects[i];
EditorUtility.DisplayProgressBar("场景分析中", $"正在分析对象: {root.name}", 0.1f + (float)i / totalRoots * 0.4f);
allRenderers.AddRange(root.GetComponentsInChildren<Renderer>(true));
var colliders = root.GetComponentsInChildren<Collider>(true);
allColliders.AddRange(colliders);
var transforms = root.GetComponentsInChildren<Transform>(true);
currentData.objectCount += transforms.Length;
// 检查 Static Batching 和 Missing Scripts
foreach (var t in transforms)
{
if (!t.gameObject.isStatic)
{
currentData.nonStaticCount++;
currentData.nonStaticObjects.Add(t.gameObject);
}
// 检查 Missing Scripts
var components = t.GetComponents<Component>();
foreach (var c in components)
{
if (c == null)
{
currentData.missingScriptCount++;
currentData.missingScriptObjects.Add(t.gameObject);
break;
}
}
}
// 检查复杂 MeshCollider
foreach(var col in colliders)
{
if(col is MeshCollider mc && !mc.convex)
{
currentData.complexMeshColliderCount++;
currentData.complexMeshColliders.Add(col.gameObject);
}
}
}
EditorUtility.DisplayProgressBar("场景分析中", "正在分析网格与材质...", 0.6f);
// 分析网格
HashSet<Mesh> uniqueMeshes = new HashSet<Mesh>();
List<ResourceInfo> meshInfos = new List<ResourceInfo>();
foreach (var renderer in allRenderers)
{
Mesh mesh = null;
if (renderer is MeshRenderer)
{
var filter = renderer.GetComponent<MeshFilter>();
if (filter != null) mesh = filter.sharedMesh;
}
else if (renderer is SkinnedMeshRenderer smr)
{
mesh = smr.sharedMesh;
currentData.skinnedMeshCount++;
currentData.skinnedMeshes.Add(smr.gameObject);
}
if (mesh != null)
{
currentData.totalVertices += mesh.vertexCount;
currentData.totalTriangles += mesh.triangles.Length / 3;
if (uniqueMeshes.Add(mesh))
{
long mem = Profiler.GetRuntimeMemorySizeLong(mesh);
currentData.meshMemory += mem;
meshInfos.Add(new ResourceInfo { name = mesh.name, size = mem, obj = mesh });
}
}
// 检查半透明物体
foreach(var mat in renderer.sharedMaterials)
{
if (mat != null && (mat.renderQueue >= 2500 || mat.FindPass("Transparent") >= 0 || mat.GetTag("RenderType", false) == "Transparent"))
{
currentData.transparentObjectCount++;
currentData.transparentObjects.Add(renderer.gameObject);
break;
}
}
}
currentData.uniqueMeshCount = uniqueMeshes.Count;
currentData.topMeshes = meshInfos.OrderByDescending(x => x.size).Take(10).ToList();
// 分析材质和纹理
HashSet<Material> uniqueMaterials = new HashSet<Material>();
HashSet<Texture> uniqueTextures = new HashSet<Texture>();
List<ResourceInfo> textureInfos = new List<ResourceInfo>();
foreach (var renderer in allRenderers)
{
foreach (var mat in renderer.sharedMaterials)
{
if (mat != null && uniqueMaterials.Add(mat))
{
var shader = mat.shader;
int propertyCount = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < propertyCount; i++)
{
if (ShaderUtil.GetPropertyType(shader, i) == ShaderUtil.ShaderPropertyType.TexEnv)
{
string propertyName = ShaderUtil.GetPropertyName(shader, i);
Texture tex = mat.GetTexture(propertyName);
if (tex != null)
{
uniqueTextures.Add(tex);
}
}
}
}
}
}
currentData.uniqueMaterialCount = uniqueMaterials.Count;
currentData.uniqueTextureCount = uniqueTextures.Count;
foreach (var tex in uniqueTextures)
{
long mem = Profiler.GetRuntimeMemorySizeLong(tex);
currentData.textureMemory += mem;
textureInfos.Add(new ResourceInfo { name = tex.name, size = mem, obj = tex });
}
currentData.topTextures = textureInfos.OrderByDescending(x => x.size).Take(10).ToList();
EditorUtility.DisplayProgressBar("场景分析中", "正在分析音频与灯光...", 0.8f);
// 分析音频
var audioSources = Resources.FindObjectsOfTypeAll<AudioSource>();
HashSet<AudioClip> uniqueClips = new HashSet<AudioClip>();
foreach(var root in rootObjects)
{
var sources = root.GetComponentsInChildren<AudioSource>(true);
foreach(var source in sources)
{
if(source.clip != null) uniqueClips.Add(source.clip);
}
}
foreach(var clip in uniqueClips)
{
currentData.audioMemory += Profiler.GetRuntimeMemorySizeLong(clip);
}
// 分析灯光
var lights = Object.FindObjectsOfType<Light>();
currentData.totalLightCount = lights.Length;
foreach (var light in lights)
{
if (light.lightmapBakeType == LightmapBakeType.Realtime || light.lightmapBakeType == LightmapBakeType.Mixed)
{
currentData.realtimeLightCount++;
currentData.realtimeLights.Add(light.gameObject);
}
if (light.shadows != LightShadows.None)
{
currentData.shadowCastingLights++;
currentData.shadowLights.Add(light.gameObject);
}
}
// 分析粒子系统
var particles = Object.FindObjectsOfType<ParticleSystem>();
currentData.particleSystemCount = particles.Length;
foreach(var ps in particles)
{
currentData.particleSystems.Add(ps.gameObject);
}
// 分析 RenderSettings
currentData.hasFog = RenderSettings.fog;
hasAnalyzed = true;
}
finally
{
EditorUtility.ClearProgressBar();
}
}
private void ShowTopResourcesWindow(string title, List<ResourceInfo> resources)
{
TopResourcesWindow.Show(title, resources);
}
private void DrawSection(string title, System.Action content)
{
EditorGUILayout.BeginVertical(boxStyle);
EditorGUILayout.LabelField(title, headerStyle);
EditorGUILayout.Space(5);
content();
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private void DrawStatItem(string label, long value, long warningThreshold, long limitThreshold,
System.Func<long, string> formatter = null, string selectButtonText = null, List<GameObject> objectsToSelect = null, System.Action onDetailClick = null)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(label, GUILayout.Width(200));
string displayValue = formatter != null ? formatter(value) : value.ToString();
string suggestion = "";
GUIStyle style = resultGoodStyle;
if (value > limitThreshold)
{
style = resultBadStyle;
suggestion = $"(严重超标! 建议 < {formatter?.Invoke(limitThreshold) ?? limitThreshold.ToString()})";
}
else if (value > warningThreshold)
{
style = resultWarnStyle;
suggestion = $"(偏高, 建议 < {formatter?.Invoke(warningThreshold) ?? warningThreshold.ToString()})";
}
EditorGUILayout.LabelField(displayValue, style, GUILayout.Width(100));
// 按钮逻辑
if (onDetailClick != null)
{
if (GUILayout.Button(selectButtonText ?? "Detail", GUILayout.Width(120)))
{
onDetailClick.Invoke();
}
}
else if (!string.IsNullOrEmpty(selectButtonText) && objectsToSelect != null && objectsToSelect.Count > 0)
{
if (GUILayout.Button("Select", GUILayout.Width(60)))
{
Selection.objects = objectsToSelect.ToArray();
EditorGUIUtility.PingObject(objectsToSelect[0]);
}
}
if (!string.IsNullOrEmpty(suggestion))
{
EditorGUILayout.LabelField(suggestion, labelStyle);
}
EditorGUILayout.EndHorizontal();
}
private string FormatNumber(long num)
{
if (num > 1000000) return (num / 1000000f).ToString("F1") + "M";
if (num > 1000) return (num / 1000f).ToString("F1") + "K";
return num.ToString();
}
private string FormatBytes(long bytes)
{
if (bytes > 1024 * 1024 * 1024) return (bytes / (1024f * 1024f * 1024f)).ToString("F2") + " GB";
if (bytes > 1024 * 1024) return (bytes / (1024f * 1024f)).ToString("F1") + " MB";
if (bytes > 1024) return (bytes / 1024f).ToString("F1") + " KB";
return bytes + " B";
}
private (long warning, long limit) GetThreshold(MetricType type)
{
bool is12G = currentTarget == TargetDevice.Android_12G;
switch (type)
{
case MetricType.ObjectCount:
return is12G ? (10000, 15000) : (5000, 10000);
case MetricType.Vertices:
return is12G ? (3000000, 5000000) : (1500000, 3000000);
case MetricType.Triangles:
return is12G ? (3000000, 5000000) : (1500000, 3000000);
case MetricType.TextureMem:
return is12G ? (1536L * 1024 * 1024, 2560L * 1024 * 1024) : (1024L * 1024 * 1024, 1536L * 1024 * 1024); // 1.5G/2.5G vs 1G/1.5G
case MetricType.MeshMem:
return is12G ? (512L * 1024 * 1024, 800L * 1024 * 1024) : (256L * 1024 * 1024, 512L * 1024 * 1024);
case MetricType.AudioMem:
return is12G ? (100L * 1024 * 1024, 200L * 1024 * 1024) : (50L * 1024 * 1024, 100L * 1024 * 1024);
case MetricType.RealtimeLights:
return is12G ? (3, 5) : (1, 3);
case MetricType.ShadowLights:
return is12G ? (1, 2) : (1, 1);
case MetricType.TotalLights:
return is12G ? (20, 50) : (10, 20); // 包含烘焙灯光
case MetricType.SkinnedMesh:
return is12G ? (50, 100) : (30, 50);
case MetricType.ParticleSystems:
return is12G ? (50, 100) : (30, 50);
case MetricType.TransparentObjects:
return is12G ? (500, 1000) : (300, 500); // 预估数量,非严格标准
default:
return (0, 0);
}
}
public class ResourceInfo
{
public string name;
public long size;
public Object obj;
}
private class AnalysisData
{
public int objectCount;
public long totalVertices;
public long totalTriangles;
public long textureMemory;
public long meshMemory;
public long audioMemory;
public int uniqueTextureCount;
public int uniqueMaterialCount;
public int uniqueMeshCount;
public int realtimeLightCount;
public int shadowCastingLights;
public int totalLightCount;
public bool hasFog;
public int skinnedMeshCount;
public int particleSystemCount;
public int transparentObjectCount;
public int nonStaticCount;
// 新增Missing Script 和 MeshCollider 统计
public int missingScriptCount;
public int complexMeshColliderCount;
public List<GameObject> realtimeLights = new List<GameObject>();
public List<GameObject> shadowLights = new List<GameObject>();
public List<GameObject> skinnedMeshes = new List<GameObject>();
public List<GameObject> particleSystems = new List<GameObject>();
public List<GameObject> transparentObjects = new List<GameObject>();
public List<GameObject> nonStaticObjects = new List<GameObject>();
public List<GameObject> missingScriptObjects = new List<GameObject>();
public List<GameObject> complexMeshColliders = new List<GameObject>();
// Top Resources
public List<ResourceInfo> topTextures = new List<ResourceInfo>();
public List<ResourceInfo> topMeshes = new List<ResourceInfo>();
}
// 独立的窗口类用于显示详细列表
public class TopResourcesWindow : EditorWindow
{
private List<ResourceInfo> resources;
private Vector2 scroll;
public static void Show(string title, List<ResourceInfo> resources)
{
var win = GetWindow<TopResourcesWindow>(true, title, true);
win.resources = resources;
win.minSize = new Vector2(400, 300);
win.Show();
}
private void OnGUI()
{
if (resources == null) return;
scroll = EditorGUILayout.BeginScrollView(scroll);
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"资源名称", "大小", EditorStyles.boldLabel);
EditorGUILayout.Space();
foreach (var res in resources)
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
EditorGUILayout.LabelField(res.name, GUILayout.Width(250));
EditorGUILayout.LabelField(EditorUtility.FormatBytes(res.size), GUILayout.Width(80));
if (GUILayout.Button("Select", GUILayout.Width(60)))
{
Selection.activeObject = res.obj;
EditorGUIUtility.PingObject(res.obj);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}
}
private enum MetricType
{
ObjectCount,
Vertices,
Triangles,
TextureMem,
MeshMem,
AudioMem,
RealtimeLights,
ShadowLights,
TotalLights,
SkinnedMesh,
ParticleSystems,
TransparentObjects
}
}
}