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("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(); if (springBones.Length == 0) { EditorUtility.DisplayDialog("오류", "선택한 오브젝트에 VRMSpringBone 컴포넌트가 없습니다.", "확인"); return; } // 아바타 루트 아래의 모든 콜라이더 그룹 찾기 VRMSpringBoneColliderGroup[] colliderGroups = avatarRoot.GetComponentsInChildren(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(); 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(); if (existingCollider != null) { DestroyImmediate(existingCollider); } // 새로운 콜라이더 그룹 추가 var newColliderGroup = correspondingTransform.gameObject.AddComponent(); // 원본과 대상 본의 월드 회전 차이 계산 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(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(true); foreach (var bone in existingBones) { DestroyImmediate(bone); } // 소스의 VRMSpringBone 컴포넌트 복사 var springBones = originalPrefab.GetComponentsInChildren(true); int successCount = 0; int failCount = 0; foreach (var springBone in springBones) { try { VRMSpringBone newSpringBone = destSecondary.gameObject.AddComponent(); 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(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 newRootBones = new List(); 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 newColliderGroups = new List(); 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(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(); } // 최소 윈도우 크기 설정 void OnEnable() { // 최소 윈도우 크기 설정 minSize = new Vector2(250, 400); } }