Meshsync 스크립트 추가

This commit is contained in:
Yamo4490 2025-06-13 02:46:22 +09:00
parent f9a33e5972
commit f0824f152b
12 changed files with 1692 additions and 0 deletions

8
Assets/External/_StudioKafka.meta vendored Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5ce652977f23eee4fa89058799522039
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a8f1ac812e3f7804aae29071532c5b3b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 289042708b12df64f989e3ea0c83a041
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<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("저장 실패", "저장할 수정된 메시가 없습니다.", "확인");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a9ecc9e1fac43634780d004760cd4f08
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f3e043bb9c7ef3446ab04fdc40ddb53b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<GameObject> bodyObjects = new List<GameObject>();
public List<GameObject> clothingObjects = new List<GameObject>();
[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<SkinnedMeshRenderer>();
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<SkinnedMeshRenderer>();
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<SkinnedMeshRenderer>();
if (smr != null && smr.sharedMesh != null)
{
return smr.sharedMesh;
}
MeshFilter mf = obj.GetComponent<MeshFilter>();
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<int>();
var filteredVertices = new List<Vector3>();
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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1386b3748e6dae0418f782afeaac23d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<SkinnedMeshRenderer>();
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<int> penetratingIndices, List<Vector3> penetratingVertices, List<Vector3> rawVertices)
PerformPenetrationDetectionLogic(
VertexData bodyData, VertexData clothingData,
float penetrationThreshold, float dotThreshold)
{
var penetratingIndices = new List<int>();
var penetratingVertices = new List<Vector3>();
var rawVertices = new List<Vector3>();
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<Vector3Int, List<int>> BuildSpatialHash(Vector3[] vertices, float cellSize)
{
var map = new Dictionary<Vector3Int, List<int>>();
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<int>();
map[cell] = list;
}
list.Add(i);
}
return map;
}
private static List<int> GetNearbyVerticesFromSpatialHash(
Dictionary<Vector3Int, List<int>> spatialHash, Vector3 position, float cellSize)
{
var nearby = new List<int>();
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<int>(),
PenetratingVertices = System.Array.Empty<Vector3>(),
RawPenetratingVertices = System.Array.Empty<Vector3>()
};
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ce010e4de1c8e24aa5825a79dd41d54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<int, Mesh> _originalMeshCache = new Dictionary<int, Mesh>();
private static Dictionary<int, SkinnedMeshRenderer> _rendererCache = new Dictionary<int, SkinnedMeshRenderer>();
#region APIUndo対応版
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<int, float>();
foreach (int penetratingIndex in penetratingIndices)
{
Queue<(int index, int step)> queue = new Queue<(int, int)>();
queue.Enqueue((penetratingIndex, 0));
HashSet<int> visited = new HashSet<int> { 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<int>();
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<int>(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<int, List<int>> adjacencyMap,
int[] targetIndices, int iterations, float smoothingFactor)
{
var smoothingTargetIndices = new HashSet<int>(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<int, List<int>> BuildVertexAdjacencyMap(int[] triangles, int vertexCount)
{
var map = new Dictionary<int, List<int>>();
for (int i = 0; i < vertexCount; i++)
map[i] = new List<int>();
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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c579fa45e59c3e746a8c2fc6079cb557
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: