/** [EasyMotionRecorder] Copyright (c) 2018 Duo.inc This software is released under the MIT License. http://opensource.org/licenses/mit-license.php */ using UnityEngine; using System; using System.Text; using System.Collections.Generic; using System.Linq; using System.IO; #if UNITY_EDITOR using UnityEditor; #endif using EasyMotionRecorder; namespace Entum { [Serializable] public class MotionDataSettings { public enum Rootbonesystem { Hipbone, Objectroot } /// /// Humanoid用のMuscleマッピング /// public static Dictionary TraitPropMap = new Dictionary { {"Left Thumb 1 Stretched", "LeftHand.Thumb.1 Stretched"}, {"Left Thumb Spread", "LeftHand.Thumb.Spread"}, {"Left Thumb 2 Stretched", "LeftHand.Thumb.2 Stretched"}, {"Left Thumb 3 Stretched", "LeftHand.Thumb.3 Stretched"}, {"Left Index 1 Stretched", "LeftHand.Index.1 Stretched"}, {"Left Index Spread", "LeftHand.Index.Spread"}, {"Left Index 2 Stretched", "LeftHand.Index.2 Stretched"}, {"Left Index 3 Stretched", "LeftHand.Index.3 Stretched"}, {"Left Middle 1 Stretched", "LeftHand.Middle.1 Stretched"}, {"Left Middle Spread", "LeftHand.Middle.Spread"}, {"Left Middle 2 Stretched", "LeftHand.Middle.2 Stretched"}, {"Left Middle 3 Stretched", "LeftHand.Middle.3 Stretched"}, {"Left Ring 1 Stretched", "LeftHand.Ring.1 Stretched"}, {"Left Ring Spread", "LeftHand.Ring.Spread"}, {"Left Ring 2 Stretched", "LeftHand.Ring.2 Stretched"}, {"Left Ring 3 Stretched", "LeftHand.Ring.3 Stretched"}, {"Left Little 1 Stretched", "LeftHand.Little.1 Stretched"}, {"Left Little Spread", "LeftHand.Little.Spread"}, {"Left Little 2 Stretched", "LeftHand.Little.2 Stretched"}, {"Left Little 3 Stretched", "LeftHand.Little.3 Stretched"}, {"Right Thumb 1 Stretched", "RightHand.Thumb.1 Stretched"}, {"Right Thumb Spread", "RightHand.Thumb.Spread"}, {"Right Thumb 2 Stretched", "RightHand.Thumb.2 Stretched"}, {"Right Thumb 3 Stretched", "RightHand.Thumb.3 Stretched"}, {"Right Index 1 Stretched", "RightHand.Index.1 Stretched"}, {"Right Index Spread", "RightHand.Index.Spread"}, {"Right Index 2 Stretched", "RightHand.Index.2 Stretched"}, {"Right Index 3 Stretched", "RightHand.Index.3 Stretched"}, {"Right Middle 1 Stretched", "RightHand.Middle.1 Stretched"}, {"Right Middle Spread", "RightHand.Middle.Spread"}, {"Right Middle 2 Stretched", "RightHand.Middle.2 Stretched"}, {"Right Middle 3 Stretched", "RightHand.Middle.3 Stretched"}, {"Right Ring 1 Stretched", "RightHand.Ring.1 Stretched"}, {"Right Ring Spread", "RightHand.Ring.Spread"}, {"Right Ring 2 Stretched", "RightHand.Ring.2 Stretched"}, {"Right Ring 3 Stretched", "RightHand.Ring.3 Stretched"}, {"Right Little 1 Stretched", "RightHand.Little.1 Stretched"}, {"Right Little Spread", "RightHand.Little.Spread"}, {"Right Little 2 Stretched", "RightHand.Little.2 Stretched"}, {"Right Little 3 Stretched", "RightHand.Little.3 Stretched"}, }; } /// /// モーションデータの中身 /// [System.Serializable] public class HumanoidPoses : ScriptableObject { [SerializeField] public string AvatarName = ""; // 아바타 이름 저장 // 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용) private string GetSessionID() { // MotionDataRecorder에서 세션 ID를 가져오려고 시도 var motionRecorder = FindObjectOfType(); if (motionRecorder != null && !string.IsNullOrEmpty(motionRecorder.SessionID)) { return motionRecorder.SessionID; } // MotionDataRecorder가 없거나 세션 ID가 없으면 현재 시간으로 생성 return DateTime.Now.ToString("yyMMdd_HHmmss"); } #if UNITY_EDITOR //Genericなanimファイルとして出力する [ContextMenu("Export as Generic animation clips")] public void ExportGenericAnim() { var clip = new AnimationClip { frameRate = 30 }; AnimationUtility.SetAnimationClipSettings(clip, new AnimationClipSettings { loopTime = false }); // 본 데이터가 있는지 확인 if (Poses.Count == 0 || Poses[0].HumanoidBones.Count == 0) { Debug.LogError("ExportGenericAnim: 본 데이터가 없습니다. Poses.Count=" + Poses.Count + (Poses.Count > 0 ? ", HumanoidBones.Count=" + Poses[0].HumanoidBones.Count : "")); return; } var bones = Poses[0].HumanoidBones; for (int i = 0; i < bones.Count; i++) { var bone = bones[i]; // 경로가 비어있는지 확인 if (string.IsNullOrEmpty(bone.Name)) { Debug.LogError($"본 {i}: 이름이 비어있습니다!"); continue; } // 경로 정리: 끝의 슬래시만 제거 string cleanPath = bone.Name.TrimEnd('/'); var positionCurveX = new AnimationCurve(); var positionCurveY = new AnimationCurve(); var positionCurveZ = new AnimationCurve(); var rotationCurveX = new AnimationCurve(); var rotationCurveY = new AnimationCurve(); var rotationCurveZ = new AnimationCurve(); var rotationCurveW = new AnimationCurve(); foreach (var p in Poses) { if (p.HumanoidBones.Count > i) { var poseBone = p.HumanoidBones[i]; positionCurveX.AddKey(p.Time, poseBone.LocalPosition.x); positionCurveY.AddKey(p.Time, poseBone.LocalPosition.y); positionCurveZ.AddKey(p.Time, poseBone.LocalPosition.z); rotationCurveX.AddKey(p.Time, poseBone.LocalRotation.x); rotationCurveY.AddKey(p.Time, poseBone.LocalRotation.y); rotationCurveZ.AddKey(p.Time, poseBone.LocalRotation.z); rotationCurveW.AddKey(p.Time, poseBone.LocalRotation.w); } } //path는 계층 //http://mebiustos.hatenablog.com/entry/2015/09/16/230000 var binding = new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalPosition.x" }; AnimationUtility.SetEditorCurve(clip, binding, positionCurveX); AnimationUtility.SetEditorCurve(clip, new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalPosition.y" }, positionCurveY); AnimationUtility.SetEditorCurve(clip, new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalPosition.z" }, positionCurveZ); AnimationUtility.SetEditorCurve(clip, new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalRotation.x" }, rotationCurveX); AnimationUtility.SetEditorCurve(clip, new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalRotation.y" }, rotationCurveY); AnimationUtility.SetEditorCurve(clip, new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalRotation.z" }, rotationCurveZ); AnimationUtility.SetEditorCurve(clip, new EditorCurveBinding { path = cleanPath, type = typeof(Transform), propertyName = "m_LocalRotation.w" }, rotationCurveW); } clip.EnsureQuaternionContinuity(); // 세션 ID 사용 (MotionDataRecorder와 동일한 세션 ID 사용) string sessionID = GetSessionID(); // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 string fileName = $"{sessionID}_{avatarName}_Generic.anim"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); if (!string.IsNullOrEmpty(assetPath)) { string directory = Path.GetDirectoryName(assetPath); if (!string.IsNullOrEmpty(directory)) { savePath = directory; } } MotionDataRecorder.SafeCreateDirectory(savePath); var path = Path.Combine(savePath, fileName); var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); AssetDatabase.CreateAsset(clip, uniqueAssetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"제네릭 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); } //Humanoidなanimファイルとして出力する。 [ContextMenu("Export as Humanoid animation clips")] public void ExportHumanoidAnim() { // 데이터 검증 if (Poses == null || Poses.Count == 0) { Debug.LogError("ExportHumanoidAnim: Poses 데이터가 없습니다. Poses.Count=" + (Poses?.Count ?? 0)); return; } Debug.Log($"ExportHumanoidAnim: Poses.Count={Poses.Count}, 첫 번째 포즈 시간={Poses[0].Time}, 마지막 포즈 시간={Poses[Poses.Count-1].Time}"); // 첫 번째 포즈 데이터 검증 var firstPose = Poses[0]; Debug.Log($"첫 번째 포즈: BodyPosition={firstPose.BodyPosition}, Muscles.Length={firstPose.Muscles?.Length ?? 0}"); if (firstPose.Muscles == null || firstPose.Muscles.Length == 0) { Debug.LogError("ExportHumanoidAnim: Muscles 데이터가 없습니다."); return; } var clip = new AnimationClip { frameRate = 30 }; // 시작할 때 설정을 적용 (커브 데이터 추가 전) var settings = new AnimationClipSettings { loopTime = false, // Loop Time: false cycleOffset = 0, // Cycle Offset: 0 loopBlend = false, // Loop Blend: false loopBlendOrientation = true, // Root Transform Rotation - Bake Into Pose: true loopBlendPositionY = true, // Root Transform Position (Y) - Bake Into Pose: true loopBlendPositionXZ = true, // Root Transform Position (XZ) - Bake Into Pose: true keepOriginalOrientation = true, // Root Transform Rotation - Based Upon: Original keepOriginalPositionY = true, // Root Transform Position (Y) - Based Upon: Original keepOriginalPositionXZ = true, // Root Transform Position (XZ) - Based Upon: Original heightFromFeet = false, // Height From Feet: false mirror = false // Mirror: false }; AnimationUtility.SetAnimationClipSettings(clip, settings); // body position { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); foreach (var item in Poses) { curveX.AddKey(item.Time, item.BodyPosition.x); curveY.AddKey(item.Time, item.BodyPosition.y); curveZ.AddKey(item.Time, item.BodyPosition.z); } Debug.Log($"Body Position 커브: 키 개수 X={curveX.length}, Y={curveY.length}, Z={curveZ.length}"); const string muscleX = "RootT.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RootT.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RootT.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); } // Leftfoot position { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); foreach (var item in Poses) { curveX.AddKey(item.Time, item.LeftfootIK_Pos.x); curveY.AddKey(item.Time, item.LeftfootIK_Pos.y); curveZ.AddKey(item.Time, item.LeftfootIK_Pos.z); } const string muscleX = "LeftFootT.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "LeftFootT.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "LeftFootT.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); } // Rightfoot position { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); foreach (var item in Poses) { curveX.AddKey(item.Time, item.RightfootIK_Pos.x); curveY.AddKey(item.Time, item.RightfootIK_Pos.y); curveZ.AddKey(item.Time, item.RightfootIK_Pos.z); } const string muscleX = "RightFootT.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RightFootT.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RightFootT.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); } // body rotation { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); var curveW = new AnimationCurve(); foreach (var item in Poses) { curveX.AddKey(item.Time, item.BodyRotation.x); curveY.AddKey(item.Time, item.BodyRotation.y); curveZ.AddKey(item.Time, item.BodyRotation.z); curveW.AddKey(item.Time, item.BodyRotation.w); } const string muscleX = "RootQ.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RootQ.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RootQ.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); const string muscleW = "RootQ.w"; clip.SetCurve("", typeof(Animator), muscleW, curveW); } // Leftfoot rotation { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); var curveW = new AnimationCurve(); foreach (var item in Poses) { curveX.AddKey(item.Time, item.LeftfootIK_Rot.x); curveY.AddKey(item.Time, item.LeftfootIK_Rot.y); curveZ.AddKey(item.Time, item.LeftfootIK_Rot.z); curveW.AddKey(item.Time, item.LeftfootIK_Rot.w); } const string muscleX = "LeftFootQ.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "LeftFootQ.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "LeftFootQ.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); const string muscleW = "LeftFootQ.w"; clip.SetCurve("", typeof(Animator), muscleW, curveW); } // Rightfoot rotation { var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); var curveZ = new AnimationCurve(); var curveW = new AnimationCurve(); foreach (var item in Poses) { curveX.AddKey(item.Time, item.RightfootIK_Rot.x); curveY.AddKey(item.Time, item.RightfootIK_Rot.y); curveZ.AddKey(item.Time, item.RightfootIK_Rot.z); curveW.AddKey(item.Time, item.RightfootIK_Rot.w); } const string muscleX = "RightFootQ.x"; clip.SetCurve("", typeof(Animator), muscleX, curveX); const string muscleY = "RightFootQ.y"; clip.SetCurve("", typeof(Animator), muscleY, curveY); const string muscleZ = "RightFootQ.z"; clip.SetCurve("", typeof(Animator), muscleZ, curveZ); const string muscleW = "RightFootQ.w"; clip.SetCurve("", typeof(Animator), muscleW, curveW); } // muscles for (int i = 0; i < HumanTrait.MuscleCount; i++) { var curve = new AnimationCurve(); foreach (var item in Poses) { curve.AddKey(item.Time, item.Muscles[i]); } var muscle = HumanTrait.MuscleName[i]; if (MotionDataSettings.TraitPropMap.ContainsKey(muscle)) { muscle = MotionDataSettings.TraitPropMap[muscle]; } clip.SetCurve("", typeof(Animator), muscle, curve); } clip.EnsureQuaternionContinuity(); // 세션 ID 사용 (MotionDataRecorder와 동일한 세션 ID 사용) string sessionID = GetSessionID(); // 아바타 이름이 있으면 포함, 없으면 기본값 사용 string avatarName = !string.IsNullOrEmpty(AvatarName) ? AvatarName : "Unknown"; // 에셋 파일의 경로를 기반으로 저장 경로 결정 string savePath = "Assets/Resources"; // 기본값 string fileName = $"{sessionID}_{avatarName}_Humanoid.anim"; // 현재 에셋 파일의 경로 가져오기 string assetPath = AssetDatabase.GetAssetPath(this); if (!string.IsNullOrEmpty(assetPath)) { string directory = Path.GetDirectoryName(assetPath); if (!string.IsNullOrEmpty(directory)) { savePath = directory; } } MotionDataRecorder.SafeCreateDirectory(savePath); var path = Path.Combine(savePath, fileName); var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path); AssetDatabase.CreateAsset(clip, uniqueAssetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"휴머노이드 애니메이션 파일이 저장되었습니다: {uniqueAssetPath}"); } // BVH export 기능 제거 - 새로운 시스템으로 대체 예정 // BVH 관련 메서드들 제거 - 새로운 시스템으로 대체 예정 // BVH 관련 헬퍼 메서드들 제거 - 새로운 시스템으로 대체 예정 #endif [Serializable] public class SerializeHumanoidPose { public Vector3 BodyRootPosition; public Quaternion BodyRootRotation; public Vector3 BodyPosition; public Quaternion BodyRotation; public Vector3 LeftfootIK_Pos; public Quaternion LeftfootIK_Rot; public Vector3 RightfootIK_Pos; public Quaternion RightfootIK_Rot; public float[] Muscles; //フレーム数 public int FrameCount; //記録開始後の経過時間。処理落ち対策 public float Time; [Serializable] public class HumanoidBone { public string Name; public Vector3 LocalPosition; public Quaternion LocalRotation; private static Dictionary _pathCache = new Dictionary(); private static string BuildRelativePath(Transform root, Transform target) { var path = ""; _pathCache.TryGetValue(target, out path); if(path != null) return path; var current = target; var pathList = new List(); // 타겟이 루트와 같은 경우 빈 문자열 반환 if (current == root) { path = ""; _pathCache.Add(target, path); return path; } // 루트까지 올라가면서 경로 구성 while (current != null && current != root) { pathList.Add(current.name); current = current.parent; } if (current == null) { Debug.LogError($"{target.name}는 {root.name}의 자식이 아닙니다."); throw new Exception(target.name + "는" + root.name + "의 자식이 아닙니다"); } // 경로를 역순으로 조합 (Unity 애니메이션 경로 형식) pathList.Reverse(); path = string.Join("/", pathList); // 경로 끝의 슬래시 제거 path = path.TrimEnd('/'); // Unity 애니메이션 시스템에서 루트 오브젝트 이름을 제거하는 경우를 대비 // 루트가 "Bip001"인 경우, 경로에서 "Bip001/" 부분을 제거 if (root.name == "Bip001" && path.StartsWith("Bip001/")) { path = path.Substring("Bip001/".Length); } _pathCache.Add(target, path); return path; } public void Set(Transform root, Transform t) { Name = BuildRelativePath(root, t); // 루트 본인지 확인 (이름이 비어있거나 루트와 같은 경우) bool isRoot = string.IsNullOrEmpty(Name) || t == root; if (isRoot) { // 루트 본: 월드 좌표계 사용 LocalPosition = t.position; // 월드 위치 LocalRotation = t.rotation; // 월드 회전 } else { // 자식 본: 로컬 좌표계 사용 LocalPosition = t.localPosition; LocalRotation = t.localRotation; } } } [SerializeField, HideInInspector] public List HumanoidBones = new List(); //CSVシリアライズ public string SerializeCSV() { StringBuilder sb = new StringBuilder(); SerializeVector3(sb, BodyRootPosition); SerializeQuaternion(sb, BodyRootRotation); SerializeVector3(sb, BodyPosition); SerializeQuaternion(sb, BodyRotation); foreach (var muscle in Muscles) { sb.Append(muscle); sb.Append(","); } sb.Append(FrameCount); sb.Append(","); sb.Append(Time); sb.Append(","); foreach (var humanoidBone in HumanoidBones) { sb.Append(humanoidBone.Name); sb.Append(","); SerializeVector3(sb, humanoidBone.LocalPosition); SerializeQuaternion(sb, humanoidBone.LocalRotation); } sb.Length = sb.Length - 1; //最後のカンマ削除 return sb.ToString(); } private static void SerializeVector3(StringBuilder sb, Vector3 vec) { sb.Append(vec.x); sb.Append(","); sb.Append(vec.y); sb.Append(","); sb.Append(vec.z); sb.Append(","); } private static void SerializeQuaternion(StringBuilder sb, Quaternion q) { sb.Append(q.x); sb.Append(","); sb.Append(q.y); sb.Append(","); sb.Append(q.z); sb.Append(","); sb.Append(q.w); sb.Append(","); } //CSVデシリアライズ public void DeserializeCSV(string str) { string[] dataString = str.Split(','); BodyRootPosition = DeserializeVector3(dataString, 0); BodyRootRotation = DeserializeQuaternion(dataString, 3); BodyPosition = DeserializeVector3(dataString, 7); BodyRotation = DeserializeQuaternion(dataString, 10); Muscles = new float[HumanTrait.MuscleCount]; for (int i = 0; i < HumanTrait.MuscleCount; i++) { Muscles[i] = float.Parse(dataString[i + 14]); } FrameCount = int.Parse(dataString[14 + HumanTrait.MuscleCount]); Time = float.Parse(dataString[15 + HumanTrait.MuscleCount]); var boneValues = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[]; for (int i = 0; i < boneValues.Length; i++) { int startIndex = 16 + HumanTrait.MuscleCount + (i * 8); if (dataString.Length <= startIndex) { break; } HumanoidBone bone = new HumanoidBone(); bone.Name = dataString[startIndex]; bone.LocalPosition = DeserializeVector3(dataString, startIndex + 1); bone.LocalRotation = DeserializeQuaternion(dataString, startIndex + 4); } } private static Vector3 DeserializeVector3(IList str, int startIndex) { return new Vector3(float.Parse(str[startIndex]), float.Parse(str[startIndex + 1]), float.Parse(str[startIndex + 2])); } private static Quaternion DeserializeQuaternion(IList str, int startIndex) { return new Quaternion(float.Parse(str[startIndex]), float.Parse(str[startIndex + 1]), float.Parse(str[startIndex + 2]), float.Parse(str[startIndex + 3])); } } [SerializeField, HideInInspector] public List Poses = new List(); // 인스펙터 최적화를 위한 요약 정보 [System.Serializable] public class SummaryInfo { public int TotalPoses; public float TotalTime; public int TotalBones; public int TotalMuscles; public float AverageFPS; } [SerializeField] public SummaryInfo Summary = new SummaryInfo(); } }