From f0824f152b55345d189b1a14265fda35c9ad8983 Mon Sep 17 00:00:00 2001 From: Yamo4490 Date: Fri, 13 Jun 2025 02:46:22 +0900 Subject: [PATCH] =?UTF-8?q?Meshsync=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/External/_StudioKafka.meta | 8 + Assets/External/_StudioKafka/MeshSyncPro.meta | 8 + .../_StudioKafka/MeshSyncPro/Editor.meta | 8 + .../MeshSyncPro/Editor/MeshSyncProWindow.cs | 537 ++++++++++++++++++ .../Editor/MeshSyncProWindow.cs.meta | 11 + .../_StudioKafka/MeshSyncPro/Runtime.meta | 8 + .../MeshSyncPro/Runtime/MeshSyncProManager.cs | 326 +++++++++++ .../Runtime/MeshSyncProManager.cs.meta | 11 + .../Runtime/PenetrationDetectionCore.cs | 406 +++++++++++++ .../Runtime/PenetrationDetectionCore.cs.meta | 11 + .../Runtime/PenetrationFixEngine.cs | 347 +++++++++++ .../Runtime/PenetrationFixEngine.cs.meta | 11 + 12 files changed, 1692 insertions(+) create mode 100644 Assets/External/_StudioKafka.meta create mode 100644 Assets/External/_StudioKafka/MeshSyncPro.meta create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Editor.meta create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs.meta create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Runtime.meta create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs.meta create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs.meta create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs create mode 100644 Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs.meta diff --git a/Assets/External/_StudioKafka.meta b/Assets/External/_StudioKafka.meta new file mode 100644 index 000000000..5d3ac27d4 --- /dev/null +++ b/Assets/External/_StudioKafka.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5ce652977f23eee4fa89058799522039 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/_StudioKafka/MeshSyncPro.meta b/Assets/External/_StudioKafka/MeshSyncPro.meta new file mode 100644 index 000000000..54e8533cd --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a8f1ac812e3f7804aae29071532c5b3b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Editor.meta b/Assets/External/_StudioKafka/MeshSyncPro/Editor.meta new file mode 100644 index 000000000..687bf3744 --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 289042708b12df64f989e3ea0c83a041 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs b/Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs new file mode 100644 index 000000000..26c18333f --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs @@ -0,0 +1,537 @@ +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("저장 실패", "저장할 수정된 메시가 없습니다.", "확인"); + } + } + } +} diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs.meta b/Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs.meta new file mode 100644 index 000000000..4b9808c4a --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Editor/MeshSyncProWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9ecc9e1fac43634780d004760cd4f08 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Runtime.meta b/Assets/External/_StudioKafka/MeshSyncPro/Runtime.meta new file mode 100644 index 000000000..bc6cfd0b0 --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f3e043bb9c7ef3446ab04fdc40ddb53b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs new file mode 100644 index 000000000..a8758c1fd --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using System.Linq; + +namespace StudioKafka.MeshSyncPro +{ + public class MeshSyncProManager : MonoBehaviour + { + #region Zone型の定義 + + [Serializable] + public class Zone + { + public Vector3 center; + public Vector3 size; + public bool active; + public Color color; + + public Zone() + { + center = Vector3.zero; + size = Vector3.one; + active = true; + color = Color.green; + } + + public Zone(Vector3 zoneCenter, Vector3 zoneSize, bool isActive = true) + { + center = zoneCenter; + size = zoneSize; + active = isActive; + color = Color.green; + } + + public bool ContainsPoint(Vector3 point) + { + Vector3 min = center - size * 0.5f; + Vector3 max = center + size * 0.5f; + return point.x >= min.x && point.x <= max.x && + point.y >= min.y && point.y <= max.y && + point.z >= min.z && point.z <= max.z; + } + } + + #endregion + + #region パブリックプロパティ(UIから設定される) + + [Header("対象オブジェクト")] + public List bodyObjects = new List(); + public List clothingObjects = new List(); + + [Header("検出設定")] + [Range(0.0001f, 0.1f)] + public float penetrationThreshold = 0.001f; + + [Range(-1f, 1f)] + public float dotProductThreshold = 0.8f; + + [Header("修正設定")] + [Range(0f, 10f)] + public float influenceRadiusSteps = 1f; + + [Range(0, 10)] + public int smoothingIterations = 2; + + [Range(0f, 1f)] + public float smoothingFactor = 0.1f; + + [Header("検出ゾーン")] + public Zone[] DetectionZones = new Zone[0]; + + #endregion + + #region プライベートフィールド + + private Dictionary<(GameObject, GameObject), int[]> penetratingIndicesResults = + new Dictionary<(GameObject, GameObject), int[]>(); + private Dictionary<(GameObject, GameObject), Vector3[]> penetratingVerticesResults = + new Dictionary<(GameObject, GameObject), Vector3[]>(); + private bool isInitialized = false; + + #endregion + + #region Unity ライフサイクル + + private void Awake() + { + Initialize(); + } + + private void Start() + { + if (DetectionZones == null || DetectionZones.Length == 0) + { + CreateDefaultZone(); + } + } + + #endregion + + #region 初期化 + + private void Initialize() + { + if (isInitialized) return; + + penetratingIndicesResults = new Dictionary<(GameObject, GameObject), int[]>(); + penetratingVerticesResults = new Dictionary<(GameObject, GameObject), Vector3[]>(); + isInitialized = true; + + Debug.Log("[MeshSyncProManager] マネージャーが初期化されました"); + } + + private void CreateDefaultZone() + { + DetectionZones = new Zone[] + { + new Zone(new Vector3(0f, 0.872f, -0.12f), new Vector3(0.2f, 0.07f, 0.2f)) + { + color = new Color(0f, 1f, 0f, 0.25f) + } + }; + } + + #endregion + + #region メイン機能(UIから呼び出される) + + public void ApplyMeshCorrections() + { + Debug.Log("[MeshSyncProManager] 貫通検出を開始します"); + ClearResults(); + + if (!ValidateInputs()) + { + Debug.LogWarning("[MeshSyncProManager] 入力が無効です。検出を中止します"); + return; + } + + int totalDetections = 0; + + foreach (GameObject bodyObj in bodyObjects.Where(obj => obj != null)) + { + SkinnedMeshRenderer bodyRenderer = bodyObj.GetComponent(); + if (bodyRenderer == null) continue; + + foreach (GameObject clothingObj in clothingObjects.Where(obj => obj != null)) + { + Mesh clothingMesh = GetMeshFromGameObject(clothingObj); + if (clothingMesh == null) continue; + + var detectionResult = Runtime.PenetrationDetectionCore.DetectPenetration( + bodyRenderer, + clothingMesh, + clothingObj.transform, + penetrationThreshold, + dotProductThreshold + ); + + if (detectionResult.IsSuccessful && detectionResult.PenetratingIndices.Length > 0) + { + var filteredResult = FilterByZones(detectionResult); + var key = (bodyObj, clothingObj); + penetratingIndicesResults[key] = filteredResult.PenetratingIndices; + penetratingVerticesResults[key] = filteredResult.PenetratingVertices; + totalDetections += filteredResult.PenetratingIndices.Length; + + Debug.Log($"[MeshSyncProManager] {bodyObj.name} と {clothingObj.name} 間で {filteredResult.PenetratingIndices.Length} 個の貫通を検出"); + } + } + } + + Debug.Log($"[MeshSyncProManager] 検出完了: 合計 {totalDetections} 個の貫通頂点を検出しました"); + } + + public void FixDetectedPenetrations(float pushOutDistance) + { + Debug.Log($"[MeshSyncProManager] 貫通修正を開始します(押し出し距離: {pushOutDistance:F4})"); + int totalFixed = 0; + + foreach (var result in penetratingIndicesResults) + { + var key = result.Key; + GameObject bodyObj = key.Item1; + int[] penetratingIndices = result.Value; + + if (bodyObj != null && penetratingIndices != null && penetratingIndices.Length > 0) + { + SkinnedMeshRenderer bodyRenderer = bodyObj.GetComponent(); + if (bodyRenderer != null) + { + Runtime.PenetrationFixEngine.FixPenetrationAdvanced( + bodyRenderer, + penetratingIndices, + pushOutDistance, + influenceRadiusSteps, + smoothingIterations, + smoothingFactor + ); + + totalFixed += penetratingIndices.Length; + Debug.Log($"[MeshSyncProManager] {bodyObj.name} の {penetratingIndices.Length} 個の頂点を修正しました"); + } + } + } + + ClearResults(); + Debug.Log($"[MeshSyncProManager] 修正完了: 合計 {totalFixed} 個の頂点を修正しました"); + } + + #endregion + + #region 結果取得メソッド(UIから呼び出される) + + public Dictionary<(GameObject, GameObject), int[]> GetPenetratingIndicesResults() + { + return new Dictionary<(GameObject, GameObject), int[]>(penetratingIndicesResults); + } + + public Dictionary<(GameObject, GameObject), Vector3[]> GetPenetratingVerticesResults() + { + return new Dictionary<(GameObject, GameObject), Vector3[]>(penetratingVerticesResults); + } + + #endregion + + #region ヘルパーメソッド + + private bool ValidateInputs() + { + if (bodyObjects == null || bodyObjects.Count == 0) + { + Debug.LogError("[MeshSyncProManager] ボディオブジェクトが設定されていません"); + return false; + } + + if (clothingObjects == null || clothingObjects.Count == 0) + { + Debug.LogError("[MeshSyncProManager] 衣装オブジェクトが設定されていません"); + return false; + } + + return true; + } + + private Mesh GetMeshFromGameObject(GameObject obj) + { + SkinnedMeshRenderer smr = obj.GetComponent(); + if (smr != null && smr.sharedMesh != null) + { + return smr.sharedMesh; + } + + MeshFilter mf = obj.GetComponent(); + if (mf != null && mf.sharedMesh != null) + { + return mf.sharedMesh; + } + + return null; + } + + private Runtime.PenetrationDetectionCore.DetectionResult FilterByZones(Runtime.PenetrationDetectionCore.DetectionResult originalResult) + { + var activeZones = DetectionZones?.Where(z => z != null && z.active).ToArray(); + if (activeZones == null || activeZones.Length == 0) + { + return originalResult; + } + + var filteredIndices = new List(); + var filteredVertices = new List(); + + for (int i = 0; i < originalResult.PenetratingIndices.Length; i++) + { + Vector3 vertex = originalResult.PenetratingVertices[i]; + bool isInAnyZone = activeZones.Any(zone => zone.ContainsPoint(vertex)); + + if (isInAnyZone) + { + filteredIndices.Add(originalResult.PenetratingIndices[i]); + filteredVertices.Add(originalResult.PenetratingVertices[i]); + } + } + + return new Runtime.PenetrationDetectionCore.DetectionResult + { + IsSuccessful = filteredIndices.Count > 0, + PenetratingIndices = filteredIndices.ToArray(), + PenetratingVertices = filteredVertices.ToArray(), + RawPenetratingVertices = originalResult.RawPenetratingVertices, + ErrorMessage = originalResult.ErrorMessage, + DebugInfo = originalResult.DebugInfo + }; + } + + private void ClearResults() + { + penetratingIndicesResults.Clear(); + penetratingVerticesResults.Clear(); + } + + #endregion + + #region デバッグ + + private void OnDrawGizmos() + { + if (DetectionZones != null) + { + foreach (var zone in DetectionZones) + { + if (zone != null && zone.active) + { + Gizmos.color = zone.color; + Gizmos.DrawWireCube(zone.center, zone.size); + } + } + } + } + + #endregion + } +} diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs.meta b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs.meta new file mode 100644 index 000000000..065572d70 --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/MeshSyncProManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1386b3748e6dae0418f782afeaac23d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs new file mode 100644 index 000000000..5068db9a1 --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs @@ -0,0 +1,406 @@ +using UnityEngine; +using System.Collections.Generic; +using System.Linq; + +namespace StudioKafka.MeshSyncPro.Runtime +{ + public static class PenetrationDetectionCore + { + private const float VectorMagnitudeThreshold = 0.0001f; + private const float SpatialHashCellSizeMultiplier = 2.0f; + private const float LooseDotProductThreshold = -0.5f; + private const float CloseProximityFactor = 0.5f; + + public struct DetectionResult + { + public bool IsSuccessful; + public int[] PenetratingIndices; + public Vector3[] PenetratingVertices; + public Vector3[] RawPenetratingVertices; + public string ErrorMessage; + public ScaleDebugInfo DebugInfo; + } + + public struct ScaleDebugInfo + { + public Vector3 BodyScale; + public Vector3 ClothingScale; + public string ScaleAnalysisLog; + } + + private struct VertexData + { + public Vector3[] WorldVertices; + public Vector3[] WorldNormals; + public Vector3[] RawWorldVertices; + } + + public static DetectionResult DetectPenetration( + SkinnedMeshRenderer bodyRenderer, + Mesh clothingMesh, + Transform clothingTransform, + float penetrationThreshold, + float dotThreshold) + { + Debug.Log($"[PenetrationDetectionCore] 検出開始 - Body: {bodyRenderer?.name}, Clothing: {clothingMesh?.name}"); + Debug.Log($"[PenetrationDetectionCore] 閾値 - 貫通: {penetrationThreshold:F5}, ドット積: {dotThreshold:F3}"); + + if (!ValidateInput(bodyRenderer, clothingMesh, clothingTransform)) + { + return ErrorResult("入力パラメータが無効です。"); + } + + try + { + var scaleDebugInfo = AnalyzeScaleHierarchy(bodyRenderer, clothingTransform); + LogScaleAnalysis(scaleDebugInfo); + + var bodyVertexData = GetBodyVertexData(bodyRenderer); + if (bodyVertexData.WorldVertices == null || bodyVertexData.WorldVertices.Length == 0) + { + return ErrorResult("Bodyメッシュの頂点データ取得に失敗、または頂点数が0です。"); + } + + var clothingVertexData = GetClothingVertexData(clothingMesh, clothingTransform); + if (clothingVertexData.WorldVertices == null || clothingVertexData.WorldVertices.Length == 0) + { + return ErrorResult("Clothingメッシュの頂点データ取得に失敗、または頂点数が0です。"); + } + + Debug.Log($"[PenetrationDetectionCore] 実処理頂点数 - Body: {bodyVertexData.WorldVertices.Length}, Clothing: {clothingVertexData.WorldVertices.Length}"); + + var (penetratingIndices, penetratingVertices, rawVertices) = PerformPenetrationDetectionLogic( + bodyVertexData, clothingVertexData, penetrationThreshold, dotThreshold); + + Debug.Log($"[PenetrationDetectionCore] 検出完了: {penetratingIndices.Count}個の貫通頂点"); + + return new DetectionResult + { + IsSuccessful = true, + PenetratingIndices = penetratingIndices.ToArray(), + PenetratingVertices = penetratingVertices.ToArray(), + RawPenetratingVertices = rawVertices.ToArray(), + DebugInfo = scaleDebugInfo + }; + } + catch (System.Exception ex) + { + Debug.LogException(ex); + return ErrorResult($"検出処理で予期せぬエラー: {ex.Message}"); + } + } + + private static ScaleDebugInfo AnalyzeScaleHierarchy(SkinnedMeshRenderer bodyRenderer, Transform clothingTransform) + { + return new ScaleDebugInfo + { + BodyScale = bodyRenderer.transform.lossyScale, + ClothingScale = clothingTransform.lossyScale, + ScaleAnalysisLog = "簡素化されたスケール処理を使用中" + }; + } + + private static void LogScaleAnalysis(ScaleDebugInfo debugInfo) + { + Debug.Log($"[PenetrationDetectionCore] {debugInfo.ScaleAnalysisLog}"); + } + + private static VertexData GetBodyVertexData(SkinnedMeshRenderer bodyRenderer) + { + Mesh bakedBodyMesh = null; + try + { + bakedBodyMesh = new Mesh(); + bodyRenderer.BakeMesh(bakedBodyMesh); + + if (bakedBodyMesh.vertexCount == 0) + { + Debug.LogError("[PenetrationDetectionCore] Body BakeMesh vertex count is 0."); + return new VertexData(); + } + + Vector3[] localVertices = bakedBodyMesh.vertices; + Vector3[] localNormals = bakedBodyMesh.normals; + Matrix4x4 localToWorld = bodyRenderer.transform.localToWorldMatrix; + + var (worldVerts, worldNorms) = TransformToWorldSpace(localVertices, localNormals, localToWorld); + + return new VertexData + { + WorldVertices = worldVerts, + WorldNormals = worldNorms, + RawWorldVertices = worldVerts + }; + } + finally + { + if (bakedBodyMesh != null) Object.DestroyImmediate(bakedBodyMesh); + } + } + + private static VertexData GetClothingVertexData(Mesh clothingMesh, Transform clothingTransform) + { + Vector3[] localVertices = null; + Vector3[] localNormals = null; + + if (!clothingMesh.isReadable) + { + Debug.LogWarning($"[PenetrationDetectionCore] Clothing mesh '{clothingMesh.name}' はRead/Write Enabledではありません。"); + Debug.LogWarning("[PenetrationDetectionCore] 解決策: インポート設定でRead/Write Enabledにチェックを入れてください。"); + Debug.LogWarning("[PenetrationDetectionCore] また、SkinnedMeshRendererコンポーネントがある場合はBakeMeshを試行します。"); + + var clothingSkinnedRenderer = clothingTransform.GetComponent(); + if (clothingSkinnedRenderer != null) + { + Debug.Log("[PenetrationDetectionCore] SkinnedMeshRendererが見つかりました。BakeMeshを使用します。"); + Mesh bakedClothingMesh = null; + try + { + bakedClothingMesh = new Mesh(); + clothingSkinnedRenderer.BakeMesh(bakedClothingMesh); + + if (bakedClothingMesh.vertexCount > 0) + { + localVertices = bakedClothingMesh.vertices; + localNormals = bakedClothingMesh.normals; + Debug.Log($"[PenetrationDetectionCore] BakeMeshで取得: {localVertices.Length}個の頂点"); + } + else + { + Debug.LogError("[PenetrationDetectionCore] BakeMeshの結果が空でした。"); + } + } + catch (System.Exception ex) + { + Debug.LogError($"[PenetrationDetectionCore] BakeMesh失敗: {ex.Message}"); + } + finally + { + if (bakedClothingMesh != null) Object.DestroyImmediate(bakedClothingMesh); + } + } + + if (localVertices == null) + { + Debug.LogError("[PenetrationDetectionCore] Clothingメッシュからデータを取得できませんでした。"); + return new VertexData(); + } + } + else + { + if (clothingMesh.vertexCount == 0) + { + Debug.LogError("[PenetrationDetectionCore] Clothing mesh vertex count is 0."); + return new VertexData(); + } + + localVertices = clothingMesh.vertices; + localNormals = clothingMesh.normals; + Debug.Log($"[PenetrationDetectionCore] 通常の方法で取得: {localVertices.Length}個の頂点"); + } + + Matrix4x4 localToWorld = clothingTransform.localToWorldMatrix; + var (worldVerts, worldNorms) = TransformToWorldSpace(localVertices, localNormals, localToWorld); + + return new VertexData + { + WorldVertices = worldVerts, + WorldNormals = worldNorms + }; + } + + private static (Vector3[] worldVertices, Vector3[] worldNormals) TransformToWorldSpace( + Vector3[] localVertices, Vector3[] localNormals, Matrix4x4 localToWorldMatrix) + { + int vertexCount = localVertices.Length; + var worldVertices = new Vector3[vertexCount]; + var worldNormals = new Vector3[vertexCount]; + bool hasValidNormals = localNormals != null && localNormals.Length == vertexCount; + + for (int i = 0; i < vertexCount; i++) + { + worldVertices[i] = localToWorldMatrix.MultiplyPoint3x4(localVertices[i]); + + if (hasValidNormals && localNormals[i].sqrMagnitude > VectorMagnitudeThreshold * VectorMagnitudeThreshold) + { + Vector3 worldNormal = localToWorldMatrix.MultiplyVector(localNormals[i]); + worldNormals[i] = worldNormal.sqrMagnitude > VectorMagnitudeThreshold * VectorMagnitudeThreshold + ? worldNormal.normalized : Vector3.zero; + } + else + { + worldNormals[i] = Vector3.zero; + } + } + + return (worldVertices, worldNormals); + } + + private static (List penetratingIndices, List penetratingVertices, List rawVertices) + PerformPenetrationDetectionLogic( + VertexData bodyData, VertexData clothingData, + float penetrationThreshold, float dotThreshold) + { + var penetratingIndices = new List(); + var penetratingVertices = new List(); + var rawVertices = new List(); + + float cellSize = Mathf.Max(penetrationThreshold * SpatialHashCellSizeMultiplier, VectorMagnitudeThreshold * 10f); + var clothingSpatialHash = BuildSpatialHash(clothingData.WorldVertices, cellSize); + + Debug.Log($"[PenetrationDetectionCore] 空間ハッシュセルサイズ: {cellSize:F6}, ハッシュエントリ数: {clothingSpatialHash.Count}"); + + int totalChecked = 0; + int totalNearbyFound = 0; + + for (int i = 0; i < bodyData.WorldVertices.Length; i++) + { + Vector3 currentBodyVertex = bodyData.WorldVertices[i]; + Vector3 currentBodyNormal = bodyData.WorldNormals[i]; + + if (currentBodyNormal.sqrMagnitude < VectorMagnitudeThreshold * VectorMagnitudeThreshold) continue; + + totalChecked++; + + var nearbyClothingIndices = GetNearbyVerticesFromSpatialHash(clothingSpatialHash, currentBodyVertex, cellSize); + if (nearbyClothingIndices.Count > 0) + { + totalNearbyFound++; + } + + bool isPenetratingThisVertex = false; + foreach (int clothingIndex in nearbyClothingIndices) + { + Vector3 clothingVertex = clothingData.WorldVertices[clothingIndex]; + Vector3 vectorToClothing = clothingVertex - currentBodyVertex; + float distanceSqr = vectorToClothing.sqrMagnitude; + + if (distanceSqr < penetrationThreshold * penetrationThreshold) + { + float distance = Mathf.Sqrt(distanceSqr); + Vector3 penetrationDirection = distance > VectorMagnitudeThreshold ? + vectorToClothing.normalized : currentBodyNormal; + + float dotProduct = Vector3.Dot(currentBodyNormal, penetrationDirection); + + if (dotProduct > dotThreshold || + (dotProduct > LooseDotProductThreshold && distance < penetrationThreshold * CloseProximityFactor)) + { + isPenetratingThisVertex = true; + break; + } + } + } + + if (isPenetratingThisVertex) + { + penetratingIndices.Add(i); + penetratingVertices.Add(currentBodyVertex); + if (bodyData.RawWorldVertices != null && i < bodyData.RawWorldVertices.Length) + { + rawVertices.Add(bodyData.RawWorldVertices[i]); + } + else + { + rawVertices.Add(currentBodyVertex); + } + } + } + + Debug.Log($"[PenetrationDetectionCore] 詳細統計 - チェック対象頂点: {totalChecked}, 近傍頂点が見つかった数: {totalNearbyFound}"); + + return (penetratingIndices, penetratingVertices, rawVertices); + } + + private static Dictionary> BuildSpatialHash(Vector3[] vertices, float cellSize) + { + var map = new Dictionary>(); + + for (int i = 0; i < vertices.Length; i++) + { + var cell = new Vector3Int( + Mathf.FloorToInt(vertices[i].x / cellSize), + Mathf.FloorToInt(vertices[i].y / cellSize), + Mathf.FloorToInt(vertices[i].z / cellSize) + ); + + if (!map.TryGetValue(cell, out var list)) + { + list = new List(); + map[cell] = list; + } + + list.Add(i); + } + + return map; + } + + private static List GetNearbyVerticesFromSpatialHash( + Dictionary> spatialHash, Vector3 position, float cellSize) + { + var nearby = new List(); + var centerCell = new Vector3Int( + Mathf.FloorToInt(position.x / cellSize), + Mathf.FloorToInt(position.y / cellSize), + Mathf.FloorToInt(position.z / cellSize) + ); + + for (int xOff = -1; xOff <= 1; xOff++) + { + for (int yOff = -1; yOff <= 1; yOff++) + { + for (int zOff = -1; zOff <= 1; zOff++) + { + if (spatialHash.TryGetValue(centerCell + new Vector3Int(xOff, yOff, zOff), out var list)) + { + nearby.AddRange(list); + } + } + } + } + + return nearby; + } + + private static bool ValidateInput(SkinnedMeshRenderer body, Mesh clothing, Transform clothingT) + { + if (body == null || body.sharedMesh == null || !body.sharedMesh.isReadable) + { + Debug.LogError("[PenetrationDetectionCore] Body SMR or its mesh is invalid/unreadable."); + return false; + } + + if (clothing == null) + { + Debug.LogError("[PenetrationDetectionCore] Clothing mesh is null."); + return false; + } + + if (clothingT == null) + { + Debug.LogError("[PenetrationDetectionCore] Clothing transform is null."); + return false; + } + + if (!clothing.isReadable) + { + Debug.LogWarning($"[PenetrationDetectionCore] Clothing mesh '{clothing.name}' のRead/Write Enabledが無効です。代替手段を試行します。"); + } + + return true; + } + + private static DetectionResult ErrorResult(string message) + { + return new DetectionResult + { + IsSuccessful = false, + ErrorMessage = message, + PenetratingIndices = System.Array.Empty(), + PenetratingVertices = System.Array.Empty(), + RawPenetratingVertices = System.Array.Empty() + }; + } + } +} diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs.meta b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs.meta new file mode 100644 index 000000000..fb9e1b85d --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationDetectionCore.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ce010e4de1c8e24aa5825a79dd41d54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs new file mode 100644 index 000000000..9c7f928b0 --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs @@ -0,0 +1,347 @@ +using UnityEngine; +using System.Collections.Generic; +using System.Linq; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace StudioKafka.MeshSyncPro.Runtime +{ + public static class PenetrationFixEngine + { + private static Dictionary _originalMeshCache = new Dictionary(); + private static Dictionary _rendererCache = new Dictionary(); + + #region レガシーAPI(Undo対応版) + + public static void FixPenetration( + SkinnedMeshRenderer targetRenderer, + int[] penetratingIndices, + float pushDistance, + int smoothingIterations) + { + if (targetRenderer == null || targetRenderer.sharedMesh == null || penetratingIndices == null || penetratingIndices.Length == 0) + { + Debug.LogWarning("[PenetrationFixEngine] Basic fix に対する入力が無効です。"); + return; + } + + Debug.Log($"[PenetrationFixEngine] Basic Fix 開始: {targetRenderer.name}, 対象頂点数: {penetratingIndices.Length}"); + + var meshInstanceId = RegisterMeshForUndo(targetRenderer, "Fix Mesh Penetration (Basic)"); + + Mesh mesh = targetRenderer.sharedMesh; + Vector3[] vertices = mesh.vertices; + Vector3[] normals = mesh.normals; + + if (normals.Length != vertices.Length) + { + mesh.RecalculateNormals(); + normals = mesh.normals; + } + + foreach (int index in penetratingIndices) + { + if (index >= 0 && index < vertices.Length && index < normals.Length) + { + Vector3 normal = normals[index]; + if (normal.sqrMagnitude > 0.000001f) + { + vertices[index] -= normal.normalized * pushDistance; + } + } + } + + if (smoothingIterations > 0) + { + ApplySmoothing(ref vertices, mesh.triangles, penetratingIndices, smoothingIterations); + } + + ApplyMeshChanges(mesh, vertices); + Debug.Log($"[PenetrationFixEngine] Basic Fix 完了: {penetratingIndices.Length} 個の頂点を修正しました。"); + } + + public static void FixPenetrationAdvanced( + SkinnedMeshRenderer targetRenderer, + int[] penetratingIndices, + float pushDistance, + float influenceRadiusSteps, + int smoothingIterations, + float smoothingFactor) + { + if (targetRenderer == null || targetRenderer.sharedMesh == null || penetratingIndices == null || penetratingIndices.Length == 0) + { + Debug.LogWarning("[PenetrationFixEngine] Advanced fix に対する入力が無効です。"); + return; + } + + Debug.Log($"[PenetrationFixEngine] Advanced Fix 開始: {targetRenderer.name}, 対象頂点数: {penetratingIndices.Length}"); + + var meshInstanceId = RegisterMeshForUndo(targetRenderer, "Fix Mesh Penetration (Advanced)"); + + Mesh mesh = targetRenderer.sharedMesh; + Vector3[] vertices = mesh.vertices; + Vector3[] normals = mesh.normals; + + if (normals.Length != vertices.Length) + { + mesh.RecalculateNormals(); + normals = mesh.normals; + } + + var adjacencyMap = BuildVertexAdjacencyMap(mesh.triangles, vertices.Length); + var influencedVertices = new Dictionary(); + + foreach (int penetratingIndex in penetratingIndices) + { + Queue<(int index, int step)> queue = new Queue<(int, int)>(); + queue.Enqueue((penetratingIndex, 0)); + HashSet visited = new HashSet { penetratingIndex }; + influencedVertices[penetratingIndex] = 1.0f; + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (current.step >= (int)influenceRadiusSteps) continue; + + if (adjacencyMap.TryGetValue(current.index, out var neighbors)) + { + foreach (int neighborIndex in neighbors) + { + if (visited.Add(neighborIndex)) + { + float influence = (influenceRadiusSteps <= 0) ? 1.0f : + (1.0f - (float)(current.step + 1) / (influenceRadiusSteps + 1.0f)); + + if (!influencedVertices.ContainsKey(neighborIndex) || influence > influencedVertices[neighborIndex]) + { + influencedVertices[neighborIndex] = influence; + } + + queue.Enqueue((neighborIndex, current.step + 1)); + } + } + } + } + } + + foreach (var pair in influencedVertices) + { + int index = pair.Key; + float influence = pair.Value; + + if (index >= 0 && index < vertices.Length && index < normals.Length) + { + Vector3 normal = normals[index]; + if (normal.sqrMagnitude > 0.000001f) + { + vertices[index] -= normal.normalized * (pushDistance * influence); + } + } + } + + if (smoothingIterations > 0) + { + ApplyAdvancedSmoothing(ref vertices, adjacencyMap, influencedVertices.Keys.ToArray(), smoothingIterations, smoothingFactor); + } + + ApplyMeshChanges(mesh, vertices); + Debug.Log($"[PenetrationFixEngine] Advanced Fix 完了。影響を受けた頂点数: {influencedVertices.Count}"); + } + + #endregion + + #region Undo対応メッシュ管理システム + + private static int RegisterMeshForUndo(SkinnedMeshRenderer renderer, string undoName) + { + var originalMesh = renderer.sharedMesh; + var meshInstanceId = originalMesh.GetInstanceID(); + + if (_originalMeshCache.ContainsKey(meshInstanceId)) + { + return meshInstanceId; + } + +#if UNITY_EDITOR + var meshBackup = Object.Instantiate(originalMesh); + meshBackup.name = originalMesh.name + "_UndoBackup"; + + _originalMeshCache[meshInstanceId] = meshBackup; + _rendererCache[meshInstanceId] = renderer; + + var workingMesh = Object.Instantiate(originalMesh); + workingMesh.name = originalMesh.name + "_Working"; + + Undo.RecordObject(renderer, undoName); + renderer.sharedMesh = workingMesh; + + Undo.undoRedoPerformed -= OnUndoRedoPerformed; + Undo.undoRedoPerformed += OnUndoRedoPerformed; +#endif + + return meshInstanceId; + } + + private static void OnUndoRedoPerformed() + { +#if UNITY_EDITOR + var keysToRemove = new List(); + + foreach (var kvp in _rendererCache) + { + var meshInstanceId = kvp.Key; + var renderer = kvp.Value; + + if (renderer == null) + { + keysToRemove.Add(meshInstanceId); + continue; + } + + if (_originalMeshCache.TryGetValue(meshInstanceId, out var originalMesh)) + { + if (renderer.sharedMesh != null && renderer.sharedMesh.name.Contains("_Working")) + { + Object.DestroyImmediate(renderer.sharedMesh); + } + + renderer.sharedMesh = originalMesh; + keysToRemove.Add(meshInstanceId); + Debug.Log($"[PenetrationFixEngine] Undo実行: {renderer.name} のメッシュを元の状態に復元しました"); + } + } + + foreach (var key in keysToRemove) + { + if (_originalMeshCache.ContainsKey(key)) + { + _originalMeshCache.Remove(key); + } + _rendererCache.Remove(key); + } + + SceneView.RepaintAll(); +#endif + } + + private static void ApplyMeshChanges(Mesh mesh, Vector3[] vertices) + { + mesh.vertices = vertices; + mesh.RecalculateNormals(); + mesh.RecalculateBounds(); + +#if UNITY_EDITOR + EditorUtility.SetDirty(mesh); +#endif + } + + #endregion + + #region ヘルパーメソッド + + private static void ApplySmoothing(ref Vector3[] vertices, int[] triangles, int[] targetIndices, int iterations) + { + var adjacencyMap = BuildVertexAdjacencyMap(triangles, vertices.Length); + var targetVertexSet = new HashSet(targetIndices); + + for (int iter = 0; iter < iterations; iter++) + { + Vector3[] tempVertices = (Vector3[])vertices.Clone(); + + foreach (int index in targetVertexSet) + { + if (adjacencyMap.TryGetValue(index, out var neighbors) && neighbors.Count > 0) + { + Vector3 averagePos = vertices[index]; + foreach (int neighborIdx in neighbors) averagePos += vertices[neighborIdx]; + tempVertices[index] = averagePos / (neighbors.Count + 1); + } + } + + vertices = tempVertices; + } + } + + private static void ApplyAdvancedSmoothing(ref Vector3[] vertices, Dictionary> adjacencyMap, + int[] targetIndices, int iterations, float smoothingFactor) + { + var smoothingTargetIndices = new HashSet(targetIndices); + + for (int iter = 0; iter < iterations; iter++) + { + Vector3[] tempVertices = (Vector3[])vertices.Clone(); + + foreach (int index in smoothingTargetIndices) + { + if (adjacencyMap.TryGetValue(index, out var neighbors) && neighbors.Count > 0) + { + Vector3 averagePos = Vector3.zero; + foreach (int neighborIdx in neighbors) averagePos += vertices[neighborIdx]; + averagePos /= neighbors.Count; + + tempVertices[index] = Vector3.Lerp(vertices[index], averagePos, smoothingFactor); + } + } + + vertices = tempVertices; + } + } + + private static Dictionary> BuildVertexAdjacencyMap(int[] triangles, int vertexCount) + { + var map = new Dictionary>(); + + for (int i = 0; i < vertexCount; i++) + map[i] = new List(); + + for (int i = 0; i < triangles.Length; i += 3) + { + int v0 = triangles[i], v1 = triangles[i + 1], v2 = triangles[i + 2]; + + if (v0 < vertexCount && v1 < vertexCount && v2 < vertexCount) + { + if (!map[v0].Contains(v1)) map[v0].Add(v1); + if (!map[v0].Contains(v2)) map[v0].Add(v2); + if (!map[v1].Contains(v0)) map[v1].Add(v0); + if (!map[v1].Contains(v2)) map[v1].Add(v2); + if (!map[v2].Contains(v0)) map[v2].Add(v0); + if (!map[v2].Contains(v1)) map[v2].Add(v1); + } + } + + return map; + } + + public static void ClearCache() + { +#if UNITY_EDITOR + foreach (var kvp in _rendererCache) + { + var renderer = kvp.Value; + if (renderer != null && renderer.sharedMesh != null && renderer.sharedMesh.name.Contains("_Working")) + { + Object.DestroyImmediate(renderer.sharedMesh); + } + } + + foreach (var backupMesh in _originalMeshCache.Values) + { + if (backupMesh != null) + { + Object.DestroyImmediate(backupMesh); + } + } + + _originalMeshCache.Clear(); + _rendererCache.Clear(); + + Undo.undoRedoPerformed -= OnUndoRedoPerformed; +#endif + Debug.Log("[PenetrationFixEngine] キャッシュとUndoシステムをクリアしました"); + } + + #endregion + } +} diff --git a/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs.meta b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs.meta new file mode 100644 index 000000000..6841069f8 --- /dev/null +++ b/Assets/External/_StudioKafka/MeshSyncPro/Runtime/PenetrationFixEngine.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c579fa45e59c3e746a8c2fc6079cb557 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: