626 lines
23 KiB
C#

using UnityEngine;
using UnityEditor;
using VRM;
using System.Linq;
using System.Collections.Generic;
public class VRMUtilityWindow : EditorWindow
{
private GameObject originalPrefab;
private GameObject destinationPrefab;
private GameObject springBoneObject;
private GameObject avatarRoot;
private Vector2 scrollPosition;
private int selectedTab = 0;
private readonly string[] tabNames = { "스프링본 이동", "콜라이더 이동", "콜라이더 설정", "잘못된 본 제거" };
[MenuItem("VRM0/VRM 유틸리티")]
static void ShowWindow()
{
GetWindow<VRMUtilityWindow>("VRM 유틸리티").Show();
}
void OnGUI()
{
// 탭 스타일 정의
var tabStyle = new GUIStyle(EditorStyles.toolbarButton);
tabStyle.fixedHeight = 30;
tabStyle.fontSize = 12;
tabStyle.fontStyle = FontStyle.Bold;
tabStyle.alignment = TextAnchor.MiddleCenter;
EditorGUILayout.Space(10);
// 윈도우 너비에 맞춰 탭 버튼 크기 조정
float windowWidth = position.width;
float minTabWidth = 100; // 최소 탭 너비
float padding = 20; // 좌우 여백
float availableWidth = windowWidth - (padding * 2); // 사용 가능한 전체 너비
// 탭 버튼들을 수평으로 배치
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
// 사용 가능한 너비가 최소 필요 너비보다 작으면 버튼을 세로로 배치
if (availableWidth < (minTabWidth * tabNames.Length))
{
EditorGUILayout.EndHorizontal();
for (int i = 0; i < tabNames.Length; i++)
{
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Toggle(selectedTab == i, tabNames[i], tabStyle, GUILayout.Width(availableWidth)))
{
selectedTab = i;
}
GUILayout.FlexibleSpace();
}
}
}
else
{
// 충분한 너비가 있으면 가로로 배치
float tabWidth = availableWidth / tabNames.Length;
for (int i = 0; i < tabNames.Length; i++)
{
if (GUILayout.Toggle(selectedTab == i, tabNames[i], tabStyle, GUILayout.Width(tabWidth)))
{
selectedTab = i;
}
}
GUILayout.FlexibleSpace();
}
}
EditorGUILayout.Space(10);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 공통 필드들을 탭에 따라 표시
switch (selectedTab)
{
case 0: DrawSpringBoneTransferTab(); break;
case 1: DrawColliderMoveTab(); break;
case 2: DrawColliderSetupTab(); break;
case 3: DrawInvalidBonesTab(); break;
}
EditorGUILayout.EndScrollView();
}
void DrawSpringBoneTransferTab()
{
DrawHeader("스프링본 이동");
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
DrawSourceDestinationFields();
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("스프링본 이동", GUILayout.Height(30), GUILayout.Width(200)))
{
if (ValidateSourceDestination())
{
TransferVRMSpringBone();
}
}
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(5);
DrawHelpBox("원본 아바타의 스프링본을 대상 아바타로 이동합니다.\n기존 스프링본은 선택적으로 제거할 수 있습니다.");
EditorGUILayout.EndVertical();
}
void DrawColliderMoveTab()
{
DrawHeader("콜라이더 이동");
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
DrawSourceDestinationFields();
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("콜라이더 이동", GUILayout.Height(30), GUILayout.Width(200)))
{
if (ValidateSourceDestination())
{
MoveColliders();
}
}
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(5);
DrawHelpBox("원본 아바타의 콜라이더를 대상 아바타의 동일한 본으로 이동합니다.");
EditorGUILayout.EndVertical();
}
void DrawColliderSetupTab()
{
DrawHeader("콜라이더 설정");
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space(5);
springBoneObject = (GameObject)EditorGUILayout.ObjectField(
"스프링본 오브젝트", springBoneObject, typeof(GameObject), true);
EditorGUILayout.Space(2);
avatarRoot = (GameObject)EditorGUILayout.ObjectField(
"아바타 루트", avatarRoot, typeof(GameObject), true);
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("콜라이더 설정", GUILayout.Height(30), GUILayout.Width(200)))
{
if (ValidateColliderSetup())
{
SetupColliders();
}
}
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(5);
DrawHelpBox("선택한 오브젝트의 모든 스프링본에 콜라이더를 자동으로 설정합니다.");
EditorGUILayout.EndVertical();
}
void DrawInvalidBonesTab()
{
DrawHeader("잘못된 본 제거");
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space(5);
destinationPrefab = (GameObject)EditorGUILayout.ObjectField(
"대상 아바타", destinationPrefab, typeof(GameObject), true);
EditorGUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("잘못된 본 제거", GUILayout.Height(30), GUILayout.Width(200)))
{
if (ValidateDestination())
{
RemoveInvalidBones();
}
}
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(5);
DrawHelpBox("참조가 깨진 스프링본을 찾아서 제거합니다.");
EditorGUILayout.EndVertical();
}
void DrawSourceDestinationFields()
{
EditorGUILayout.Space(5);
originalPrefab = (GameObject)EditorGUILayout.ObjectField(
"원본 아바타", originalPrefab, typeof(GameObject), true);
EditorGUILayout.Space(2);
destinationPrefab = (GameObject)EditorGUILayout.ObjectField(
"대상 아바타", destinationPrefab, typeof(GameObject), true);
}
void DrawHeader(string title)
{
EditorGUILayout.Space(5);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField(title, EditorStyles.boldLabel, GUILayout.Width(200));
GUILayout.FlexibleSpace();
}
EditorGUILayout.Space(5);
}
void DrawHelpBox(string message)
{
EditorGUILayout.HelpBox(message, MessageType.Info);
EditorGUILayout.Space(5);
}
// 검증 메서드들
bool ValidateSourceDestination()
{
if (originalPrefab == null || destinationPrefab == null)
{
EditorUtility.DisplayDialog("오류", "원본과 대상 아바타를 모두 지정해주세요.", "확인");
return false;
}
return true;
}
bool ValidateColliderSetup()
{
if (springBoneObject == null || avatarRoot == null)
{
EditorUtility.DisplayDialog("오류", "스프링본 오브젝트와 아바타 루트를 모두 지정해주세요.", "확인");
return false;
}
return true;
}
bool ValidateDestination()
{
if (destinationPrefab == null)
{
EditorUtility.DisplayDialog("오류", "대상 아바타를 지정해주세요.", "확인");
return false;
}
return true;
}
// 콜라이더 설정
void SetupColliders()
{
if (springBoneObject == null || avatarRoot == null)
{
EditorUtility.DisplayDialog("오류", "스프링본 오브젝트와 아바타 루트를 모두 지정해주세요.", "확인");
return;
}
// 스프링본 컴포넌트들 가져오기
VRMSpringBone[] springBones = springBoneObject.GetComponents<VRMSpringBone>();
if (springBones.Length == 0)
{
EditorUtility.DisplayDialog("오류", "선택한 오브젝트에 VRMSpringBone 컴포넌트가 없습니다.", "확인");
return;
}
// 아바타 루트 아래의 모든 콜라이더 그룹 찾기
VRMSpringBoneColliderGroup[] colliderGroups = avatarRoot.GetComponentsInChildren<VRMSpringBoneColliderGroup>(true);
// 각 스프링본 컴포넌트에 콜라이더 그룹 설정
Undo.RecordObjects(springBones, "Setup Spring Bone Colliders");
foreach (var springBone in springBones)
{
springBone.ColliderGroups = colliderGroups;
}
EditorUtility.SetDirty(springBoneObject);
Debug.Log($"콜라이더 설정 완료: {colliderGroups.Length}개의 콜라이더 그룹이 {springBones.Length}개의 스프링본에 설정되었습니다.");
}
// 콜라이더 이동
void MoveColliders()
{
VRMSpringBoneColliderGroup[] originalColliderGroups = originalPrefab.GetComponentsInChildren<VRMSpringBoneColliderGroup>();
int successCount = 0;
int failCount = 0;
int createdCount = 0;
foreach (VRMSpringBoneColliderGroup originalColliderGroup in originalColliderGroups)
{
try
{
Transform correspondingTransform = FindCorrespondingTransform(originalColliderGroup.transform, destinationPrefab.transform);
if (correspondingTransform == null)
{
// 원본 본의 부모 경로를 찾음
Transform originalParent = originalColliderGroup.transform.parent;
Transform targetParent = destinationPrefab.transform;
// 부모가 있는 경우, 대상 프리팹에서 대응하는 부모를 찾음
if (originalParent != originalPrefab.transform)
{
Transform correspondingParent = FindCorrespondingTransform(originalParent, destinationPrefab.transform);
if (correspondingParent != null)
{
targetParent = correspondingParent;
}
else
{
Debug.LogWarning($"부모 본을 찾을 수 없습니다: {originalParent.name}. 루트에 생성합니다.");
}
}
// 새로운 본 생성
GameObject newBone = new GameObject(originalColliderGroup.transform.name);
newBone.transform.SetParent(targetParent, false); // false로 설정하여 로컬 트랜스폼 유지
newBone.transform.localPosition = originalColliderGroup.transform.localPosition;
newBone.transform.localRotation = originalColliderGroup.transform.localRotation;
newBone.transform.localScale = originalColliderGroup.transform.localScale;
correspondingTransform = newBone.transform;
createdCount++;
Debug.Log($"새로운 본 생성됨: {newBone.name} (부모: {targetParent.name})");
}
// 기존 콜라이더 그룹이 있다면 제거
var existingCollider = correspondingTransform.GetComponent<VRMSpringBoneColliderGroup>();
if (existingCollider != null)
{
DestroyImmediate(existingCollider);
}
// 새로운 콜라이더 그룹 추가
var newColliderGroup = correspondingTransform.gameObject.AddComponent<VRMSpringBoneColliderGroup>();
// 원본과 대상 본의 월드 회전 차이 계산
Quaternion rotationDiff = Quaternion.Inverse(originalColliderGroup.transform.rotation) * correspondingTransform.rotation;
// 콜라이더 설정 복사 및 회전 보정
newColliderGroup.Colliders = new VRMSpringBoneColliderGroup.SphereCollider[originalColliderGroup.Colliders.Length];
for (int i = 0; i < originalColliderGroup.Colliders.Length; i++)
{
var originalCollider = originalColliderGroup.Colliders[i];
// 원본 오프셋을 월드 공간으로 변환
Vector3 worldOffset = originalColliderGroup.transform.TransformDirection(originalCollider.Offset);
// 대상 본의 로컬 공간으로 변환
Vector3 newOffset = correspondingTransform.InverseTransformDirection(worldOffset);
newColliderGroup.Colliders[i] = new VRMSpringBoneColliderGroup.SphereCollider
{
Offset = newOffset,
Radius = originalCollider.Radius
};
}
successCount++;
Debug.Log($"콜라이더 그룹 이동 완료: {correspondingTransform.name}");
}
catch (System.Exception e)
{
failCount++;
Debug.LogError($"콜라이더 이동 중 오류 발생: {e.Message}");
}
}
if (successCount > 0 || createdCount > 0)
{
EditorUtility.SetDirty(destinationPrefab);
AssetDatabase.SaveAssets();
}
EditorUtility.DisplayDialog("작업 완료",
$"성공: {successCount}개\n실패: {failCount}개\n새로 생성된 본: {createdCount}개", "확인");
}
private Transform FindTransformByName(string name, Transform searchRoot)
{
// 대상 프리팹에서 같은 이름을 가진 모든 Transform을 찾음
var allTransforms = searchRoot.GetComponentsInChildren<Transform>(true);
return allTransforms.FirstOrDefault(t => t.name == name);
}
private void CopyColliderGroupSettings(VRMSpringBoneColliderGroup source, VRMSpringBoneColliderGroup destination)
{
destination.Colliders = new VRMSpringBoneColliderGroup.SphereCollider[source.Colliders.Length];
for (int i = 0; i < source.Colliders.Length; i++)
{
destination.Colliders[i] = new VRMSpringBoneColliderGroup.SphereCollider
{
Offset = source.Colliders[i].Offset,
Radius = source.Colliders[i].Radius
};
}
}
// 스프링본 이동
void TransferVRMSpringBone()
{
if (!EditorUtility.DisplayDialog("확인", "대상 프리팹의 모든 VRMSpringBone이 삭제됩니다. 계속하시겠습니까?", "예", "아니오"))
{
return;
}
// Secondary 오브젝트 찾기 또는 생성
Transform destSecondary = destinationPrefab.transform.Find("Secondary");
if (destSecondary == null)
{
GameObject secondaryObj = new GameObject("Secondary");
secondaryObj.transform.SetParent(destinationPrefab.transform, false);
destSecondary = secondaryObj.transform;
Debug.Log("Secondary 오브젝트가 생성되었습니다.");
}
// 기존 VRMSpringBone 컴포넌트 제거
var existingBones = destinationPrefab.GetComponentsInChildren<VRMSpringBone>(true);
foreach (var bone in existingBones)
{
DestroyImmediate(bone);
}
// 소스의 VRMSpringBone 컴포넌트 복사
var springBones = originalPrefab.GetComponentsInChildren<VRMSpringBone>(true);
int successCount = 0;
int failCount = 0;
foreach (var springBone in springBones)
{
try
{
VRMSpringBone newSpringBone = destSecondary.gameObject.AddComponent<VRMSpringBone>();
if (CopyVRMSpringBoneComponents(springBone, newSpringBone))
{
successCount++;
}
else
{
DestroyImmediate(newSpringBone);
failCount++;
}
}
catch (System.Exception e)
{
Debug.LogError($"스프링본 복사 중 오류 발생: {e.Message}");
failCount++;
}
}
EditorUtility.SetDirty(destinationPrefab);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("작업 완료", $"성공: {successCount}개\n실패: {failCount}개", "확인");
}
// 잘못된 본 제거
void RemoveInvalidBones()
{
var springBones = destinationPrefab.GetComponentsInChildren<VRMSpringBone>(true);
int removedCount = 0;
foreach (var bone in springBones)
{
if (bone.RootBones.Any(rb => rb == null) || bone.ColliderGroups.Any(cg => cg == null))
{
DestroyImmediate(bone);
removedCount++;
}
}
if (removedCount > 0)
{
EditorUtility.SetDirty(destinationPrefab);
AssetDatabase.SaveAssets();
}
EditorUtility.DisplayDialog("작업 완료", $"제거된 잘못된 스프링본: {removedCount}개", "확인");
}
// 유틸리티 메서드들
private Transform FindCorrespondingBone(Transform originalBone, Transform[] copyBones)
{
return System.Array.Find(copyBones, bone => bone.name == originalBone.name);
}
private bool CopyVRMSpringBoneComponents(VRMSpringBone original, VRMSpringBone copy)
{
try
{
copy.m_comment = original.m_comment;
copy.m_stiffnessForce = original.m_stiffnessForce;
copy.m_gravityPower = original.m_gravityPower;
copy.m_gravityDir = original.m_gravityDir;
copy.m_dragForce = original.m_dragForce;
copy.m_hitRadius = original.m_hitRadius;
copy.m_updateType = original.m_updateType;
// Center 본 설정
if (original.m_center != null)
{
copy.m_center = FindCorrespondingTransform(original.m_center, destinationPrefab.transform);
if (copy.m_center == null)
{
Debug.LogWarning($"Center 본을 찾을 수 없습니다: {original.m_center.name} - Center 본 없이 계속 진행합니다.");
}
}
// Root 본들 설정
List<Transform> newRootBones = new List<Transform>();
foreach (var rootBone in original.RootBones)
{
if (rootBone == null) continue;
var correspondingBone = FindCorrespondingTransform(rootBone, destinationPrefab.transform);
if (correspondingBone != null)
{
newRootBones.Add(correspondingBone);
}
else
{
Debug.LogWarning($"Root 본을 찾을 수 없습니다: {rootBone.name} - 이 본은 건너뜁니다.");
}
}
// 찾은 본이 하나도 없으면 false 반환
if (newRootBones.Count == 0)
{
Debug.LogError("유효한 Root 본을 하나도 찾을 수 없습니다.");
return false;
}
copy.RootBones = newRootBones;
// Collider Groups 설정
if (original.ColliderGroups != null && original.ColliderGroups.Length > 0)
{
List<VRMSpringBoneColliderGroup> newColliderGroups = new List<VRMSpringBoneColliderGroup>();
foreach (var colliderGroup in original.ColliderGroups)
{
if (colliderGroup == null) continue;
var correspondingCollider = FindCorrespondingColliderGroup(colliderGroup);
if (correspondingCollider != null)
{
newColliderGroups.Add(correspondingCollider);
}
else
{
Debug.LogWarning($"콜라이더 그룹을 찾을 수 없습니다: {colliderGroup.name} - 이 콜라이더 그룹은 건너뜁니다.");
}
}
copy.ColliderGroups = newColliderGroups.ToArray();
}
return true;
}
catch (System.Exception e)
{
Debug.LogError($"컴포넌트 복사 중 오류 발생: {e.Message}");
return false;
}
}
private Transform FindCorrespondingTransform(Transform original, Transform searchRoot)
{
if (original == null) return null;
string path = GetTransformPath(original);
Transform result = searchRoot.Find(path);
if (result == null)
{
var allTransforms = searchRoot.GetComponentsInChildren<Transform>(true);
result = allTransforms.FirstOrDefault(t => t.name == original.name);
}
return result;
}
private string GetTransformPath(Transform transform)
{
string path = transform.name;
Transform parent = transform.parent;
while (parent != null && parent != originalPrefab.transform)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
private VRMSpringBoneColliderGroup FindCorrespondingColliderGroup(VRMSpringBoneColliderGroup original)
{
if (original == null) return null;
Transform correspondingTransform = FindCorrespondingTransform(original.transform, destinationPrefab.transform);
return correspondingTransform?.GetComponent<VRMSpringBoneColliderGroup>();
}
// 최소 윈도우 크기 설정
void OnEnable()
{
// 최소 윈도우 크기 설정
minSize = new Vector2(250, 400);
}
}