149 lines
5.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|