feat: ActorCore 모션 배치 import 자동화 추가

- MotionBatch_20260424에 Motion* 후보 FBX 10개를 import하고 Humanoid 리그를 정리
- Blender 기반 ActorCore 101본 정규화 스크립트와 Unity manifest 적용 메뉴 추가
- orc-stomp 단일 애셋의 extra root 리그를 정규화하고 사용 문서와 manifest를 보강
- Unity 컴파일, ApplyManifest 실제 실행, 콘솔 경고/오류 없음으로 검증
This commit is contained in:
2026-04-24 14:50:21 +09:00
parent ed92a1bc37
commit b4648672f6
31 changed files with 13102 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9479e994fffecfc14973a2bb6331f1e2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,100 @@
{
"schema": "colosseum.actorcoreMotionBatchImport@1",
"createdAtUtc": "2026-04-24T05:33:29.240646+00:00",
"projectRoot": "/home/dal4segno/Colosseum",
"referenceFbx": "Assets/External/Animations/ActorCore/orc-jog.fbx",
"actorCoreAvatar": "Assets/External/Animations/ActorCore/ActorCoreAvatar.asset",
"targetFolder": "Assets/External/Animations/ActorCore/MotionBatch_20260424",
"entries": [
{
"source": "/home/dal4segno/다운로드/Motion/forward-straight-punch-r.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/forward-straight-punch-r.fbx",
"sha256": "9e49fe2cfc6e316b45d124ab77c4468e8af2b85c50c10d3438be0d1c07122a86",
"size": 2007568,
"processing": "direct",
"unityImportMode": "copy",
"clipName": "forward-straight-punch-r"
},
{
"source": "/home/dal4segno/다운로드/Motion (1)/Motion/forward-straight-punch-r-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/forward-straight-punch-r-mirror.fbx",
"sha256": "664e82f0f5e541de18d75ae99c29edb277fb08e18e24c34edae8a6b37be95ee9",
"size": 2007568,
"processing": "direct",
"unityImportMode": "copy",
"clipName": "forward-straight-punch-r-mirror"
},
{
"source": "/home/dal4segno/다운로드/Motion (2)/Motion/uppercut-l.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/uppercut-l.fbx",
"sha256": "f9e1cf0dc3b9ab0d90799ffd67402a82d17e75a5ec6e5162203580b9f814bf65",
"size": 1838800,
"processing": "direct",
"unityImportMode": "copy",
"clipName": "uppercut-l"
},
{
"source": "/home/dal4segno/다운로드/Motion (3)/Motion/uppercut-l-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/uppercut-l-mirror.fbx",
"sha256": "5072e63abdecbd1c30f57d86909199bf4d9c0c06333c81931015f037df796945",
"size": 1838800,
"processing": "direct",
"unityImportMode": "copy",
"clipName": "uppercut-l-mirror"
},
{
"source": "/home/dal4segno/다운로드/Motion (4)/Motion/orc-stomp.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-stomp.fbx",
"sha256": "4ef22bbaf3792ebd4d8236c52427be48ab9808521364da33c1892e83077e717b",
"size": 2511648,
"processing": "normalized",
"unityImportMode": "create",
"clipName": "orc-stomp"
},
{
"source": "/home/dal4segno/다운로드/Motion (5)/Motion/orc-stomp-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-stomp-mirror.fbx",
"sha256": "ab6a578b3f9d0ecabf01a6d648e5b6315f129a86a63fa96bd305ec963bb6d048",
"size": 2511648,
"processing": "normalized",
"unityImportMode": "create",
"clipName": "orc-stomp-mirror"
},
{
"source": "/home/dal4segno/다운로드/Motion (6)/Motion/orc-chop.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-chop.fbx",
"sha256": "ba8045d2075e7ac867cdd2b701b06ad3f661c41ca64cd078227b7316f77c1b82",
"size": 2329712,
"processing": "normalized",
"unityImportMode": "create",
"clipName": "orc-chop"
},
{
"source": "/home/dal4segno/다운로드/Motion (7)/Motion/orc-chop-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-chop-mirror.fbx",
"sha256": "9151fad6fddd44c5c7f7cbb3ebf529616532db0376bf1cdb2f279c4afb23f69a",
"size": 2329712,
"processing": "normalized",
"unityImportMode": "create",
"clipName": "orc-chop-mirror"
},
{
"source": "/home/dal4segno/다운로드/Motion (8)/Motion/uppercut-l.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/uppercut-l_motion8.fbx",
"sha256": "f9e1cf0dc3b9ab0d90799ffd67402a82d17e75a5ec6e5162203580b9f814bf65",
"size": 1838800,
"processing": "direct",
"unityImportMode": "copy",
"clipName": "uppercut-l_motion8"
},
{
"source": "/home/dal4segno/다운로드/Motion (9)/Motion/orc-stomp.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-stomp_motion9.fbx",
"sha256": "1d5cb6051038b9c9ac61198a1cc355a9117788b0eec44390974aa48e4e26a260",
"size": 1979136,
"processing": "normalized",
"unityImportMode": "create",
"clipName": "orc-stomp_motion9"
}
]
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 01ff949f13e1241f78f77ebf2b04f8b1
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,62 @@
[
{
"source": "/home/dal4segno/다운로드/Motion/forward-straight-punch-r.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/forward-straight-punch-r.fbx",
"sha256": "9e49fe2cfc6e316b45d124ab77c4468e8af2b85c50c10d3438be0d1c07122a86",
"size": 2007568
},
{
"source": "/home/dal4segno/다운로드/Motion (1)/Motion/forward-straight-punch-r-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/forward-straight-punch-r-mirror.fbx",
"sha256": "664e82f0f5e541de18d75ae99c29edb277fb08e18e24c34edae8a6b37be95ee9",
"size": 2007568
},
{
"source": "/home/dal4segno/다운로드/Motion (2)/Motion/uppercut-l.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/uppercut-l.fbx",
"sha256": "f9e1cf0dc3b9ab0d90799ffd67402a82d17e75a5ec6e5162203580b9f814bf65",
"size": 1838800
},
{
"source": "/home/dal4segno/다운로드/Motion (3)/Motion/uppercut-l-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/uppercut-l-mirror.fbx",
"sha256": "5072e63abdecbd1c30f57d86909199bf4d9c0c06333c81931015f037df796945",
"size": 1838800
},
{
"source": "/home/dal4segno/다운로드/Motion (4)/Motion/orc-stomp.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-stomp.fbx",
"sha256": "4ef22bbaf3792ebd4d8236c52427be48ab9808521364da33c1892e83077e717b",
"size": 2511648
},
{
"source": "/home/dal4segno/다운로드/Motion (5)/Motion/orc-stomp-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-stomp-mirror.fbx",
"sha256": "ab6a578b3f9d0ecabf01a6d648e5b6315f129a86a63fa96bd305ec963bb6d048",
"size": 2511648
},
{
"source": "/home/dal4segno/다운로드/Motion (6)/Motion/orc-chop.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-chop.fbx",
"sha256": "ba8045d2075e7ac867cdd2b701b06ad3f661c41ca64cd078227b7316f77c1b82",
"size": 2329712
},
{
"source": "/home/dal4segno/다운로드/Motion (7)/Motion/orc-chop-mirror.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-chop-mirror.fbx",
"sha256": "9151fad6fddd44c5c7f7cbb3ebf529616532db0376bf1cdb2f279c4afb23f69a",
"size": 2329712
},
{
"source": "/home/dal4segno/다운로드/Motion (8)/Motion/uppercut-l.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/uppercut-l_motion8.fbx",
"sha256": "f9e1cf0dc3b9ab0d90799ffd67402a82d17e75a5ec6e5162203580b9f814bf65",
"size": 1838800
},
{
"source": "/home/dal4segno/다운로드/Motion (9)/Motion/orc-stomp.fbx",
"asset": "Assets/External/Animations/ActorCore/MotionBatch_20260424/orc-stomp_motion9.fbx",
"sha256": "1d5cb6051038b9c9ac61198a1cc355a9117788b0eec44390974aa48e4e26a260",
"size": 1979136
}
]

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 36de6839bfb6ef867a3668282a656d45
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,340 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEditor;
/// <summary>
/// ActorCore 모션 배치 manifest를 읽어 FBX ModelImporter 휴머노이드 설정을 적용합니다.
/// Blender 정규화 도구가 생성한 `_actorcore_motion_import_manifest.json`을 대상으로 사용합니다.
/// </summary>
public static class ActorCoreMotionBatchImportSettings
{
private const string MenuPath = "Tools/Animation/Apply ActorCore Motion Batch Import Settings";
private const string ExpectedSchema = "colosseum.actorcoreMotionBatchImport@1";
private const string DefaultActorCoreAvatarPath = "Assets/External/Animations/ActorCore/ActorCoreAvatar.asset";
[Serializable]
private sealed class ActorCoreMotionBatchManifest
{
public string schema;
public string createdAtUtc;
public string referenceFbx;
public string actorCoreAvatar;
public string targetFolder;
public List<ActorCoreMotionBatchEntry> entries;
}
[Serializable]
private sealed class ActorCoreMotionBatchEntry
{
public string source;
public string asset;
public string sha256;
public long size;
public string processing;
public string unityImportMode;
public string clipName;
public string[] removedBones;
public string[] sourceActions;
public string error;
}
private struct ImportValidation
{
public bool success;
public int transformCount;
public bool hasArmatureExtraRoot;
public string rootParent;
public string[] humanClips;
public string[] avatars;
public string message;
}
/// <summary>
/// 선택된 manifest 또는 파일 선택창의 manifest를 적용합니다.
/// </summary>
[MenuItem(MenuPath, false, 26)]
public static void ApplyFromManifestMenu()
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
{
EditorUtility.DisplayDialog("ActorCore Motion Batch", "플레이 모드에서는 import 설정을 변경하지 않습니다.", "확인");
return;
}
string manifestPath = GetManifestPathFromSelectionOrDialog();
if (string.IsNullOrEmpty(manifestPath))
return;
ApplyManifest(manifestPath, true);
}
/// <summary>
/// manifest 경로를 받아 배치 import 설정을 적용합니다.
/// </summary>
public static bool ApplyManifest(string manifestPath, bool showDialog)
{
if (!TryLoadManifest(manifestPath, out ActorCoreMotionBatchManifest manifest))
return false;
string avatarPath = string.IsNullOrEmpty(manifest.actorCoreAvatar)
? DefaultActorCoreAvatarPath
: manifest.actorCoreAvatar;
Avatar actorCoreAvatar = AssetDatabase.LoadAssetAtPath<Avatar>(avatarPath);
if (actorCoreAvatar == null && HasCopyEntries(manifest))
{
Debug.LogError($"[ActorCoreMotionBatchImport] ActorCoreAvatar를 찾지 못했습니다: {avatarPath}");
return false;
}
int appliedCount = 0;
int fallbackCount = 0;
int failedCount = 0;
List<string> reportLines = new List<string>();
foreach (ActorCoreMotionBatchEntry entry in manifest.entries ?? new List<ActorCoreMotionBatchEntry>())
{
if (entry == null || string.IsNullOrEmpty(entry.asset))
continue;
if (string.Equals(entry.unityImportMode, "skip", StringComparison.OrdinalIgnoreCase))
{
failedCount++;
reportLines.Add($"SKIP {entry.asset}: {entry.error}");
continue;
}
ModelImporter importer = AssetImporter.GetAtPath(entry.asset) as ModelImporter;
if (importer == null)
{
failedCount++;
reportLines.Add($"FAIL {entry.asset}: ModelImporter 없음");
continue;
}
string requestedMode = NormalizeImportMode(entry.unityImportMode);
ApplyImporterSettings(entry, importer, requestedMode, actorCoreAvatar);
ImportValidation validation = ValidateImportedAsset(entry.asset, requestedMode);
if (!validation.success && requestedMode == "copy")
{
ModelImporter fallbackImporter = AssetImporter.GetAtPath(entry.asset) as ModelImporter;
ApplyImporterSettings(entry, fallbackImporter, "create", actorCoreAvatar);
validation = ValidateImportedAsset(entry.asset, "create");
fallbackCount++;
requestedMode = "create";
}
if (validation.success)
{
appliedCount++;
reportLines.Add($"OK {requestedMode.ToUpperInvariant()} {Path.GetFileName(entry.asset)} clips=[{string.Join(", ", validation.humanClips)}] rootParent={validation.rootParent}");
}
else
{
failedCount++;
reportLines.Add($"FAIL {Path.GetFileName(entry.asset)}: {validation.message}");
}
}
string summary = $"적용 {appliedCount}개, fallback {fallbackCount}개, 실패 {failedCount}개";
Debug.Log($"[ActorCoreMotionBatchImport] {summary}\n{string.Join("\n", reportLines)}");
if (showDialog)
EditorUtility.DisplayDialog("ActorCore Motion Batch", summary, "확인");
return failedCount == 0;
}
private static bool HasCopyEntries(ActorCoreMotionBatchManifest manifest)
{
return manifest.entries != null
&& manifest.entries.Any(entry => string.Equals(entry.unityImportMode, "copy", StringComparison.OrdinalIgnoreCase));
}
private static void ApplyImporterSettings(
ActorCoreMotionBatchEntry entry,
ModelImporter importer,
string mode,
Avatar actorCoreAvatar)
{
importer.importAnimation = true;
importer.animationType = ModelImporterAnimationType.Human;
importer.avatarSetup = mode == "copy"
? ModelImporterAvatarSetup.CopyFromOther
: ModelImporterAvatarSetup.CreateFromThisModel;
importer.sourceAvatar = mode == "copy" ? actorCoreAvatar : null;
importer.animationCompression = ModelImporterAnimationCompression.Optimal;
importer.optimizeGameObjects = false;
importer.optimizeBones = true;
importer.preserveHierarchy = false;
TakeInfo take = SelectPrimaryTake(importer.importedTakeInfos);
if (string.IsNullOrEmpty(take.name))
{
importer.clipAnimations = Array.Empty<ModelImporterClipAnimation>();
importer.SaveAndReimport();
ModelImporter refreshedImporter = AssetImporter.GetAtPath(entry.asset) as ModelImporter;
take = SelectPrimaryTake(refreshedImporter.importedTakeInfos);
}
if (string.IsNullOrEmpty(take.name))
return;
ModelImporter targetImporter = AssetImporter.GetAtPath(entry.asset) as ModelImporter;
targetImporter.clipAnimations = new[]
{
CreateClipAnimation(entry, take),
};
targetImporter.SaveAndReimport();
}
private static ModelImporterClipAnimation CreateClipAnimation(ActorCoreMotionBatchEntry entry, TakeInfo take)
{
float sampleRate = take.sampleRate > 0f ? take.sampleRate : 30f;
float firstFrame = take.startTime * sampleRate;
float lastFrame = take.stopTime * sampleRate;
return new ModelImporterClipAnimation
{
takeName = take.name,
name = GetClipName(entry),
firstFrame = firstFrame,
lastFrame = Mathf.Max(firstFrame + 1f, lastFrame),
loopTime = false,
loopPose = false,
lockRootRotation = false,
lockRootHeightY = false,
lockRootPositionXZ = false,
};
}
private static TakeInfo SelectPrimaryTake(TakeInfo[] takes)
{
if (takes == null || takes.Length == 0)
return default;
return takes
.Where(take => !string.IsNullOrEmpty(take.name))
.Where(take => take.name.IndexOf("T-Pose", StringComparison.OrdinalIgnoreCase) < 0)
.OrderByDescending(take => take.stopTime - take.startTime)
.FirstOrDefault();
}
private static ImportValidation ValidateImportedAsset(string assetPath, string mode)
{
UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
Transform[] transforms = assets.OfType<Transform>().ToArray();
Transform armatureTransform = transforms.FirstOrDefault(transform => transform.name == "Armature");
Transform rootTransform = transforms.FirstOrDefault(transform => transform.name == "root");
AnimationClip[] humanClips = assets
.OfType<AnimationClip>()
.Where(clip => !clip.name.StartsWith("__preview__", StringComparison.Ordinal))
.Where(clip => clip.humanMotion)
.ToArray();
Avatar[] avatars = assets.OfType<Avatar>().ToArray();
string[] humanClipNames = humanClips
.Select(clip => $"{clip.name}:{clip.length:0.###}")
.ToArray();
string[] avatarNames = avatars
.Select(avatar => $"{avatar.name}:valid={avatar.isValid}:human={avatar.isHuman}")
.ToArray();
bool hasValidOwnAvatar = mode == "copy" || avatars.Any(avatar => avatar.isValid && avatar.isHuman);
bool success = armatureTransform == null
&& rootTransform != null
&& humanClips.Length > 0
&& hasValidOwnAvatar;
string message = success
? "OK"
: $"Armature={armatureTransform != null}, root={rootTransform != null}, humanClips={humanClips.Length}, avatars=[{string.Join(", ", avatarNames)}]";
return new ImportValidation
{
success = success,
transformCount = transforms.Length,
hasArmatureExtraRoot = armatureTransform != null,
rootParent = rootTransform != null && rootTransform.parent != null ? rootTransform.parent.name : "(none)",
humanClips = humanClipNames,
avatars = avatarNames,
message = message,
};
}
private static string GetClipName(ActorCoreMotionBatchEntry entry)
{
if (!string.IsNullOrEmpty(entry.clipName))
return entry.clipName;
return Path.GetFileNameWithoutExtension(entry.asset);
}
private static string NormalizeImportMode(string mode)
{
return string.Equals(mode, "copy", StringComparison.OrdinalIgnoreCase)
? "copy"
: "create";
}
private static bool TryLoadManifest(string manifestPath, out ActorCoreMotionBatchManifest manifest)
{
manifest = null;
if (string.IsNullOrEmpty(manifestPath) || !File.Exists(manifestPath))
{
Debug.LogError($"[ActorCoreMotionBatchImport] manifest를 찾지 못했습니다: {manifestPath}");
return false;
}
manifest = JsonUtility.FromJson<ActorCoreMotionBatchManifest>(File.ReadAllText(manifestPath));
if (manifest == null || manifest.entries == null)
{
Debug.LogError($"[ActorCoreMotionBatchImport] manifest 형식이 올바르지 않습니다: {manifestPath}");
return false;
}
if (!string.Equals(manifest.schema, ExpectedSchema, StringComparison.Ordinal))
Debug.LogWarning($"[ActorCoreMotionBatchImport] 예상 schema와 다릅니다: {manifest.schema}");
return true;
}
private static string GetManifestPathFromSelectionOrDialog()
{
string selectedPath = AssetDatabase.GetAssetPath(Selection.activeObject);
if (IsJsonAssetPath(selectedPath))
return selectedPath;
string absolutePath = EditorUtility.OpenFilePanel(
"ActorCore Motion Batch Manifest 선택",
Application.dataPath,
"json");
if (string.IsNullOrEmpty(absolutePath))
return null;
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/');
string normalizedPath = Path.GetFullPath(absolutePath).Replace('\\', '/');
if (!normalizedPath.StartsWith(projectRoot + "/", StringComparison.Ordinal))
{
EditorUtility.DisplayDialog("ActorCore Motion Batch", "프로젝트 내부 manifest만 사용할 수 있습니다.", "확인");
return null;
}
string projectRelativePath = normalizedPath.Substring(projectRoot.Length + 1);
return IsJsonAssetPath(projectRelativePath) ? projectRelativePath : null;
}
private static bool IsJsonAssetPath(string assetPath)
{
return !string.IsNullOrEmpty(assetPath)
&& assetPath.StartsWith("Assets/", StringComparison.Ordinal)
&& assetPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2c79ec38acf2200d9a465f565d978ea8