Fix : 인스턴스 출력 기능 패치

This commit is contained in:
KINDNICK 2025-07-26 16:17:10 +09:00
parent 98d207583a
commit 0abf6970d4
4 changed files with 389 additions and 34 deletions

View File

@ -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;

View File

@ -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<EasyMotionRecorder.SavePathManager>();
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<EasyMotionRecorder.SavePathManager>();
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<EasyMotionRecorder.SavePathManager>();
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);

View File

@ -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<HumanoidPoses>();
Poses.AvatarName = _animator.name;
Poses.Poses = new List<HumanoidPoses.SerializeHumanoidPose>();
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<string, Dictionary<string, AnimationCurve>>();
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<string, AnimationCurve>
{
["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()

View File

@ -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<Transform, AnimationClip>();
@ -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)
{