538 lines
25 KiB
C#

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<GameObject> bodyObjects = new List<GameObject>();
private List<GameObject> clothingObjects = new List<GameObject>();
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<MeshSyncProManager.Zone> zones = new List<MeshSyncProManager.Zone>();
private GameObject debugSphere;
[MenuItem("Tools/MeshSyncPro")]
public static void ShowWindow()
{
GetWindow<MeshSyncProWindow>("MeshSyncPro 도구");
}
private void OnEnable()
{
SceneView.duringSceneGui += OnSceneGUIDelegate;
Undo.undoRedoPerformed += OnUndoRedo;
if (zones == null || zones.Count == 0)
{
zones = new List<MeshSyncProManager.Zone>();
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<SkinnedMeshRenderer>() != 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<MeshSyncProManager>();
if (manager == null)
{
GameObject managerGo = new GameObject("MeshSyncPro_ManagerComponent");
Undo.RegisterCreatedObjectUndo(managerGo, "MeshSyncPro 매니저 생성");
manager = managerGo.AddComponent<MeshSyncProManager>();
Debug.Log("씬에 MeshSyncPro의 매니저 컴포넌트를 찾을 수 없어서 새로 생성했습니다.", managerGo);
}
return manager;
}
private void ConfigureManager(MeshSyncProManager manager)
{
Undo.RecordObject(manager, "MeshSyncPro 매니저 설정 변경");
manager.bodyObjects = new List<GameObject>(bodyObjects.Where(go => go != null));
manager.clothingObjects = new List<GameObject>(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<GameObject> 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<SkinnedMeshRenderer>();
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("저장 실패", "저장할 수정된 메시가 없습니다.", "확인");
}
}
}
}