using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.IO; using System; namespace Streamingle.Editor { public class PoseRotationBaker : EditorWindow { private GameObject selectedAvatar; private Animator avatarAnimator; private string jsonFilePath = ""; private PoseData loadedPoseData; private Vector2 scrollPosition; // 베이크 옵션 private bool applyToAllBones = true; private bool applyToSpecificBones = false; private List specificBones = new List(); // 미리보기 옵션 private bool showBoneInfo = true; [MenuItem("Tools/Animation Tools/Pose Rotation Baker")] public static void ShowWindow() { GetWindow("포즈 로테이션 베이커"); } private void OnEnable() { if (Selection.activeGameObject != null) { selectedAvatar = Selection.activeGameObject; avatarAnimator = selectedAvatar.GetComponent(); if (avatarAnimator == null) { avatarAnimator = selectedAvatar.GetComponentInChildren(); } } } private void OnGUI() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); GUILayout.Label("포즈 로테이션 베이커", EditorStyles.boldLabel); EditorGUILayout.Space(); // 아바타 선택 EditorGUILayout.BeginHorizontal(); GUILayout.Label("아바타:", GUILayout.Width(60)); selectedAvatar = (GameObject)EditorGUILayout.ObjectField(selectedAvatar, typeof(GameObject), true); EditorGUILayout.EndHorizontal(); if (selectedAvatar != null) { avatarAnimator = selectedAvatar.GetComponent(); if (avatarAnimator == null) { avatarAnimator = selectedAvatar.GetComponentInChildren(); } if (avatarAnimator == null) { EditorGUILayout.HelpBox("선택된 오브젝트에 Animator가 없습니다!", MessageType.Error); EditorGUILayout.EndScrollView(); return; } EditorGUILayout.HelpBox($"아바타: {selectedAvatar.name}", MessageType.Info); } else { EditorGUILayout.HelpBox("아바타를 선택해주세요.", MessageType.Warning); } EditorGUILayout.Space(); // JSON 파일 로드 EditorGUILayout.LabelField("포즈 데이터 로드:", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); GUILayout.Label("JSON 파일:", GUILayout.Width(60)); jsonFilePath = EditorGUILayout.TextField(jsonFilePath); if (GUILayout.Button("파일 선택", GUILayout.Width(80))) { string path = EditorUtility.OpenFilePanel("포즈 JSON 파일 선택", "", "json"); if (!string.IsNullOrEmpty(path)) { jsonFilePath = path; LoadPoseData(); } } EditorGUILayout.EndHorizontal(); if (loadedPoseData != null) { EditorGUILayout.HelpBox($"로드된 포즈: {loadedPoseData.poseName}\n아바타: {loadedPoseData.avatarName}\n본 개수: {loadedPoseData.bonePoses.Count}개", MessageType.Info); } EditorGUILayout.Space(); // 베이크 옵션 EditorGUILayout.LabelField("베이크 옵션:", EditorStyles.boldLabel); EditorGUILayout.BeginVertical("box"); applyToAllBones = EditorGUILayout.Toggle("모든 본에 적용", applyToAllBones); applyToSpecificBones = EditorGUILayout.Toggle("특정 본만 적용", applyToSpecificBones); if (applyToSpecificBones) { EditorGUILayout.Space(); EditorGUILayout.LabelField("적용할 본들:"); EditorGUILayout.BeginVertical("box"); if (loadedPoseData != null) { foreach (var bonePose in loadedPoseData.bonePoses) { bool isSelected = specificBones.Contains(bonePose.boneName); bool newSelection = EditorGUILayout.Toggle(bonePose.boneName, isSelected); if (newSelection != isSelected) { if (newSelection) { specificBones.Add(bonePose.boneName); } else { specificBones.Remove(bonePose.boneName); } } } } EditorGUILayout.EndVertical(); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(); // 미리보기 옵션 EditorGUILayout.LabelField("미리보기 옵션:", EditorStyles.boldLabel); EditorGUILayout.BeginVertical("box"); showBoneInfo = EditorGUILayout.Toggle("본 정보 표시", showBoneInfo); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); // 버튼들 EditorGUI.BeginDisabledGroup(selectedAvatar == null || avatarAnimator == null || loadedPoseData == null); if (GUILayout.Button("본 회전 베이크 (메쉬 고정)", GUILayout.Height(30))) { BakeWithRiggingData(); } EditorGUI.EndDisabledGroup(); EditorGUILayout.Space(); // 정보 표시 if (loadedPoseData != null && showBoneInfo) { EditorGUILayout.LabelField("포즈 데이터 정보:", EditorStyles.boldLabel); EditorGUILayout.BeginVertical("box"); foreach (var bonePose in loadedPoseData.bonePoses) { Transform bone = FindBoneByName(selectedAvatar, bonePose.boneName); if (bone != null) { EditorGUILayout.LabelField($"✓ {bonePose.boneName}"); EditorGUILayout.LabelField($" 회전: {bonePose.localRotation}"); } else { EditorGUILayout.LabelField($"✗ {bonePose.boneName} (찾을 수 없음)"); } } EditorGUILayout.EndVertical(); } EditorGUILayout.EndScrollView(); } private void LoadPoseData() { if (string.IsNullOrEmpty(jsonFilePath) || !File.Exists(jsonFilePath)) { EditorUtility.DisplayDialog("오류", "JSON 파일을 찾을 수 없습니다.", "확인"); return; } try { string json = File.ReadAllText(jsonFilePath); loadedPoseData = JsonUtility.FromJson(json); if (loadedPoseData != null && loadedPoseData.bonePoses.Count > 0) { UnityEngine.Debug.Log($"포즈 데이터 로드 완료: {loadedPoseData.poseName}, {loadedPoseData.bonePoses.Count}개 본"); } else { EditorUtility.DisplayDialog("오류", "유효하지 않은 포즈 데이터입니다.", "확인"); loadedPoseData = null; } } catch (Exception e) { EditorUtility.DisplayDialog("오류", $"파일 로드 중 오류 발생: {e.Message}", "확인"); loadedPoseData = null; } } private Transform FindBoneByName(GameObject targetAvatar, string boneName) { if (targetAvatar == null) return null; // HumanBodyBones enum으로 찾기 if (System.Enum.TryParse(boneName, out HumanBodyBones humanBone)) { Animator targetAnimator = targetAvatar.GetComponent(); if (targetAnimator == null) { targetAnimator = targetAvatar.GetComponentInChildren(); } return targetAnimator?.GetBoneTransform(humanBone); } // 이름으로 직접 찾기 Transform bone = targetAvatar.transform.Find(boneName); if (bone == null) { bone = FindChildRecursively(targetAvatar.transform, boneName); } return bone; } private Transform FindChildRecursively(Transform parent, string childName) { foreach (Transform child in parent) { if (child.name == childName) return child; Transform result = FindChildRecursively(child, childName); if (result != null) return result; } return null; } // 본 회전 베이크 (메쉬 고정) private void BakeWithRiggingData() { if (loadedPoseData == null || selectedAvatar == null) { EditorUtility.DisplayDialog("오류", "포즈 데이터와 아바타를 선택해주세요.", "확인"); return; } if (!EditorUtility.DisplayDialog("본 회전 베이크", "본의 로테이션을 변경하되 메쉬는 원래 모양을 유지합니다.\n\n" + "본은 회전하지만 스킨메쉬렌더러는 변형되지 않습니다.", "베이크 실행", "취소")) return; try { // 1단계: 아바타 복사본 생성 GameObject originalAvatar = selectedAvatar; GameObject copiedAvatar = Instantiate(originalAvatar); copiedAvatar.name = originalAvatar.name + "_Baked_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); // 복사본을 씬에 추가 copiedAvatar.transform.SetParent(originalAvatar.transform.parent); copiedAvatar.transform.position = originalAvatar.transform.position; copiedAvatar.transform.rotation = originalAvatar.transform.rotation; copiedAvatar.transform.localScale = originalAvatar.transform.localScale; // 2단계: 모든 스킨메쉬렌더러 찾기 SkinnedMeshRenderer[] allSkinnedMeshes = copiedAvatar.GetComponentsInChildren(); if (allSkinnedMeshes.Length == 0) { EditorUtility.DisplayDialog("오류", "스킨메쉬렌더러를 찾을 수 없습니다.", "확인"); DestroyImmediate(copiedAvatar); return; } UnityEngine.Debug.Log($"복사본 아바타: {copiedAvatar.name}, 스킨메쉬 개수: {allSkinnedMeshes.Length}개"); // 각 스킨메쉬렌더러별 메쉬 복사본 생성 Dictionary copiedMeshes = new Dictionary(); Dictionary storedBindPoses = new Dictionary(); Dictionary storedBones = new Dictionary(); foreach (SkinnedMeshRenderer skinnedMesh in allSkinnedMeshes) { UnityEngine.Debug.Log($"스킨메쉬 처리 중: {skinnedMesh.name}"); // 메쉬 복사본 생성 Mesh originalMesh = skinnedMesh.sharedMesh; Mesh copiedMesh = Instantiate(originalMesh); copiedMesh.name = originalMesh.name + "_Baked_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); // 복사된 메쉬를 스킨메쉬렌더러에 할당 skinnedMesh.sharedMesh = copiedMesh; copiedMeshes[skinnedMesh] = copiedMesh; // 원래 바인드포즈와 본 정보 저장 Matrix4x4[] bindPoses = copiedMesh.bindposes; Transform[] bones = skinnedMesh.bones; storedBindPoses[skinnedMesh] = bindPoses; storedBones[skinnedMesh] = bones; UnityEngine.Debug.Log($"메쉬 복사 완료: {copiedMesh.name}, 바인드포즈: {bindPoses.Length}개, 본: {bones.Length}개"); } // 4단계: 본의 원래 상태 기록 Dictionary originalParents = new Dictionary(); Dictionary originalBoneRotations = new Dictionary(); List allBones = GetAllBones(copiedAvatar); UnityEngine.Debug.Log($"본 상태 기록 시작: {allBones.Count}개 본"); foreach (Transform bone in allBones) { originalParents[bone] = bone.parent; originalBoneRotations[bone] = bone.localEulerAngles; bone.SetParent(copiedAvatar.transform); // 모든 본을 아바타의 직접 자식으로 } // 5단계: JSON 데이터로 본 로테이션 적용 int appliedBones = 0; foreach (var bonePose in loadedPoseData.bonePoses) { if (applyToSpecificBones && !specificBones.Contains(bonePose.boneName)) continue; Transform bone = FindBoneByName(copiedAvatar, bonePose.boneName); if (bone != null) { UnityEngine.Debug.Log($"본 로테이션 적용: {bonePose.boneName} - {bone.localEulerAngles} → {bonePose.localRotation}"); bone.localEulerAngles = bonePose.localRotation; appliedBones++; } } // 5.5단계: 본 변형을 메쉬에 강제 반영 (바인드포즈 보존) UnityEngine.Debug.Log("본 변형을 메쉬에 강제 반영 시작"); foreach (SkinnedMeshRenderer skinnedMesh in allSkinnedMeshes) { // 1단계: 원본 메쉬의 바인드포즈와 본 가중치 백업 Mesh originalMesh = copiedMeshes[skinnedMesh]; Matrix4x4[] originalBindPoses = originalMesh.bindposes; BoneWeight[] originalBoneWeights = originalMesh.boneWeights; Transform[] originalBones = storedBones[skinnedMesh]; Matrix4x4[] originalStoredBindPoses = storedBindPoses[skinnedMesh]; UnityEngine.Debug.Log($"바인드포즈 백업 완료: {skinnedMesh.name}, 바인드포즈: {originalBindPoses.Length}개, 본가중치: {originalBoneWeights.Length}개"); // 2단계: BakeMesh로 본 변형 반영 Mesh tempMesh = new Mesh(); skinnedMesh.BakeMesh(tempMesh); UnityEngine.Debug.Log($"BakeMesh 완료: {skinnedMesh.name}, 버텍스: {tempMesh.vertices.Length}개"); // 3단계: 바인드포즈 조정을 통한 메쉬 고정 (X축 -90도 보정 포함) Matrix4x4[] newBindPoses = new Matrix4x4[originalBindPoses.Length]; // X축 -90도 회전 보정 행렬 Matrix4x4 correctionMatrix = Matrix4x4.Rotate(Quaternion.Euler(90f, 0f, 0f)); // 각 본의 회전 변화량을 바인드포즈에 반영 for (int boneIndex = 0; boneIndex < originalBones.Length; boneIndex++) { Transform bone = originalBones[boneIndex]; Matrix4x4 originalBindPose = originalStoredBindPoses[boneIndex]; // 현재 본의 Transform의 역행렬에 X축 -90도 보정 적용 Matrix4x4 currentBoneTransform = bone.localToWorldMatrix; Matrix4x4 newBindPose = (correctionMatrix * currentBoneTransform).inverse; newBindPoses[boneIndex] = newBindPose; UnityEngine.Debug.Log($"바인드포즈 조정: {bone.name} (X축 -90도 보정 적용)"); } // 원래 버텍스와 조정된 바인드포즈 사용 tempMesh.vertices = originalMesh.vertices; tempMesh.bindposes = newBindPoses; tempMesh.boneWeights = originalBoneWeights; // 블렌드쉐입 데이터 보존 if (originalMesh.blendShapeCount > 0) { UnityEngine.Debug.Log($"블렌드쉐입 복사 시작: {originalMesh.blendShapeCount}개"); for (int i = 0; i < originalMesh.blendShapeCount; i++) { string shapeName = originalMesh.GetBlendShapeName(i); int frameCount = originalMesh.GetBlendShapeFrameCount(i); for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { float frameWeight = originalMesh.GetBlendShapeFrameWeight(i, frameIndex); Vector3[] deltaVertices = new Vector3[originalMesh.vertexCount]; Vector3[] deltaNormals = new Vector3[originalMesh.vertexCount]; Vector3[] deltaTangents = new Vector3[originalMesh.vertexCount]; originalMesh.GetBlendShapeFrameVertices(i, frameIndex, deltaVertices, deltaNormals, deltaTangents); tempMesh.AddBlendShapeFrame(shapeName, frameWeight, deltaVertices, deltaNormals, deltaTangents); } } UnityEngine.Debug.Log($"블렌드쉐입 복사 완료: {tempMesh.blendShapeCount}개"); } // 4단계: SkinnedMeshRenderer에 조정된 메쉬 적용 tempMesh.RecalculateNormals(); tempMesh.RecalculateBounds(); // 조정된 메쉬를 SkinnedMeshRenderer에 적용 skinnedMesh.sharedMesh = tempMesh; UnityEngine.Debug.Log($"메쉬 고정 완료: {skinnedMesh.name}, 바인드포즈 조정됨"); } // 6단계: 완료 (5.5단계에서 모든 작업 완료) UnityEngine.Debug.Log("모든 스킨메쉬 처리 완료"); // 7단계: 원래 계층 구조 복원 UnityEngine.Debug.Log("원래 계층 구조 복원 시작"); foreach (Transform bone in allBones) { if (originalParents.ContainsKey(bone)) { bone.SetParent(originalParents[bone]); } } EditorUtility.SetDirty(copiedAvatar); EditorUtility.DisplayDialog("베이크 완료", $"본 회전 베이크가 완료되었습니다!\n" + $"적용된 본: {appliedBones}개\n" + $"바인드포즈 조정된 메쉬: {allSkinnedMeshes.Length}개\n\n" + $"복사본 아바타가 생성되었습니다: {copiedAvatar.name}\n\n" + "본은 회전했지만 바인드포즈 조정으로 메쉬는 변형되지 않습니다.", "확인"); UnityEngine.Debug.Log($"본 회전 베이크 완료: {appliedBones}개 본"); // 생성된 아바타를 선택 Selection.activeGameObject = copiedAvatar; } catch (Exception e) { EditorUtility.DisplayDialog("오류", $"베이크 중 오류가 발생했습니다: {e.Message}", "확인"); UnityEngine.Debug.LogError($"베이크 오류: {e.Message}"); } } // 모든 본을 가져오는 헬퍼 메서드 private List GetAllBones(GameObject avatar) { List bones = new List(); Transform[] allTransforms = avatar.GetComponentsInChildren(); foreach (Transform t in allTransforms) { if (t != avatar.transform) // 아바타 루트는 제외 { bones.Add(t); } } return bones; } } // 리깅 데이터 백업을 위한 컴포넌트 [System.Serializable] public class RiggingDataBackup : MonoBehaviour { public BoneWeight[] boneWeights; public Matrix4x4[] bindPoses; public Transform[] originalBones; public Vector3[] originalVertices; } }