/**
[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();
}
}