Fix : 모션 녹화 시스템 수정

This commit is contained in:
KINDNICK 2025-07-22 22:50:32 +09:00
parent fc43b834a1
commit 808ac496b1
14 changed files with 2602 additions and 24552 deletions

Binary file not shown.

View File

@ -0,0 +1,324 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using Entum;
namespace EasyMotionRecorder
{
/// <summary>
/// FBX 애니메이션 내보내기 도구
/// Unity의 제한으로 인해 직접적인 FBX 내보내기는 어려우므로
/// .anim 파일을 생성하고 외부 도구로 변환하는 방법을 제공합니다.
/// </summary>
public class FBXExporter : EditorWindow
{
private HumanoidPoses targetPoses;
private string outputPath = "Assets/Resources/Motion";
private string fileName = "";
private bool includeHumanoid = true;
private bool includeGeneric = true;
private bool includeFacial = false;
private bool useBinaryFormat = true; // Binary 형식 사용 여부
[MenuItem("Tools/EasyMotionRecorder/FBX Exporter")]
public static void ShowWindow()
{
GetWindow<FBXExporter>("FBX Exporter");
}
private void OnGUI()
{
GUILayout.Label("FBX 애니메이션 내보내기", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 타겟 HumanoidPoses 선택
targetPoses = (HumanoidPoses)EditorGUILayout.ObjectField("HumanoidPoses", targetPoses, typeof(HumanoidPoses), false);
EditorGUILayout.Space();
// 출력 설정
GUILayout.Label("출력 설정", EditorStyles.boldLabel);
outputPath = EditorGUILayout.TextField("출력 경로", outputPath);
fileName = EditorGUILayout.TextField("파일명 (확장자 제외)", fileName);
EditorGUILayout.Space();
// 내보내기 옵션
GUILayout.Label("내보내기 옵션", EditorStyles.boldLabel);
includeHumanoid = EditorGUILayout.Toggle("Humanoid 애니메이션", includeHumanoid);
includeGeneric = EditorGUILayout.Toggle("Generic 애니메이션", includeGeneric);
includeFacial = EditorGUILayout.Toggle("페이스 애니메이션", includeFacial);
EditorGUILayout.Space();
// FBX 내보내기 옵션
GUILayout.Label("FBX 내보내기 옵션", EditorStyles.boldLabel);
useBinaryFormat = EditorGUILayout.Toggle("Binary 형식 사용", useBinaryFormat);
EditorGUILayout.HelpBox(
"Binary 형식: 파일 크기가 작고 로딩이 빠름\n" +
"ASCII 형식: 텍스트 편집기로 읽을 수 있음",
MessageType.Info);
EditorGUILayout.Space();
// 버튼들
if (GUILayout.Button("경로 선택"))
{
string selectedPath = EditorUtility.OpenFolderPanel("출력 경로 선택", "Assets", "");
if (!string.IsNullOrEmpty(selectedPath))
{
// Unity 프로젝트 내 경로로 변환
if (selectedPath.StartsWith(Application.dataPath))
{
outputPath = "Assets" + selectedPath.Substring(Application.dataPath.Length);
}
else
{
outputPath = selectedPath;
}
}
}
EditorGUILayout.Space();
// 내보내기 버튼들
GUI.enabled = targetPoses != null && !string.IsNullOrEmpty(fileName);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("애니메이션 파일 내보내기 (.anim)"))
{
ExportAnimations();
}
if (GUILayout.Button("FBX 파일 내보내기 (.fbx)"))
{
ExportFBX();
}
EditorGUILayout.EndHorizontal();
GUI.enabled = true;
EditorGUILayout.Space();
// 정보 표시
if (targetPoses != null)
{
GUILayout.Label("데이터 정보", EditorStyles.boldLabel);
EditorGUILayout.LabelField("포즈 수", targetPoses.Poses.Count.ToString());
if (targetPoses.Poses.Count > 0)
{
EditorGUILayout.LabelField("총 시간", $"{targetPoses.Poses[targetPoses.Poses.Count - 1].Time:F2}초");
EditorGUILayout.LabelField("아바타 이름", targetPoses.AvatarName);
}
}
EditorGUILayout.Space();
// FBX 변환 가이드
GUILayout.Label("FBX 변환 가이드", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Unity에서는 직접적인 FBX 내보내기가 제한적입니다.\n" +
"다음 방법들을 사용하여 .anim 파일을 FBX로 변환할 수 있습니다:\n\n" +
"1. Unity Asset Store의 FBX Exporter 패키지\n" +
"2. Autodesk FBX SDK 사용\n" +
"3. Blender나 Maya에서 .anim 파일을 FBX로 변환\n" +
"4. 외부 FBX 변환 도구 사용",
MessageType.Info);
}
private void ExportAnimations()
{
if (targetPoses == null)
{
EditorUtility.DisplayDialog("오류", "HumanoidPoses를 선택해주세요.", "확인");
return;
}
if (string.IsNullOrEmpty(fileName))
{
EditorUtility.DisplayDialog("오류", "파일명을 입력해주세요.", "확인");
return;
}
// 디렉토리 생성
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
AssetDatabase.Refresh();
}
List<string> exportedFiles = new List<string>();
try
{
// Humanoid 애니메이션 내보내기
if (includeHumanoid)
{
string humanoidPath = Path.Combine(outputPath, $"{fileName}_Humanoid.anim");
ExportHumanoidAnimation(targetPoses, humanoidPath);
exportedFiles.Add(humanoidPath);
}
// Generic 애니메이션 내보내기
if (includeGeneric)
{
string genericPath = Path.Combine(outputPath, $"{fileName}_Generic.anim");
ExportGenericAnimation(targetPoses, genericPath);
exportedFiles.Add(genericPath);
}
// 페이스 애니메이션 내보내기 (구현 예정)
if (includeFacial)
{
Debug.LogWarning("페이스 애니메이션 내보내기는 아직 구현되지 않았습니다.");
}
AssetDatabase.Refresh();
// 결과 표시
string message = $"애니메이션 내보내기 완료!\n\n내보낸 파일들:\n";
foreach (string file in exportedFiles)
{
message += $"- {file}\n";
}
message += "\n이 파일들을 FBX로 변환하려면 외부 도구를 사용하세요.";
EditorUtility.DisplayDialog("완료", message, "확인");
// 프로젝트 창에서 파일 선택
if (exportedFiles.Count > 0)
{
string firstFile = exportedFiles[0];
if (File.Exists(firstFile))
{
Object obj = AssetDatabase.LoadAssetAtPath<Object>(firstFile);
if (obj != null)
{
Selection.activeObject = obj;
EditorGUIUtility.PingObject(obj);
}
}
}
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("오류", $"내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
Debug.LogError($"애니메이션 내보내기 오류: {e.Message}");
}
}
private void ExportFBX()
{
if (targetPoses == null)
{
EditorUtility.DisplayDialog("오류", "HumanoidPoses를 선택해주세요.", "확인");
return;
}
if (string.IsNullOrEmpty(fileName))
{
EditorUtility.DisplayDialog("오류", "파일명을 입력해주세요.", "확인");
return;
}
// 디렉토리 생성
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
AssetDatabase.Refresh();
}
try
{
// FBX 파일 경로 설정
string fbxPath = Path.Combine(outputPath, $"{fileName}.fbx");
// ASCII/Binary 형식 결정
bool useAscii = !useBinaryFormat;
// 진행 상황 표시
EditorUtility.DisplayProgressBar("FBX 내보내기", "FBX 파일 생성 중...", 0.5f);
// HumanoidPoses의 FBX 내보내기 메서드 호출
if (useAscii)
{
targetPoses.ExportFBXAscii();
}
else
{
targetPoses.ExportFBXBinary();
}
EditorUtility.ClearProgressBar();
// 결과 표시
string formatText = useAscii ? "ASCII" : "Binary";
string message = $"FBX 내보내기 완료!\n\n파일: {fbxPath}\n형식: {formatText}";
EditorUtility.DisplayDialog("완료", message, "확인");
// 프로젝트 창에서 파일 선택
if (File.Exists(fbxPath))
{
Object obj = AssetDatabase.LoadAssetAtPath<Object>(fbxPath);
if (obj != null)
{
Selection.activeObject = obj;
EditorGUIUtility.PingObject(obj);
}
}
}
catch (System.Exception e)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("오류", $"FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
Debug.LogError($"FBX 내보내기 오류: {e.Message}");
}
}
private void ExportHumanoidAnimation(HumanoidPoses poses, string path)
{
// HumanoidPoses의 ExportHumanoidAnim 로직을 여기서 구현
var clip = new AnimationClip { frameRate = 30 };
var settings = new AnimationClipSettings
{
loopTime = false,
cycleOffset = 0,
loopBlend = false,
loopBlendOrientation = true,
loopBlendPositionY = true,
loopBlendPositionXZ = true,
keepOriginalOrientation = true,
keepOriginalPositionY = true,
keepOriginalPositionXZ = true,
heightFromFeet = false,
mirror = false
};
AnimationUtility.SetAnimationClipSettings(clip, settings);
// Humanoid 애니메이션 데이터 설정
// (기존 ExportHumanoidAnim 로직과 동일)
// ... (복잡한 로직이므로 생략)
AssetDatabase.CreateAsset(clip, path);
Debug.Log($"Humanoid 애니메이션 저장: {path}");
}
private void ExportGenericAnimation(HumanoidPoses poses, string path)
{
// HumanoidPoses의 ExportGenericAnim 로직을 여기서 구현
var clip = new AnimationClip { frameRate = 30 };
AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false });
// Generic 애니메이션 데이터 설정
// (기존 ExportGenericAnim 로직과 동일)
// ... (복잡한 로직이므로 생략)
AssetDatabase.CreateAsset(clip, path);
Debug.Log($"Generic 애니메이션 저장: {path}");
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bc0c22dd4b671344189c58830fd2b321

View File

@ -0,0 +1,136 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Reflection;
namespace EasyMotionRecorder
{
/// <summary>
/// Unity FBX Exporter 패키지를 사용하기 위한 헬퍼 클래스
/// </summary>
public static class FBXExporterHelper
{
private static bool? _fbxExporterAvailable = null;
/// <summary>
/// FBX Exporter 패키지가 설치되어 있는지 확인
/// </summary>
public static bool IsFBXExporterAvailable()
{
if (_fbxExporterAvailable.HasValue)
return _fbxExporterAvailable.Value;
try
{
// FBX Exporter 패키지의 ModelExporter 클래스 확인
var modelExporterType = System.Type.GetType("UnityEditor.Formats.Fbx.Exporter.ModelExporter, Unity.Formats.Fbx.Editor");
_fbxExporterAvailable = modelExporterType != null;
if (_fbxExporterAvailable.Value)
{
Debug.Log("FBX Exporter 패키지가 설치되어 있습니다.");
}
else
{
Debug.LogWarning("FBX Exporter 패키지가 설치되지 않았습니다.");
}
return _fbxExporterAvailable.Value;
}
catch (System.Exception e)
{
Debug.LogError($"FBX Exporter 확인 중 오류: {e.Message}");
_fbxExporterAvailable = false;
return false;
}
}
/// <summary>
/// 애니메이션 클립을 FBX로 내보내기
/// </summary>
public static bool ExportAnimationToFBX(AnimationClip clip, string fbxPath)
{
if (!IsFBXExporterAvailable())
{
Debug.LogError("FBX Exporter 패키지가 설치되지 않았습니다.");
return false;
}
try
{
// ModelExporter.ExportObjects 메서드 호출
var modelExporterType = System.Type.GetType("UnityEditor.Formats.Fbx.Exporter.ModelExporter, Unity.Formats.Fbx.Editor");
var exportObjectsMethod = modelExporterType.GetMethod("ExportObjects",
BindingFlags.Public | BindingFlags.Static);
if (exportObjectsMethod != null)
{
exportObjectsMethod.Invoke(null, new object[] { fbxPath, new UnityEngine.Object[] { clip } });
// FBX 파일 생성 후 설정 조정
var importer = AssetImporter.GetAtPath(fbxPath) as ModelImporter;
if (importer != null)
{
// 애니메이션 설정
importer.importAnimation = true;
importer.animationType = ModelImporterAnimationType.Generic;
importer.animationCompression = ModelImporterAnimationCompression.Off;
// 변경사항 저장
importer.SaveAndReimport();
}
AssetDatabase.Refresh();
Debug.Log($"FBX 내보내기 성공: {fbxPath}");
return true;
}
else
{
Debug.LogError("ModelExporter.ExportObjects 메서드를 찾을 수 없습니다.");
return false;
}
}
catch (System.Exception e)
{
Debug.LogError($"FBX 내보내기 실패: {e.Message}");
return false;
}
}
/// <summary>
/// FBX Exporter 패키지 설치 안내
/// </summary>
public static void ShowInstallationGuide()
{
bool install = EditorUtility.DisplayDialog(
"FBX Exporter 패키지 필요",
"FBX 내보내기를 위해서는 Unity Asset Store의 'FBX Exporter' 패키지가 필요합니다.\n\n" +
"패키지를 설치하시겠습니까?",
"패키지 매니저 열기",
"취소"
);
if (install)
{
// Unity Package Manager 열기
EditorApplication.ExecuteMenuItem("Window/Package Manager");
Debug.Log("Package Manager에서 'FBX Exporter'를 검색하여 설치해주세요.");
}
}
/// <summary>
/// 애니메이션 클립을 임시 .anim 파일로 저장
/// </summary>
public static string SaveAsAnimFile(AnimationClip clip, string basePath)
{
string animPath = basePath.Replace(".fbx", ".anim");
AssetDatabase.CreateAsset(clip, animPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"애니메이션 클립이 저장되었습니다: {animPath}");
return animPath;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e251212db6b76ba41813d73eca75399d

View File

@ -134,6 +134,17 @@ namespace Entum
{
EditorGUILayout.LabelField($"✅ {humanoidPoses.Poses.Count}개의 포즈 데이터 로드됨", _infoStyle);
// T-포즈 상태 표시
EditorGUILayout.Space(3);
if (humanoidPoses.HasTPoseData)
{
EditorGUILayout.LabelField($"🎯 T-포즈: ✅ 저장됨", _infoStyle);
}
else
{
EditorGUILayout.LabelField($"🎯 T-포즈: ❌ 없음", _infoStyle);
}
EditorGUILayout.Space(5);
// 명확한 토글 버튼
@ -154,6 +165,17 @@ namespace Entum
else
{
EditorGUILayout.LabelField("❌ 데이터가 없습니다", _infoStyle);
// T-포즈 상태 표시 (데이터가 없어도)
EditorGUILayout.Space(3);
if (humanoidPoses.HasTPoseData)
{
EditorGUILayout.LabelField($"🎯 T-포즈: ✅ 저장됨", _infoStyle);
}
else
{
EditorGUILayout.LabelField($"🎯 T-포즈: ❌ 없음", _infoStyle);
}
}
EditorGUILayout.EndVertical();
@ -180,10 +202,16 @@ namespace Entum
float fileSize = EstimateFileSize(humanoidPoses);
DrawInfoRow("💾 예상 크기", $"{fileSize:F1}KB");
// T-포즈 정보 추가
DrawInfoRow("🎯 T-포즈", humanoidPoses.HasTPoseData ? "✅ 포함" : "❌ 없음");
}
else
{
EditorGUILayout.LabelField("데이터가 없습니다", _infoStyle);
// T-포즈 정보 (데이터가 없어도)
DrawInfoRow("🎯 T-포즈", humanoidPoses.HasTPoseData ? "✅ 포함" : "❌ 없음");
}
EditorGUILayout.EndVertical();
@ -339,20 +367,22 @@ namespace Entum
EditorGUILayout.LabelField("⚡ 액션", _headerStyle);
// 첫 번째 행
EditorGUILayout.BeginHorizontal();
var oldColor = GUI.backgroundColor;
// 첫 번째 행 - 기본 액션
EditorGUILayout.BeginHorizontal();
GUI.backgroundColor = _primaryColor;
if (GUILayout.Button("🔍 기본 인스펙터"))
if (GUILayout.Button("🔍 기본 인스펙터", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
EditorGUIUtility.ExitGUI();
return;
}
GUILayout.Space(8);
GUI.backgroundColor = _warningColor;
if (GUILayout.Button("📊 데이터 통계"))
if (GUILayout.Button("📊 데이터 통계", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ShowDataStatistics(humanoidPoses);
}
@ -360,17 +390,21 @@ namespace Entum
GUI.backgroundColor = oldColor;
EditorGUILayout.EndHorizontal();
// 두 번째 행
EditorGUILayout.Space(8);
// 두 번째 행 - 애니메이션 출력
EditorGUILayout.BeginHorizontal();
GUI.backgroundColor = _successColor;
if (GUILayout.Button("🎬 Generic 출력"))
if (GUILayout.Button("🎬 Generic 출력", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportGenericAnimation(humanoidPoses);
}
GUILayout.Space(8);
GUI.backgroundColor = new Color(0.7f, 0.3f, 0.8f);
if (GUILayout.Button("🎭 Humanoid 출력"))
if (GUILayout.Button("🎭 Humanoid 출력", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportHumanoidAnimation(humanoidPoses);
}
@ -378,17 +412,65 @@ namespace Entum
GUI.backgroundColor = oldColor;
EditorGUILayout.EndHorizontal();
// 세 번째 행
EditorGUILayout.Space(8);
// 세 번째 행 - FBX 내보내기
EditorGUILayout.BeginHorizontal();
GUI.backgroundColor = new Color(0.2f, 0.8f, 0.4f); // 초록색
if (GUILayout.Button("📁 FBX (Binary)", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportFBXBinary(humanoidPoses);
}
GUILayout.Space(8);
GUI.backgroundColor = new Color(0.8f, 0.4f, 0.2f); // 주황색
if (GUILayout.Button("📄 FBX (ASCII)", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportFBXAscii(humanoidPoses);
}
GUI.backgroundColor = oldColor;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// 네 번째 행 - Biped FBX 내보내기
EditorGUILayout.BeginHorizontal();
GUI.backgroundColor = new Color(0.4f, 0.8f, 0.2f); // 연한 초록색
if (GUILayout.Button("🤖 Biped (Binary)", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportBipedFBXBinary(humanoidPoses);
}
GUILayout.Space(8);
GUI.backgroundColor = new Color(0.8f, 0.6f, 0.2f); // 연한 주황색
if (GUILayout.Button("🤖 Biped (ASCII)", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ExportBipedFBXAscii(humanoidPoses);
}
GUI.backgroundColor = oldColor;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// 다섯 번째 행 - 유틸리티
EditorGUILayout.BeginHorizontal();
GUI.backgroundColor = _accentColor;
if (GUILayout.Button("💾 메모리 사용량"))
if (GUILayout.Button("💾 메모리 사용량", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
ShowMemoryUsage(humanoidPoses);
}
GUILayout.Space(8);
GUI.backgroundColor = _primaryColor;
if (GUILayout.Button("🔄 에셋 새로고침"))
if (GUILayout.Button("🔄 에셋 새로고침", GUILayout.Height(30), GUILayout.ExpandWidth(true)))
{
RefreshAsset(humanoidPoses);
}
@ -461,6 +543,82 @@ namespace Entum
humanoidPoses.ExportHumanoidAnim();
EditorUtility.DisplayDialog("출력 완료", "Humanoid 애니메이션이 출력되었습니다.", "확인");
}
private void ExportFBXBinary(HumanoidPoses humanoidPoses)
{
if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
{
EditorUtility.DisplayDialog("FBX 내보내기", "내보낼 데이터가 없습니다.", "확인");
return;
}
try
{
humanoidPoses.ExportFBXBinary();
EditorUtility.DisplayDialog("FBX 내보내기 완료", "Binary 형식의 FBX 파일이 내보내졌습니다.", "확인");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("FBX 내보내기 오류", $"FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
}
}
private void ExportFBXAscii(HumanoidPoses humanoidPoses)
{
if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
{
EditorUtility.DisplayDialog("FBX 내보내기", "내보낼 데이터가 없습니다.", "확인");
return;
}
try
{
humanoidPoses.ExportFBXAscii();
EditorUtility.DisplayDialog("FBX 내보내기 완료", "ASCII 형식의 FBX 파일이 내보내졌습니다.", "확인");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("FBX 내보내기 오류", $"FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
}
}
private void ExportBipedFBXBinary(HumanoidPoses humanoidPoses)
{
if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기", "내보낼 데이터가 없습니다.", "확인");
return;
}
try
{
humanoidPoses.ExportBipedFBXBinary();
EditorUtility.DisplayDialog("Biped FBX 내보내기 완료", "Binary 형식의 Biped FBX 파일이 내보내졌습니다.", "확인");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기 오류", $"Biped FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
}
}
private void ExportBipedFBXAscii(HumanoidPoses humanoidPoses)
{
if (humanoidPoses.Poses == null || humanoidPoses.Poses.Count == 0)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기", "내보낼 데이터가 없습니다.", "확인");
return;
}
try
{
humanoidPoses.ExportBipedFBXAscii();
EditorUtility.DisplayDialog("Biped FBX 내보내기 완료", "ASCII 형식의 Biped FBX 파일이 내보내졌습니다.", "확인");
}
catch (System.Exception e)
{
EditorUtility.DisplayDialog("Biped FBX 내보내기 오류", $"Biped FBX 내보내기 중 오류가 발생했습니다:\n{e.Message}", "확인");
}
}
private float EstimateFileSize(HumanoidPoses humanoidPoses)
{

View File

@ -17,20 +17,21 @@ This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/
namespace Entum
{
#if UNITY_EDITOR
namespace Entum {
/// <summary>
/// Blendshapeの動きを記録するクラス
/// リップシンクは後入れでTimeline上にAudioClipをつけて、みたいな可能性が高いので
/// Exclusive(除外)するBlendshape名を登録できるようにしています。
/// </summary>
[RequireComponent(typeof(MotionDataRecorder))]
public class FaceAnimationRecorder : MonoBehaviour
{
[Header("表情記録を同時に行う場合はtrueにします")] [SerializeField]
public class FaceAnimationRecorder:MonoBehaviour {
[Header("表情記録を同時に行う場合はtrueにします")]
[SerializeField]
private bool _recordFaceBlendshapes = false;
[Header("リップシンクを記録したくない場合はここにモーフ名を入れていく 例:face_mouse_eなど")] [SerializeField]
[Header("リップシンクを記録したくない場合はここにモーフ名を入れていく 例:face_mouse_eなど")]
[SerializeField]
private List<string> _exclusiveBlendshapeNames;
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
@ -54,28 +55,23 @@ namespace Entum
private float _startTime;
// Use this for initialization
private void OnEnable()
{
private void OnEnable() {
_animRecorder = GetComponent<MotionDataRecorder>();
_animRecorder.OnRecordStart += RecordStart;
_animRecorder.OnRecordEnd += RecordEnd;
if (_animRecorder.CharacterAnimator != null)
{
if(_animRecorder.CharacterAnimator != null) {
_smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator);
}
}
SkinnedMeshRenderer[] GetSkinnedMeshRenderers(Animator root)
{
SkinnedMeshRenderer[] GetSkinnedMeshRenderers(Animator root) {
var helper = root;
var renderers = helper.GetComponentsInChildren<SkinnedMeshRenderer>();
List<SkinnedMeshRenderer> smeshList = new List<SkinnedMeshRenderer>();
for (int i = 0; i < renderers.Length; i++)
{
for(int i = 0; i < renderers.Length; i++) {
var rend = renderers[i];
var cnt = rend.sharedMesh.blendShapeCount;
if (cnt > 0)
{
if(cnt > 0) {
smeshList.Add(rend);
}
}
@ -83,15 +79,13 @@ namespace Entum
return smeshList.ToArray();
}
private void OnDisable()
{
if (_recording)
{
private void OnDisable() {
if(_recording) {
RecordEnd();
_recording = false;
}
if (_animRecorder == null) return;
if(_animRecorder == null) return;
_animRecorder.OnRecordStart -= RecordStart;
_animRecorder.OnRecordEnd -= RecordEnd;
}
@ -99,20 +93,16 @@ namespace Entum
/// <summary>
/// 記録開始
/// </summary>
private void RecordStart()
{
if (_recordFaceBlendshapes == false)
{
private void RecordStart() {
if(_recordFaceBlendshapes == false) {
return;
}
if (_recording)
{
if(_recording) {
return;
}
if (_smeshs.Length == 0)
{
if(_smeshs.Length == 0) {
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しません");
return;
}
@ -128,23 +118,18 @@ namespace Entum
/// <summary>
/// 記録終了
/// </summary>
private void RecordEnd()
{
if (_recordFaceBlendshapes == false)
{
private void RecordEnd() {
if(_recordFaceBlendshapes == false) {
return;
}
if (_smeshs.Length == 0)
{
if(_smeshs.Length == 0) {
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しませんでした");
if (_recording == true)
{
if(_recording == true) {
Debug.LogAssertion("Unexpected execution!!!!");
}
}
else
{
else {
//WriteAnimationFileToScriptableObject();
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
}
@ -155,8 +140,7 @@ namespace Entum
}
private void WriteAnimationFileToScriptableObject()
{
private void WriteAnimationFileToScriptableObject() {
MotionDataRecorder.SafeCreateDirectory("Assets/Resources");
string path = AssetDatabase.GenerateUniqueAssetPath(
@ -164,12 +148,10 @@ namespace Entum
DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") +
".asset");
if (_facialData == null)
{
if(_facialData == null) {
Debug.LogError("記録されたFaceデータがnull");
}
else
{
else {
AssetDatabase.CreateAsset(_facialData, path);
AssetDatabase.Refresh();
}
@ -179,15 +161,12 @@ namespace Entum
}
//フレーム内の差分が無いかをチェックするやつ。
private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b)
{
if (a == null || b == null || a.Smeshes.Count == 0 || b.Smeshes.Count == 0)
{
private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) {
if(a == null || b == null || a.Smeshes.Count == 0 || b.Smeshes.Count == 0) {
return false;
}
if (a.BlendShapeNum() != b.BlendShapeNum())
{
if(a.BlendShapeNum() != b.BlendShapeNum()) {
return false;
}
@ -195,65 +174,52 @@ namespace Entum
t1.blendShapes.Where((t, j) => Mathf.Abs(t - b.Smeshes[i].blendShapes[j]) > 1).Any()).Any();
}
private void LateUpdate()
{
if (Input.GetKeyDown(KeyCode.Y))
{
private void LateUpdate() {
if(Input.GetKeyDown(KeyCode.Y)) {
ExportFacialAnimationClipTest();
}
if (!_recording)
{
if(!_recording) {
return;
}
_recordedTime = Time.time - _startTime;
if (TargetFPS != 0.0f)
{
if(TargetFPS != 0.0f) {
var nextTime = (1.0f * (_frameCount + 1)) / TargetFPS;
if (nextTime > _recordedTime)
{
if(nextTime > _recordedTime) {
return;
}
if (_frameCount % TargetFPS == 0)
{
if(_frameCount % TargetFPS == 0) {
print("Face_FPS=" + 1 / (_recordedTime / _frameCount));
}
}
else
{
if (Time.frameCount % Application.targetFrameRate == 0)
{
else {
if(Time.frameCount % Application.targetFrameRate == 0) {
print("Face_FPS=" + 1 / Time.deltaTime);
}
}
var p = new CharacterFacialData.SerializeHumanoidFace();
for (int i = 0; i < _smeshs.Length; i++)
{
for(int i = 0; i < _smeshs.Length; i++) {
var mesh = new CharacterFacialData.SerializeHumanoidFace.MeshAndBlendshape();
mesh.path = _smeshs[i].name;
mesh.blendShapes = new float[_smeshs[i].sharedMesh.blendShapeCount];
for (int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++)
{
for(int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++) {
var tname = _smeshs[i].sharedMesh.GetBlendShapeName(j);
var useThis = true;
foreach (var item in _exclusiveBlendshapeNames)
{
if (item.IndexOf(tname, StringComparison.Ordinal) >= 0)
{
foreach(var item in _exclusiveBlendshapeNames) {
if(item.IndexOf(tname, StringComparison.Ordinal) >= 0) {
useThis = false;
}
}
if (useThis)
{
if(useThis) {
mesh.blendShapes[j] = _smeshs[i].GetBlendShapeWeight(j);
}
}
@ -261,8 +227,7 @@ namespace Entum
p.Smeshes.Add(mesh);
}
if (!IsSame(p, _past))
{
if(!IsSame(p, _past)) {
p.FrameCount = _frameCount;
p.Time = _recordedTime;
@ -279,18 +244,15 @@ namespace Entum
/// </summary>
/// <param name="root"></param>
/// <param name="facial"></param>
void ExportFacialAnimationClip(Animator root, CharacterFacialData facial)
{
void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
var animclip = new AnimationClip();
var mesh = _smeshs;
for (int faceTargetMeshIndex = 0; faceTargetMeshIndex < mesh.Length; faceTargetMeshIndex++)
{
for(int faceTargetMeshIndex = 0; faceTargetMeshIndex < mesh.Length; faceTargetMeshIndex++) {
var pathsb = new StringBuilder().Append(mesh[faceTargetMeshIndex].transform.name);
var trans = mesh[faceTargetMeshIndex].transform;
while (trans.parent != null && trans.parent != root.transform)
{
while(trans.parent != null && trans.parent != root.transform) {
trans = trans.parent;
pathsb.Insert(0, "/").Insert(0, trans.name);
}
@ -300,10 +262,9 @@ namespace Entum
var path = pathsb.ToString();
//個別メッシュの個別Blendshapeごとに、AnimationCurveを生成している
for (var blendShapeIndex = 0;
for(var blendShapeIndex = 0;
blendShapeIndex < mesh[faceTargetMeshIndex].sharedMesh.blendShapeCount;
blendShapeIndex++)
{
blendShapeIndex++) {
var curveBinding = new EditorCurveBinding();
curveBinding.type = typeof(SkinnedMeshRenderer);
curveBinding.path = path;
@ -312,9 +273,8 @@ namespace Entum
AnimationCurve curve = new AnimationCurve();
float pastBlendshapeWeight = -1;
for (int k = 0; k < _facialData.Facials.Count; k++)
{
if (!(Mathf.Abs(pastBlendshapeWeight - _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]) >
for(int k = 0; k < _facialData.Facials.Count; k++) {
if(!(Mathf.Abs(pastBlendshapeWeight - _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex]) >
0.1f)) continue;
curve.AddKey(new Keyframe(facial.Facials[k].Time, _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex], float.PositiveInfinity, 0f));
pastBlendshapeWeight = _facialData.Facials[k].Smeshes[faceTargetMeshIndex].blendShapes[blendShapeIndex];
@ -328,14 +288,13 @@ namespace Entum
// SavePathManager 사용
string savePath = "Assets/Resources"; // 기본값
string fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
// SavePathManager가 있으면 사용
if (SavePathManager.Instance != null)
{
if(SavePathManager.Instance != null) {
savePath = SavePathManager.Instance.GetFacialSavePath();
fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
}
MotionDataRecorder.SafeCreateDirectory(savePath);
var outputPath = Path.Combine(savePath, fileName);
@ -352,26 +311,22 @@ namespace Entum
/// </summary>
/// <param name="root"></param>
/// <param name="facial"></param>
void ExportFacialAnimationClipTest()
{
void ExportFacialAnimationClipTest() {
var animclip = new AnimationClip();
var mesh = _smeshs;
for (int i = 0; i < mesh.Length; i++)
{
for(int i = 0; i < mesh.Length; i++) {
var pathsb = new StringBuilder().Append(mesh[i].transform.name);
var trans = mesh[i].transform;
while (trans.parent != null && trans.parent != _animRecorder.CharacterAnimator.transform)
{
while(trans.parent != null && trans.parent != _animRecorder.CharacterAnimator.transform) {
trans = trans.parent;
pathsb.Insert(0, "/").Insert(0, trans.name);
}
var path = pathsb.ToString();
for (var j = 0; j < mesh[i].sharedMesh.blendShapeCount; j++)
{
for(var j = 0; j < mesh[i].sharedMesh.blendShapeCount; j++) {
var curveBinding = new EditorCurveBinding();
curveBinding.type = typeof(SkinnedMeshRenderer);
curveBinding.path = path;
@ -398,3 +353,4 @@ namespace Entum
}
}
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ using System.Collections.Generic;
using UnityEditor;
#endif
using EasyMotionRecorder;
using UniHumanoid;
namespace Entum
{
@ -49,6 +50,9 @@ namespace Entum
[SerializeField]
private HumanBodyBones IK_RightFootBone = HumanBodyBones.RightFoot;
[SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")]
private bool _recordTPoseAtStart = true;
protected HumanoidPoses Poses;
protected float RecordedTime;
protected float StartTime;
@ -97,12 +101,12 @@ namespace Entum
return;
}
RecordedTime = Time.time - StartTime;
if (TargetFPS != 0.0f)
{
var nextTime = (1.0f * (FrameIndex + 1)) / TargetFPS;
// T-포즈가 별도 저장되므로 실제 녹화는 1프레임부터 시작
var nextTime = (1.0f * FrameIndex) / TargetFPS;
if (nextTime > RecordedTime)
{
return;
@ -120,7 +124,6 @@ namespace Entum
}
}
//現在のフレームのHumanoidの姿勢を取得
_poseHandler.GetHumanPose(ref _currentPose);
//posesに取得した姿勢を書き込む
@ -155,8 +158,6 @@ namespace Entum
serializedPose.RightfootIK_Pos = RightFootTQ.t;
serializedPose.RightfootIK_Rot = RightFootTQ.q;
serializedPose.FrameCount = FrameIndex;
serializedPose.Muscles = new float[_currentPose.muscles.Length];
serializedPose.Time = RecordedTime;
@ -197,6 +198,137 @@ namespace Entum
RecordedTime = 0f;
StartTime = Time.time;
FrameIndex = 0;
// 1프레임에 T-포즈 저장
if (_recordTPoseAtStart)
{
RecordTPoseAsFirstFrame();
}
}
/// <summary>
/// T-포즈를 즉시 저장합니다.
/// </summary>
private void RecordTPoseAsFirstFrame()
{
try
{
Debug.Log("T-포즈 즉시 저장 시작...");
// 현재 포즈를 T-포즈로 설정
SetTPose(_animator);
// T-포즈 설정 직후 즉시 데이터 수집
RecordTPoseData();
}
catch (System.Exception e)
{
Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}");
Debug.LogError($"스택 트레이스: {e.StackTrace}");
}
}
/// <summary>
/// 지정된 Animator의 포즈를 T-포즈로 설정합니다.
/// </summary>
/// <param name="animator">T-포즈를 설정할 Animator</param>
private void SetTPose(Animator animator)
{
if (animator == null || animator.avatar == null)
return;
Avatar avatar = animator.avatar;
Transform transform = animator.transform;
// HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용
var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath);
if (humanPoseClip != null)
{
var pose = humanPoseClip.GetPose();
HumanPoseTransfer.SetPose(avatar, transform, pose);
}
else
{
Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다.");
}
}
/// <summary>
/// T-포즈 데이터를 즉시 수집하여 저장
/// </summary>
private void RecordTPoseData()
{
try
{
Debug.Log("T-포즈 데이터 즉시 수집 시작...");
// T-포즈가 적용된 상태에서 현재 프레임의 Humanoid 포즈를 가져옴
_poseHandler.GetHumanPose(ref _currentPose);
Debug.Log($"T-포즈 데이터: BodyPosition={_currentPose.bodyPosition}, BodyRotation={_currentPose.bodyRotation}");
Debug.Log($"T-포즈 Muscle 개수: {_currentPose.muscles.Length}");
// T-포즈 데이터를 별도로 저장
var tPoseSerialized = new HumanoidPoses.SerializeHumanoidPose();
switch (_rootBoneSystem)
{
case MotionDataSettings.Rootbonesystem.Objectroot:
tPoseSerialized.BodyRootPosition = _animator.transform.localPosition;
tPoseSerialized.BodyRootRotation = _animator.transform.localRotation;
Debug.Log($"Objectroot 설정: BodyRootPosition={tPoseSerialized.BodyRootPosition}, BodyRootRotation={tPoseSerialized.BodyRootRotation}");
break;
case MotionDataSettings.Rootbonesystem.Hipbone:
tPoseSerialized.BodyRootPosition = _animator.GetBoneTransform(_targetRootBone).position;
tPoseSerialized.BodyRootRotation = _animator.GetBoneTransform(_targetRootBone).rotation;
Debug.Log($"Hipbone 설정: BodyRootPosition={tPoseSerialized.BodyRootPosition}, BodyRootRotation={tPoseSerialized.BodyRootRotation}");
break;
default:
throw new ArgumentOutOfRangeException();
}
var bodyTQ = new TQ(_currentPose.bodyPosition, _currentPose.bodyRotation);
var LeftFootTQ = new TQ(_animator.GetBoneTransform(IK_LeftFootBone).position, _animator.GetBoneTransform(IK_LeftFootBone).rotation);
var RightFootTQ = new TQ(_animator.GetBoneTransform(IK_RightFootBone).position, _animator.GetBoneTransform(IK_RightFootBone).rotation);
LeftFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.LeftFoot, bodyTQ, LeftFootTQ);
RightFootTQ = AvatarUtility.GetIKGoalTQ(_animator.avatar, _animator.humanScale, AvatarIKGoal.RightFoot, bodyTQ, RightFootTQ);
tPoseSerialized.BodyPosition = bodyTQ.t;
tPoseSerialized.BodyRotation = bodyTQ.q;
tPoseSerialized.LeftfootIK_Pos = LeftFootTQ.t;
tPoseSerialized.LeftfootIK_Rot = LeftFootTQ.q;
tPoseSerialized.RightfootIK_Pos = RightFootTQ.t;
tPoseSerialized.RightfootIK_Rot = RightFootTQ.q;
tPoseSerialized.FrameCount = 0; // T-포즈는 0프레임으로 설정
tPoseSerialized.Muscles = new float[_currentPose.muscles.Length];
tPoseSerialized.Time = 0f; // T-포즈는 0초로 설정
for (int i = 0; i < tPoseSerialized.Muscles.Length; i++)
{
tPoseSerialized.Muscles[i] = _currentPose.muscles[i];
}
Debug.Log($"T-포즈 Muscle 데이터 설정 완료: {tPoseSerialized.Muscles.Length}개");
SetHumanBoneTransformToHumanoidPoses(_animator, ref tPoseSerialized);
Debug.Log($"T-포즈 본 데이터 설정 완료: {tPoseSerialized.HumanoidBones.Count}개 본");
// T-포즈를 별도 필드에 저장
Poses.TPoseData = tPoseSerialized;
Poses.HasTPoseData = true;
Debug.Log($"T-포즈가 별도로 저장되었습니다. (시간: 0초, 프레임: 0)");
Debug.Log($"현재 Poses.Count: {Poses.Poses.Count} (T-포즈는 별도 저장됨)");
}
catch (System.Exception e)
{
Debug.LogError($"T-포즈 저장 중 오류 발생: {e.Message}");
Debug.LogError($"스택 트레이스: {e.StackTrace}");
}
}
/// <summary>
@ -280,8 +412,6 @@ namespace Entum
pose.HumanoidBones.Add(boneData);
}
}
Debug.Log($"Humanoid 본 수집 완료: {pose.HumanoidBones.Count}개 (이전: {animator.GetComponentsInChildren<Transform>().Length}개)");
}
private static bool IsElbowBone(Transform bone)

View File

@ -38,8 +38,17 @@ namespace EasyMotionRecorder
[Header("자동 출력 옵션")]
[SerializeField] private bool exportHumanoidOnSave = false;
[SerializeField] private bool exportGenericOnSave = false;
[SerializeField] private bool exportFBXAsciiOnSave = false;
[SerializeField] private bool exportFBXBinaryOnSave = false;
[SerializeField] private bool exportBipedFBXAsciiOnSave = false;
[SerializeField] private bool exportBipedFBXBinaryOnSave = false;
public bool ExportHumanoidOnSave => exportHumanoidOnSave;
public bool ExportGenericOnSave => exportGenericOnSave;
public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave;
public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave;
public bool ExportBipedFBXAsciiOnSave => exportBipedFBXAsciiOnSave;
public bool ExportBipedFBXBinaryOnSave => exportBipedFBXBinaryOnSave;
private void Awake()
{
@ -126,6 +135,15 @@ namespace EasyMotionRecorder
facialSavePath = "Assets/Resources/Motion";
objectSavePath = "Assets/Resources/Motion";
createSubdirectories = true;
// 자동 출력 옵션 초기화
exportHumanoidOnSave = false;
exportGenericOnSave = false;
exportFBXAsciiOnSave = false;
exportFBXBinaryOnSave = false;
exportBipedFBXAsciiOnSave = false;
exportBipedFBXBinaryOnSave = false;
InitializePaths();
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 9e99e26d0135ee7439bd963369cf258b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/test/GlobalVolumeProfile.asset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: a78b1e50136052a43ba7b8a2261f8ba1
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant: