Add : 아바타 로컬로테이션 변조 툴 1차 업데이트
This commit is contained in:
parent
a114b8cce5
commit
61763d81d0
BIN
Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md
(Stored with Git LFS)
vendored
BIN
Assets/External/EasyMotionRecorder/Scripts/README_SavePathManager.md
(Stored with Git LFS)
vendored
Binary file not shown.
BIN
Assets/Motion/T-Pose_20250726_024111.json
(Stored with Git LFS)
Normal file
BIN
Assets/Motion/T-Pose_20250726_024111.json
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d52d79965c0c87f4dbc7d4ea99597abe
|
||||
guid: 963c54edbba1b4b45bed1b3a96506b08
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
BIN
Assets/Scenes/PoseRotationBaker.unity
(Stored with Git LFS)
Normal file
BIN
Assets/Scenes/PoseRotationBaker.unity
(Stored with Git LFS)
Normal file
Binary file not shown.
7
Assets/Scenes/PoseRotationBaker.unity.meta
Normal file
7
Assets/Scenes/PoseRotationBaker.unity.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4aa63adf1196d0743ac835945bd69261
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
373
Assets/Scripts/Editor/PoseRecorderWindow.cs
Normal file
373
Assets/Scripts/Editor/PoseRecorderWindow.cs
Normal file
@ -0,0 +1,373 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System;
|
||||
|
||||
namespace Streamingle.Editor
|
||||
{
|
||||
public class PoseRecorderWindow : EditorWindow
|
||||
{
|
||||
private GameObject selectedAvatar;
|
||||
private Animator avatarAnimator;
|
||||
private string poseName = "NewPose";
|
||||
private string savePath = "Assets/Recordings/";
|
||||
|
||||
// 휴먼본 본들 (Animator HumanBodyBones 사용)
|
||||
private readonly HumanBodyBones[] humanBodyBones = {
|
||||
HumanBodyBones.Hips,
|
||||
HumanBodyBones.Spine,
|
||||
HumanBodyBones.Chest,
|
||||
HumanBodyBones.UpperChest,
|
||||
HumanBodyBones.Neck,
|
||||
HumanBodyBones.Head,
|
||||
HumanBodyBones.LeftShoulder,
|
||||
HumanBodyBones.LeftUpperArm,
|
||||
HumanBodyBones.LeftLowerArm,
|
||||
HumanBodyBones.LeftHand,
|
||||
HumanBodyBones.RightShoulder,
|
||||
HumanBodyBones.RightUpperArm,
|
||||
HumanBodyBones.RightLowerArm,
|
||||
HumanBodyBones.RightHand,
|
||||
HumanBodyBones.LeftUpperLeg,
|
||||
HumanBodyBones.LeftLowerLeg,
|
||||
HumanBodyBones.LeftFoot,
|
||||
HumanBodyBones.LeftToes,
|
||||
HumanBodyBones.RightUpperLeg,
|
||||
HumanBodyBones.RightLowerLeg,
|
||||
HumanBodyBones.RightFoot,
|
||||
HumanBodyBones.RightToes
|
||||
};
|
||||
|
||||
// 손가락 본들
|
||||
private readonly HumanBodyBones[] fingerBones = {
|
||||
// 왼손 손가락
|
||||
HumanBodyBones.LeftThumbProximal,
|
||||
HumanBodyBones.LeftThumbIntermediate,
|
||||
HumanBodyBones.LeftThumbDistal,
|
||||
HumanBodyBones.LeftIndexProximal,
|
||||
HumanBodyBones.LeftIndexIntermediate,
|
||||
HumanBodyBones.LeftIndexDistal,
|
||||
HumanBodyBones.LeftMiddleProximal,
|
||||
HumanBodyBones.LeftMiddleIntermediate,
|
||||
HumanBodyBones.LeftMiddleDistal,
|
||||
HumanBodyBones.LeftRingProximal,
|
||||
HumanBodyBones.LeftRingIntermediate,
|
||||
HumanBodyBones.LeftRingDistal,
|
||||
HumanBodyBones.LeftLittleProximal,
|
||||
HumanBodyBones.LeftLittleIntermediate,
|
||||
HumanBodyBones.LeftLittleDistal,
|
||||
// 오른손 손가락
|
||||
HumanBodyBones.RightThumbProximal,
|
||||
HumanBodyBones.RightThumbIntermediate,
|
||||
HumanBodyBones.RightThumbDistal,
|
||||
HumanBodyBones.RightIndexProximal,
|
||||
HumanBodyBones.RightIndexIntermediate,
|
||||
HumanBodyBones.RightIndexDistal,
|
||||
HumanBodyBones.RightMiddleProximal,
|
||||
HumanBodyBones.RightMiddleIntermediate,
|
||||
HumanBodyBones.RightMiddleDistal,
|
||||
HumanBodyBones.RightRingProximal,
|
||||
HumanBodyBones.RightRingIntermediate,
|
||||
HumanBodyBones.RightRingDistal,
|
||||
HumanBodyBones.RightLittleProximal,
|
||||
HumanBodyBones.RightLittleIntermediate,
|
||||
HumanBodyBones.RightLittleDistal
|
||||
};
|
||||
|
||||
[MenuItem("Tools/Pose Recorder")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
GetWindow<PoseRecorderWindow>("포즈 기록기");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 선택된 GameObject가 아바타인지 확인
|
||||
if (Selection.activeGameObject != null)
|
||||
{
|
||||
selectedAvatar = Selection.activeGameObject;
|
||||
avatarAnimator = selectedAvatar.GetComponent<Animator>();
|
||||
if (avatarAnimator == null)
|
||||
{
|
||||
avatarAnimator = selectedAvatar.GetComponentInChildren<Animator>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 scrollPosition;
|
||||
|
||||
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();
|
||||
|
||||
// 포즈 이름 입력
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("포즈 이름:", GUILayout.Width(60));
|
||||
poseName = EditorGUILayout.TextField(poseName);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// 저장 경로 설정
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("저장 경로:", GUILayout.Width(60));
|
||||
savePath = EditorGUILayout.TextField(savePath);
|
||||
if (GUILayout.Button("폴더 선택", GUILayout.Width(80)))
|
||||
{
|
||||
string newPath = EditorUtility.OpenFolderPanel("저장 폴더 선택", savePath, "");
|
||||
if (!string.IsNullOrEmpty(newPath))
|
||||
{
|
||||
// Assets 폴더 내부로 경로 변환
|
||||
if (newPath.StartsWith(Application.dataPath))
|
||||
{
|
||||
savePath = "Assets" + newPath.Substring(Application.dataPath.Length) + "/";
|
||||
}
|
||||
else
|
||||
{
|
||||
savePath = "Assets/Recordings/";
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// 버튼들
|
||||
EditorGUI.BeginDisabledGroup(selectedAvatar == null || avatarAnimator == null);
|
||||
|
||||
if (GUILayout.Button("현재 포즈 기록 (전체)", GUILayout.Height(30)))
|
||||
{
|
||||
RecordCurrentPose(true);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("현재 포즈 기록 (본체만)", GUILayout.Height(30)))
|
||||
{
|
||||
RecordCurrentPose(false);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("T-Pose 기록", GUILayout.Height(30)))
|
||||
{
|
||||
RecordTPose();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("A-Pose 기록", GUILayout.Height(30)))
|
||||
{
|
||||
RecordAPose();
|
||||
}
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// 정보 표시
|
||||
if (selectedAvatar != null && avatarAnimator != null)
|
||||
{
|
||||
EditorGUILayout.LabelField("기록 가능한 본들:", EditorStyles.boldLabel);
|
||||
|
||||
// 본체 본들
|
||||
EditorGUILayout.LabelField("본체 본들:", EditorStyles.boldLabel);
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
|
||||
int foundBodyBones = 0;
|
||||
foreach (HumanBodyBones humanBone in humanBodyBones)
|
||||
{
|
||||
Transform bone = avatarAnimator.GetBoneTransform(humanBone);
|
||||
if (bone != null)
|
||||
{
|
||||
EditorGUILayout.LabelField($"✓ {humanBone}");
|
||||
foundBodyBones++;
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.LabelField($"본체 본: {foundBodyBones}개");
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// 손가락 본들
|
||||
EditorGUILayout.LabelField("손가락 본들:", EditorStyles.boldLabel);
|
||||
EditorGUILayout.BeginVertical("box");
|
||||
|
||||
int foundFingerBones = 0;
|
||||
foreach (HumanBodyBones fingerBone in fingerBones)
|
||||
{
|
||||
Transform bone = avatarAnimator.GetBoneTransform(fingerBone);
|
||||
if (bone != null)
|
||||
{
|
||||
EditorGUILayout.LabelField($"✓ {fingerBone}");
|
||||
foundFingerBones++;
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.LabelField($"손가락 본: {foundFingerBones}개");
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField($"총 {foundBodyBones + foundFingerBones}개 본 발견", EditorStyles.boldLabel);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void RecordCurrentPose(bool includeFingers = true)
|
||||
{
|
||||
if (selectedAvatar == null || avatarAnimator == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("오류", "아바타를 선택해주세요.", "확인");
|
||||
return;
|
||||
}
|
||||
|
||||
PoseData poseData = new PoseData();
|
||||
poseData.avatarName = selectedAvatar.name;
|
||||
poseData.poseName = poseName;
|
||||
poseData.recordedTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
int recordedBones = 0;
|
||||
|
||||
// 본체 본들 기록
|
||||
foreach (HumanBodyBones humanBone in humanBodyBones)
|
||||
{
|
||||
Transform bone = avatarAnimator.GetBoneTransform(humanBone);
|
||||
if (bone != null)
|
||||
{
|
||||
BonePoseData boneData = new BonePoseData();
|
||||
boneData.boneName = humanBone.ToString();
|
||||
boneData.localPosition = bone.localPosition;
|
||||
boneData.localRotation = bone.localEulerAngles;
|
||||
boneData.localScale = bone.localScale;
|
||||
|
||||
poseData.bonePoses.Add(boneData);
|
||||
recordedBones++;
|
||||
}
|
||||
}
|
||||
|
||||
// 손가락 본들 기록 (옵션)
|
||||
if (includeFingers)
|
||||
{
|
||||
int fingerBonesCount = 0;
|
||||
foreach (HumanBodyBones fingerBone in fingerBones)
|
||||
{
|
||||
Transform bone = avatarAnimator.GetBoneTransform(fingerBone);
|
||||
if (bone != null)
|
||||
{
|
||||
BonePoseData boneData = new BonePoseData();
|
||||
boneData.boneName = fingerBone.ToString();
|
||||
boneData.localPosition = bone.localPosition;
|
||||
boneData.localRotation = bone.localEulerAngles;
|
||||
boneData.localScale = bone.localScale;
|
||||
|
||||
poseData.bonePoses.Add(boneData);
|
||||
recordedBones++;
|
||||
fingerBonesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (fingerBonesCount > 0)
|
||||
{
|
||||
UnityEngine.Debug.Log($"손가락 본 {fingerBonesCount}개 기록됨");
|
||||
}
|
||||
}
|
||||
|
||||
if (recordedBones > 0)
|
||||
{
|
||||
SavePoseData(poseData);
|
||||
string message = includeFingers ?
|
||||
$"포즈가 기록되었습니다!\n본체 본: {recordedBones - fingerBones.Length}개\n손가락 본: {fingerBones.Length}개\n총 {recordedBones}개 본" :
|
||||
$"포즈가 기록되었습니다!\n본체 본: {recordedBones}개";
|
||||
EditorUtility.DisplayDialog("성공", message, "확인");
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog("오류", "기록할 본을 찾을 수 없습니다.", "확인");
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordTPose()
|
||||
{
|
||||
poseName = "T-Pose";
|
||||
RecordCurrentPose(true); // 손가락 포함
|
||||
}
|
||||
|
||||
private void RecordAPose()
|
||||
{
|
||||
poseName = "A-Pose";
|
||||
RecordCurrentPose(true); // 손가락 포함
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void SavePoseData(PoseData poseData)
|
||||
{
|
||||
// 저장 폴더 생성
|
||||
if (!Directory.Exists(savePath))
|
||||
{
|
||||
Directory.CreateDirectory(savePath);
|
||||
}
|
||||
|
||||
// 파일명 생성
|
||||
string fileName = $"{poseData.poseName}_{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
||||
string filePath = Path.Combine(savePath, fileName);
|
||||
|
||||
// JSON으로 저장
|
||||
string json = JsonUtility.ToJson(poseData, true);
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
// Unity 에디터에서 파일 새로고침
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
UnityEngine.Debug.Log($"포즈가 저장되었습니다: {filePath}");
|
||||
}
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class BonePoseData
|
||||
{
|
||||
public string boneName;
|
||||
public Vector3 localPosition;
|
||||
public Vector3 localRotation;
|
||||
public Vector3 localScale;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class PoseData
|
||||
{
|
||||
public string avatarName;
|
||||
public string poseName;
|
||||
public string recordedTime;
|
||||
public List<BonePoseData> bonePoses = new List<BonePoseData>();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/PoseRecorderWindow.cs.meta
Normal file
2
Assets/Scripts/Editor/PoseRecorderWindow.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03b437e4b8ab0fa4191196f14605c61c
|
||||
499
Assets/Scripts/Editor/PoseRotationBaker.cs
Normal file
499
Assets/Scripts/Editor/PoseRotationBaker.cs
Normal file
@ -0,0 +1,499 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/PoseRotationBaker.cs.meta
Normal file
2
Assets/Scripts/Editor/PoseRotationBaker.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 150464f3f9d1e5c49a568fbcada754b7
|
||||
Loading…
x
Reference in New Issue
Block a user