1623 lines
56 KiB
C#
1623 lines
56 KiB
C#
using System.Collections.Concurrent;
|
||||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Linq;
|
|||
|
|
using System.Threading;
|
|||
|
|
using System.Threading.Tasks;
|
|||
|
|
using UnityEditor;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
public class HumanoidBoneMismatchChecker : EditorWindow
|
|||
|
|
{
|
|||
|
|
// 参考对象(可以是Prefab或Hierarchy中的对象)
|
|||
|
|
private GameObject referenceObject;
|
|||
|
|
|
|||
|
|
// 从参考对象读取的实际骨骼结构(带完整路径)
|
|||
|
|
private List<string> actualBoneFullPaths = new List<string>();
|
|||
|
|
private List<string> actualBoneNames = new List<string>();
|
|||
|
|
private Dictionary<string, string> bonePathToName = new Dictionary<string, string>();
|
|||
|
|
|
|||
|
|
private List<AnimationClip> clips = new List<AnimationClip>();
|
|||
|
|
|
|||
|
|
private Dictionary<AnimationClip, List<string>> mismatchDetails = new Dictionary<AnimationClip, List<string>>();
|
|||
|
|
private Dictionary<AnimationClip, int> mismatchCountMap = new Dictionary<AnimationClip, int>();
|
|||
|
|
private Dictionary<AnimationClip, float> matchPercentageMap = new Dictionary<AnimationClip, float>();
|
|||
|
|
private Dictionary<AnimationClip, List<string>> animationBonesCache = new Dictionary<AnimationClip, List<string>>();
|
|||
|
|
|
|||
|
|
// 多条件检测设置
|
|||
|
|
private bool useMismatchCount = true;
|
|||
|
|
private bool useMatchPercentage = true;
|
|||
|
|
private bool useRequiredBonesList = false;
|
|||
|
|
private bool useMinBoneCount = false;
|
|||
|
|
|
|||
|
|
private int mismatchCountThreshold = 3;
|
|||
|
|
private float matchPercentageRequired = 80f;
|
|||
|
|
private int minBoneCountRequired = 10;
|
|||
|
|
private List<string> requiredBonesList = new List<string>();
|
|||
|
|
private string newRequiredBone = "";
|
|||
|
|
private Vector2 requiredBonesScrollPos;
|
|||
|
|
|
|||
|
|
// 匹配模式
|
|||
|
|
private bool useFullPathMatch = true;
|
|||
|
|
|
|||
|
|
// 判断逻辑
|
|||
|
|
private bool useAndLogic = true;
|
|||
|
|
|
|||
|
|
private Vector2 clipListScrollPos;
|
|||
|
|
private Vector2 resultScrollPos;
|
|||
|
|
private Vector2 boneListScrollPos;
|
|||
|
|
|
|||
|
|
private string rootFolder = "Assets/Animations";
|
|||
|
|
private string goodSubFolder = "good";
|
|||
|
|
private string badSubFolder = "bad";
|
|||
|
|
private bool exportBadClips = true;
|
|||
|
|
|
|||
|
|
private bool hasRunCheck = false;
|
|||
|
|
private bool showAdvancedInfo = false;
|
|||
|
|
private bool showBoneList = true;
|
|||
|
|
private bool showConditionSettings = true;
|
|||
|
|
|
|||
|
|
// 异步处理
|
|||
|
|
private bool isProcessing = false;
|
|||
|
|
private float progress = 0f;
|
|||
|
|
private string currentProgressText = "";
|
|||
|
|
private Task currentTask;
|
|||
|
|
|
|||
|
|
// 缓存数据用于异步处理
|
|||
|
|
private struct ClipData
|
|||
|
|
{
|
|||
|
|
public string clipPath;
|
|||
|
|
public string clipName;
|
|||
|
|
public List<string> bonePaths; // 预缓存的骨骼路径
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private List<ClipData> cachedClipData = new List<ClipData>();
|
|||
|
|
|
|||
|
|
[MenuItem("Tools/动画骨骼检测工具")]
|
|||
|
|
public static void ShowWindow()
|
|||
|
|
{
|
|||
|
|
GetWindow<HumanoidBoneMismatchChecker>("动画骨骼检测工具");
|
|||
|
|
}
|
|||
|
|
Vector2 scrollPos;
|
|||
|
|
private void OnGUI()
|
|||
|
|
{
|
|||
|
|
// 整体滚动区域
|
|||
|
|
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
|||
|
|
|
|||
|
|
GUILayout.Label("动画骨骼检测工具", EditorStyles.boldLabel);
|
|||
|
|
|
|||
|
|
// 1. 参考对象
|
|||
|
|
DrawReferenceObjectSection();
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
// 2. 动画列表
|
|||
|
|
DrawClipList();
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
// 3. 条件设置
|
|||
|
|
DrawConditionSettings();
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
// 4. 检测按钮
|
|||
|
|
if (!isProcessing)
|
|||
|
|
{
|
|||
|
|
if (GUILayout.Button("开始检测动画", GUILayout.Height(40)))
|
|||
|
|
RunCheckAsync();
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
DrawProgressBar();
|
|||
|
|
EditorGUILayout.EndScrollView();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
// 5. 检测结果
|
|||
|
|
GUILayout.Label("检测结果", EditorStyles.boldLabel);
|
|||
|
|
DrawResultList();
|
|||
|
|
|
|||
|
|
GUILayout.Space(20);
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndScrollView();
|
|||
|
|
|
|||
|
|
// 6. 导出区域(固定在底部,不随滚动)
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
EditorGUILayout.BeginVertical("box");
|
|||
|
|
GUILayout.Label("导出设置", EditorStyles.boldLabel);
|
|||
|
|
|
|||
|
|
rootFolder = EditorGUILayout.TextField("导出根目录", rootFolder);
|
|||
|
|
goodSubFolder = EditorGUILayout.TextField("适配子目录", goodSubFolder);
|
|||
|
|
badSubFolder = EditorGUILayout.TextField("不适配子目录", badSubFolder);
|
|||
|
|
exportBadClips = EditorGUILayout.Toggle("导出不适配动画", exportBadClips);
|
|||
|
|
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
if (GUILayout.Button("导出动画", GUILayout.Height(35)))
|
|||
|
|
ExportResults();
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndVertical();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
private void DrawProgressBar()
|
|||
|
|
{
|
|||
|
|
GUILayout.Label("正在检测动画...", EditorStyles.boldLabel);
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
EditorGUILayout.HelpBox(currentProgressText, MessageType.Info);
|
|||
|
|
|
|||
|
|
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
|
|||
|
|
EditorGUI.ProgressBar(rect, progress, $"{progress * 100:F1}%");
|
|||
|
|
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
if (GUILayout.Button("取消", GUILayout.Height(30)))
|
|||
|
|
{
|
|||
|
|
isProcessing = false;
|
|||
|
|
if (currentTask != null)
|
|||
|
|
{
|
|||
|
|
currentTask = null;
|
|||
|
|
}
|
|||
|
|
Repaint();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 刷新动画缓存(修复:刷新界面 + 清空旧结果)
|
|||
|
|
// -----------------------------
|
|||
|
|
private void RefreshAnimationCache()
|
|||
|
|
{
|
|||
|
|
if (isProcessing) return;
|
|||
|
|
|
|||
|
|
Debug.Log("开始刷新动画缓存...");
|
|||
|
|
PreCacheAnimationBones();
|
|||
|
|
Debug.Log($"动画缓存刷新完成!共缓存 {cachedClipData.Count} 个动画");
|
|||
|
|
|
|||
|
|
// 清空旧检测结果
|
|||
|
|
ClearResults();
|
|||
|
|
|
|||
|
|
// 强制刷新界面
|
|||
|
|
Repaint();
|
|||
|
|
|
|||
|
|
EditorUtility.DisplayDialog(
|
|||
|
|
"刷新完成",
|
|||
|
|
$"动画缓存刷新完成!\n共缓存 {cachedClipData.Count} 个动画",
|
|||
|
|
"确定"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 参考对象选择区域
|
|||
|
|
// -----------------------------
|
|||
|
|
private void DrawReferenceObjectSection()
|
|||
|
|
{
|
|||
|
|
GUILayout.Label("1. 选择参考对象(骨骼来源)", EditorStyles.boldLabel);
|
|||
|
|
|
|||
|
|
Rect dropArea = GUILayoutUtility.GetRect(0, 50, GUILayout.ExpandWidth(true));
|
|||
|
|
GUI.Box(dropArea, "拖拽 GameObject 到这里\n(可从Hierarchy或Project拖入)");
|
|||
|
|
|
|||
|
|
HandleReferenceObjectDragDrop(dropArea);
|
|||
|
|
|
|||
|
|
EditorGUI.BeginChangeCheck();
|
|||
|
|
referenceObject = (GameObject)EditorGUILayout.ObjectField("参考对象", referenceObject, typeof(GameObject), true);
|
|||
|
|
|
|||
|
|
if (EditorGUI.EndChangeCheck() && referenceObject != null)
|
|||
|
|
{
|
|||
|
|
LoadGameObjectBones(referenceObject);
|
|||
|
|
ClearResults();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (referenceObject != null && actualBoneFullPaths.Count > 0)
|
|||
|
|
{
|
|||
|
|
string sourceType = PrefabUtility.IsPartOfAnyPrefab(referenceObject) ? "Prefab" : "场景对象";
|
|||
|
|
EditorGUILayout.HelpBox($"✓ 已加载: {referenceObject.name} ({sourceType})\n骨骼数量: {actualBoneFullPaths.Count}", MessageType.Info);
|
|||
|
|
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
if (GUILayout.Button("刷新骨骼列表", GUILayout.Width(100)))
|
|||
|
|
{
|
|||
|
|
LoadGameObjectBones(referenceObject);
|
|||
|
|
}
|
|||
|
|
if (GUILayout.Button("显示骨骼层级", GUILayout.Width(100)))
|
|||
|
|
{
|
|||
|
|
ShowBoneHierarchy();
|
|||
|
|
}
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
}
|
|||
|
|
else if (referenceObject != null && actualBoneFullPaths.Count == 0)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("⚠️ 未能从该对象读取到骨骼结构,请确保对象包含骨骼层级", MessageType.Warning);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("请拖入一个带骨骼结构的GameObject(场景中的角色或Prefab)", MessageType.Warning);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void HandleReferenceObjectDragDrop(Rect dropArea)
|
|||
|
|
{
|
|||
|
|
Event evt = Event.current;
|
|||
|
|
if (!dropArea.Contains(evt.mousePosition))
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
switch (evt.type)
|
|||
|
|
{
|
|||
|
|
case EventType.DragUpdated:
|
|||
|
|
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
|||
|
|
evt.Use();
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case EventType.DragPerform:
|
|||
|
|
DragAndDrop.AcceptDrag();
|
|||
|
|
foreach (Object obj in DragAndDrop.objectReferences)
|
|||
|
|
{
|
|||
|
|
if (obj is GameObject go)
|
|||
|
|
{
|
|||
|
|
referenceObject = go;
|
|||
|
|
LoadGameObjectBones(referenceObject);
|
|||
|
|
ClearResults();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
evt.Use();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ClearResults()
|
|||
|
|
{
|
|||
|
|
mismatchDetails.Clear();
|
|||
|
|
mismatchCountMap.Clear();
|
|||
|
|
matchPercentageMap.Clear();
|
|||
|
|
animationBonesCache.Clear();
|
|||
|
|
hasRunCheck = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 加载GameObject的骨骼结构(带完整层级路径)
|
|||
|
|
// -----------------------------
|
|||
|
|
private void LoadGameObjectBones(GameObject obj)
|
|||
|
|
{
|
|||
|
|
actualBoneFullPaths.Clear();
|
|||
|
|
actualBoneNames.Clear();
|
|||
|
|
bonePathToName.Clear();
|
|||
|
|
|
|||
|
|
if (obj == null) return;
|
|||
|
|
|
|||
|
|
GameObject sourceObject = obj;
|
|||
|
|
GameObject tempInstance = null;
|
|||
|
|
|
|||
|
|
if (PrefabUtility.IsPartOfPrefabAsset(obj) && !PrefabUtility.IsPartOfNonAssetPrefabInstance(obj))
|
|||
|
|
{
|
|||
|
|
tempInstance = (GameObject)PrefabUtility.InstantiatePrefab(obj);
|
|||
|
|
sourceObject = tempInstance;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
GetAllBonesWithPath(sourceObject.transform, "");
|
|||
|
|
Debug.Log($"成功从 {(PrefabUtility.IsPartOfAnyPrefab(obj) ? "Prefab" : "场景对象")} 加载 {actualBoneFullPaths.Count} 个骨骼");
|
|||
|
|
|
|||
|
|
if (actualBoneFullPaths.Count > 0)
|
|||
|
|
{
|
|||
|
|
Debug.Log("骨骼层级示例(前10个):");
|
|||
|
|
foreach (string path in actualBoneFullPaths.Take(10))
|
|||
|
|
{
|
|||
|
|
Debug.Log($" └─ {path}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
if (tempInstance != null)
|
|||
|
|
{
|
|||
|
|
DestroyImmediate(tempInstance);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void GetAllBonesWithPath(Transform trans, string parentPath)
|
|||
|
|
{
|
|||
|
|
string currentPath = string.IsNullOrEmpty(parentPath) ? trans.name : parentPath + "/" + trans.name;
|
|||
|
|
|
|||
|
|
actualBoneFullPaths.Add(currentPath);
|
|||
|
|
actualBoneNames.Add(trans.name);
|
|||
|
|
bonePathToName[currentPath] = trans.name;
|
|||
|
|
|
|||
|
|
foreach (Transform child in trans)
|
|||
|
|
{
|
|||
|
|
GetAllBonesWithPath(child, currentPath);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ShowBoneHierarchy()
|
|||
|
|
{
|
|||
|
|
string message = "骨骼层级结构:\n\n";
|
|||
|
|
foreach (string path in actualBoneFullPaths)
|
|||
|
|
{
|
|||
|
|
int depth = path.Count(c => c == '/');
|
|||
|
|
string indent = new string(' ', depth * 2);
|
|||
|
|
string boneName = path.Split('/').Last();
|
|||
|
|
message += $"{indent}└─ {boneName}\n";
|
|||
|
|
|
|||
|
|
if (message.Length > 30000)
|
|||
|
|
{
|
|||
|
|
message += "\n... (继续显示会过长,已截断)";
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorUtility.DisplayDialog("骨骼层级结构", message, "确定");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 显示骨骼列表
|
|||
|
|
// -----------------------------
|
|||
|
|
private void DrawBoneList()
|
|||
|
|
{
|
|||
|
|
showBoneList = EditorGUILayout.Foldout(showBoneList, $"骨骼结构 ({actualBoneFullPaths.Count} 个骨骼,带完整路径)", true);
|
|||
|
|
|
|||
|
|
if (showBoneList)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.BeginVertical("box");
|
|||
|
|
boneListScrollPos = EditorGUILayout.BeginScrollView(boneListScrollPos, GUILayout.Height(150));
|
|||
|
|
|
|||
|
|
string search = EditorGUILayout.TextField("搜索:", "");
|
|||
|
|
|
|||
|
|
var bonesToShow = actualBoneFullPaths;
|
|||
|
|
if (!string.IsNullOrEmpty(search))
|
|||
|
|
{
|
|||
|
|
bonesToShow = actualBoneFullPaths.Where(b => b.ToLower().Contains(search.ToLower())).ToList();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorGUILayout.LabelField($"共 {bonesToShow.Count} 个骨骼", EditorStyles.miniLabel);
|
|||
|
|
|
|||
|
|
foreach (string bonePath in bonesToShow.Take(80))
|
|||
|
|
{
|
|||
|
|
GUILayout.Label($"• {bonePath}", GUILayout.Width(350));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (bonesToShow.Count > 80)
|
|||
|
|
{
|
|||
|
|
GUILayout.Label($"... 还有 {bonesToShow.Count - 80} 个骨骼", EditorStyles.miniLabel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndScrollView();
|
|||
|
|
EditorGUILayout.EndVertical();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 动画列表
|
|||
|
|
// -----------------------------
|
|||
|
|
private void DrawClipList()
|
|||
|
|
{
|
|||
|
|
Rect dropArea = GUILayoutUtility.GetRect(0, 60, GUILayout.ExpandWidth(true));
|
|||
|
|
GUI.Box(dropArea, "拖拽 AnimationClip 到这里\n支持多选拖拽");
|
|||
|
|
|
|||
|
|
HandleClipDragDrop(dropArea);
|
|||
|
|
|
|||
|
|
GUILayout.BeginHorizontal();
|
|||
|
|
GUILayout.Label($"当前动画列表 ({clips.Count} 个)", EditorStyles.boldLabel);
|
|||
|
|
if (GUILayout.Button("清空列表", GUILayout.Width(80)))
|
|||
|
|
{
|
|||
|
|
clips.Clear();
|
|||
|
|
ClearResults();
|
|||
|
|
cachedClipData.Clear();
|
|||
|
|
}
|
|||
|
|
GUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
if (clips.Count > 0)
|
|||
|
|
{
|
|||
|
|
clipListScrollPos = EditorGUILayout.BeginScrollView(clipListScrollPos, GUILayout.Height(100));
|
|||
|
|
|
|||
|
|
for (int i = 0; i < clips.Count; i++)
|
|||
|
|
{
|
|||
|
|
GUILayout.BeginHorizontal();
|
|||
|
|
clips[i] = (AnimationClip)EditorGUILayout.ObjectField(clips[i], typeof(AnimationClip), false);
|
|||
|
|
if (GUILayout.Button("✖", GUILayout.Width(25)))
|
|||
|
|
{
|
|||
|
|
var clipToRemove = clips[i];
|
|||
|
|
clips.RemoveAt(i);
|
|||
|
|
mismatchDetails.Remove(clipToRemove);
|
|||
|
|
mismatchCountMap.Remove(clipToRemove);
|
|||
|
|
matchPercentageMap.Remove(clipToRemove);
|
|||
|
|
animationBonesCache.Remove(clipToRemove);
|
|||
|
|
// 同时从缓存中移除
|
|||
|
|
cachedClipData.RemoveAll(c => c.clipPath == AssetDatabase.GetAssetPath(clipToRemove));
|
|||
|
|
i--;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
GUILayout.EndHorizontal();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndScrollView();
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("暂无动画,请拖入 AnimationClip", MessageType.Info);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GUILayout.BeginHorizontal();
|
|||
|
|
if (GUILayout.Button("从文件夹添加"))
|
|||
|
|
{
|
|||
|
|
string folder = EditorUtility.OpenFolderPanel("选择动画文件夹", "Assets", "");
|
|||
|
|
if (!string.IsNullOrEmpty(folder))
|
|||
|
|
{
|
|||
|
|
string relativePath = GetRelativePath(folder);
|
|||
|
|
AddClipsFromFolder(relativePath);
|
|||
|
|
ClearResults();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (GUILayout.Button("添加选中的动画"))
|
|||
|
|
{
|
|||
|
|
foreach (var obj in Selection.objects)
|
|||
|
|
{
|
|||
|
|
if (obj is AnimationClip clip && !clips.Contains(clip))
|
|||
|
|
clips.Add(clip);
|
|||
|
|
}
|
|||
|
|
ClearResults();
|
|||
|
|
}
|
|||
|
|
GUILayout.EndHorizontal();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void HandleClipDragDrop(Rect dropArea)
|
|||
|
|
{
|
|||
|
|
Event evt = Event.current;
|
|||
|
|
if (!dropArea.Contains(evt.mousePosition))
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
switch (evt.type)
|
|||
|
|
{
|
|||
|
|
case EventType.DragUpdated:
|
|||
|
|
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
|||
|
|
evt.Use();
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case EventType.DragPerform:
|
|||
|
|
DragAndDrop.AcceptDrag();
|
|||
|
|
foreach (Object obj in DragAndDrop.objectReferences)
|
|||
|
|
{
|
|||
|
|
if (obj is AnimationClip clip && !clips.Contains(clip))
|
|||
|
|
clips.Add(clip);
|
|||
|
|
}
|
|||
|
|
ClearResults();
|
|||
|
|
evt.Use();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void AddClipsFromFolder(string folderPath)
|
|||
|
|
{
|
|||
|
|
string[] guids = AssetDatabase.FindAssets("t:AnimationClip", new[] { folderPath });
|
|||
|
|
foreach (string guid in guids)
|
|||
|
|
{
|
|||
|
|
string path = AssetDatabase.GUIDToAssetPath(guid);
|
|||
|
|
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
|
|||
|
|
if (clip != null && !clips.Contains(clip))
|
|||
|
|
clips.Add(clip);
|
|||
|
|
}
|
|||
|
|
Debug.Log($"从 {folderPath} 添加了 {guids.Length} 个动画");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string GetRelativePath(string absolutePath)
|
|||
|
|
{
|
|||
|
|
string dataPath = Application.dataPath;
|
|||
|
|
if (absolutePath.StartsWith(dataPath))
|
|||
|
|
{
|
|||
|
|
return "Assets" + absolutePath.Substring(dataPath.Length);
|
|||
|
|
}
|
|||
|
|
return absolutePath;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 多条件检测设置
|
|||
|
|
// -----------------------------
|
|||
|
|
private void DrawConditionSettings()
|
|||
|
|
{
|
|||
|
|
showConditionSettings = EditorGUILayout.Foldout(showConditionSettings, "3. 多条件检测设置", true);
|
|||
|
|
|
|||
|
|
if (!showConditionSettings) return;
|
|||
|
|
|
|||
|
|
EditorGUILayout.BeginVertical("box");
|
|||
|
|
|
|||
|
|
// 匹配模式选择
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
GUILayout.Label("骨骼匹配模式:", GUILayout.Width(100));
|
|||
|
|
if (GUILayout.Toggle(useFullPathMatch, "完整路径匹配(推荐)", GUILayout.Width(150)))
|
|||
|
|
useFullPathMatch = true;
|
|||
|
|
if (GUILayout.Toggle(!useFullPathMatch, "仅名称匹配(宽松)", GUILayout.Width(130)))
|
|||
|
|
useFullPathMatch = false;
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
if (useFullPathMatch)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("完整路径匹配:动画中的 \"Hips/Spine/Chest\" 必须与模型中的路径完全一致", MessageType.Info);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("仅名称匹配:只比较骨骼名称,不比较层级路径(可能误匹配)", MessageType.Warning);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
// 判断逻辑选择
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
GUILayout.Label("可选条件判断逻辑:", GUILayout.Width(120));
|
|||
|
|
if (GUILayout.Toggle(useAndLogic, "AND(全部满足才算适配)", GUILayout.Width(180)))
|
|||
|
|
useAndLogic = true;
|
|||
|
|
if (GUILayout.Toggle(!useAndLogic, "OR(任一满足即适配)", GUILayout.Width(150)))
|
|||
|
|
useAndLogic = false;
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
EditorGUILayout.HelpBox(
|
|||
|
|
"⚠️ 注意:必要骨骼和最小骨骼数是强制条件,不满足直接标记为不适配!",
|
|||
|
|
MessageType.Warning);
|
|||
|
|
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
// ========== 强制条件区域 ==========
|
|||
|
|
GUILayout.Label("【强制条件】不满足直接标记不适配", EditorStyles.boldLabel);
|
|||
|
|
|
|||
|
|
// 条件3:最小骨骼数(强制)
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
useMinBoneCount = EditorGUILayout.Toggle(useMinBoneCount, GUILayout.Width(20));
|
|||
|
|
GUILayout.Label("强制条件A:最小骨骼数量", GUILayout.Width(150));
|
|||
|
|
if (useMinBoneCount)
|
|||
|
|
{
|
|||
|
|
minBoneCountRequired = EditorGUILayout.IntSlider(minBoneCountRequired, 1, 100);
|
|||
|
|
GUILayout.Label($"≥ {minBoneCountRequired}", GUILayout.Width(40));
|
|||
|
|
}
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
if (useMinBoneCount)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("动画使用的骨骼数量必须达到最小值,防止单根骨骼动画被误判为适配", MessageType.None);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 条件4:必要骨骼列表(强制)
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
useRequiredBonesList = EditorGUILayout.Toggle(useRequiredBonesList, GUILayout.Width(20));
|
|||
|
|
GUILayout.Label("强制条件B:必要骨骼列表", GUILayout.Width(150));
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
if (useRequiredBonesList)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.BeginVertical("box");
|
|||
|
|
|
|||
|
|
EditorGUILayout.HelpBox(
|
|||
|
|
"⚠️ 强制条件:缺失任何必要骨骼,动画将直接标记为不适配!",
|
|||
|
|
MessageType.Error);
|
|||
|
|
|
|||
|
|
GUILayout.BeginHorizontal();
|
|||
|
|
newRequiredBone = EditorGUILayout.TextField("添加必要骨骼路径:", newRequiredBone);
|
|||
|
|
if (GUILayout.Button("添加", GUILayout.Width(60)) && !string.IsNullOrEmpty(newRequiredBone))
|
|||
|
|
{
|
|||
|
|
if (!requiredBonesList.Contains(newRequiredBone))
|
|||
|
|
requiredBonesList.Add(newRequiredBone);
|
|||
|
|
newRequiredBone = "";
|
|||
|
|
}
|
|||
|
|
GUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
// 快捷按钮行
|
|||
|
|
GUILayout.BeginHorizontal();
|
|||
|
|
if (GUILayout.Button("从当前骨骼列表选择", GUILayout.Width(150)))
|
|||
|
|
{
|
|||
|
|
ShowBoneSelectionDialog();
|
|||
|
|
}
|
|||
|
|
if (GUILayout.Button("添加根骨骼作为必要", GUILayout.Width(150)))
|
|||
|
|
{
|
|||
|
|
if (actualBoneFullPaths.Count > 0)
|
|||
|
|
{
|
|||
|
|
string rootBone = actualBoneFullPaths[0];
|
|||
|
|
if (!requiredBonesList.Contains(rootBone))
|
|||
|
|
requiredBonesList.Add(rootBone);
|
|||
|
|
Debug.Log($"添加根骨骼: {rootBone}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (GUILayout.Button("清空列表", GUILayout.Width(100)))
|
|||
|
|
{
|
|||
|
|
requiredBonesList.Clear();
|
|||
|
|
}
|
|||
|
|
GUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
if (requiredBonesList.Count > 0)
|
|||
|
|
{
|
|||
|
|
requiredBonesScrollPos = EditorGUILayout.BeginScrollView(requiredBonesScrollPos, GUILayout.Height(100));
|
|||
|
|
for (int i = 0; i < requiredBonesList.Count; i++)
|
|||
|
|
{
|
|||
|
|
GUILayout.BeginHorizontal();
|
|||
|
|
GUILayout.Label($"• {requiredBonesList[i]}", GUILayout.Width(350));
|
|||
|
|
if (GUILayout.Button("删除", GUILayout.Width(50)))
|
|||
|
|
{
|
|||
|
|
requiredBonesList.RemoveAt(i);
|
|||
|
|
i--;
|
|||
|
|
}
|
|||
|
|
GUILayout.EndHorizontal();
|
|||
|
|
}
|
|||
|
|
EditorGUILayout.EndScrollView();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndVertical();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GUILayout.Space(10);
|
|||
|
|
|
|||
|
|
// ========== 可选条件区域 ==========
|
|||
|
|
GUILayout.Label("【可选条件】根据AND/OR逻辑判断", EditorStyles.boldLabel);
|
|||
|
|
|
|||
|
|
// 条件1:缺失数量
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
useMismatchCount = EditorGUILayout.Toggle(useMismatchCount, GUILayout.Width(20));
|
|||
|
|
GUILayout.Label("可选条件A:缺失骨骼数量", GUILayout.Width(130));
|
|||
|
|
if (useMismatchCount)
|
|||
|
|
{
|
|||
|
|
mismatchCountThreshold = EditorGUILayout.IntSlider(mismatchCountThreshold, 0, 50);
|
|||
|
|
GUILayout.Label($"< {mismatchCountThreshold}", GUILayout.Width(40));
|
|||
|
|
}
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
// 条件2:匹配百分比
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
useMatchPercentage = EditorGUILayout.Toggle(useMatchPercentage, GUILayout.Width(20));
|
|||
|
|
GUILayout.Label("可选条件B:骨骼匹配百分比", GUILayout.Width(130));
|
|||
|
|
if (useMatchPercentage)
|
|||
|
|
{
|
|||
|
|
matchPercentageRequired = EditorGUILayout.Slider(matchPercentageRequired, 0f, 100f);
|
|||
|
|
GUILayout.Label($"≥ {matchPercentageRequired}%", GUILayout.Width(60));
|
|||
|
|
}
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndVertical();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ShowBoneSelectionDialog()
|
|||
|
|
{
|
|||
|
|
GenericMenu menu = new GenericMenu();
|
|||
|
|
|
|||
|
|
foreach (string bonePath in actualBoneFullPaths)
|
|||
|
|
{
|
|||
|
|
string path = bonePath;
|
|||
|
|
string cleanPath = RemoveCommonPrefixes(path);
|
|||
|
|
string displayName = $"{path}";
|
|||
|
|
|
|||
|
|
if (cleanPath != path.ToLower())
|
|||
|
|
displayName += $" (匹配名: {cleanPath})";
|
|||
|
|
|
|||
|
|
menu.AddItem(new GUIContent(displayName), requiredBonesList.Contains(path), () => {
|
|||
|
|
if (!requiredBonesList.Contains(path))
|
|||
|
|
requiredBonesList.Add(path);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
menu.ShowAsContext();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 移除常见的前缀
|
|||
|
|
// -----------------------------
|
|||
|
|
private string RemoveCommonPrefixes(string path)
|
|||
|
|
{
|
|||
|
|
string[] prefixes = { "mixamorig:", "uma:", "character:", "armature|", "root|" };
|
|||
|
|
string result = path.ToLower();
|
|||
|
|
|
|||
|
|
foreach (string prefix in prefixes)
|
|||
|
|
{
|
|||
|
|
if (result.StartsWith(prefix))
|
|||
|
|
{
|
|||
|
|
result = result.Substring(prefix.Length);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 从动画中提取骨骼路径(必须在主线程调用)
|
|||
|
|
// -----------------------------
|
|||
|
|
private List<string> GetAnimationBonePaths(AnimationClip clip)
|
|||
|
|
{
|
|||
|
|
if (animationBonesCache.ContainsKey(clip))
|
|||
|
|
return animationBonesCache[clip];
|
|||
|
|
|
|||
|
|
HashSet<string> animationBones = new HashSet<string>();
|
|||
|
|
|
|||
|
|
var bindings = AnimationUtility.GetCurveBindings(clip);
|
|||
|
|
|
|||
|
|
foreach (var binding in bindings)
|
|||
|
|
{
|
|||
|
|
if (binding.type == typeof(Transform) || binding.type == typeof(Animator))
|
|||
|
|
{
|
|||
|
|
string path = binding.path;
|
|||
|
|
if (string.IsNullOrEmpty(path)) continue;
|
|||
|
|
animationBones.Add(path);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
|
|||
|
|
foreach (var binding in objectBindings)
|
|||
|
|
{
|
|||
|
|
if (binding.type == typeof(Transform) || binding.type == typeof(Animator))
|
|||
|
|
{
|
|||
|
|
string path = binding.path;
|
|||
|
|
if (string.IsNullOrEmpty(path)) continue;
|
|||
|
|
animationBones.Add(path);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var result = animationBones.ToList();
|
|||
|
|
animationBonesCache[clip] = result;
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 分析动画骨骼(完整修复版)
|
|||
|
|
// -----------------------------
|
|||
|
|
private (List<string> mismatchedBones, List<string> matchedBones, int matchedCount, int totalAnimBones, float matchPercentage)
|
|||
|
|
AnalyzeAnimationBonesData(
|
|||
|
|
List<string> animationBonePaths,
|
|||
|
|
List<string> modelBoneFullPaths,
|
|||
|
|
bool useFullPathMatchLocal)
|
|||
|
|
{
|
|||
|
|
List<string> mismatchedBones = new List<string>();
|
|||
|
|
List<string> matchedBones = new List<string>();
|
|||
|
|
|
|||
|
|
// 模型没有骨骼
|
|||
|
|
if (modelBoneFullPaths.Count == 0)
|
|||
|
|
{
|
|||
|
|
mismatchedBones.Add("错误:参考对象没有骨骼结构");
|
|||
|
|
return (mismatchedBones, matchedBones, 0, 0, 0f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 动画没有骨骼
|
|||
|
|
if (animationBonePaths.Count == 0)
|
|||
|
|
{
|
|||
|
|
mismatchedBones.Add("警告:动画中没有检测到任何骨骼曲线");
|
|||
|
|
return (mismatchedBones, matchedBones, 0, 0, 0f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int totalAnimBones = animationBonePaths.Count;
|
|||
|
|
|
|||
|
|
// 预处理模型骨骼路径
|
|||
|
|
HashSet<string> modelPathSet = new HashSet<string>(modelBoneFullPaths.Select(p => p.ToLower()));
|
|||
|
|
Dictionary<string, string> modelCleanMap = new Dictionary<string, string>();
|
|||
|
|
|
|||
|
|
foreach (string modelPath in modelBoneFullPaths)
|
|||
|
|
{
|
|||
|
|
modelCleanMap[modelPath.ToLower()] = RemoveCommonPrefixes(modelPath.ToLower());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool[] matchResults = new bool[totalAnimBones];
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 并行匹配动画骨骼
|
|||
|
|
// -----------------------------
|
|||
|
|
Parallel.For(0, totalAnimBones, idx =>
|
|||
|
|
{
|
|||
|
|
string animPath = animationBonePaths[idx];
|
|||
|
|
string lowerAnimPath = animPath.ToLower();
|
|||
|
|
string cleanAnimPath = RemoveCommonPrefixes(lowerAnimPath);
|
|||
|
|
|
|||
|
|
bool found = false;
|
|||
|
|
|
|||
|
|
foreach (var kvp in modelCleanMap)
|
|||
|
|
{
|
|||
|
|
string modelFull = kvp.Key;
|
|||
|
|
string modelClean = kvp.Value;
|
|||
|
|
|
|||
|
|
if (useFullPathMatchLocal)
|
|||
|
|
{
|
|||
|
|
// 完整路径匹配
|
|||
|
|
if (lowerAnimPath == modelFull || cleanAnimPath == modelClean)
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 末尾匹配(防止前缀不同)
|
|||
|
|
if (cleanAnimPath.EndsWith("/" + modelClean) ||
|
|||
|
|
cleanAnimPath == modelClean ||
|
|||
|
|
modelClean.EndsWith("/" + cleanAnimPath))
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 名称匹配
|
|||
|
|
string animName = lowerAnimPath.Split('/').Last();
|
|||
|
|
string modelName = modelFull.Split('/').Last();
|
|||
|
|
|
|||
|
|
if (animName == modelName)
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
matchResults[idx] = found;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 收集结果
|
|||
|
|
// -----------------------------
|
|||
|
|
for (int i = 0; i < totalAnimBones; i++)
|
|||
|
|
{
|
|||
|
|
if (matchResults[i])
|
|||
|
|
matchedBones.Add(animationBonePaths[i]);
|
|||
|
|
else
|
|||
|
|
mismatchedBones.Add(animationBonePaths[i]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int matchedCount = matchedBones.Count;
|
|||
|
|
float matchPercentage = totalAnimBones > 0 ? (matchedCount * 100f / totalAnimBones) : 0f;
|
|||
|
|
|
|||
|
|
return (mismatchedBones, matchedBones, matchedCount, totalAnimBones, matchPercentage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 检查必要骨骼条件(纯数据计算)- 强制条件
|
|||
|
|
// -----------------------------
|
|||
|
|
private (bool passed, List<string> missingBones) CheckRequiredBonesConditionData(
|
|||
|
|
List<string> animationBonePaths,
|
|||
|
|
List<string> requiredBones,
|
|||
|
|
bool useFullPathMatchLocal)
|
|||
|
|
{
|
|||
|
|
if (requiredBones.Count == 0)
|
|||
|
|
return (true, new List<string>());
|
|||
|
|
|
|||
|
|
// 获取动画中的骨骼信息
|
|||
|
|
HashSet<string> animPathSet = new HashSet<string>(animationBonePaths.Select(p => p.ToLower()));
|
|||
|
|
HashSet<string> animCleanSet = new HashSet<string>(animationBonePaths.Select(p => RemoveCommonPrefixes(p)));
|
|||
|
|
HashSet<string> animNameSet = new HashSet<string>(animationBonePaths.Select(p => p.Split('/').Last().ToLower()));
|
|||
|
|
|
|||
|
|
List<string> missingBones = new List<string>();
|
|||
|
|
|
|||
|
|
foreach (string required in requiredBones)
|
|||
|
|
{
|
|||
|
|
string lowerRequired = required.ToLower();
|
|||
|
|
string cleanRequired = RemoveCommonPrefixes(lowerRequired);
|
|||
|
|
string requiredName = lowerRequired.Split('/').Last();
|
|||
|
|
|
|||
|
|
bool found = false;
|
|||
|
|
|
|||
|
|
if (useFullPathMatchLocal)
|
|||
|
|
{
|
|||
|
|
// 1. 完整路径匹配
|
|||
|
|
if (animPathSet.Contains(lowerRequired))
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
}
|
|||
|
|
// 2. 清理前缀后匹配
|
|||
|
|
else if (animCleanSet.Contains(cleanRequired))
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
}
|
|||
|
|
// 3. 骨骼名称匹配
|
|||
|
|
else if (animNameSet.Contains(requiredName))
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
}
|
|||
|
|
// 4. 后缀匹配
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
foreach (string animPath in animationBonePaths)
|
|||
|
|
{
|
|||
|
|
string cleanAnim = RemoveCommonPrefixes(animPath);
|
|||
|
|
if (cleanAnim.EndsWith("/" + cleanRequired) || cleanAnim == cleanRequired)
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
found = animNameSet.Contains(requiredName);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!found)
|
|||
|
|
{
|
|||
|
|
missingBones.Add(required);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (missingBones.Count == 0, missingBones);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 动画适配判定(完整修复版)
|
|||
|
|
// -----------------------------
|
|||
|
|
private (bool isCompatible, string forceReason, string optionalReason) IsClipCompatibleData(
|
|||
|
|
int mismatchCount,
|
|||
|
|
int totalAnimBones,
|
|||
|
|
float matchPercentage,
|
|||
|
|
List<string> animPaths,
|
|||
|
|
List<string> requiredBones,
|
|||
|
|
bool useRequiredBonesList,
|
|||
|
|
bool useMinBoneCount,
|
|||
|
|
bool useMismatchCount,
|
|||
|
|
bool useMatchPercentage,
|
|||
|
|
int mismatchThreshold,
|
|||
|
|
float matchPercentageRequired,
|
|||
|
|
int minBoneCountRequired,
|
|||
|
|
bool useAndLogic,
|
|||
|
|
bool useFullPathMatch)
|
|||
|
|
{
|
|||
|
|
string forceReason = "";
|
|||
|
|
string optionalReason = "";
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 1. 强制条件(不满足直接不适配)
|
|||
|
|
// -----------------------------
|
|||
|
|
|
|||
|
|
// A. 最小骨骼数
|
|||
|
|
if (useMinBoneCount)
|
|||
|
|
{
|
|||
|
|
if (totalAnimBones < minBoneCountRequired)
|
|||
|
|
{
|
|||
|
|
forceReason += $"[强制条件] 动画骨骼数 {totalAnimBones} < 最小要求 {minBoneCountRequired}\n";
|
|||
|
|
return (false, forceReason, optionalReason);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// B. 必要骨骼列表
|
|||
|
|
if (useRequiredBonesList && requiredBones.Count > 0)
|
|||
|
|
{
|
|||
|
|
foreach (var req in requiredBones)
|
|||
|
|
{
|
|||
|
|
bool found = false;
|
|||
|
|
|
|||
|
|
foreach (var animBone in animPaths)
|
|||
|
|
{
|
|||
|
|
if (useFullPathMatch)
|
|||
|
|
{
|
|||
|
|
// 完整路径匹配
|
|||
|
|
if (animBone.ToLower() == req.ToLower())
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 名称匹配
|
|||
|
|
string animName = animBone.Split('/').Last().ToLower();
|
|||
|
|
string reqName = req.Split('/').Last().ToLower();
|
|||
|
|
if (animName == reqName)
|
|||
|
|
{
|
|||
|
|
found = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!found)
|
|||
|
|
{
|
|||
|
|
forceReason += $"[强制条件] 缺少必要骨骼:{req}\n";
|
|||
|
|
return (false, forceReason, optionalReason);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 2. 可选条件(根据 AND / OR 判断)
|
|||
|
|
// -----------------------------
|
|||
|
|
List<bool> optionalResults = new List<bool>();
|
|||
|
|
|
|||
|
|
// A. 缺失数量
|
|||
|
|
if (useMismatchCount)
|
|||
|
|
{
|
|||
|
|
bool ok = mismatchCount < mismatchThreshold;
|
|||
|
|
optionalResults.Add(ok);
|
|||
|
|
|
|||
|
|
if (!ok)
|
|||
|
|
optionalReason += $"[可选条件] 缺失骨骼数 {mismatchCount} ≥ 阈值 {mismatchThreshold}\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// B. 匹配百分比
|
|||
|
|
if (useMatchPercentage)
|
|||
|
|
{
|
|||
|
|
bool ok = matchPercentage >= matchPercentageRequired;
|
|||
|
|
optionalResults.Add(ok);
|
|||
|
|
|
|||
|
|
if (!ok)
|
|||
|
|
optionalReason += $"[可选条件] 匹配率 {matchPercentage:F1}% < 要求 {matchPercentageRequired}%\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有启用任何可选条件 → 默认通过
|
|||
|
|
if (optionalResults.Count == 0)
|
|||
|
|
return (true, forceReason, optionalReason);
|
|||
|
|
|
|||
|
|
// AND 逻辑:全部满足才通过
|
|||
|
|
if (useAndLogic)
|
|||
|
|
{
|
|||
|
|
bool allOK = optionalResults.All(r => r);
|
|||
|
|
return (allOK, forceReason, optionalReason);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// OR 逻辑:任意满足即可
|
|||
|
|
bool anyOK = optionalResults.Any(r => r);
|
|||
|
|
return (anyOK, forceReason, optionalReason);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
private string GetConditionDescription()
|
|||
|
|
{
|
|||
|
|
List<string> forcedConditions = new List<string>();
|
|||
|
|
List<string> optionalConditions = new List<string>();
|
|||
|
|
|
|||
|
|
if (useMinBoneCount)
|
|||
|
|
forcedConditions.Add($"最小骨骼 ≥ {minBoneCountRequired}");
|
|||
|
|
if (useRequiredBonesList && requiredBonesList.Count > 0)
|
|||
|
|
forcedConditions.Add($"必要骨骼({requiredBonesList.Count}个)");
|
|||
|
|
|
|||
|
|
if (useMismatchCount)
|
|||
|
|
optionalConditions.Add($"缺失 < {mismatchCountThreshold}");
|
|||
|
|
if (useMatchPercentage)
|
|||
|
|
optionalConditions.Add($"匹配 ≥ {matchPercentageRequired}%");
|
|||
|
|
|
|||
|
|
string result = "";
|
|||
|
|
if (forcedConditions.Count > 0)
|
|||
|
|
result += $"【强制】{string.Join("、", forcedConditions)}";
|
|||
|
|
|
|||
|
|
if (optionalConditions.Count > 0)
|
|||
|
|
{
|
|||
|
|
if (result.Length > 0) result += " + ";
|
|||
|
|
result += $"【可选】{string.Join(useAndLogic ? " 且 " : " 或 ", optionalConditions)}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (result.Length == 0)
|
|||
|
|
return "无条件";
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void PreCacheAnimationBones()
|
|||
|
|
{
|
|||
|
|
cachedClipData.Clear();
|
|||
|
|
|
|||
|
|
foreach (var clip in clips)
|
|||
|
|
{
|
|||
|
|
if (clip == null) continue;
|
|||
|
|
|
|||
|
|
string path = AssetDatabase.GetAssetPath(clip);
|
|||
|
|
string name = clip.name;
|
|||
|
|
List<string> bones = GetAnimationBonePaths(clip); // 这里必须在主线程
|
|||
|
|
|
|||
|
|
cachedClipData.Add(new ClipData
|
|||
|
|
{
|
|||
|
|
clipPath = path,
|
|||
|
|
clipName = name,
|
|||
|
|
bonePaths = bones
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// -----------------------------
|
|||
|
|
// 异步执行检测
|
|||
|
|
// -----------------------------
|
|||
|
|
// 添加临时存储
|
|||
|
|
private ConcurrentDictionary<string, (List<string> mismatched, int count, float percentage)> tempResults;
|
|||
|
|
|
|||
|
|
// ... 中间的代码保持不变 ...
|
|||
|
|
|
|||
|
|
// 异步检测入口(主线程)
|
|||
|
|
private async void RunCheckAsync()
|
|||
|
|
{
|
|||
|
|
if (isProcessing || clips.Count == 0 || referenceObject == null)
|
|||
|
|
{
|
|||
|
|
EditorUtility.DisplayDialog("提示", "请先选择参考对象并添加动画!", "确定");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保缓存已构建(主线程中构建)
|
|||
|
|
if (cachedClipData == null || cachedClipData.Count == 0)
|
|||
|
|
{
|
|||
|
|
PreCacheAnimationBones(); // 这里面要在主线程里填好 clipName / bonePaths
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isProcessing = true;
|
|||
|
|
hasRunCheck = false;
|
|||
|
|
progress = 0f;
|
|||
|
|
currentProgressText = "准备开始检测...";
|
|||
|
|
Repaint();
|
|||
|
|
|
|||
|
|
mismatchDetails.Clear();
|
|||
|
|
mismatchCountMap.Clear();
|
|||
|
|
matchPercentageMap.Clear();
|
|||
|
|
|
|||
|
|
// 拷贝一份本地数据,避免并发修改
|
|||
|
|
var clipDataListLocal = new List<ClipData>(cachedClipData);
|
|||
|
|
var modelBones = new List<string>(actualBoneFullPaths);
|
|||
|
|
var requiredBones = new List<string>(requiredBonesList);
|
|||
|
|
|
|||
|
|
// 把当前设置也拍一份,避免子线程访问 Editor 状态
|
|||
|
|
bool useFullPathMatchLocal = useFullPathMatch;
|
|||
|
|
bool useRequiredBonesListLocal = useRequiredBonesList;
|
|||
|
|
bool useMinBoneCountLocal = useMinBoneCount;
|
|||
|
|
bool useMismatchCountLocal = useMismatchCount;
|
|||
|
|
bool useMatchPercentageLocal = useMatchPercentage;
|
|||
|
|
int mismatchCountThresholdLocal = mismatchCountThreshold;
|
|||
|
|
float matchPercentageRequiredLocal = matchPercentageRequired;
|
|||
|
|
int minBoneCountRequiredLocal = minBoneCountRequired;
|
|||
|
|
bool useAndLogicLocal = useAndLogic;
|
|||
|
|
|
|||
|
|
// 子线程执行纯数据计算
|
|||
|
|
await Task.Run(() =>
|
|||
|
|
{
|
|||
|
|
RunCheckInternal(
|
|||
|
|
clipDataListLocal,
|
|||
|
|
modelBones,
|
|||
|
|
requiredBones,
|
|||
|
|
useFullPathMatchLocal,
|
|||
|
|
useRequiredBonesListLocal,
|
|||
|
|
useMinBoneCountLocal,
|
|||
|
|
useMismatchCountLocal,
|
|||
|
|
useMatchPercentageLocal,
|
|||
|
|
mismatchCountThresholdLocal,
|
|||
|
|
matchPercentageRequiredLocal,
|
|||
|
|
minBoneCountRequiredLocal,
|
|||
|
|
useAndLogicLocal
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 子线程结束后,把结果应用回主线程
|
|||
|
|
if (tempResults != null)
|
|||
|
|
{
|
|||
|
|
foreach (var clip in clips)
|
|||
|
|
{
|
|||
|
|
string path = AssetDatabase.GetAssetPath(clip);
|
|||
|
|
if (tempResults.TryGetValue(path, out var data))
|
|||
|
|
{
|
|||
|
|
var (mismatched, count, percentage) = data;
|
|||
|
|
|
|||
|
|
mismatchDetails[clip] = mismatched;
|
|||
|
|
mismatchCountMap[clip] = count;
|
|||
|
|
matchPercentageMap[clip] = percentage;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isProcessing = false;
|
|||
|
|
hasRunCheck = true;
|
|||
|
|
currentProgressText = "检测完成!";
|
|||
|
|
progress = 1f;
|
|||
|
|
Repaint();
|
|||
|
|
|
|||
|
|
EditorUtility.DisplayDialog("完成", "动画检测已完成!", "确定");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void RunCheckInternal(
|
|||
|
|
List<ClipData> clipDataListLocal,
|
|||
|
|
List<string> modelBones,
|
|||
|
|
List<string> requiredBones,
|
|||
|
|
bool useFullPathMatchLocal,
|
|||
|
|
bool useRequiredBonesListLocal,
|
|||
|
|
bool useMinBoneCountLocal,
|
|||
|
|
bool useMismatchCountLocal,
|
|||
|
|
bool useMatchPercentageLocal,
|
|||
|
|
int mismatchCountThresholdLocal,
|
|||
|
|
float matchPercentageRequiredLocal,
|
|||
|
|
int minBoneCountRequiredLocal,
|
|||
|
|
bool useAndLogicLocal)
|
|||
|
|
{
|
|||
|
|
int total = clipDataListLocal.Count;
|
|||
|
|
int badCount = 0;
|
|||
|
|
int perfectCount = 0;
|
|||
|
|
int goodCount = 0;
|
|||
|
|
|
|||
|
|
var results = new ConcurrentDictionary<string, (List<string> mismatched, int count, float percentage)>();
|
|||
|
|
|
|||
|
|
Parallel.For(0, total, i =>
|
|||
|
|
{
|
|||
|
|
var clipData = clipDataListLocal[i];
|
|||
|
|
|
|||
|
|
List<string> animationBones = clipData.bonePaths;
|
|||
|
|
if (animationBones == null || animationBones.Count == 0)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
// 分析动画骨骼(纯数据)
|
|||
|
|
var (mismatchedBones, matchedBones, matchedCount, totalAnimBones, matchPercentage) =
|
|||
|
|
AnalyzeAnimationBonesData(animationBones, modelBones, useFullPathMatchLocal);
|
|||
|
|
|
|||
|
|
results[clipData.clipPath] = (mismatchedBones, mismatchedBones.Count, matchPercentage);
|
|||
|
|
|
|||
|
|
// 判定适配
|
|||
|
|
var (isCompatible, forceReason, optionalReason) = IsClipCompatibleData(
|
|||
|
|
mismatchedBones.Count,
|
|||
|
|
totalAnimBones,
|
|||
|
|
matchPercentage,
|
|||
|
|
animationBones,
|
|||
|
|
requiredBones,
|
|||
|
|
useRequiredBonesListLocal,
|
|||
|
|
useMinBoneCountLocal,
|
|||
|
|
useMismatchCountLocal,
|
|||
|
|
useMatchPercentageLocal,
|
|||
|
|
mismatchCountThresholdLocal,
|
|||
|
|
matchPercentageRequiredLocal,
|
|||
|
|
minBoneCountRequiredLocal,
|
|||
|
|
useAndLogicLocal,
|
|||
|
|
useFullPathMatchLocal);
|
|||
|
|
|
|||
|
|
bool forcedFailed = !string.IsNullOrEmpty(forceReason);
|
|||
|
|
string reason = string.IsNullOrEmpty(forceReason)
|
|||
|
|
? optionalReason
|
|||
|
|
: forceReason + optionalReason;
|
|||
|
|
|
|||
|
|
if (!isCompatible)
|
|||
|
|
Interlocked.Increment(ref badCount);
|
|||
|
|
else if (mismatchedBones.Count == 0 && matchPercentage >= 99.9f)
|
|||
|
|
Interlocked.Increment(ref perfectCount);
|
|||
|
|
else
|
|||
|
|
Interlocked.Increment(ref goodCount);
|
|||
|
|
|
|||
|
|
string statusIcon = isCompatible ? "✓" : "✗";
|
|||
|
|
string statusText;
|
|||
|
|
if (!isCompatible && forcedFailed)
|
|||
|
|
statusText = "不适配(强制条件)";
|
|||
|
|
else if (!isCompatible)
|
|||
|
|
statusText = "不适配(可选条件)";
|
|||
|
|
else if (mismatchedBones.Count == 0 && matchPercentage >= 99.9f)
|
|||
|
|
statusText = "完美适配";
|
|||
|
|
else
|
|||
|
|
statusText = "适配";
|
|||
|
|
|
|||
|
|
// 这里只输出纯文本日志,不访问 Unity 对象
|
|||
|
|
lock (this)
|
|||
|
|
{
|
|||
|
|
Debug.Log($"[{statusIcon}] 动画 [{clipData.clipName}]");
|
|||
|
|
Debug.Log($" ├─ 动画骨骼数: {totalAnimBones}");
|
|||
|
|
Debug.Log($" ├─ 匹配骨骼: {matchedCount}/{totalAnimBones} ({matchPercentage:F1}%)");
|
|||
|
|
Debug.Log($" ├─ 判定结果: {statusText}");
|
|||
|
|
if (!string.IsNullOrEmpty(reason))
|
|||
|
|
Debug.Log($" └─ 原因: {reason}");
|
|||
|
|
|
|||
|
|
if (mismatchedBones.Count > 0 && mismatchedBones.Count <= 5)
|
|||
|
|
{
|
|||
|
|
foreach (string bone in mismatchedBones)
|
|||
|
|
Debug.Log($" └─ 不匹配: {bone}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
tempResults = results;
|
|||
|
|
|
|||
|
|
currentProgressText = $"检测完成! 完美:{perfectCount} 适配:{goodCount} 不适配:{badCount}";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
private void DrawResultList()
|
|||
|
|
{
|
|||
|
|
if (!hasRunCheck)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("尚未检测,请先点击“开始检测动画”", MessageType.Info);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (clips.Count == 0)
|
|||
|
|
{
|
|||
|
|
EditorGUILayout.HelpBox("没有动画可显示", MessageType.Warning);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 先把所有结果算一遍,按“是否符合条件 + 匹配率”排序
|
|||
|
|
var resultList = new List<(AnimationClip clip, int totalBones, int mismatch, int matched, float percent, bool isCompatible, bool forcedFailed, string reason)>();
|
|||
|
|
|
|||
|
|
foreach (var clip in clips)
|
|||
|
|
{
|
|||
|
|
if (!mismatchDetails.ContainsKey(clip))
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
// 动画骨骼路径 & 统计
|
|||
|
|
List<string> animPaths = GetAnimationBonePaths(clip);
|
|||
|
|
int totalAnimBones = animPaths.Count;
|
|||
|
|
int mismatch = mismatchCountMap.TryGetValue(clip, out var mis) ? mis : 0;
|
|||
|
|
float percentage = matchPercentageMap.TryGetValue(clip, out var p) ? p : 0f;
|
|||
|
|
int matchedCount = Mathf.Max(0, totalAnimBones - mismatch);
|
|||
|
|
|
|||
|
|
// 用当前设置重新跑一遍判定逻辑(关键:这里会用到最小节点数 + 必要骨骼)
|
|||
|
|
var (isCompatible, forceReason, optionalReason) = IsClipCompatibleData(
|
|||
|
|
mismatch,
|
|||
|
|
totalAnimBones,
|
|||
|
|
percentage,
|
|||
|
|
animPaths,
|
|||
|
|
requiredBonesList,
|
|||
|
|
useRequiredBonesList,
|
|||
|
|
useMinBoneCount,
|
|||
|
|
useMismatchCount,
|
|||
|
|
useMatchPercentage,
|
|||
|
|
mismatchCountThreshold,
|
|||
|
|
matchPercentageRequired,
|
|||
|
|
minBoneCountRequired,
|
|||
|
|
useAndLogic,
|
|||
|
|
useFullPathMatch
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
bool forcedFailed = !string.IsNullOrEmpty(forceReason);
|
|||
|
|
string reason = string.IsNullOrEmpty(forceReason)
|
|||
|
|
? optionalReason
|
|||
|
|
: forceReason + optionalReason;
|
|||
|
|
|
|||
|
|
resultList.Add((clip, totalAnimBones, mismatch, matchedCount, percentage, isCompatible, forcedFailed, reason));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 排序:先符合条件的,再不符合的;同类里按匹配率从高到低
|
|||
|
|
resultList = resultList
|
|||
|
|
.OrderByDescending(r => r.isCompatible)
|
|||
|
|
.ThenByDescending(r => r.percent)
|
|||
|
|
.ToList();
|
|||
|
|
|
|||
|
|
// 开始画 UI
|
|||
|
|
resultScrollPos = EditorGUILayout.BeginScrollView(resultScrollPos, GUILayout.Height(260));
|
|||
|
|
|
|||
|
|
// 表头
|
|||
|
|
EditorGUILayout.BeginHorizontal();
|
|||
|
|
GUILayout.Label("状态", GUILayout.Width(40));
|
|||
|
|
GUILayout.Label("动画名", GUILayout.Width(200));
|
|||
|
|
GUILayout.Label("总骨骼", GUILayout.Width(60));
|
|||
|
|
GUILayout.Label("匹配数", GUILayout.Width(70));
|
|||
|
|
GUILayout.Label("缺失数", GUILayout.Width(60));
|
|||
|
|
GUILayout.Label("匹配率", GUILayout.Width(70));
|
|||
|
|
GUILayout.Label("判定结果", GUILayout.Width(120));
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|
|||
|
|
GUILayout.Space(4);
|
|||
|
|
|
|||
|
|
foreach (var r in resultList)
|
|||
|
|
{
|
|||
|
|
var clip = r.clip;
|
|||
|
|
|
|||
|
|
// 状态 & 文案
|
|||
|
|
string statusIcon;
|
|||
|
|
string statusText;
|
|||
|
|
Color rowColor;
|
|||
|
|
|
|||
|
|
if (!r.isCompatible && r.forcedFailed)
|
|||
|
|
{
|
|||
|
|
statusIcon = "✗";
|
|||
|
|
statusText = "不适配(强制条件)";
|
|||
|
|
rowColor = new Color(1f, 0.8f, 0.8f);
|
|||
|
|
}
|
|||
|
|
else if (!r.isCompatible)
|
|||
|
|
{
|
|||
|
|
statusIcon = "✗";
|
|||
|
|
statusText = "不适配(可选条件)";
|
|||
|
|
rowColor = new Color(1f, 0.9f, 0.8f);
|
|||
|
|
}
|
|||
|
|
else if (r.mismatch == 0 && r.percent >= 99.9f)
|
|||
|
|
{
|
|||
|
|
statusIcon = "✓";
|
|||
|
|
statusText = "完美适配";
|
|||
|
|
rowColor = new Color(0.8f, 1f, 0.8f);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
statusIcon = "•";
|
|||
|
|
statusText = "适配";
|
|||
|
|
rowColor = new Color(1f, 1f, 0.9f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 横向一行平铺
|
|||
|
|
var oldColor = GUI.backgroundColor;
|
|||
|
|
GUI.backgroundColor = rowColor;
|
|||
|
|
EditorGUILayout.BeginHorizontal("box");
|
|||
|
|
GUI.backgroundColor = oldColor;
|
|||
|
|
|
|||
|
|
GUILayout.Label(statusIcon, GUILayout.Width(40));
|
|||
|
|
GUILayout.Label(clip.name, GUILayout.Width(200));
|
|||
|
|
GUILayout.Label(r.totalBones.ToString(), GUILayout.Width(60));
|
|||
|
|
GUILayout.Label(r.matched.ToString(), GUILayout.Width(70));
|
|||
|
|
GUILayout.Label(r.mismatch.ToString(), GUILayout.Width(60));
|
|||
|
|
GUILayout.Label($"{r.percent:F1}%", GUILayout.Width(70));
|
|||
|
|
GUILayout.Label(statusText, GUILayout.Width(120));
|
|||
|
|
|
|||
|
|
// 详情按钮:展开缺失骨骼 + 原因
|
|||
|
|
if (GUILayout.Button("详情", GUILayout.Width(50)))
|
|||
|
|
{
|
|||
|
|
var msg = $"动画:{clip.name}\n" +
|
|||
|
|
$"总骨骼:{r.totalBones}\n" +
|
|||
|
|
$"匹配骨骼:{r.matched}\n" +
|
|||
|
|
$"缺失骨骼:{r.mismatch}\n" +
|
|||
|
|
$"匹配率:{r.percent:F1}%\n\n";
|
|||
|
|
|
|||
|
|
if (!string.IsNullOrEmpty(r.reason))
|
|||
|
|
msg += $"判定原因:\n{r.reason}\n\n";
|
|||
|
|
|
|||
|
|
if (mismatchDetails.TryGetValue(clip, out var list) && list.Count > 0)
|
|||
|
|
{
|
|||
|
|
msg += "缺失骨骼列表(最多显示 50 个):\n";
|
|||
|
|
foreach (var bone in list.Take(50))
|
|||
|
|
msg += $" • {bone}\n";
|
|||
|
|
|
|||
|
|
if (list.Count > 50)
|
|||
|
|
msg += $" ... 还有 {list.Count - 50} 个未显示\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorUtility.DisplayDialog("检测详情", msg, "确定");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
GUILayout.Space(2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorGUILayout.EndScrollView();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
private void ShowDetailsPopup(AnimationClip clip, List<string> mismatchedBones, float percentage, int matchedCount, int totalAnimBones, bool isCompatible, string reason, bool forcedFailed)
|
|||
|
|
{
|
|||
|
|
string message = $"━━━━━━━━━━━━━━━━━━━━\n";
|
|||
|
|
message += $"🎬 动画: {clip.name}\n";
|
|||
|
|
message += $"━━━━━━━━━━━━━━━━━━━━\n\n";
|
|||
|
|
|
|||
|
|
message += $"📦 参考对象: {referenceObject.name}\n";
|
|||
|
|
message += $"🦴 参考骨骼数: {actualBoneFullPaths.Count}\n";
|
|||
|
|
message += $"🔍 匹配模式: {(useFullPathMatch ? "完整路径匹配" : "仅名称匹配")}\n\n";
|
|||
|
|
|
|||
|
|
message += $"━━━━━━━━━━━━━━━━━━━━\n";
|
|||
|
|
message += $"📊 检测结果:\n";
|
|||
|
|
message += $" 动画使用骨骼: {totalAnimBones} 个\n";
|
|||
|
|
message += $" 匹配骨骼: {matchedCount} 个\n";
|
|||
|
|
message += $" 匹配度: {percentage:F1}%\n\n";
|
|||
|
|
|
|||
|
|
message += $"━━━━━━━━━━━━━━━━━━━━\n";
|
|||
|
|
message += $"⚙️ 判定条件:\n";
|
|||
|
|
message += $" {GetConditionDescription()}\n";
|
|||
|
|
|
|||
|
|
// 显示各条件状态
|
|||
|
|
List<string> animPaths = GetAnimationBonePaths(clip);
|
|||
|
|
|
|||
|
|
// 强制条件状态
|
|||
|
|
message += $"\n【强制条件】\n";
|
|||
|
|
if (useMinBoneCount)
|
|||
|
|
{
|
|||
|
|
bool minOk = totalAnimBones >= minBoneCountRequired;
|
|||
|
|
message += $" 最小骨骼数({minBoneCountRequired}): {(minOk ? "✓ " + totalAnimBones : "✗ " + totalAnimBones)}\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (useRequiredBonesList && requiredBonesList.Count > 0)
|
|||
|
|
{
|
|||
|
|
var (requiredOk, missing) = CheckRequiredBonesConditionData(animPaths, requiredBonesList, useFullPathMatch);
|
|||
|
|
message += $" 必要骨骼: {(requiredOk ? "✓ 满足" : "✗ 不满足")}\n";
|
|||
|
|
if (!requiredOk && missing.Count > 0)
|
|||
|
|
{
|
|||
|
|
foreach (string m in missing)
|
|||
|
|
{
|
|||
|
|
message += $" • 缺失: {m}\n";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 可选条件状态
|
|||
|
|
message += $"\n【可选条件】\n";
|
|||
|
|
if (useMismatchCount)
|
|||
|
|
{
|
|||
|
|
bool mismatchOk = mismatchedBones.Count < mismatchCountThreshold;
|
|||
|
|
message += $" 缺失数量({mismatchedBones.Count}): {(mismatchOk ? "✓ <" + mismatchCountThreshold : "✗ ≥" + mismatchCountThreshold)}\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (useMatchPercentage)
|
|||
|
|
{
|
|||
|
|
bool percentOk = percentage >= matchPercentageRequired;
|
|||
|
|
message += $" 匹配百分比({percentage:F1}%): {(percentOk ? "✓ ≥" + matchPercentageRequired : "✗ <" + matchPercentageRequired)}\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message += $"\n━━━━━━━━━━━━━━━━━━━━\n";
|
|||
|
|
message += $"📋 最终判定: {(isCompatible ? "✓ 适配" : "✗ 不适配")}\n";
|
|||
|
|
|
|||
|
|
if (!isCompatible && !string.IsNullOrEmpty(reason))
|
|||
|
|
{
|
|||
|
|
message += $"📌 原因: {reason}\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message += $"\n━━━━━━━━━━━━━━━━━━━━\n";
|
|||
|
|
|
|||
|
|
if (mismatchedBones != null && mismatchedBones.Count > 0)
|
|||
|
|
{
|
|||
|
|
var realMissing = mismatchedBones.Where(b => !b.Contains("警告") && !b.Contains("请先")).ToList();
|
|||
|
|
|
|||
|
|
if (realMissing.Count > 0)
|
|||
|
|
{
|
|||
|
|
message += $"\n⚠️ 不匹配的骨骼路径 ({realMissing.Count} 个):\n\n";
|
|||
|
|
message += string.Join("\n", realMissing.Select(b => "• " + b));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
message += $"\n✓ 所有动画骨骼路径都匹配!";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EditorUtility.DisplayDialog("骨骼匹配详情", message, "确定");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
private void ExportResults()
|
|||
|
|
{
|
|||
|
|
if (!hasRunCheck)
|
|||
|
|
{
|
|||
|
|
EditorUtility.DisplayDialog("提示", "请先执行检测!", "确定");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
string root = rootFolder;
|
|||
|
|
if (!Directory.Exists(root))
|
|||
|
|
Directory.CreateDirectory(root);
|
|||
|
|
|
|||
|
|
string goodPath = Path.Combine(root, goodSubFolder);
|
|||
|
|
string badPath = Path.Combine(root, badSubFolder);
|
|||
|
|
|
|||
|
|
if (!Directory.Exists(goodPath))
|
|||
|
|
Directory.CreateDirectory(goodPath);
|
|||
|
|
|
|||
|
|
if (exportBadClips && !Directory.Exists(badPath))
|
|||
|
|
Directory.CreateDirectory(badPath);
|
|||
|
|
|
|||
|
|
int goodCount = 0;
|
|||
|
|
int badCount = 0;
|
|||
|
|
|
|||
|
|
foreach (var clip in clips)
|
|||
|
|
{
|
|||
|
|
if (!mismatchDetails.ContainsKey(clip))
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
List<string> animPaths = GetAnimationBonePaths(clip);
|
|||
|
|
int totalAnimBones = animPaths.Count;
|
|||
|
|
int mismatch = mismatchCountMap[clip];
|
|||
|
|
float percentage = matchPercentageMap[clip];
|
|||
|
|
|
|||
|
|
// 使用最终判定逻辑
|
|||
|
|
var (isCompatible, forceReason, optionalReason) = IsClipCompatibleData(
|
|||
|
|
mismatch,
|
|||
|
|
totalAnimBones,
|
|||
|
|
percentage,
|
|||
|
|
animPaths,
|
|||
|
|
requiredBonesList,
|
|||
|
|
useRequiredBonesList,
|
|||
|
|
useMinBoneCount,
|
|||
|
|
useMismatchCount,
|
|||
|
|
useMatchPercentage,
|
|||
|
|
mismatchCountThreshold,
|
|||
|
|
matchPercentageRequired,
|
|||
|
|
minBoneCountRequired,
|
|||
|
|
useAndLogic,
|
|||
|
|
useFullPathMatch
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
string src = AssetDatabase.GetAssetPath(clip);
|
|||
|
|
string dst = Path.Combine(isCompatible ? goodPath : badPath, clip.name + ".anim");
|
|||
|
|
|
|||
|
|
if (isCompatible)
|
|||
|
|
{
|
|||
|
|
File.Copy(src, dst, true);
|
|||
|
|
goodCount++;
|
|||
|
|
}
|
|||
|
|
else if (exportBadClips)
|
|||
|
|
{
|
|||
|
|
File.Copy(src, dst, true);
|
|||
|
|
badCount++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
AssetDatabase.Refresh();
|
|||
|
|
|
|||
|
|
EditorUtility.DisplayDialog(
|
|||
|
|
"导出完成",
|
|||
|
|
$"适配动画:{goodCount}\n不适配动画:{badCount}",
|
|||
|
|
"确定"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
}
|