397 lines
15 KiB
C#

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
public class OptitrackSetupWindow : EditorWindow
{
private OptitrackSkeletonAnimator_Mingle m_target;
private int m_tab = 0;
private static readonly string[] TabLabels = { "본 매핑", "T-포즈" };
private bool m_showMappingFoldout = true;
private Vector2 m_mappingScroll;
private Vector2 m_restPoseScroll;
private Dictionary<string, Transform> m_cachedTransformMap;
private OptitrackSkeletonAnimator_Mingle m_cachedTransformTarget;
// ── 진입점 ────────────────────────────────────────────────────────────────
[MenuItem("OptiTrack/Animator 설정 창")]
public static void OpenFromMenu() => GetWindow<OptitrackSetupWindow>("OptiTrack 설정").Show();
public static void Open(OptitrackSkeletonAnimator_Mingle target)
{
var win = GetWindow<OptitrackSetupWindow>("OptiTrack 설정");
win.m_target = target;
win.Show();
}
private void OnSelectionChange()
{
if (Selection.activeGameObject == null) return;
var found = Selection.activeGameObject.GetComponent<OptitrackSkeletonAnimator_Mingle>();
if (found != null) { m_target = found; InvalidateTransformCache(); Repaint(); }
}
// ── 창 ────────────────────────────────────────────────────────────────────
private void OnGUI()
{
// 대상 선택
EditorGUI.BeginChangeCheck();
var newTarget = (OptitrackSkeletonAnimator_Mingle)EditorGUILayout.ObjectField(
"대상", m_target, typeof(OptitrackSkeletonAnimator_Mingle), true);
if (EditorGUI.EndChangeCheck()) { m_target = newTarget; InvalidateTransformCache(); }
if (m_target == null)
{
EditorGUILayout.HelpBox("대상 컴포넌트를 선택하거나 Inspector에서 창을 여세요.", MessageType.Info);
return;
}
// SO 없으면 경고 + 생성 버튼
if (m_target.RestPoseAsset == null)
{
DrawSeparator();
EditorGUILayout.HelpBox("스켈레톤 설정 에셋이 없습니다. 생성하거나 기존 에셋을 연결하세요.", MessageType.Warning);
EditorGUI.BeginChangeCheck();
var linked = (OptitrackRestPoseData)EditorGUILayout.ObjectField(
"Setup Asset", null, typeof(OptitrackRestPoseData), false);
if (EditorGUI.EndChangeCheck() && linked != null)
AssignAsset(linked);
if (GUILayout.Button("새 설정 에셋 생성", GUILayout.Height(28)))
CreateAndAssignAsset();
return;
}
DrawSeparator();
// SO 필드
EditorGUI.BeginChangeCheck();
var newAsset = (OptitrackRestPoseData)EditorGUILayout.ObjectField(
"Setup Asset", m_target.RestPoseAsset, typeof(OptitrackRestPoseData), false);
if (EditorGUI.EndChangeCheck()) AssignAsset(newAsset);
DrawSeparator();
m_tab = GUILayout.Toolbar(m_tab, TabLabels, GUILayout.Height(26));
DrawSeparator();
switch (m_tab)
{
case 0: DrawMappingTab(); break;
case 1: DrawRestPoseTab(); break;
}
if (Application.isPlaying) Repaint();
}
// ── 본 매핑 탭 ────────────────────────────────────────────────────────────
private void DrawMappingTab()
{
var so = m_target.RestPoseAsset;
// 자동 생성 버튼
var prevBg = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.4f, 0.8f, 0.4f);
if (GUILayout.Button("FBX 분석 → 자동 매핑 생성", GUILayout.Height(32)))
{
Undo.RecordObject(so, "Auto Generate Bone Mappings");
AutoGenerateMappings(so);
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
}
GUI.backgroundColor = prevBg;
EditorGUILayout.Space(4);
if (so.boneMappings == null || so.boneMappings.Count == 0)
{
EditorGUILayout.HelpBox("매핑 없음. 위 버튼으로 자동 생성하세요.", MessageType.None);
}
else
{
int mapped = 0, unmapped = 0;
foreach (var m in so.boneMappings)
{
if (m.isMapped) mapped++;
else unmapped++;
}
EditorGUILayout.HelpBox(
$"매핑: {mapped} 성공 / {unmapped} 실패 (총 {so.boneMappings.Count})",
unmapped > 0 ? MessageType.Warning : MessageType.Info);
EditorGUILayout.Space(4);
m_showMappingFoldout = EditorGUILayout.Foldout(m_showMappingFoldout,
$"본 매핑 목록 ({so.boneMappings.Count})", true, EditorStyles.foldoutHeader);
if (m_showMappingFoldout)
{
m_mappingScroll = EditorGUILayout.BeginScrollView(m_mappingScroll, GUILayout.MaxHeight(320));
DrawMappingList(so);
EditorGUILayout.EndScrollView();
}
EditorGUILayout.Space(4);
prevBg = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.85f, 0.35f, 0.35f);
if (GUILayout.Button("매핑 초기화"))
{
Undo.RecordObject(so, "Clear Bone Mappings");
so.boneMappings.Clear();
EditorUtility.SetDirty(so);
}
GUI.backgroundColor = prevBg;
}
}
private void DrawMappingList(OptitrackRestPoseData so)
{
var transformMap = GetTransformMap();
for (int i = 0; i < so.boneMappings.Count; i++)
{
var mapping = so.boneMappings[i];
EditorGUILayout.BeginHorizontal();
// 상태 아이콘
var prevContent = GUI.contentColor;
GUI.contentColor = mapping.isMapped ? Color.green : Color.red;
GUILayout.Label(mapping.isMapped ? "●" : "○", GUILayout.Width(14));
GUI.contentColor = prevContent;
GUILayout.Label(mapping.optiTrackBoneName, GUILayout.Width(90));
GUILayout.Label("→", GUILayout.Width(16));
// FBX 노드 이름 편집
EditorGUI.BeginChangeCheck();
string newName = EditorGUILayout.TextField(mapping.fbxNodeName);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(so, "Edit Bone Mapping");
mapping.fbxNodeName = newName;
mapping.isMapped = transformMap.ContainsKey(newName);
EditorUtility.SetDirty(so);
}
// P / R 토글
EditorGUI.BeginChangeCheck();
bool newPos = GUILayout.Toggle(mapping.applyPosition, "P", "Button", GUILayout.Width(24));
bool newRot = GUILayout.Toggle(mapping.applyRotation, "R", "Button", GUILayout.Width(24));
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(so, "Toggle Bone Apply");
mapping.applyPosition = newPos;
mapping.applyRotation = newRot;
EditorUtility.SetDirty(so);
}
EditorGUILayout.EndHorizontal();
}
}
// ── T-포즈 탭 ─────────────────────────────────────────────────────────────
private void DrawRestPoseTab()
{
var so = m_target.RestPoseAsset;
bool hasEntries = so.restPoseEntries.Count > 0;
if (hasEntries)
EditorGUILayout.HelpBox($"{so.restPoseEntries.Count}개 본 저장됨", MessageType.Info);
else
EditorGUILayout.HelpBox("T-포즈 없음 — 아바타를 T-포즈로 맞추고 캡처하세요.", MessageType.Warning);
EditorGUILayout.Space(6);
var prevBg = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.35f, 0.65f, 1f);
if (GUILayout.Button("현재 포즈 → T-포즈로 캡처", GUILayout.Height(32)))
CaptureRestPose(so);
GUI.backgroundColor = prevBg;
using (new EditorGUI.DisabledScope(!hasEntries))
{
if (GUILayout.Button("씬에 적용 (미리보기)", GUILayout.Height(26)))
PreviewRestPose(so);
}
if (hasEntries)
{
DrawSeparator();
EditorGUILayout.LabelField($"저장된 본 목록 ({so.restPoseEntries.Count})", EditorStyles.boldLabel);
m_restPoseScroll = EditorGUILayout.BeginScrollView(m_restPoseScroll, GUILayout.MaxHeight(260));
foreach (var e in so.restPoseEntries)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label(e.boneName, GUILayout.Width(110));
GUILayout.Label($"pos {e.localPosition:F3}", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
}
private void CaptureRestPose(OptitrackRestPoseData so)
{
var transformMap = GetTransformMap();
Undo.RecordObject(so, "Capture Rest Pose");
so.restPoseEntries.Clear();
int count = 0;
foreach (var mapping in so.boneMappings)
{
if (string.IsNullOrEmpty(mapping.fbxNodeName) || mapping.fbxNodeName.Contains("(미발견)"))
continue;
if (!transformMap.TryGetValue(mapping.fbxNodeName, out Transform t))
continue;
so.restPoseEntries.Add(new OptitrackRestPoseData.RestPoseEntry
{
boneName = mapping.optiTrackBoneName,
localPosition = t.localPosition,
localRotation = t.localRotation
});
count++;
}
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
Debug.Log($"[OptiTrack] T-포즈 캡처 완료: {count}개 본 → {AssetDatabase.GetAssetPath(so)}");
}
private void PreviewRestPose(OptitrackRestPoseData so)
{
var transformMap = GetTransformMap();
var poseByName = new Dictionary<string, OptitrackRestPoseData.RestPoseEntry>(so.restPoseEntries.Count);
foreach (var e in so.restPoseEntries)
poseByName[e.boneName] = e;
foreach (var mapping in so.boneMappings)
{
if (!transformMap.TryGetValue(mapping.fbxNodeName, out Transform t)) continue;
if (!poseByName.TryGetValue(mapping.optiTrackBoneName, out var entry)) continue;
Undo.RecordObject(t, "Preview Rest Pose");
t.localPosition = entry.localPosition;
t.localRotation = entry.localRotation;
}
}
// ── SO 관리 ───────────────────────────────────────────────────────────────
private void AssignAsset(OptitrackRestPoseData asset)
{
Undo.RecordObject(m_target, "Set Skeleton Setup Asset");
m_target.RestPoseAsset = asset;
EditorUtility.SetDirty(m_target);
}
private void CreateAndAssignAsset()
{
string path = EditorUtility.SaveFilePanelInProject(
"스켈레톤 설정 에셋 저장", "OptitrackSkeletonSetup", "asset", "저장 위치를 선택하세요.");
if (string.IsNullOrEmpty(path)) return;
var asset = ScriptableObject.CreateInstance<OptitrackRestPoseData>();
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
AssignAsset(asset);
}
// ── 자동 매핑 생성 ────────────────────────────────────────────────────────
private void AutoGenerateMappings(OptitrackRestPoseData so)
{
so.boneMappings.Clear();
var transformMap = GetTransformMap();
string prefix = "";
foreach (var kvp in transformMap)
{
if (kvp.Key.EndsWith("Hips"))
{
int idx = kvp.Key.LastIndexOf("Hips");
if (idx > 0) { prefix = kvp.Key.Substring(0, idx); break; }
}
}
if (!string.IsNullOrEmpty(prefix))
Debug.Log($"[OptiTrack 매핑] 감지된 접두사: \"{prefix}\"");
int mapped = 0;
foreach (var kvp in OptitrackSkeletonAnimator_Mingle.DefaultOptiToFbxSuffix)
{
var mapping = new OptiTrackBoneMapping
{
optiTrackBoneName = kvp.Key,
applyPosition = true,
applyRotation = true,
isMapped = false
};
string fullName = prefix + kvp.Value;
if (transformMap.TryGetValue(fullName, out Transform found))
{
mapping.fbxNodeName = fullName;
mapping.cachedTransform = found;
mapping.isMapped = true;
}
else if (transformMap.TryGetValue(kvp.Value, out Transform found2))
{
mapping.fbxNodeName = kvp.Value;
mapping.cachedTransform = found2;
mapping.isMapped = true;
}
else
{
foreach (var t in transformMap)
{
if (t.Key.EndsWith(kvp.Value) || t.Key.EndsWith("_" + kvp.Value))
{
mapping.fbxNodeName = t.Key;
mapping.cachedTransform = t.Value;
mapping.isMapped = true;
break;
}
}
}
if (!mapping.isMapped)
mapping.fbxNodeName = prefix + kvp.Value + " (미발견)";
else
mapped++;
so.boneMappings.Add(mapping);
}
Debug.Log($"[OptiTrack 매핑] 완료: {mapped}/{so.boneMappings.Count} 본 매핑 성공");
}
// ── 유틸 ──────────────────────────────────────────────────────────────────
private Dictionary<string, Transform> GetTransformMap()
{
if (m_target == null) return new Dictionary<string, Transform>();
if (m_cachedTransformMap != null && m_cachedTransformTarget == m_target)
return m_cachedTransformMap;
m_cachedTransformMap = new Dictionary<string, Transform>();
foreach (var t in m_target.GetComponentsInChildren<Transform>(true))
if (!m_cachedTransformMap.ContainsKey(t.name))
m_cachedTransformMap[t.name] = t;
m_cachedTransformTarget = m_target;
return m_cachedTransformMap;
}
private void InvalidateTransformCache() => m_cachedTransformMap = null;
private static void DrawSeparator()
{
EditorGUILayout.Space(5);
EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1f), new Color(0.35f, 0.35f, 0.35f));
EditorGUILayout.Space(4);
}
}