397 lines
15 KiB
C#
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);
|
|
}
|
|
}
|