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 m_cachedTransformMap; private OptitrackSkeletonAnimator_Mingle m_cachedTransformTarget; // ── 진입점 ──────────────────────────────────────────────────────────────── [MenuItem("OptiTrack/Animator 설정 창")] public static void OpenFromMenu() => GetWindow("OptiTrack 설정").Show(); public static void Open(OptitrackSkeletonAnimator_Mingle target) { var win = GetWindow("OptiTrack 설정"); win.m_target = target; win.Show(); } private void OnSelectionChange() { if (Selection.activeGameObject == null) return; var found = Selection.activeGameObject.GetComponent(); 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(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(); 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 GetTransformMap() { if (m_target == null) return new Dictionary(); if (m_cachedTransformMap != null && m_cachedTransformTarget == m_target) return m_cachedTransformMap; m_cachedTransformMap = new Dictionary(); foreach (var t in m_target.GetComponentsInChildren(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); } }