diff --git a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs index 6979ec89..df778aba 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/FaceAnimationRecorder.cs @@ -177,7 +177,12 @@ namespace Entum { clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value); } - string fileName = $"{facial.SessionID}_Facial"; + // 캐릭터 이름 가져오기 + string characterName = GetCharacterName(); + string fileName = string.IsNullOrEmpty(characterName) + ? $"{facial.SessionID}_Facial" + : $"{facial.SessionID}_{characterName}_Facial"; + string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".anim"); // 인스턴스별 고유 경로 생성 @@ -193,6 +198,75 @@ namespace Entum { #endif } + private string GetCharacterName() + { + if (_animRecorder?.CharacterAnimator == null) return ""; + + var animator = _animRecorder.CharacterAnimator; + + // 1. GameObject 이름 사용 + string objectName = animator.gameObject.name; + + // 2. Avatar 이름이 있으면 우선 사용 + if (animator.avatar != null && !string.IsNullOrEmpty(animator.avatar.name)) + { + string avatarName = animator.avatar.name; + // "Avatar" 접미사 제거 + if (avatarName.EndsWith("Avatar")) + { + avatarName = avatarName.Substring(0, avatarName.Length - 6); + } + if (!string.IsNullOrEmpty(avatarName)) + { + return SanitizeFileName(avatarName); + } + } + + // 3. 부모 오브젝트에서 캐릭터 루트 찾기 + Transform current = animator.transform.parent; + while (current != null) + { + // VRM, humanoid, character 등의 키워드가 있는 경우 + string parentName = current.name.ToLower(); + if (parentName.Contains("character") || parentName.Contains("humanoid") || + parentName.Contains("avatar") || parentName.Contains("vrm")) + { + return SanitizeFileName(current.name); + } + current = current.parent; + } + + // 4. GameObject 이름에서 불필요한 부분 제거 + objectName = objectName.Replace("(Clone)", "").Trim(); + return SanitizeFileName(objectName); + } + + private string SanitizeFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) return ""; + + // 파일명에 사용할 수 없는 문자 제거 + char[] invalidChars = Path.GetInvalidFileNameChars(); + foreach (char c in invalidChars) + { + fileName = fileName.Replace(c, '_'); + } + + // 공백을 언더스코어로 변경 + fileName = fileName.Replace(' ', '_'); + + // 연속된 언더스코어 제거 + while (fileName.Contains("__")) + { + fileName = fileName.Replace("__", "_"); + } + + // 앞뒤 언더스코어 제거 + fileName = fileName.Trim('_'); + + return fileName; + } + private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) { if(a.BlendShapeNames.Count != b.BlendShapeNames.Count) { return false; diff --git a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs index 93ab5d44..ea673881 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/HumanoidPoses.cs @@ -112,18 +112,14 @@ namespace Entum // 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도 if (!string.IsNullOrEmpty(this.name)) { - // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_abc12345_Motion) + // 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_Motion_abc12345) var nameParts = this.name.Split('_'); - if (nameParts.Length >= 3) + if (nameParts.Length >= 2) { // 첫 번째 두 부분이 날짜와 시간인지 확인 if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 { string sessionID = $"{nameParts[0]}_{nameParts[1]}"; - if (nameParts.Length > 2) - { - sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함 - } Debug.Log($"스크립터블 오브젝트 이름에서 세션 ID 추출: {sessionID}"); return sessionID; } @@ -136,15 +132,11 @@ namespace Entum { string fileName = Path.GetFileNameWithoutExtension(assetPath); var nameParts = fileName.Split('_'); - if (nameParts.Length >= 3) + if (nameParts.Length >= 2) { if (nameParts[0].Length == 6 && nameParts[1].Length == 6) // yyMMdd_HHmmss 형식 { string sessionID = $"{nameParts[0]}_{nameParts[1]}"; - if (nameParts.Length > 2) - { - sessionID += "_" + nameParts[2]; // 인스턴스 ID 포함 - } Debug.Log($"에셋 파일명에서 세션 ID 추출: {sessionID}"); return sessionID; } @@ -309,9 +301,32 @@ namespace Entum // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; + // 인스턴스 ID 디버깅 로그 + Debug.Log($"출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})"); + + // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 + string currentInstanceID = InstanceID; + if (string.IsNullOrEmpty(currentInstanceID)) + { + var savePathManager = FindObjectOfType(); + if (savePathManager != null) + { + currentInstanceID = savePathManager.InstanceID; + Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); + } + else + { + Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다."); + } + } + + // 인스턴스 ID 포함 파일명 생성 + string fileName = !string.IsNullOrEmpty(currentInstanceID) + ? $"{sessionID}_{avatarName}_Generic_{currentInstanceID}.anim" + : $"{sessionID}_{avatarName}_Generic.anim"; + // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 - string fileName = $"{sessionID}_{avatarName}_Generic.anim"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); @@ -657,9 +672,32 @@ namespace Entum // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; + // 인스턴스 ID 디버깅 로그 + Debug.Log($"Humanoid 출력 시 인스턴스 ID 상태: '{InstanceID}' (비어있음: {string.IsNullOrEmpty(InstanceID)})"); + + // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 + string currentInstanceID = InstanceID; + if (string.IsNullOrEmpty(currentInstanceID)) + { + var savePathManager = FindObjectOfType(); + if (savePathManager != null) + { + currentInstanceID = savePathManager.InstanceID; + Debug.Log($"SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); + } + else + { + Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 파일 생성됩니다."); + } + } + + // 인스턴스 ID 포함 파일명 생성 + string fileName = !string.IsNullOrEmpty(currentInstanceID) + ? $"{sessionID}_{avatarName}_Humanoid_{currentInstanceID}.anim" + : $"{sessionID}_{avatarName}_Humanoid.anim"; + // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 - string fileName = $"{sessionID}_{avatarName}_Humanoid.anim"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); @@ -721,9 +759,29 @@ namespace Entum string sessionID = GetSessionID(); string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; + // 인스턴스 ID가 비어있으면 현재 씬의 SavePathManager에서 가져오기 + string currentInstanceID = InstanceID; + if (string.IsNullOrEmpty(currentInstanceID)) + { + var savePathManager = FindObjectOfType(); + if (savePathManager != null) + { + currentInstanceID = savePathManager.InstanceID; + Debug.Log($"FBX 출력 시 SavePathManager에서 인스턴스 ID 가져옴: '{currentInstanceID}'"); + } + else + { + Debug.LogWarning("SavePathManager를 찾을 수 없습니다. 인스턴스 ID 없이 FBX 파일 생성됩니다."); + } + } + // 저장 경로 결정 string savePath = "Assets/Resources"; - string fileName = $"{sessionID}_{avatarName}_Motion.fbx"; + + // 인스턴스 ID 포함 파일명 생성 + string fileName = !string.IsNullOrEmpty(currentInstanceID) + ? $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}_{currentInstanceID}.fbx" + : $"{sessionID}_{avatarName}_Motion_{(useAscii ? "ASCII" : "Binary")}.fbx"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); diff --git a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs index 38ec6e6d..14d0080a 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/MotionDataRecorder.cs @@ -214,13 +214,14 @@ namespace Entum return; } - // 세션 ID 생성 (인스턴스별 고유) - SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID; + // 세션 ID 생성 (인스턴스 ID 제외) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); Poses = ScriptableObject.CreateInstance(); Poses.AvatarName = _animator.name; Poses.Poses = new List(); Poses.SessionID = SessionID; + Poses.InstanceID = instanceID; // 인스턴스 ID 설정 RecordedTime = 0f; StartTime = Time.time; @@ -229,11 +230,6 @@ namespace Entum Debug.Log($"모션 녹화 시작 - 인스턴스: {instanceID}, 세션: {SessionID}"); - if (_recordTPoseAtStart) - { - RecordTPoseAsFirstFrame(); - } - OnRecordStart?.Invoke(); } @@ -367,7 +363,12 @@ namespace Entum UpdateSummaryInfo(); - string fileName = $"{SessionID}_Motion"; + // 캐릭터 이름 가져오기 + string characterName = GetCharacterName(); + string fileName = string.IsNullOrEmpty(characterName) + ? $"{SessionID}_Motion" + : $"{SessionID}_{characterName}_Motion"; + string filePath = Path.Combine(_savePathManager.GetMotionSavePath(), fileName + ".asset"); // 인스턴스별 고유 경로 생성 @@ -407,6 +408,73 @@ namespace Entum #endif } + private string GetCharacterName() + { + if (_animator == null) return ""; + + // 1. GameObject 이름 사용 + string objectName = _animator.gameObject.name; + + // 2. Avatar 이름이 있으면 우선 사용 + if (_animator.avatar != null && !string.IsNullOrEmpty(_animator.avatar.name)) + { + string avatarName = _animator.avatar.name; + // "Avatar" 접미사 제거 + if (avatarName.EndsWith("Avatar")) + { + avatarName = avatarName.Substring(0, avatarName.Length - 6); + } + if (!string.IsNullOrEmpty(avatarName)) + { + return SanitizeFileName(avatarName); + } + } + + // 3. 부모 오브젝트에서 캐릭터 루트 찾기 + Transform current = _animator.transform.parent; + while (current != null) + { + // VRM, humanoid, character 등의 키워드가 있는 경우 + string parentName = current.name.ToLower(); + if (parentName.Contains("character") || parentName.Contains("humanoid") || + parentName.Contains("avatar") || parentName.Contains("vrm")) + { + return SanitizeFileName(current.name); + } + current = current.parent; + } + + // 4. GameObject 이름에서 불필요한 부분 제거 + objectName = objectName.Replace("(Clone)", "").Trim(); + return SanitizeFileName(objectName); + } + + private string SanitizeFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) return ""; + + // 파일명에 사용할 수 없는 문자 제거 + char[] invalidChars = Path.GetInvalidFileNameChars(); + foreach (char c in invalidChars) + { + fileName = fileName.Replace(c, '_'); + } + + // 공백을 언더스코어로 변경 + fileName = fileName.Replace(' ', '_'); + + // 연속된 언더스코어 제거 + while (fileName.Contains("__")) + { + fileName = fileName.Replace("__", "_"); + } + + // 앞뒤 언더스코어 제거 + fileName = fileName.Trim('_'); + + return fileName; + } + #if UNITY_EDITOR private void ExportHumanoidAnimation(string baseFileName) { @@ -415,9 +483,16 @@ namespace Entum string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Humanoid.anim"); animPath = _savePathManager.GetInstanceSpecificPath(animPath); - // HumanoidPoses의 기존 내보내기 메서드 사용 - Poses.ExportHumanoidAnim(); - Debug.Log($"휴머노이드 애니메이션 출력 완료: {baseFileName}_Humanoid"); + // 직접 휴머노이드 애니메이션 클립 생성 + var clip = CreateHumanoidAnimationClip(); + if (clip != null) + { + SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath)); + AssetDatabase.CreateAsset(clip, animPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + Debug.Log($"휴머노이드 애니메이션 출력 완료: {animPath}"); + } } catch (System.Exception e) { @@ -432,9 +507,16 @@ namespace Entum string animPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_Generic.anim"); animPath = _savePathManager.GetInstanceSpecificPath(animPath); - // HumanoidPoses의 기존 내보내기 메서드 사용 - Poses.ExportGenericAnim(); - Debug.Log($"제네릭 애니메이션 출력 완료: {baseFileName}_Generic"); + // 직접 제네릭 애니메이션 클립 생성 + var clip = CreateGenericAnimationClip(); + if (clip != null) + { + SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(animPath)); + AssetDatabase.CreateAsset(clip, animPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + Debug.Log($"제네릭 애니메이션 출력 완료: {animPath}"); + } } catch (System.Exception e) { @@ -446,7 +528,10 @@ namespace Entum { try { - // HumanoidPoses의 기존 FBX 내보내기 메서드 사용 + string fbxPath = Path.Combine(_savePathManager.GetMotionSavePath(), $"{baseFileName}_{(ascii ? "ASCII" : "Binary")}.fbx"); + fbxPath = _savePathManager.GetInstanceSpecificPath(fbxPath); + + // FBX 출력은 HumanoidPoses의 기존 메서드 사용 (경로 지정 불가) if (ascii) { Poses.ExportFBXAscii(); @@ -462,6 +547,115 @@ namespace Entum Debug.LogError($"FBX 애니메이션 출력 실패: {e.Message}"); } } + + private AnimationClip CreateHumanoidAnimationClip() + { + if (Poses == null || Poses.Poses.Count == 0) return null; + + var clip = new AnimationClip { frameRate = 30 }; + + // Humanoid 애니메이션 설정 + 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, + hasAdditiveReferencePose = false, + additiveReferencePoseTime = 0 + }; + AnimationUtility.SetAnimationClipSettings(clip, settings); + + // Muscles 데이터를 커브로 변환 + var muscleCurves = new AnimationCurve[HumanTrait.MuscleCount]; + for (int i = 0; i < HumanTrait.MuscleCount; i++) + { + muscleCurves[i] = new AnimationCurve(); + } + + foreach (var pose in Poses.Poses) + { + if (pose.Muscles != null && pose.Muscles.Length == HumanTrait.MuscleCount) + { + for (int i = 0; i < HumanTrait.MuscleCount; i++) + { + muscleCurves[i].AddKey(pose.Time, pose.Muscles[i]); + } + } + } + + // 커브를 애니메이션 클립에 적용 + for (int i = 0; i < HumanTrait.MuscleCount; i++) + { + string muscleName = HumanTrait.MuscleName[i]; + clip.SetCurve("", typeof(Animator), muscleName, muscleCurves[i]); + } + + return clip; + } + + private AnimationClip CreateGenericAnimationClip() + { + if (Poses == null || Poses.Poses.Count == 0) return null; + + var clip = new AnimationClip { frameRate = 30 }; + + // 본별 커브 생성 + var boneCurves = new Dictionary>(); + + foreach (var pose in Poses.Poses) + { + if (pose.HumanoidBones != null) + { + foreach (var bone in pose.HumanoidBones) + { + if (!boneCurves.ContainsKey(bone.Name)) + { + boneCurves[bone.Name] = new Dictionary + { + ["localPosition.x"] = new AnimationCurve(), + ["localPosition.y"] = new AnimationCurve(), + ["localPosition.z"] = new AnimationCurve(), + ["localRotation.x"] = new AnimationCurve(), + ["localRotation.y"] = new AnimationCurve(), + ["localRotation.z"] = new AnimationCurve(), + ["localRotation.w"] = new AnimationCurve() + }; + } + + var curves = boneCurves[bone.Name]; + curves["localPosition.x"].AddKey(pose.Time, bone.LocalPosition.x); + curves["localPosition.y"].AddKey(pose.Time, bone.LocalPosition.y); + curves["localPosition.z"].AddKey(pose.Time, bone.LocalPosition.z); + curves["localRotation.x"].AddKey(pose.Time, bone.LocalRotation.x); + curves["localRotation.y"].AddKey(pose.Time, bone.LocalRotation.y); + curves["localRotation.z"].AddKey(pose.Time, bone.LocalRotation.z); + curves["localRotation.w"].AddKey(pose.Time, bone.LocalRotation.w); + } + } + } + + // 커브를 애니메이션 클립에 적용 + foreach (var bonePair in boneCurves) + { + string boneName = bonePair.Key; + var curves = bonePair.Value; + + foreach (var curvePair in curves) + { + clip.SetCurve(boneName, typeof(Transform), curvePair.Key, curvePair.Value); + } + } + + return clip; + } #endif private void UpdateSummaryInfo() diff --git a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs index 9016f446..0b0f1fa8 100644 --- a/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs +++ b/Assets/External/EasyMotionRecorder/Scripts/ObjectMotionRecorder.cs @@ -144,8 +144,8 @@ namespace Entum return; } - // 세션 ID 생성 (인스턴스별 고유) - SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID; + // 세션 ID 생성 (인스턴스 ID 제외) + SessionID = DateTime.Now.ToString("yyMMdd_HHmmss"); // 데이터 초기화 objectClips = new Dictionary(); @@ -227,8 +227,8 @@ namespace Entum // Quaternion 연속성 보장 clip.EnsureQuaternionContinuity(); - // 파일명 생성 - string objectName = target.name; + // 파일명 생성 (오브젝트 이름 정리) + string objectName = SanitizeFileName(target.name); string fileName = $"{SessionID}_{objectName}_Object.anim"; // SavePathManager 사용 @@ -262,6 +262,35 @@ namespace Entum #endif } + private string SanitizeFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) return ""; + + // 불필요한 부분 제거 + fileName = fileName.Replace("(Clone)", "").Trim(); + + // 파일명에 사용할 수 없는 문자 제거 + char[] invalidChars = Path.GetInvalidFileNameChars(); + foreach (char c in invalidChars) + { + fileName = fileName.Replace(c, '_'); + } + + // 공백을 언더스코어로 변경 + fileName = fileName.Replace(' ', '_'); + + // 연속된 언더스코어 제거 + while (fileName.Contains("__")) + { + fileName = fileName.Replace("__", "_"); + } + + // 앞뒤 언더스코어 제거 + fileName = fileName.Trim('_'); + + return fileName; + } + #if UNITY_EDITOR private void ExportObjectAnimationAsHumanoid(Transform target, string baseFileName) {