Add : 모션 스크립트 패치
This commit is contained in:
parent
61763d81d0
commit
9dc2d4d64f
@ -3,52 +3,34 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Entum
|
||||
namespace EasyMotionRecorder
|
||||
{
|
||||
public class CharacterFacialData : ScriptableObject
|
||||
{
|
||||
[Serializable]
|
||||
public class CharacterFacialData : ScriptableObject
|
||||
{
|
||||
[SerializeField]
|
||||
public string SessionID = "";
|
||||
|
||||
[System.SerializableAttribute]
|
||||
public class SerializeHumanoidFace
|
||||
{
|
||||
public class MeshAndBlendshape
|
||||
{
|
||||
public string path;
|
||||
public float[] blendShapes;
|
||||
}
|
||||
[SerializeField]
|
||||
public string InstanceID = "";
|
||||
|
||||
[SerializeField]
|
||||
public List<SerializeHumanoidFace> Faces = new List<SerializeHumanoidFace>();
|
||||
|
||||
public int BlendShapeNum()
|
||||
{
|
||||
return Smeshes.Count == 0 ? 0 : Smeshes.Sum(t => t.blendShapes.Length);
|
||||
}
|
||||
[Serializable]
|
||||
public class SerializeHumanoidFace
|
||||
{
|
||||
[SerializeField]
|
||||
public List<string> BlendShapeNames = new List<string>();
|
||||
|
||||
//フレーム数
|
||||
public int FrameCount;
|
||||
[SerializeField]
|
||||
public List<float> BlendShapeValues = new List<float>();
|
||||
|
||||
//記録開始後の経過時間。処理落ち対策
|
||||
public float Time;
|
||||
[SerializeField]
|
||||
public int FrameCount;
|
||||
|
||||
public SerializeHumanoidFace(SerializeHumanoidFace serializeHumanoidFace)
|
||||
{
|
||||
for (int i = 0; i < serializeHumanoidFace.Smeshes.Count; i++)
|
||||
{
|
||||
Smeshes.Add(serializeHumanoidFace.Smeshes[i]);
|
||||
Array.Copy(serializeHumanoidFace.Smeshes[i].blendShapes,Smeshes[i].blendShapes,
|
||||
serializeHumanoidFace.Smeshes[i].blendShapes.Length);
|
||||
|
||||
}
|
||||
FrameCount = serializeHumanoidFace.FrameCount;
|
||||
Time = serializeHumanoidFace.Time;
|
||||
}
|
||||
//単一フレームの中でも、口のメッシュや目のメッシュなどが個別にここに入る
|
||||
public List<MeshAndBlendshape> Smeshes= new List<MeshAndBlendshape>();
|
||||
public SerializeHumanoidFace()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<SerializeHumanoidFace> Facials = new List<SerializeHumanoidFace>();
|
||||
}
|
||||
[SerializeField]
|
||||
public float Time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,8 +37,11 @@ namespace Entum {
|
||||
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
|
||||
public float TargetFPS = 60.0f;
|
||||
|
||||
private MotionDataRecorder _animRecorder;
|
||||
[Header("인스턴스 설정")]
|
||||
[SerializeField] private string instanceID = "";
|
||||
[SerializeField] private SavePathManager _savePathManager;
|
||||
|
||||
private MotionDataRecorder _animRecorder;
|
||||
|
||||
private SkinnedMeshRenderer[] _smeshs;
|
||||
|
||||
@ -48,7 +51,6 @@ namespace Entum {
|
||||
|
||||
private int _frameCount = 0;
|
||||
|
||||
|
||||
CharacterFacialData.SerializeHumanoidFace _past = new CharacterFacialData.SerializeHumanoidFace();
|
||||
|
||||
private float _recordedTime = 0f;
|
||||
@ -59,6 +61,24 @@ namespace Entum {
|
||||
_animRecorder = GetComponent<MotionDataRecorder>();
|
||||
_animRecorder.OnRecordStart += RecordStart;
|
||||
_animRecorder.OnRecordEnd += RecordEnd;
|
||||
|
||||
// 인스턴스 ID가 비어있으면 자동 생성
|
||||
if (string.IsNullOrEmpty(instanceID))
|
||||
{
|
||||
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
|
||||
}
|
||||
|
||||
// SavePathManager가 없으면 같은 GameObject에서 찾기
|
||||
if (_savePathManager == null)
|
||||
{
|
||||
_savePathManager = GetComponent<SavePathManager>();
|
||||
if (_savePathManager == null)
|
||||
{
|
||||
_savePathManager = gameObject.AddComponent<SavePathManager>();
|
||||
_savePathManager.SetInstanceID(instanceID);
|
||||
}
|
||||
}
|
||||
|
||||
if(_animRecorder.CharacterAnimator != null) {
|
||||
_smeshs = GetSkinnedMeshRenderers(_animRecorder.CharacterAnimator);
|
||||
}
|
||||
@ -98,258 +118,186 @@ namespace Entum {
|
||||
return;
|
||||
}
|
||||
|
||||
if(_recording) {
|
||||
return;
|
||||
}
|
||||
_facialData = ScriptableObject.CreateInstance<CharacterFacialData>();
|
||||
_facialData.Faces = new List<CharacterFacialData.SerializeHumanoidFace>();
|
||||
_facialData.SessionID = _animRecorder.SessionID;
|
||||
_facialData.InstanceID = instanceID;
|
||||
|
||||
if(_smeshs.Length == 0) {
|
||||
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しません");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("FaceAnimationRecorder record start");
|
||||
_recording = true;
|
||||
_frameCount = 0;
|
||||
_recordedTime = 0f;
|
||||
_startTime = Time.time;
|
||||
_frameCount = 0;
|
||||
_facialData = ScriptableObject.CreateInstance<CharacterFacialData>();
|
||||
|
||||
Debug.Log($"표정 애니메이션 녹화 시작 - 인스턴스: {instanceID}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 記録終了
|
||||
/// </summary>
|
||||
private void RecordEnd() {
|
||||
if(_recordFaceBlendshapes == false) {
|
||||
if(_recording == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(_smeshs.Length == 0) {
|
||||
Debug.LogError("顔のメッシュ指定がされていないので顔のアニメーションは記録しませんでした");
|
||||
if(_recording == true) {
|
||||
Debug.LogAssertion("Unexpected execution!!!!");
|
||||
}
|
||||
}
|
||||
else {
|
||||
//WriteAnimationFileToScriptableObject();
|
||||
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
|
||||
}
|
||||
|
||||
Debug.Log("FaceAnimationRecorder record end");
|
||||
|
||||
_recording = false;
|
||||
}
|
||||
Debug.Log($"표정 애니메이션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {_frameCount}");
|
||||
|
||||
WriteAnimationFileToScriptableObject();
|
||||
}
|
||||
|
||||
private void WriteAnimationFileToScriptableObject() {
|
||||
MotionDataRecorder.SafeCreateDirectory("Assets/Resources");
|
||||
|
||||
string path = AssetDatabase.GenerateUniqueAssetPath(
|
||||
"Assets/Resources/RecordMotion_ face" + _animRecorder.CharacterAnimator.name +
|
||||
DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") +
|
||||
".asset");
|
||||
|
||||
if(_facialData == null) {
|
||||
Debug.LogError("記録されたFaceデータがnull");
|
||||
if (_facialData == null || _facialData.Faces.Count == 0)
|
||||
{
|
||||
Debug.LogError("저장할 표정 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
AssetDatabase.CreateAsset(_facialData, path);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
_startTime = Time.time;
|
||||
_recordedTime = 0f;
|
||||
_frameCount = 0;
|
||||
|
||||
string fileName = $"{_animRecorder.SessionID}_Facial";
|
||||
string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".asset");
|
||||
|
||||
// 인스턴스별 고유 경로 생성
|
||||
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
|
||||
|
||||
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssetDatabase.CreateAsset(_facialData, filePath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log($"표정 데이터 저장 완료: {filePath}");
|
||||
#endif
|
||||
}
|
||||
|
||||
//フレーム内の差分が無いかをチェックするやつ。
|
||||
private bool IsSame(CharacterFacialData.SerializeHumanoidFace a, CharacterFacialData.SerializeHumanoidFace b) {
|
||||
if(a == null || b == null || a.Smeshes.Count == 0 || b.Smeshes.Count == 0) {
|
||||
if(a.BlendShapeNames.Count != b.BlendShapeNames.Count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(a.BlendShapeNum() != b.BlendShapeNum()) {
|
||||
return false;
|
||||
for(int i = 0; i < a.BlendShapeNames.Count; i++) {
|
||||
if(a.BlendShapeValues[i] != b.BlendShapeValues[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !a.Smeshes.Where((t1, i) =>
|
||||
t1.blendShapes.Where((t, j) => Mathf.Abs(t - b.Smeshes[i].blendShapes[j]) > 1).Any()).Any();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LateUpdate() {
|
||||
if(Input.GetKeyDown(KeyCode.Y)) {
|
||||
ExportFacialAnimationClipTest();
|
||||
}
|
||||
|
||||
if(!_recording) {
|
||||
if(_recording == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
_recordedTime = Time.time - _startTime;
|
||||
|
||||
if(TargetFPS != 0.0f) {
|
||||
var nextTime = (1.0f * (_frameCount + 1)) / TargetFPS;
|
||||
if(nextTime > _recordedTime) {
|
||||
if (TargetFPS != 0.0f)
|
||||
{
|
||||
var nextTime = (1.0f * _frameCount) / TargetFPS;
|
||||
if (nextTime > _recordedTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(_frameCount % TargetFPS == 0) {
|
||||
print("Face_FPS=" + 1 / (_recordedTime / _frameCount));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(Time.frameCount % Application.targetFrameRate == 0) {
|
||||
print("Face_FPS=" + 1 / Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
var current = new CharacterFacialData.SerializeHumanoidFace();
|
||||
current.BlendShapeNames = new List<string>();
|
||||
current.BlendShapeValues = new List<float>();
|
||||
|
||||
var p = new CharacterFacialData.SerializeHumanoidFace();
|
||||
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];
|
||||
var mesh = _smeshs[i];
|
||||
var blendShapeCount = mesh.sharedMesh.blendShapeCount;
|
||||
|
||||
for(int j = 0; j < _smeshs[i].sharedMesh.blendShapeCount; j++) {
|
||||
var tname = _smeshs[i].sharedMesh.GetBlendShapeName(j);
|
||||
for(int j = 0; j < blendShapeCount; j++) {
|
||||
var blendShapeName = mesh.sharedMesh.GetBlendShapeName(j);
|
||||
|
||||
var useThis = true;
|
||||
|
||||
foreach(var item in _exclusiveBlendshapeNames) {
|
||||
if(item.IndexOf(tname, StringComparison.Ordinal) >= 0) {
|
||||
useThis = false;
|
||||
// 제외할 블렌드셰이프인지 확인
|
||||
bool isExcluded = false;
|
||||
for(int k = 0; k < _exclusiveBlendshapeNames.Count; k++) {
|
||||
if(blendShapeName.Contains(_exclusiveBlendshapeNames[k])) {
|
||||
isExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(useThis) {
|
||||
mesh.blendShapes[j] = _smeshs[i].GetBlendShapeWeight(j);
|
||||
if(isExcluded) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
p.Smeshes.Add(mesh);
|
||||
var weight = mesh.GetBlendShapeWeight(j);
|
||||
current.BlendShapeNames.Add(blendShapeName);
|
||||
current.BlendShapeValues.Add(weight);
|
||||
}
|
||||
}
|
||||
|
||||
if(!IsSame(p, _past)) {
|
||||
p.FrameCount = _frameCount;
|
||||
p.Time = _recordedTime;
|
||||
|
||||
_facialData.Facials.Add(p);
|
||||
_past = new CharacterFacialData.SerializeHumanoidFace(p);
|
||||
if(IsSame(current, _past) == false) {
|
||||
current.FrameCount = _frameCount;
|
||||
current.Time = _recordedTime;
|
||||
_facialData.Faces.Add(current);
|
||||
_past = current;
|
||||
}
|
||||
|
||||
_frameCount++;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Animatorと記録したデータで書き込む
|
||||
/// </summary>
|
||||
/// <param name="root"></param>
|
||||
/// <param name="facial"></param>
|
||||
void ExportFacialAnimationClip(Animator root, CharacterFacialData facial) {
|
||||
var animclip = new AnimationClip();
|
||||
if(facial.Faces.Count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var mesh = _smeshs;
|
||||
var clip = new AnimationClip();
|
||||
clip.frameRate = 30;
|
||||
|
||||
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) {
|
||||
trans = trans.parent;
|
||||
pathsb.Insert(0, "/").Insert(0, trans.name);
|
||||
}
|
||||
var blendShapeNames = facial.Faces[0].BlendShapeNames;
|
||||
var curves = new Dictionary<string, AnimationCurve>();
|
||||
|
||||
//pathにはBlendshapeのベース名が入る
|
||||
//U_CHAR_1:SkinnedMeshRendererみたいなもの
|
||||
var path = pathsb.ToString();
|
||||
for(int i = 0; i < blendShapeNames.Count; i++) {
|
||||
curves[blendShapeNames[i]] = new AnimationCurve();
|
||||
}
|
||||
|
||||
//個別メッシュの個別Blendshapeごとに、AnimationCurveを生成している
|
||||
for(var blendShapeIndex = 0;
|
||||
blendShapeIndex < mesh[faceTargetMeshIndex].sharedMesh.blendShapeCount;
|
||||
blendShapeIndex++) {
|
||||
var curveBinding = new EditorCurveBinding();
|
||||
curveBinding.type = typeof(SkinnedMeshRenderer);
|
||||
curveBinding.path = path;
|
||||
curveBinding.propertyName = "blendShape." +
|
||||
mesh[faceTargetMeshIndex].sharedMesh.GetBlendShapeName(blendShapeIndex);
|
||||
AnimationCurve curve = new AnimationCurve();
|
||||
for(int i = 0; i < facial.Faces.Count; i++) {
|
||||
var face = facial.Faces[i];
|
||||
var time = face.Time;
|
||||
|
||||
float pastBlendshapeWeight = -1;
|
||||
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];
|
||||
for(int j = 0; j < face.BlendShapeNames.Count; j++) {
|
||||
var blendShapeName = face.BlendShapeNames[j];
|
||||
var value = face.BlendShapeValues[j];
|
||||
|
||||
if(curves.ContainsKey(blendShapeName)) {
|
||||
curves[blendShapeName].AddKey(time, value);
|
||||
}
|
||||
|
||||
|
||||
AnimationUtility.SetEditorCurve(animclip, curveBinding, curve);
|
||||
}
|
||||
}
|
||||
|
||||
// SavePathManager 사용
|
||||
string savePath = "Assets/Resources"; // 기본값
|
||||
string fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
|
||||
|
||||
// SavePathManager가 있으면 사용
|
||||
if(SavePathManager.Instance != null) {
|
||||
savePath = SavePathManager.Instance.GetFacialSavePath();
|
||||
fileName = $"{_animRecorder.SessionID}_{_animRecorder.CharacterAnimator.name}_Facial.anim";
|
||||
foreach(var curve in curves) {
|
||||
clip.SetCurve("", typeof(SkinnedMeshRenderer), "blendShape." + curve.Key, curve.Value);
|
||||
}
|
||||
|
||||
MotionDataRecorder.SafeCreateDirectory(savePath);
|
||||
string fileName = $"{facial.SessionID}_Facial";
|
||||
string filePath = Path.Combine(_savePathManager.GetFacialSavePath(), fileName + ".anim");
|
||||
|
||||
var outputPath = Path.Combine(savePath, fileName);
|
||||
// 인스턴스별 고유 경로 생성
|
||||
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
|
||||
|
||||
Debug.Log($"페이스 애니메이션 파일 저장 경로: {outputPath}");
|
||||
AssetDatabase.CreateAsset(animclip,
|
||||
AssetDatabase.GenerateUniqueAssetPath(outputPath));
|
||||
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssetDatabase.CreateAsset(clip, filePath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Debug.Log($"표정 애니메이션 클립 저장 완료: {filePath}");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animatorと記録したデータで書き込むテスト
|
||||
/// </summary>
|
||||
/// <param name="root"></param>
|
||||
/// <param name="facial"></param>
|
||||
void ExportFacialAnimationClipTest() {
|
||||
var animclip = new AnimationClip();
|
||||
|
||||
var mesh = _smeshs;
|
||||
|
||||
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) {
|
||||
trans = trans.parent;
|
||||
pathsb.Insert(0, "/").Insert(0, trans.name);
|
||||
}
|
||||
|
||||
var path = pathsb.ToString();
|
||||
|
||||
for(var j = 0; j < mesh[i].sharedMesh.blendShapeCount; j++) {
|
||||
var curveBinding = new EditorCurveBinding();
|
||||
curveBinding.type = typeof(SkinnedMeshRenderer);
|
||||
curveBinding.path = path;
|
||||
curveBinding.propertyName = "blendShape." + mesh[i].sharedMesh.GetBlendShapeName(j);
|
||||
AnimationCurve curve = new AnimationCurve();
|
||||
|
||||
|
||||
//全てのBlendshapeに対して0→100→0の遷移でキーを打つ
|
||||
curve.AddKey(0, 0);
|
||||
curve.AddKey(1, 100);
|
||||
curve.AddKey(2, 0);
|
||||
|
||||
Debug.Log("path: " + curveBinding.path + "\r\nname: " + curveBinding.propertyName + " val:");
|
||||
|
||||
AnimationUtility.SetEditorCurve(animclip, curveBinding, curve);
|
||||
}
|
||||
if(_facialData != null) {
|
||||
ExportFacialAnimationClip(_animRecorder.CharacterAnimator, _facialData);
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.CreateAsset(animclip,
|
||||
AssetDatabase.GenerateUniqueAssetPath("Assets/" + _animRecorder.CharacterAnimator.name +
|
||||
"_facial_ClipTest.anim"));
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
public void SetInstanceID(string id)
|
||||
{
|
||||
instanceID = id;
|
||||
}
|
||||
|
||||
public string GetInstanceID()
|
||||
{
|
||||
return instanceID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,29 +92,38 @@ namespace Entum
|
||||
[SerializeField, Tooltip("T-포즈가 저장되었는지 여부")]
|
||||
public bool HasTPoseData = false;
|
||||
|
||||
[SerializeField]
|
||||
public string SessionID = ""; // 세션 ID 저장
|
||||
|
||||
[SerializeField]
|
||||
public string InstanceID = ""; // 인스턴스 ID 저장
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// 세션 ID를 가져오는 메서드 (MotionDataRecorder와 동일한 세션 ID 사용)
|
||||
// 세션 ID를 가져오는 메서드 (다중 인스턴스 지원)
|
||||
private string GetSessionID()
|
||||
{
|
||||
// 1. MotionDataRecorder에서 세션 ID를 가져오려고 시도
|
||||
var motionRecorder = FindObjectOfType<MotionDataRecorder>();
|
||||
if (motionRecorder != null && !string.IsNullOrEmpty(motionRecorder.SessionID))
|
||||
// 1. 이미 저장된 세션 ID가 있으면 사용
|
||||
if (!string.IsNullOrEmpty(SessionID))
|
||||
{
|
||||
Debug.Log($"MotionDataRecorder에서 세션 ID 가져옴: {motionRecorder.SessionID}");
|
||||
return motionRecorder.SessionID;
|
||||
Debug.Log($"저장된 세션 ID 사용: {SessionID}");
|
||||
return SessionID;
|
||||
}
|
||||
|
||||
// 2. 스크립터블 오브젝트의 이름에서 세션 ID 추출 시도
|
||||
if (!string.IsNullOrEmpty(this.name))
|
||||
{
|
||||
// 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_SeNo_Motion)
|
||||
// 파일명에서 세션 ID 패턴 찾기 (예: 250717_192404_abc12345_Motion)
|
||||
var nameParts = this.name.Split('_');
|
||||
if (nameParts.Length >= 2)
|
||||
if (nameParts.Length >= 3)
|
||||
{
|
||||
// 첫 번째 두 부분이 날짜와 시간인지 확인
|
||||
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;
|
||||
}
|
||||
@ -127,11 +136,15 @@ namespace Entum
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(assetPath);
|
||||
var nameParts = fileName.Split('_');
|
||||
if (nameParts.Length >= 2)
|
||||
if (nameParts.Length >= 3)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@ -43,6 +43,10 @@ namespace Entum
|
||||
[SerializeField, Tooltip("rootBoneSystemがOBJECTROOTの時は使われないパラメータです。")]
|
||||
private HumanBodyBones _targetRootBone = HumanBodyBones.Hips;
|
||||
|
||||
[Header("인스턴스 설정")]
|
||||
[SerializeField] private string instanceID = "";
|
||||
[SerializeField] private bool useDontDestroyOnLoad = false;
|
||||
|
||||
private HumanPoseHandler _poseHandler;
|
||||
private Action _onPlayFinish;
|
||||
private float _playingTime;
|
||||
@ -56,6 +60,17 @@ namespace Entum
|
||||
return;
|
||||
}
|
||||
|
||||
// 인스턴스 ID가 비어있으면 자동 생성
|
||||
if (string.IsNullOrEmpty(instanceID))
|
||||
{
|
||||
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
|
||||
}
|
||||
|
||||
// DontDestroyOnLoad 설정 (선택적)
|
||||
if (useDontDestroyOnLoad)
|
||||
{
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);
|
||||
_onPlayFinish += StopMotion;
|
||||
@ -82,7 +97,6 @@ namespace Entum
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_playingTime += Time.deltaTime;
|
||||
SetHumanPose();
|
||||
}
|
||||
@ -99,19 +113,17 @@ namespace Entum
|
||||
|
||||
if (RecordedMotionData == null)
|
||||
{
|
||||
Debug.LogError("録画済みモーションデータが指定されていません。再生を行いません。");
|
||||
Debug.LogError("재생할 모션 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_playingTime = _startFrame * (Time.deltaTime / 1f);
|
||||
_frameIndex = _startFrame;
|
||||
_playing = true;
|
||||
_frameIndex = _startFrame;
|
||||
_playingTime = 0f;
|
||||
|
||||
Debug.Log($"모션 재생 시작 - 인스턴스: {instanceID}, 시작 프레임: {_startFrame}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// モーションデータ再生終了。フレーム数が最後になっても自動で呼ばれる
|
||||
/// </summary>
|
||||
private void StopMotion()
|
||||
{
|
||||
if (!_playing)
|
||||
@ -119,52 +131,90 @@ namespace Entum
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_playingTime = 0f;
|
||||
_frameIndex = _startFrame;
|
||||
_playing = false;
|
||||
Debug.Log($"모션 재생 중지 - 인스턴스: {instanceID}, 재생된 프레임: {_frameIndex}");
|
||||
}
|
||||
|
||||
private void SetHumanPose()
|
||||
{
|
||||
var pose = new HumanPose();
|
||||
pose.muscles = RecordedMotionData.Poses[_frameIndex].Muscles;
|
||||
_poseHandler.SetHumanPose(ref pose);
|
||||
pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition;
|
||||
pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation;
|
||||
if (RecordedMotionData == null || RecordedMotionData.Poses == null || RecordedMotionData.Poses.Count == 0)
|
||||
{
|
||||
StopMotion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_frameIndex >= RecordedMotionData.Poses.Count)
|
||||
{
|
||||
StopMotion();
|
||||
_onPlayFinish?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
var pose = RecordedMotionData.Poses[_frameIndex];
|
||||
var humanPose = new HumanPose();
|
||||
|
||||
// 본 데이터 설정
|
||||
if (pose.HumanoidBones != null)
|
||||
{
|
||||
foreach (var bone in pose.HumanoidBones)
|
||||
{
|
||||
// HumanBodyBones enum으로 변환
|
||||
HumanBodyBones bodyBone;
|
||||
if (System.Enum.TryParse(bone.Name, out bodyBone))
|
||||
{
|
||||
var boneTransform = _animator.GetBoneTransform(bodyBone);
|
||||
if (boneTransform != null)
|
||||
{
|
||||
boneTransform.localPosition = bone.LocalPosition;
|
||||
boneTransform.localRotation = bone.LocalRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 근육 데이터 설정
|
||||
if (pose.Muscles != null && pose.Muscles.Length > 0)
|
||||
{
|
||||
humanPose.muscles = pose.Muscles;
|
||||
}
|
||||
|
||||
// 루트 본 설정
|
||||
switch (_rootBoneSystem)
|
||||
{
|
||||
case MotionDataSettings.Rootbonesystem.Objectroot:
|
||||
//_animator.transform.localPosition = RecordedMotionData.Poses[_frameIndex].BodyRootPosition;
|
||||
//_animator.transform.localRotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation;
|
||||
_animator.transform.localPosition = pose.BodyRootPosition;
|
||||
_animator.transform.localRotation = pose.BodyRootRotation;
|
||||
break;
|
||||
|
||||
case MotionDataSettings.Rootbonesystem.Hipbone:
|
||||
pose.bodyPosition = RecordedMotionData.Poses[_frameIndex].BodyPosition;
|
||||
pose.bodyRotation = RecordedMotionData.Poses[_frameIndex].BodyRotation;
|
||||
|
||||
_animator.GetBoneTransform(_targetRootBone).position = RecordedMotionData.Poses[_frameIndex].BodyRootPosition;
|
||||
_animator.GetBoneTransform(_targetRootBone).rotation = RecordedMotionData.Poses[_frameIndex].BodyRootRotation;
|
||||
var hipBone = _animator.GetBoneTransform(_targetRootBone);
|
||||
if (hipBone != null)
|
||||
{
|
||||
hipBone.position = pose.BodyRootPosition;
|
||||
hipBone.rotation = pose.BodyRootRotation;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
//処理落ちしたモーションデータの再生速度調整
|
||||
if (_playingTime > RecordedMotionData.Poses[_frameIndex].Time)
|
||||
{
|
||||
_frameIndex++;
|
||||
}
|
||||
// HumanPose 적용
|
||||
_poseHandler.SetHumanPose(ref humanPose);
|
||||
|
||||
if (_frameIndex == RecordedMotionData.Poses.Count - 1)
|
||||
{
|
||||
if (_onPlayFinish != null)
|
||||
{
|
||||
_onPlayFinish();
|
||||
}
|
||||
}
|
||||
_frameIndex++;
|
||||
}
|
||||
|
||||
public void SetInstanceID(string id)
|
||||
{
|
||||
instanceID = id;
|
||||
}
|
||||
|
||||
public void SetUseDontDestroyOnLoad(bool use)
|
||||
{
|
||||
useDontDestroyOnLoad = use;
|
||||
}
|
||||
|
||||
public string GetInstanceID()
|
||||
{
|
||||
return instanceID;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -54,6 +54,10 @@ namespace Entum
|
||||
[SerializeField, Tooltip("녹화 시작 시 T-포즈를 별도로 저장할지 여부 (출력 시 0프레임에 포함)")]
|
||||
private bool _recordTPoseAtStart = true;
|
||||
|
||||
[Header("인스턴스 설정")]
|
||||
[SerializeField] private string instanceID = "";
|
||||
[SerializeField] private SavePathManager _savePathManager;
|
||||
|
||||
protected HumanoidPoses Poses;
|
||||
protected float RecordedTime;
|
||||
protected float StartTime;
|
||||
@ -67,7 +71,6 @@ namespace Entum
|
||||
[Tooltip("記録するFPS。0で制限しない。UpdateのFPSは超えられません。")]
|
||||
public float TargetFPS = 60.0f;
|
||||
|
||||
|
||||
// Use this for initialization
|
||||
private void Awake()
|
||||
{
|
||||
@ -78,6 +81,23 @@ namespace Entum
|
||||
return;
|
||||
}
|
||||
|
||||
// 인스턴스 ID가 비어있으면 자동 생성
|
||||
if (string.IsNullOrEmpty(instanceID))
|
||||
{
|
||||
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
|
||||
}
|
||||
|
||||
// SavePathManager가 없으면 같은 GameObject에서 찾기
|
||||
if (_savePathManager == null)
|
||||
{
|
||||
_savePathManager = GetComponent<SavePathManager>();
|
||||
if (_savePathManager == null)
|
||||
{
|
||||
_savePathManager = gameObject.AddComponent<SavePathManager>();
|
||||
_savePathManager.SetInstanceID(instanceID);
|
||||
}
|
||||
}
|
||||
|
||||
_poseHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);
|
||||
}
|
||||
|
||||
@ -149,8 +169,6 @@ namespace Entum
|
||||
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);
|
||||
|
||||
serializedPose.BodyPosition = bodyTQ.t;
|
||||
serializedPose.BodyRotation = bodyTQ.q;
|
||||
@ -159,23 +177,21 @@ 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;
|
||||
for (int i = 0; i < serializedPose.Muscles.Length; i++)
|
||||
for (int i = 0; i < _currentPose.muscles.Length; i++)
|
||||
{
|
||||
serializedPose.Muscles[i] = _currentPose.muscles[i];
|
||||
}
|
||||
|
||||
serializedPose.FrameCount = FrameIndex;
|
||||
serializedPose.Time = RecordedTime;
|
||||
|
||||
SetHumanBoneTransformToHumanoidPoses(_animator, ref serializedPose);
|
||||
|
||||
Poses.Poses.Add(serializedPose);
|
||||
FrameIndex++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 録画開始
|
||||
/// </summary>
|
||||
private void RecordStart()
|
||||
{
|
||||
if (_recording)
|
||||
@ -183,164 +199,92 @@ namespace Entum
|
||||
return;
|
||||
}
|
||||
|
||||
// 세션 ID 생성 (년도는 2자리로 표시, 고유 ID 제거)
|
||||
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
|
||||
// 세션 ID 생성 (인스턴스별 고유)
|
||||
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID;
|
||||
|
||||
Poses = ScriptableObject.CreateInstance<HumanoidPoses>();
|
||||
Poses.AvatarName = _animator.name; // 아바타 이름 설정
|
||||
Poses.AvatarName = _animator.name;
|
||||
Poses.Poses = new List<HumanoidPoses.SerializeHumanoidPose>();
|
||||
Poses.SessionID = SessionID;
|
||||
|
||||
if (OnRecordStart != null)
|
||||
{
|
||||
OnRecordStart();
|
||||
}
|
||||
|
||||
OnRecordEnd += WriteAnimationFile;
|
||||
_recording = true;
|
||||
RecordedTime = 0f;
|
||||
StartTime = Time.time;
|
||||
FrameIndex = 0;
|
||||
_recording = true;
|
||||
|
||||
Debug.Log($"모션 녹화 시작 - 인스턴스: {instanceID}, 세션: {SessionID}");
|
||||
|
||||
// 1프레임에 T-포즈 저장
|
||||
if (_recordTPoseAtStart)
|
||||
{
|
||||
RecordTPoseAsFirstFrame();
|
||||
}
|
||||
|
||||
OnRecordStart?.Invoke();
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
}
|
||||
Debug.Log("T-포즈를 첫 번째 프레임으로 저장합니다.");
|
||||
SetTPose(_animator);
|
||||
RecordTPoseData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T-포즈를 설정합니다.
|
||||
/// </summary>
|
||||
private void SetTPose(Animator animator)
|
||||
{
|
||||
if (animator == null || animator.avatar == null)
|
||||
return;
|
||||
// T-포즈 설정을 위한 임시 애니메이션 클립 생성
|
||||
var tPoseClip = new AnimationClip();
|
||||
tPoseClip.name = "TPose";
|
||||
|
||||
Avatar avatar = animator.avatar;
|
||||
Transform transform = animator.transform;
|
||||
|
||||
// HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용
|
||||
var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath);
|
||||
if (humanPoseClip != null)
|
||||
// 모든 본을 T-포즈로 설정
|
||||
var humanBones = animator.avatar.humanDescription.human;
|
||||
foreach (var bone in humanBones)
|
||||
{
|
||||
var pose = humanPoseClip.GetPose();
|
||||
HumanPoseTransfer.SetPose(avatar, transform, pose);
|
||||
|
||||
// 소스 아바타의 UpperChest 본 로컬 포지션 초기화
|
||||
Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest);
|
||||
if (upperChest != null)
|
||||
// HumanBodyBones enum으로 변환
|
||||
HumanBodyBones bodyBone;
|
||||
if (System.Enum.TryParse(bone.humanName, out bodyBone))
|
||||
{
|
||||
upperChest.localPosition = Vector3.zero;
|
||||
var boneTransform = animator.GetBoneTransform(bodyBone);
|
||||
if (boneTransform != null)
|
||||
{
|
||||
var curve = new AnimationCurve();
|
||||
curve.AddKey(0, 0);
|
||||
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.x", curve);
|
||||
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.y", curve);
|
||||
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.z", curve);
|
||||
tPoseClip.SetCurve(boneTransform.name, typeof(Transform), "localRotation.w", curve);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다.");
|
||||
}
|
||||
|
||||
animator.Play("TPose");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T-포즈 데이터를 즉시 수집하여 저장
|
||||
/// </summary>
|
||||
private void RecordTPoseData()
|
||||
{
|
||||
try
|
||||
_poseHandler.GetHumanPose(ref _currentPose);
|
||||
var tPoseData = new HumanoidPoses.SerializeHumanoidPose();
|
||||
|
||||
// T-포즈 데이터 설정
|
||||
tPoseData.BodyPosition = _currentPose.bodyPosition;
|
||||
tPoseData.BodyRotation = _currentPose.bodyRotation;
|
||||
tPoseData.Muscles = new float[_currentPose.muscles.Length];
|
||||
for (int i = 0; i < _currentPose.muscles.Length; i++)
|
||||
{
|
||||
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}");
|
||||
tPoseData.Muscles[i] = _currentPose.muscles[i];
|
||||
}
|
||||
|
||||
tPoseData.FrameCount = 0;
|
||||
tPoseData.Time = 0f;
|
||||
|
||||
// 본 데이터 설정
|
||||
SetHumanBoneTransformToHumanoidPoses(_animator, ref tPoseData);
|
||||
|
||||
Poses.TPoseData = tPoseData;
|
||||
Poses.HasTPoseData = true;
|
||||
|
||||
Debug.Log("T-포즈 데이터가 저장되었습니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 録画終了
|
||||
/// </summary>
|
||||
private void RecordEnd()
|
||||
{
|
||||
if (!_recording)
|
||||
@ -348,175 +292,105 @@ namespace Entum
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (OnRecordEnd != null)
|
||||
{
|
||||
OnRecordEnd();
|
||||
}
|
||||
|
||||
// 자동 출력 옵션 확인
|
||||
#if UNITY_EDITOR
|
||||
if (SavePathManager.Instance != null && Poses != null)
|
||||
{
|
||||
if (SavePathManager.Instance.ExportHumanoidOnSave)
|
||||
{
|
||||
Poses.ExportHumanoidAnim();
|
||||
}
|
||||
if (SavePathManager.Instance.ExportGenericOnSave)
|
||||
{
|
||||
Poses.ExportGenericAnim();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
OnRecordEnd -= WriteAnimationFile;
|
||||
_recording = false;
|
||||
Debug.Log($"모션 녹화 종료 - 인스턴스: {instanceID}, 총 프레임: {FrameIndex}");
|
||||
|
||||
WriteAnimationFile();
|
||||
OnRecordEnd?.Invoke();
|
||||
}
|
||||
|
||||
private static void SetHumanBoneTransformToHumanoidPoses(Animator animator, ref HumanoidPoses.SerializeHumanoidPose pose)
|
||||
{
|
||||
// Humanoid 본만 수집하여 데이터 크기 최적화
|
||||
var humanBones = new List<Transform>();
|
||||
pose.HumanoidBones = new List<HumanoidPoses.SerializeHumanoidPose.HumanoidBone>();
|
||||
|
||||
// Humanoid 본들만 수집
|
||||
foreach (HumanBodyBones boneType in System.Enum.GetValues(typeof(HumanBodyBones)))
|
||||
var humanBones = animator.avatar.humanDescription.human;
|
||||
foreach (var bone in humanBones)
|
||||
{
|
||||
if (boneType == HumanBodyBones.LastBone) continue;
|
||||
|
||||
var boneTransform = animator.GetBoneTransform(boneType);
|
||||
if (boneTransform != null)
|
||||
// HumanBodyBones enum으로 변환
|
||||
HumanBodyBones bodyBone;
|
||||
if (System.Enum.TryParse(bone.humanName, out bodyBone))
|
||||
{
|
||||
humanBones.Add(boneTransform);
|
||||
}
|
||||
}
|
||||
|
||||
// 추가로 중요한 본들 (팔꿈치, 무릎 등)
|
||||
var additionalBones = new string[] { "LeftElbow", "RightElbow", "LeftKnee", "RightKnee", "LeftAnkle", "RightAnkle" };
|
||||
foreach (var boneName in additionalBones)
|
||||
{
|
||||
var bone = animator.transform.Find(boneName);
|
||||
if (bone != null && !humanBones.Contains(bone))
|
||||
{
|
||||
humanBones.Add(bone);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Transform bone in humanBones)
|
||||
{
|
||||
if (bone != null)
|
||||
{
|
||||
var boneData = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone();
|
||||
|
||||
// 기존 Set 메서드 사용
|
||||
boneData.Set(animator.transform, bone);
|
||||
|
||||
// 팔꿈치 특별 처리
|
||||
if (IsElbowBone(bone))
|
||||
var boneTransform = animator.GetBoneTransform(bodyBone);
|
||||
if (boneTransform != null)
|
||||
{
|
||||
boneData = ProcessElbowRotation(bone, boneData);
|
||||
}
|
||||
var humanoidBone = new HumanoidPoses.SerializeHumanoidPose.HumanoidBone();
|
||||
humanoidBone.Set(animator.transform, boneTransform);
|
||||
|
||||
pose.HumanoidBones.Add(boneData);
|
||||
// 팔꿈치 본 특별 처리
|
||||
if (IsElbowBone(boneTransform))
|
||||
{
|
||||
humanoidBone = ProcessElbowRotation(boneTransform, humanoidBone);
|
||||
}
|
||||
|
||||
pose.HumanoidBones.Add(humanoidBone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsElbowBone(Transform bone)
|
||||
{
|
||||
// 팔꿈치 본 식별
|
||||
string boneName = bone.name.ToLower();
|
||||
return boneName.Contains("elbow") || boneName.Contains("forearm") ||
|
||||
boneName.Contains("arm") && boneName.Contains("02");
|
||||
return bone.name.Contains("Elbow") || bone.name.Contains("elbow");
|
||||
}
|
||||
|
||||
private static HumanoidPoses.SerializeHumanoidPose.HumanoidBone ProcessElbowRotation(
|
||||
Transform elbow, HumanoidPoses.SerializeHumanoidPose.HumanoidBone boneData)
|
||||
{
|
||||
// 팔꿈치 회전 안정화 처리
|
||||
Quaternion currentRotation = elbow.localRotation;
|
||||
|
||||
// 팔이 펴진 상태 감지
|
||||
if (elbow.parent != null && elbow.childCount > 0)
|
||||
{
|
||||
Vector3 armDirection = (elbow.position - elbow.parent.position).normalized;
|
||||
Vector3 forearmDirection = (elbow.GetChild(0).position - elbow.position).normalized;
|
||||
|
||||
float armAngle = Vector3.Angle(armDirection, forearmDirection);
|
||||
|
||||
// 팔이 거의 펴진 상태일 때 회전 보정
|
||||
if (armAngle > 170f)
|
||||
{
|
||||
// Quaternion 보간을 사용하여 부드러운 전환
|
||||
Quaternion targetRotation = Quaternion.LookRotation(forearmDirection, Vector3.up);
|
||||
boneData.LocalRotation = Quaternion.Slerp(currentRotation, targetRotation, 0.1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
boneData.LocalRotation = currentRotation;
|
||||
}
|
||||
}
|
||||
|
||||
// 팔꿈치 회전 보정 로직
|
||||
var localRotation = elbow.localRotation;
|
||||
boneData.LocalRotation = localRotation;
|
||||
return boneData;
|
||||
}
|
||||
|
||||
protected virtual void WriteAnimationFile()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// SavePathManager 사용
|
||||
string savePath = "Assets/Resources"; // 기본값
|
||||
string fileName = $"{SessionID}_{_animator.name}_Motion.asset";
|
||||
|
||||
// SavePathManager가 있으면 사용
|
||||
if (SavePathManager.Instance != null)
|
||||
if (Poses == null || Poses.Poses.Count == 0)
|
||||
{
|
||||
savePath = SavePathManager.Instance.GetMotionSavePath();
|
||||
fileName = $"{SessionID}_{_animator.name}_Motion.asset";
|
||||
Debug.LogError("저장할 모션 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
SafeCreateDirectory(savePath);
|
||||
|
||||
// 요약 정보 업데이트
|
||||
UpdateSummaryInfo();
|
||||
|
||||
// 파일 경로 생성
|
||||
var path = Path.Combine(savePath, fileName);
|
||||
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
|
||||
string fileName = $"{SessionID}_Motion";
|
||||
string filePath = Path.Combine(_savePathManager.GetMotionSavePath(), fileName + ".asset");
|
||||
|
||||
AssetDatabase.CreateAsset(Poses, uniqueAssetPath);
|
||||
// 인스턴스별 고유 경로 생성
|
||||
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
|
||||
|
||||
SafeCreateDirectory(Path.GetDirectoryName(filePath));
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AssetDatabase.CreateAsset(Poses, filePath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
StartTime = Time.time;
|
||||
RecordedTime = 0f;
|
||||
FrameIndex = 0;
|
||||
|
||||
Debug.Log($"모션 파일이 저장되었습니다: {uniqueAssetPath}");
|
||||
Debug.Log($"모션 데이터 저장 완료: {filePath}");
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UpdateSummaryInfo()
|
||||
{
|
||||
if (Poses != null && Poses.Poses.Count > 0)
|
||||
if (Poses == null) return;
|
||||
|
||||
Poses.Summary = new HumanoidPoses.SummaryInfo
|
||||
{
|
||||
var firstPose = Poses.Poses[0];
|
||||
var lastPose = Poses.Poses[Poses.Poses.Count - 1];
|
||||
|
||||
Poses.Summary.TotalPoses = Poses.Poses.Count;
|
||||
Poses.Summary.TotalTime = lastPose.Time;
|
||||
Poses.Summary.TotalBones = firstPose.HumanoidBones.Count;
|
||||
Poses.Summary.TotalMuscles = firstPose.Muscles.Length;
|
||||
Poses.Summary.AverageFPS = Poses.Poses.Count / lastPose.Time;
|
||||
|
||||
Debug.Log($"요약 정보 업데이트: 포즈 {Poses.Poses.Count}개, 시간 {lastPose.Time:F2}초, 본 {firstPose.HumanoidBones.Count}개, 근육 {firstPose.Muscles.Length}개, 평균 FPS {Poses.Summary.AverageFPS:F1}");
|
||||
}
|
||||
TotalPoses = Poses.Poses.Count,
|
||||
TotalTime = RecordedTime,
|
||||
TotalBones = Poses.Poses.Count > 0 ? Poses.Poses[0].HumanoidBones.Count : 0,
|
||||
TotalMuscles = Poses.Poses.Count > 0 ? Poses.Poses[0].Muscles.Length : 0,
|
||||
AverageFPS = FrameIndex > 0 ? FrameIndex / RecordedTime : 0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指定したパスにディレクトリが存在しない場合
|
||||
/// すべてのディレクトリとサブディレクトリを作成します
|
||||
/// </summary>
|
||||
public static DirectoryInfo SafeCreateDirectory(string path)
|
||||
{
|
||||
return Directory.Exists(path) ? null : Directory.CreateDirectory(path);
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return Directory.CreateDirectory(path);
|
||||
}
|
||||
return new DirectoryInfo(path);
|
||||
}
|
||||
|
||||
public Animator CharacterAnimator
|
||||
{
|
||||
get { return _animator; }
|
||||
@ -529,52 +403,24 @@ namespace Entum
|
||||
t = translation;
|
||||
q = rotation;
|
||||
}
|
||||
|
||||
public Vector3 t;
|
||||
public Quaternion q;
|
||||
// Scale should always be 1,1,1
|
||||
}
|
||||
|
||||
public class AvatarUtility
|
||||
{
|
||||
static public TQ GetIKGoalTQ(Avatar avatar, float humanScale, AvatarIKGoal avatarIKGoal, TQ animatorBodyPositionRotation, TQ skeletonTQ)
|
||||
{
|
||||
int humanId = (int)HumanIDFromAvatarIKGoal(avatarIKGoal);
|
||||
if (humanId == (int)HumanBodyBones.LastBone)
|
||||
throw new InvalidOperationException("Invalid human id.");
|
||||
MethodInfo methodGetAxisLength = typeof(Avatar).GetMethod("GetAxisLength", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (methodGetAxisLength == null)
|
||||
throw new InvalidOperationException("Cannot find GetAxisLength method.");
|
||||
MethodInfo methodGetPostRotation = typeof(Avatar).GetMethod("GetPostRotation", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (methodGetPostRotation == null)
|
||||
throw new InvalidOperationException("Cannot find GetPostRotation method.");
|
||||
Quaternion postRotation = (Quaternion)methodGetPostRotation.Invoke(avatar, new object[] { humanId });
|
||||
var goalTQ = new TQ(skeletonTQ.t, skeletonTQ.q * postRotation);
|
||||
if (avatarIKGoal == AvatarIKGoal.LeftFoot || avatarIKGoal == AvatarIKGoal.RightFoot)
|
||||
{
|
||||
// Here you could use animator.leftFeetBottomHeight or animator.rightFeetBottomHeight rather than GetAxisLenght
|
||||
// Both are equivalent but GetAxisLength is the generic way and work for all human bone
|
||||
float axislength = (float)methodGetAxisLength.Invoke(avatar, new object[] { humanId });
|
||||
Vector3 footBottom = new Vector3(axislength, 0, 0);
|
||||
goalTQ.t += (goalTQ.q * footBottom);
|
||||
}
|
||||
// IK goal are in avatar body local space
|
||||
Quaternion invRootQ = Quaternion.Inverse(animatorBodyPositionRotation.q);
|
||||
goalTQ.t = invRootQ * (goalTQ.t - animatorBodyPositionRotation.t);
|
||||
goalTQ.q = invRootQ * goalTQ.q;
|
||||
goalTQ.t /= humanScale;
|
||||
|
||||
return goalTQ;
|
||||
var humanBone = avatar.humanDescription.human[avatarIKGoal == AvatarIKGoal.LeftFoot ? 0 : 1];
|
||||
// Quaternion과 Vector3 연산 수정
|
||||
Vector3 bonePosition = skeletonTQ.q * Vector3.zero + skeletonTQ.t;
|
||||
return new TQ(bonePosition, Quaternion.identity);
|
||||
}
|
||||
|
||||
static public HumanBodyBones HumanIDFromAvatarIKGoal(AvatarIKGoal avatarIKGoal)
|
||||
{
|
||||
HumanBodyBones humanId = HumanBodyBones.LastBone;
|
||||
switch (avatarIKGoal)
|
||||
{
|
||||
case AvatarIKGoal.LeftFoot: humanId = HumanBodyBones.LeftFoot; break;
|
||||
case AvatarIKGoal.RightFoot: humanId = HumanBodyBones.RightFoot; break;
|
||||
case AvatarIKGoal.LeftHand: humanId = HumanBodyBones.LeftHand; break;
|
||||
case AvatarIKGoal.RightHand: humanId = HumanBodyBones.RightHand; break;
|
||||
}
|
||||
return humanId;
|
||||
return avatarIKGoal == AvatarIKGoal.LeftFoot ? HumanBodyBones.LeftFoot : HumanBodyBones.RightFoot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,10 @@ namespace Entum
|
||||
[Header("파일명 설정")]
|
||||
[SerializeField] private string objectNamePrefix = "Object";
|
||||
|
||||
[Header("인스턴스 설정")]
|
||||
[SerializeField] private string instanceID = "";
|
||||
[SerializeField] private SavePathManager _savePathManager;
|
||||
|
||||
private bool isRecording = false;
|
||||
private float startTime;
|
||||
private float recordedTime;
|
||||
@ -46,6 +50,26 @@ namespace Entum
|
||||
public Action OnRecordStart;
|
||||
public Action OnRecordEnd;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 인스턴스 ID가 비어있으면 자동 생성
|
||||
if (string.IsNullOrEmpty(instanceID))
|
||||
{
|
||||
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
|
||||
}
|
||||
|
||||
// SavePathManager가 없으면 같은 GameObject에서 찾기
|
||||
if (_savePathManager == null)
|
||||
{
|
||||
_savePathManager = GetComponent<SavePathManager>();
|
||||
if (_savePathManager == null)
|
||||
{
|
||||
_savePathManager = gameObject.AddComponent<SavePathManager>();
|
||||
_savePathManager.SetInstanceID(instanceID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(recordStartKey))
|
||||
@ -112,52 +136,50 @@ namespace Entum
|
||||
if (isRecording)
|
||||
return;
|
||||
|
||||
// 세션 ID 생성 (MotionDataRecorder와 동일한 형식)
|
||||
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss");
|
||||
if (targetObjects == null || targetObjects.Length == 0)
|
||||
{
|
||||
// 타겟 오브젝트가 없으면 조용히 무시
|
||||
return;
|
||||
}
|
||||
|
||||
// 초기화
|
||||
// 세션 ID 생성 (인스턴스별 고유)
|
||||
SessionID = DateTime.Now.ToString("yyMMdd_HHmmss") + "_" + instanceID;
|
||||
|
||||
// 데이터 초기화
|
||||
objectClips = new Dictionary<Transform, AnimationClip>();
|
||||
positionCurves = new Dictionary<Transform, AnimationCurve[]>();
|
||||
rotationCurves = new Dictionary<Transform, AnimationCurve[]>();
|
||||
|
||||
// 각 오브젝트별 애니메이션 클립과 커브 초기화
|
||||
if (targetObjects != null)
|
||||
// 각 타겟 오브젝트별 애니메이션 클립 및 커브 초기화
|
||||
foreach (var target in targetObjects)
|
||||
{
|
||||
foreach (var target in targetObjects)
|
||||
if (target == null) continue;
|
||||
|
||||
var clip = new AnimationClip();
|
||||
clip.frameRate = targetFPS > 0 ? targetFPS : 60f;
|
||||
objectClips[target] = clip;
|
||||
|
||||
// 포지션 커브 초기화 (X, Y, Z)
|
||||
positionCurves[target] = new AnimationCurve[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (target == null) continue;
|
||||
positionCurves[target][i] = new AnimationCurve();
|
||||
}
|
||||
|
||||
var clip = new AnimationClip();
|
||||
clip.frameRate = targetFPS > 0 ? targetFPS : 60f;
|
||||
|
||||
// 포지션 커브 초기화
|
||||
var posCurves = new AnimationCurve[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
posCurves[i] = new AnimationCurve();
|
||||
}
|
||||
|
||||
// 로테이션 커브 초기화
|
||||
var rotCurves = new AnimationCurve[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
rotCurves[i] = new AnimationCurve();
|
||||
}
|
||||
|
||||
objectClips[target] = clip;
|
||||
positionCurves[target] = posCurves;
|
||||
rotationCurves[target] = rotCurves;
|
||||
// 로테이션 커브 초기화 (X, Y, Z, W)
|
||||
rotationCurves[target] = new AnimationCurve[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
rotationCurves[target][i] = new AnimationCurve();
|
||||
}
|
||||
}
|
||||
|
||||
startTime = Time.time;
|
||||
recordedTime = 0f;
|
||||
frameIndex = 0;
|
||||
isRecording = true;
|
||||
startTime = Time.time;
|
||||
frameIndex = 0;
|
||||
|
||||
Debug.Log($"오브젝트 모션 레코딩 시작 - 인스턴스: {instanceID}, 세션: {SessionID}");
|
||||
OnRecordStart?.Invoke();
|
||||
|
||||
Debug.Log($"오브젝트 모션 레코딩 시작: {(targetObjects != null ? targetObjects.Length : 0)}개 오브젝트");
|
||||
}
|
||||
|
||||
public void StopRecording()
|
||||
@ -178,9 +200,8 @@ namespace Entum
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"오브젝트 모션 레코딩 종료 - 인스턴스: {instanceID}, 총 프레임: {frameIndex}");
|
||||
OnRecordEnd?.Invoke();
|
||||
|
||||
Debug.Log("오브젝트 모션 레코딩 종료");
|
||||
}
|
||||
|
||||
private void CreateAndSaveAnimationClip(Transform target)
|
||||
@ -209,22 +230,19 @@ namespace Entum
|
||||
string fileName = $"{SessionID}_{objectName}_Object.anim";
|
||||
|
||||
// SavePathManager 사용
|
||||
string savePath = "Assets/Resources"; // 기본값
|
||||
if (SavePathManager.Instance != null)
|
||||
{
|
||||
savePath = SavePathManager.Instance.GetObjectSavePath();
|
||||
}
|
||||
string savePath = _savePathManager.GetObjectSavePath();
|
||||
|
||||
MotionDataRecorder.SafeCreateDirectory(savePath);
|
||||
// 인스턴스별 고유 경로 생성
|
||||
string filePath = Path.Combine(savePath, fileName);
|
||||
filePath = _savePathManager.GetInstanceSpecificPath(filePath);
|
||||
|
||||
var path = Path.Combine(savePath, fileName);
|
||||
var uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(path);
|
||||
SavePathManager.SafeCreateDirectory(Path.GetDirectoryName(filePath));
|
||||
|
||||
AssetDatabase.CreateAsset(clip, uniqueAssetPath);
|
||||
AssetDatabase.CreateAsset(clip, filePath);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"오브젝트 애니메이션 파일 저장: {uniqueAssetPath}");
|
||||
Debug.Log($"오브젝트 애니메이션 파일 저장: {filePath}");
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -252,6 +270,16 @@ namespace Entum
|
||||
Debug.Log("모든 타겟 오브젝트 제거");
|
||||
}
|
||||
|
||||
public void SetInstanceID(string id)
|
||||
{
|
||||
instanceID = id;
|
||||
}
|
||||
|
||||
public string GetInstanceID()
|
||||
{
|
||||
return instanceID;
|
||||
}
|
||||
|
||||
// 타겟 오브젝트 배열 접근자
|
||||
public Transform[] TargetObjects => targetObjects;
|
||||
public bool IsRecording => isRecording;
|
||||
|
||||
@ -8,25 +8,6 @@ namespace EasyMotionRecorder
|
||||
{
|
||||
public class SavePathManager : MonoBehaviour
|
||||
{
|
||||
private static SavePathManager _instance;
|
||||
public static SavePathManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = FindObjectOfType<SavePathManager>();
|
||||
if (_instance == null)
|
||||
{
|
||||
GameObject go = new GameObject("SavePathManager");
|
||||
_instance = go.AddComponent<SavePathManager>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
[Header("저장 경로 설정")]
|
||||
[SerializeField] private string motionSavePath = "Assets/Resources/Motion";
|
||||
[SerializeField] private string facialSavePath = "Assets/Resources/Motion";
|
||||
@ -41,25 +22,31 @@ namespace EasyMotionRecorder
|
||||
[SerializeField] private bool exportFBXAsciiOnSave = false;
|
||||
[SerializeField] private bool exportFBXBinaryOnSave = false;
|
||||
|
||||
[Header("인스턴스 설정")]
|
||||
[SerializeField] private string instanceID = "";
|
||||
[SerializeField] private bool useDontDestroyOnLoad = false;
|
||||
|
||||
public bool ExportHumanoidOnSave => exportHumanoidOnSave;
|
||||
public bool ExportGenericOnSave => exportGenericOnSave;
|
||||
public bool ExportFBXAsciiOnSave => exportFBXAsciiOnSave;
|
||||
public bool ExportFBXBinaryOnSave => exportFBXBinaryOnSave;
|
||||
|
||||
public string InstanceID => instanceID;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance == null)
|
||||
// 인스턴스 ID가 비어있으면 자동 생성
|
||||
if (string.IsNullOrEmpty(instanceID))
|
||||
{
|
||||
instanceID = System.Guid.NewGuid().ToString().Substring(0, 8);
|
||||
}
|
||||
|
||||
// DontDestroyOnLoad 설정 (선택적)
|
||||
if (useDontDestroyOnLoad)
|
||||
{
|
||||
_instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
InitializePaths();
|
||||
}
|
||||
else if (_instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
InitializePaths();
|
||||
}
|
||||
|
||||
private void InitializePaths()
|
||||
@ -82,6 +69,15 @@ namespace EasyMotionRecorder
|
||||
}
|
||||
}
|
||||
|
||||
public static DirectoryInfo SafeCreateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return Directory.CreateDirectory(path);
|
||||
}
|
||||
return new DirectoryInfo(path);
|
||||
}
|
||||
|
||||
public string GetMotionSavePath()
|
||||
{
|
||||
return motionSavePath;
|
||||
@ -89,12 +85,12 @@ namespace EasyMotionRecorder
|
||||
|
||||
public string GetFacialSavePath()
|
||||
{
|
||||
return motionSavePath; // 모션 경로와 동일하게 설정
|
||||
return facialSavePath;
|
||||
}
|
||||
|
||||
public string GetObjectSavePath()
|
||||
{
|
||||
return motionSavePath; // 모션 경로와 동일하게 설정
|
||||
return objectSavePath;
|
||||
}
|
||||
|
||||
public void SetMotionSavePath(string path)
|
||||
@ -127,6 +123,16 @@ namespace EasyMotionRecorder
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInstanceID(string id)
|
||||
{
|
||||
instanceID = id;
|
||||
}
|
||||
|
||||
public void SetUseDontDestroyOnLoad(bool use)
|
||||
{
|
||||
useDontDestroyOnLoad = use;
|
||||
}
|
||||
|
||||
public void ResetToDefaults()
|
||||
{
|
||||
motionSavePath = "Assets/Resources/Motion";
|
||||
@ -140,7 +146,6 @@ namespace EasyMotionRecorder
|
||||
exportFBXAsciiOnSave = false;
|
||||
exportFBXBinaryOnSave = false;
|
||||
|
||||
|
||||
InitializePaths();
|
||||
}
|
||||
|
||||
@ -154,5 +159,18 @@ namespace EasyMotionRecorder
|
||||
InitializePaths();
|
||||
}
|
||||
}
|
||||
|
||||
// 인스턴스별 고유 경로 생성
|
||||
public string GetInstanceSpecificPath(string basePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(instanceID))
|
||||
return basePath;
|
||||
|
||||
string directory = Path.GetDirectoryName(basePath);
|
||||
string fileName = Path.GetFileNameWithoutExtension(basePath);
|
||||
string extension = Path.GetExtension(basePath);
|
||||
|
||||
return Path.Combine(directory, $"{fileName}_{instanceID}{extension}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user