UnityVATTool/Assets/Common/Scripts/Editor/MonsterOrientationCheck.cs

149 lines
5.6 KiB
C#

/*
* MonsterOrientationCheck
*
* 一键检测 Assets/_External/Monsters 和 Assets/_External/Bosses 下所有怪物 FBX 的朝向。
* 判定逻辑:实例化 FBX 到隐藏临时场景,读 SkinnedMeshRenderer.bounds:
* - height = bounds.size.y (Unity 站立高度)
* - depth = bounds.size.z
* - width = bounds.size.x
* 如果 height > max(depth, width) * 1.2 → 站立 OK
* 否则需要 X=-90 (从 Z-up 转 Y-up) 或 X=90
*
* 输出表格到 Console + 写 Assets/_External/_OrientationReport.txt。
*/
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace OctoberStudio.EditorTools
{
public static class MonsterOrientationCheck
{
private const string MonstersDir = "Assets/Res/Role/Monster";
private const string BossesDir = "Assets/Res/Role/Boss"; // 可选,有就扫
private const string ReportPath = "Assets/_OrientationReport.txt";
[MenuItem("Tools/OctoberStudio/VAT/Check Monster Orientation", priority = 50)]
public static void Run()
{
var sb = new StringBuilder();
sb.AppendLine("# Monster/Boss FBX 朝向检测报告");
sb.AppendLine($"# 生成时间: {System.DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine("# 判定:height 是 size.y; 若 height 不是最大维度,需要 X 校正");
sb.AppendLine();
sb.AppendLine($"{"",-30} {"X",8} {"Y",8} {"Z",8} {"",4} {" X ?",-15}");
sb.AppendLine(new string('-', 80));
var results = new List<Entry>();
CollectFromDir(MonstersDir, results);
CollectFromDir(BossesDir, results);
int needXCorrect = 0;
foreach (var r in results.OrderBy(e => e.Category).ThenBy(e => e.Name))
{
string mainAxis = GetMainAxis(r.Size);
bool needFix = mainAxis != "Y";
string fixHint = needFix
? (mainAxis == "Z" ? "X=-90 (Z-up)" : (mainAxis == "X" ? "Y=90 (X-up?)" : "?"))
: "OK (Y-up)";
if (needFix) needXCorrect++;
sb.AppendLine($"{r.Name,-30} {r.Size.x,8:F2} {r.Size.y,8:F2} {r.Size.z,8:F2} {mainAxis,4} {fixHint,-15}");
}
sb.AppendLine();
sb.AppendLine($"# 总计: {results.Count}, 需要 X 校正: {needXCorrect}, 直接可用: {results.Count - needXCorrect}");
var text = sb.ToString();
File.WriteAllText(Path.GetFullPath(ReportPath), text);
AssetDatabase.ImportAsset(ReportPath);
Debug.Log(text);
Debug.Log($"[Orientation Check] 报告已写入: {ReportPath}");
}
private static void CollectFromDir(string dir, List<Entry> results)
{
if (!AssetDatabase.IsValidFolder(dir)) return;
// 找每个子目录的主 FBX(不带 @ 的那个)
var subDirs = AssetDatabase.GetSubFolders(dir);
foreach (var sub in subDirs)
{
var fbxGuids = AssetDatabase.FindAssets("t:Model", new[] { sub });
foreach (var g in fbxGuids)
{
var path = AssetDatabase.GUIDToAssetPath(g);
var fileName = Path.GetFileName(path);
// 跳过 @animation / @Run 这种动画 FBX,只测主 FBX
if (fileName.Contains("@")) continue;
var fbx = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (fbx == null) continue;
var bounds = GetCombinedBounds(fbx);
if (bounds.HasValue)
{
results.Add(new Entry
{
Category = dir,
Name = Path.GetFileNameWithoutExtension(path) + " (" + Path.GetFileName(sub) + ")",
Size = bounds.Value.size,
Path = path,
});
}
}
}
}
private static Bounds? GetCombinedBounds(GameObject fbxAsset)
{
// 临时实例化(不入场景),取 SMR.sharedMesh.bounds(这是 local space,反映 FBX 内部朝向)
// 注意: SkinnedMeshRenderer.bounds 是 world space, 但 sharedMesh.bounds 才是本地(模型空间)
var smrs = fbxAsset.GetComponentsInChildren<SkinnedMeshRenderer>(true);
var mfs = fbxAsset.GetComponentsInChildren<MeshFilter>(true);
bool inited = false;
Bounds b = new Bounds();
foreach (var smr in smrs)
{
if (smr.sharedMesh == null) continue;
if (!inited) { b = smr.sharedMesh.bounds; inited = true; }
else b.Encapsulate(smr.sharedMesh.bounds);
}
foreach (var mf in mfs)
{
if (mf.sharedMesh == null) continue;
if (!inited) { b = mf.sharedMesh.bounds; inited = true; }
else b.Encapsulate(mf.sharedMesh.bounds);
}
return inited ? b : (Bounds?)null;
}
private static string GetMainAxis(Vector3 size)
{
float x = Mathf.Abs(size.x);
float y = Mathf.Abs(size.y);
float z = Mathf.Abs(size.z);
if (y >= x && y >= z) return "Y";
if (z >= x && z >= y) return "Z";
return "X";
}
private class Entry
{
public string Category;
public string Name;
public Vector3 Size;
public string Path;
}
}
}