499 lines
23 KiB
C#
499 lines
23 KiB
C#
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<string> specificBones = new List<string>();
|
|
|
|
// 미리보기 옵션
|
|
private bool showBoneInfo = true;
|
|
|
|
[MenuItem("Tools/Pose Rotation Baker")]
|
|
public static void ShowWindow()
|
|
{
|
|
GetWindow<PoseRotationBaker>("포즈 로테이션 베이커");
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (Selection.activeGameObject != null)
|
|
{
|
|
selectedAvatar = Selection.activeGameObject;
|
|
avatarAnimator = selectedAvatar.GetComponent<Animator>();
|
|
if (avatarAnimator == null)
|
|
{
|
|
avatarAnimator = selectedAvatar.GetComponentInChildren<Animator>();
|
|
}
|
|
}
|
|
}
|
|
|
|
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<Animator>();
|
|
if (avatarAnimator == null)
|
|
{
|
|
avatarAnimator = selectedAvatar.GetComponentInChildren<Animator>();
|
|
}
|
|
|
|
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<PoseData>(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<Animator>();
|
|
if (targetAnimator == null)
|
|
{
|
|
targetAnimator = targetAvatar.GetComponentInChildren<Animator>();
|
|
}
|
|
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<SkinnedMeshRenderer>();
|
|
if (allSkinnedMeshes.Length == 0)
|
|
{
|
|
EditorUtility.DisplayDialog("오류", "스킨메쉬렌더러를 찾을 수 없습니다.", "확인");
|
|
DestroyImmediate(copiedAvatar);
|
|
return;
|
|
}
|
|
|
|
UnityEngine.Debug.Log($"복사본 아바타: {copiedAvatar.name}, 스킨메쉬 개수: {allSkinnedMeshes.Length}개");
|
|
|
|
// 각 스킨메쉬렌더러별 메쉬 복사본 생성
|
|
Dictionary<SkinnedMeshRenderer, Mesh> copiedMeshes = new Dictionary<SkinnedMeshRenderer, Mesh>();
|
|
Dictionary<SkinnedMeshRenderer, Matrix4x4[]> storedBindPoses = new Dictionary<SkinnedMeshRenderer, Matrix4x4[]>();
|
|
Dictionary<SkinnedMeshRenderer, Transform[]> storedBones = new Dictionary<SkinnedMeshRenderer, Transform[]>();
|
|
|
|
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<Transform, Transform> originalParents = new Dictionary<Transform, Transform>();
|
|
Dictionary<Transform, Vector3> originalBoneRotations = new Dictionary<Transform, Vector3>();
|
|
List<Transform> 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<Transform> GetAllBones(GameObject avatar)
|
|
{
|
|
List<Transform> bones = new List<Transform>();
|
|
Transform[] allTransforms = avatar.GetComponentsInChildren<Transform>();
|
|
|
|
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;
|
|
}
|
|
} |