using UnityEngine; using UnityEditor; using System.Collections.Generic; using StudioKafka.MeshSyncPro.Runtime; using System; using System.Linq; namespace StudioKafka.MeshSyncPro { public class MeshSyncProWindow : EditorWindow { private List bodyObjects = new List(); private List clothingObjects = new List(); private float penetrationThreshold = 0.001f; private float pushOutDistance = 0.05f; private float dotProductThreshold = 0.8f; private float influenceRadiusSteps = 1f; private int smoothingIterations = 2; private float smoothingFactor = 0.1f; private Dictionary<(GameObject, GameObject), int[]> penetratingIndicesResults = new Dictionary<(GameObject, GameObject), int[]>(); private Dictionary<(GameObject, GameObject), Vector3[]> penetratingVerticesResults = new Dictionary<(GameObject, GameObject), Vector3[]>(); private bool visualizePenetratingVertices = true; private Vector2 scrollPosition; private List zones = new List(); private GameObject debugSphere; [MenuItem("Tools/MeshSyncPro")] public static void ShowWindow() { GetWindow("MeshSyncPro 도구"); } private void OnEnable() { SceneView.duringSceneGui += OnSceneGUIDelegate; Undo.undoRedoPerformed += OnUndoRedo; if (zones == null || zones.Count == 0) { zones = new List(); zones.Add(new MeshSyncProManager.Zone() { center = new Vector3(0f, 0.872f, -0.12f), size = new Vector3(0.2f, 0.07f, 0.2f), active = true, color = new Color(0f, 1f, 0f, 0.25f) }); } } private void OnDisable() { SceneView.duringSceneGui -= OnSceneGUIDelegate; Undo.undoRedoPerformed -= OnUndoRedo; if (debugSphere != null) DestroyImmediate(debugSphere); } private void OnUndoRedo() { penetratingIndicesResults.Clear(); penetratingVerticesResults.Clear(); Repaint(); SceneView.RepaintAll(); Debug.Log("작업이 취소되었습니다. 관통 검출 결과는 초기화되었습니다."); } void OnGUI() { try { GUILayout.BeginVertical(); try { EditorGUILayout.LabelField("MeshSyncPro: 메시 관통 검출·수정 도구", EditorStyles.boldLabel); EditorGUILayout.Space(); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.HelpBox( "이 도구는 아바타 등의 기본 메시와 의상 메시 간의 관통을 검출하고 자동 수정을 시도합니다.\n" + "기본 사용법:\n" + "1. '기본 오브젝트'와 '의상 오브젝트'에 대상 GameObject를 지정합니다.\n" + "2. 각 설정값을 조정합니다. 툴팁에 조정 힌트가 있습니다.\n" + "3. '관통 부분 검출' 버튼으로 관통 부분을 특정합니다.\n" + "4. '검출 부분 수정' 버튼으로 자동 수정을 실행합니다.\n" + "모든 작업은 Undo (Ctrl+Z / Cmd+Z)로 되돌릴 수 있습니다.", MessageType.Info); GUILayout.Label("대상 오브젝트 설정", EditorStyles.boldLabel); EditorGUILayout.LabelField("기본 오브젝트 (Skinned Mesh Renderer)", EditorStyles.miniBoldLabel); DrawGameObjectList(bodyObjects); EditorGUILayout.LabelField("의상 오브젝트 (Skinned Mesh Renderer 또는 MeshFilter)", EditorStyles.miniBoldLabel); DrawGameObjectList(clothingObjects); EditorGUILayout.Space(); GUILayout.Label("검출 대상 영역 설정", EditorStyles.boldLabel); EditorGUILayout.HelpBox("관통 검출의 대상으로 하고 싶은 범위를 설정합니다.\n영역 설정은, 광범위한 메시에서 특정 부분(예: 가슴, 겨드랑이 등)의 관통만을 중점적으로 체크하고 싶은 경우에 유효합니다.", MessageType.None); for (int i = 0; i < zones.Count; i++) { MeshSyncProManager.Zone zone = zones[i]; EditorGUILayout.BeginVertical(GUI.skin.box); EditorGUILayout.LabelField($"영역 {i}", EditorStyles.boldLabel); Undo.RecordObject(this, "영역 파라미터 변경"); zone.center = EditorGUILayout.Vector3Field(new GUIContent("중심 좌표", "영역의 중심이 되는 월드 좌표입니다. 대상 오브젝트의 로컬 좌표가 아닙니다."), zone.center); zone.size = EditorGUILayout.Vector3Field(new GUIContent("범위 크기", "영역의 XYZ 각 방향의 크기(지름)입니다."), zone.size); zone.active = EditorGUILayout.Toggle(new GUIContent("이 영역을 활성화", "체크를 해제하면 이 영역은 검출 대상에서 제외됩니다."), zone.active); zone.color = EditorGUILayout.ColorField(new GUIContent("영역 표시 색상", "씬 뷰에서 이 영역을 와이어프레임 표시할 때의 색상입니다. 반투명하게 하면 보기 쉽습니다."), zone.color); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("이 영역 삭제", GUILayout.Width(120))) { Undo.RecordObject(this, "영역 삭제"); zones.RemoveAt(i--); } if (GUILayout.Button("이 영역 초기화", GUILayout.Width(120))) { Undo.RecordObject(this, "영역 초기화"); if (i == 0 && zones.Count > 0) { zone.center = new Vector3(0f, 0.872f, -0.12f); zone.size = new Vector3(0.2f, 0.07f, 0.2f); zone.color = new Color(0f, 1f, 0f, 0.25f); } else { zone.center = Vector3.zero; zone.size = new Vector3(0.2f, 0.2f, 0.2f); zone.color = new Color(0.3f, 1f, 0.3f, 0.25f); } zone.active = true; } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } if (GUILayout.Button("새 영역 추가")) { Undo.RecordObject(this, "영역 추가"); zones.Add(new MeshSyncProManager.Zone() { center = Vector3.zero, size = new Vector3(0.2f, 0.2f, 0.2f), active = true, color = new Color(0.3f, 0.3f, 1f, 0.25f) }); } EditorGUILayout.Space(); GUILayout.Label("관통 검출의 기본 설정", EditorStyles.boldLabel); penetrationThreshold = EditorGUILayout.Slider( new GUIContent("관통으로 간주하는 거리 임계값", "의상이 기본 메시에 이 값 이상으로 파고들어 있는 경우에 관통으로 판정합니다.\n작은 값일수록 경미한 파고듦도 검출하지만, 과검출의 가능성도 증가합니다.(단위:미터)"), penetrationThreshold, 0.0001f, 0.1f); dotProductThreshold = EditorGUILayout.Slider( new GUIContent("법선 방향의 일치도 임계값", "기본 메시의 법선과 관통 방향 벡터의 일치도. 값이 클수록, 기본 메시 표면에서 거의 수직으로 뚫고 나가는 관통만을 대상으로 합니다.\n-1에 가까울수록 모든 방향의 접촉을 검출하고, 1에 가까울수록 엄격한 뚫고 나감만을 검출합니다. 보통은 0.5 이상을 권장합니다."), dotProductThreshold, -1f, 1f); EditorGUILayout.Space(); GUILayout.Label("수정의 기본 설정", EditorStyles.boldLabel); EditorGUILayout.HelpBox("관통 수정하는 정점의 이동량이나 스무딩을 설정합니다", MessageType.None); pushOutDistance = EditorGUILayout.Slider( new GUIContent("수정 시 밀어내는 양", "관통 수정 시, 기본 메시를 밀어내는 거리의 기준입니다.\n크게 하면 수정이 강해지지만, 형태가 왜곡될 수 있습니다.(단위:미터)"), pushOutDistance, 0.0f, 0.1f); influenceRadiusSteps = EditorGUILayout.Slider( new GUIContent("수정 영향 스텝 수", "관통 수정 시, 직접 수정되는 정점의 주변 몇 스텝분의 정점까지 영향을 미치는지.\n값을 크게 하면 광범위가 부드럽게 수정되지만, 의도하지 않은 부분까지 변형될 가능성이 있습니다. 0으로 하면 직접 수정만 합니다."), influenceRadiusSteps, 0f, 10f); smoothingIterations = EditorGUILayout.IntSlider( new GUIContent("스무딩 반복 횟수", "수정 후의 메시를 부드럽게 하는 처리의 반복 횟수입니다.\n횟수를 늘리면 더 부드러워지지만, 처리 시간이 증가하고 디테일이 손실될 수 있습니다."), smoothingIterations, 0, 10); smoothingFactor = EditorGUILayout.Slider( new GUIContent("스무딩 강도", "수정 후의 메시를 부드럽게 할 때의 각 반복의 강도입니다.(0.0으로 효과 없음, 1.0으로 최대 효과)\n강도가 높을수록, 1회의 반복으로 크게 부드러워집니다."), smoothingFactor, 0f, 1f); visualizePenetratingVertices = EditorGUILayout.Toggle( new GUIContent("관통 정점을 씬에 표시", "검출된 관통 정점을 빨간 구체로 씬 뷰에 표시합니다. 디버그나 설정 조정에 도움이 됩니다."), visualizePenetratingVertices); GUILayout.Space(20); if (GUILayout.Button("관통 부분 검출", GUILayout.Height(35))) { MeshSyncProManager manager = GetOrCreateManager(); ConfigureManager(manager); Undo.RecordObject(manager, "관통 검출 (매니저 상태)"); manager.ApplyMeshCorrections(); penetratingIndicesResults = manager.GetPenetratingIndicesResults(); penetratingVerticesResults = manager.GetPenetratingVerticesResults(); Repaint(); } GUI.enabled = penetratingIndicesResults != null && penetratingIndicesResults.Count > 0 && penetratingIndicesResults.Any(kvp => kvp.Value != null && kvp.Value.Length > 0); if (GUILayout.Button("검출 부분 수정", GUILayout.Height(35))) { MeshSyncProManager manager = GetOrCreateManager(); ConfigureManager(manager); manager.FixDetectedPenetrations(pushOutDistance); penetratingIndicesResults.Clear(); penetratingVerticesResults.Clear(); Repaint(); } GUI.enabled = true; GUILayout.Space(10); // 수정된 메시가 있는지 확인 bool hasModifiedMeshes = false; foreach (var clothingObj in clothingObjects) { if (clothingObj != null && clothingObj.GetComponent() != null) { hasModifiedMeshes = true; break; } } GUI.enabled = hasModifiedMeshes; if (GUILayout.Button("수정된 메시 저장", GUILayout.Height(35))) { SaveModifiedMeshes(); } GUI.enabled = true; GUILayout.Space(10); GUILayout.Label("검출 결과 개요", EditorStyles.boldLabel); if (penetratingIndicesResults != null && penetratingIndicesResults.Count > 0 && penetratingIndicesResults.Any(kvp => kvp.Value != null && kvp.Value.Length > 0)) { EditorGUILayout.BeginVertical(GUI.skin.box); int totalPenetratingVertices = 0; foreach (var pair in penetratingIndicesResults) { var key = pair.Key; GameObject bodyObj = key.Item1; GameObject clothingObj = key.Item2; if (bodyObj == null || clothingObj == null) { Debug.LogWarning($"[MeshSyncPro] 결과 표시 스킵: GameObject가 무효합니다. 기본: {(bodyObj == null ? "null" : bodyObj.name)}, 의상: {(clothingObj == null ? "null" : clothingObj.name)}"); continue; } int[] penetratingIndices = pair.Value; if (penetratingIndices != null && penetratingIndices.Length > 0) { totalPenetratingVertices += penetratingIndices.Length; string resultText = $"기본「{(bodyObj?.name ?? "N/A")}」과 의상「{(clothingObj?.name ?? "N/A")}」사이: {penetratingIndices.Length} 정점의 관통을 검출"; EditorGUILayout.LabelField(resultText); } } if (totalPenetratingVertices > 0) { EditorGUILayout.HelpBox($"총 {totalPenetratingVertices} 부분의 관통 정점을 검출했습니다.", MessageType.Info); } else { EditorGUILayout.LabelField("선택된 오브젝트 사이에는, 현재 설정으로 관통으로 판정되는 부분은 없었습니다."); } EditorGUILayout.EndVertical(); } else { EditorGUILayout.LabelField("아직 관통 검출은 실행되지 않았거나, 관통 부분은 발견되지 않았습니다.\n위의 버튼으로 검출을 실행해 주세요."); } EditorGUILayout.EndScrollView(); } finally { GUILayout.EndVertical(); } } catch (ExitGUIException) { throw; } catch (Exception e) { Debug.LogException(e); } } private MeshSyncProManager GetOrCreateManager() { MeshSyncProManager manager = FindObjectOfType(); if (manager == null) { GameObject managerGo = new GameObject("MeshSyncPro_ManagerComponent"); Undo.RegisterCreatedObjectUndo(managerGo, "MeshSyncPro 매니저 생성"); manager = managerGo.AddComponent(); Debug.Log("씬에 MeshSyncPro의 매니저 컴포넌트를 찾을 수 없어서 새로 생성했습니다.", managerGo); } return manager; } private void ConfigureManager(MeshSyncProManager manager) { Undo.RecordObject(manager, "MeshSyncPro 매니저 설정 변경"); manager.bodyObjects = new List(bodyObjects.Where(go => go != null)); manager.clothingObjects = new List(clothingObjects.Where(go => go != null)); manager.penetrationThreshold = penetrationThreshold; manager.dotProductThreshold = dotProductThreshold; manager.influenceRadiusSteps = influenceRadiusSteps; manager.smoothingIterations = smoothingIterations; manager.smoothingFactor = smoothingFactor; manager.DetectionZones = zones.FindAll(z => z.active).ToArray(); } void DrawGameObjectList(List list) { EditorGUILayout.BeginVertical(GUI.skin.box); for (int i = 0; i < list.Count; i++) { EditorGUILayout.BeginHorizontal(); GameObject newObj = (GameObject)EditorGUILayout.ObjectField(list[i], typeof(GameObject), true); if (newObj != list[i]) { Undo.RecordObject(this, "대상 오브젝트 리스트 변경"); list[i] = newObj; } if (GUILayout.Button("-", GUILayout.Width(20))) { Undo.RecordObject(this, "대상 오브젝트 리스트에서 삭제"); list.RemoveAt(i); i--; } EditorGUILayout.EndHorizontal(); } Rect dropArea = EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Height(50)); GUI.Box(dropArea, "대상의 GameObject를 여기에 드래그&드롭하여 추가"); EditorGUILayout.EndVertical(); Event currentEvent = Event.current; if (dropArea.Contains(currentEvent.mousePosition)) { switch (currentEvent.type) { case EventType.DragUpdated: case EventType.DragPerform: DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (currentEvent.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); foreach (UnityEngine.Object draggedObject in DragAndDrop.objectReferences) { if (draggedObject is GameObject go) { if (!list.Contains(go)) { Undo.RecordObject(this, "드래그 작업으로 대상 오브젝트를 리스트에 추가"); list.Add(go); } } } DragAndDrop.activeControlID = 0; currentEvent.Use(); } break; } } if (GUILayout.Button("+ 선택 중인 액티브 오브젝트 추가")) { if (Selection.activeGameObject != null) { if (!list.Contains(Selection.activeGameObject)) { Undo.RecordObject(this, "선택 중 오브젝트를 리스트에 추가"); list.Add(Selection.activeGameObject); } } else { Debug.LogWarning("대상이 되는 GameObject가 Hierarchy에서 선택되어 있지 않습니다."); } } EditorGUILayout.EndVertical(); } private void OnSceneGUIDelegate(SceneView sceneView) { foreach (MeshSyncProManager.Zone zone in zones) { if (zone.active) { Handles.color = zone.color; Handles.DrawWireCube(zone.center, zone.size); } } if (visualizePenetratingVertices && penetratingVerticesResults != null) { Handles.color = Color.red; foreach (var pair in penetratingVerticesResults) { GameObject bodyObj = pair.Key.Item1; GameObject clothingObj = pair.Key.Item2; if (bodyObj == null || clothingObj == null) { continue; } Vector3[] vertices = pair.Value; if (vertices != null && vertices.Length > 0) { foreach (Vector3 vertex in vertices) { float handleSize = HandleUtility.GetHandleSize(vertex) * 0.03f; Handles.SphereHandleCap(0, vertex, Quaternion.identity, handleSize, EventType.Repaint); } if (vertices.Length > 0 && vertices[0] != null) { Vector3 labelPos = vertices[0] + Vector3.up * HandleUtility.GetHandleSize(vertices[0]) * 0.2f; Handles.Label(labelPos, $"관통: {vertices.Length}정점"); } } } } sceneView.Repaint(); } public void VisualizePenetrationFix(Vector3 beforeFix, Vector3 afterFix) { if (debugSphere != null) { DestroyImmediate(debugSphere); } debugSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); debugSphere.name = "MeshSyncPro_FixVisualizationSphere"; debugSphere.transform.localScale = Vector3.one * 0.02f; debugSphere.transform.position = beforeFix; Debug.DrawLine(beforeFix, afterFix, Color.green, 5.0f); Handles.color = Color.green; Handles.DrawLine(beforeFix, afterFix); SceneView.RepaintAll(); } private void SaveModifiedMeshes() { bool anyMeshSaved = false; foreach (var clothingObj in clothingObjects) { if (clothingObj == null) continue; SkinnedMeshRenderer skinnedMeshRenderer = clothingObj.GetComponent(); if (skinnedMeshRenderer != null && skinnedMeshRenderer.sharedMesh != null) { try { string originalPath = AssetDatabase.GetAssetPath(skinnedMeshRenderer.sharedMesh); string directory; string fileName; // 에셋 경로가 없는 경우 (씬에서 생성된 메시) if (string.IsNullOrEmpty(originalPath)) { // 기본 저장 경로 설정 directory = "Assets/Meshes"; fileName = $"{clothingObj.name}_Mesh"; // 디렉토리가 없으면 생성 if (!System.IO.Directory.Exists(directory)) { System.IO.Directory.CreateDirectory(directory); } } else { directory = System.IO.Path.GetDirectoryName(originalPath); fileName = System.IO.Path.GetFileNameWithoutExtension(originalPath); } string newPath = $"{directory}/{fileName}_Fixed.asset"; // 메시 복사 및 저장 Mesh newMesh = Instantiate(skinnedMeshRenderer.sharedMesh); AssetDatabase.CreateAsset(newMesh, newPath); AssetDatabase.SaveAssets(); // 새로운 메시 적용 skinnedMeshRenderer.sharedMesh = newMesh; EditorUtility.SetDirty(clothingObj); anyMeshSaved = true; Debug.Log($"메시가 저장되었습니다: {newPath}"); } catch (System.Exception e) { Debug.LogError($"메시 '{clothingObj.name}' 저장 중 오류 발생: {e.Message}"); } } } if (anyMeshSaved) { AssetDatabase.Refresh(); EditorUtility.DisplayDialog("저장 완료", "수정된 메시가 성공적으로 저장되었습니다.", "확인"); } else { EditorUtility.DisplayDialog("저장 실패", "저장할 수정된 메시가 없습니다.", "확인"); } } } }