using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; public static class HumanoidAnimationFbxImportSettings { public const string ActorRootPath = "Assets/Game/Art/Actor/"; public const string CityboyAvatarPath = "Assets/Game/Art/Actor/cityboy/cityboy_sk.fbx"; public const string CityboyAvatarName = "Cityboy_sk Avatar"; public const string CityboyHipBoneName = "mixamorig:Hips"; public static bool ShouldProcessAsset(string assetPath) { string normalizedPath = NormalizeAssetPath(assetPath); return normalizedPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase) && normalizedPath.StartsWith(ActorRootPath, StringComparison.OrdinalIgnoreCase) && !string.Equals(normalizedPath, CityboyAvatarPath, StringComparison.OrdinalIgnoreCase); } public static ModelImporterClipAnimation ApplyClipSettings(ModelImporterClipAnimation clip) { clip.lockRootRotation = true; clip.keepOriginalOrientation = true; clip.lockRootHeightY = true; return clip; } public static ModelImporterClipAnimation[] ApplyClipSettings(ModelImporterClipAnimation[] clips) { for (int i = 0; i < clips.Length; i++) { clips[i] = ApplyClipSettings(clips[i]); } return clips; } public static bool ShouldUseCityboyAvatar(string assetPath) { string filePath = ResolveAssetFilePath(assetPath); if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { return false; } return ShouldUseCityboyAvatar(File.ReadAllBytes(filePath)); } public static bool ShouldUseCityboyAvatar(byte[] assetBytes) { if (assetBytes == null || assetBytes.Length == 0) { return false; } byte[] marker = System.Text.Encoding.ASCII.GetBytes(CityboyHipBoneName); for (int i = 0; i <= assetBytes.Length - marker.Length; i++) { bool matched = true; for (int markerIndex = 0; markerIndex < marker.Length; markerIndex++) { if (assetBytes[i + markerIndex] != marker[markerIndex]) { matched = false; break; } } if (matched) { return true; } } return false; } public static ModelImporterClipAnimation CreateClipAnimation(TakeInfo takeInfo) { string clipName = string.IsNullOrEmpty(takeInfo.defaultClipName) ? takeInfo.name : takeInfo.defaultClipName; return ApplyClipSettings(new ModelImporterClipAnimation { name = clipName, takeName = takeInfo.name, firstFrame = takeInfo.startTime * takeInfo.sampleRate, lastFrame = takeInfo.stopTime * takeInfo.sampleRate }); } public static ModelImporterClipAnimation CreateClipAnimation(AnimationClip animationClip) { float frameRate = animationClip.frameRate > 0f ? animationClip.frameRate : 30f; return ApplyClipSettings(new ModelImporterClipAnimation { name = animationClip.name, takeName = animationClip.name, firstFrame = 0f, lastFrame = animationClip.length * frameRate }); } private static string NormalizeAssetPath(string assetPath) { return string.IsNullOrEmpty(assetPath) ? string.Empty : assetPath.Replace('\\', '/'); } private static string ResolveAssetFilePath(string assetPath) { if (string.IsNullOrEmpty(assetPath)) { return string.Empty; } if (Path.IsPathRooted(assetPath) || File.Exists(assetPath)) { return assetPath; } DirectoryInfo projectRoot = Directory.GetParent(Application.dataPath); return projectRoot == null ? assetPath : Path.Combine(projectRoot.FullName, NormalizeAssetPath(assetPath)); } } public sealed class HumanoidAnimationFbxPostprocessor : AssetPostprocessor { private static readonly HashSet ReimportingAssets = new HashSet(StringComparer.OrdinalIgnoreCase); private void OnPreprocessModel() { if (!(assetImporter is ModelImporter modelImporter)) { return; } if (!HumanoidAnimationFbxImportSettings.ShouldProcessAsset(assetPath)) { return; } modelImporter.importAnimation = true; modelImporter.animationType = ModelImporterAnimationType.Human; if (HumanoidAnimationFbxImportSettings.ShouldUseCityboyAvatar(assetPath)) { Avatar cityboyAvatar = LoadCityboyAvatar(); if (cityboyAvatar == null) { Debug.LogWarning( $"[HumanoidAnimationFbxPostprocessor] Cannot find Cityboy avatar at {HumanoidAnimationFbxImportSettings.CityboyAvatarPath}. Skip import settings for {assetPath}."); return; } modelImporter.avatarSetup = ModelImporterAvatarSetup.CopyFromOther; modelImporter.sourceAvatar = cityboyAvatar; } else { modelImporter.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel; modelImporter.sourceAvatar = null; } ModelImporterClipAnimation[] clips = GetAnimationClips(modelImporter); if (clips.Length == 0) { return; } modelImporter.clipAnimations = HumanoidAnimationFbxImportSettings.ApplyClipSettings(clips); } private static void OnPostprocessAllAssets( string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { foreach (string importedAsset in importedAssets) { ConfigureImportedAnimationClips(importedAsset); } } private static void ConfigureImportedAnimationClips(string assetPath) { if (!HumanoidAnimationFbxImportSettings.ShouldProcessAsset(assetPath)) { return; } if (ReimportingAssets.Remove(assetPath)) { return; } if (!(AssetImporter.GetAtPath(assetPath) is ModelImporter modelImporter)) { return; } ModelImporterClipAnimation[] existingClips = modelImporter.clipAnimations; if (existingClips != null && existingClips.Length > 0) { return; } ModelImporterClipAnimation[] clips = CreateClipAnimationsFromImportedAssets(assetPath); if (clips.Length == 0) { return; } modelImporter.clipAnimations = HumanoidAnimationFbxImportSettings.ApplyClipSettings(clips); ReimportingAssets.Add(assetPath); modelImporter.SaveAndReimport(); } private static ModelImporterClipAnimation[] GetAnimationClips(ModelImporter modelImporter) { ModelImporterClipAnimation[] clips = modelImporter.clipAnimations; if (clips != null && clips.Length > 0) { return clips; } clips = modelImporter.defaultClipAnimations; if (clips != null && clips.Length > 0) { return clips; } TakeInfo[] takes = modelImporter.importedTakeInfos; if (takes == null || takes.Length == 0) { return Array.Empty(); } clips = new ModelImporterClipAnimation[takes.Length]; for (int i = 0; i < takes.Length; i++) { clips[i] = HumanoidAnimationFbxImportSettings.CreateClipAnimation(takes[i]); } return clips; } private static Avatar LoadCityboyAvatar() { Avatar fallbackAvatar = null; UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(HumanoidAnimationFbxImportSettings.CityboyAvatarPath); foreach (UnityEngine.Object asset in assets) { if (!(asset is Avatar avatar)) { continue; } if (fallbackAvatar == null) { fallbackAvatar = avatar; } if (string.Equals(avatar.name, HumanoidAnimationFbxImportSettings.CityboyAvatarName, StringComparison.OrdinalIgnoreCase)) { return avatar; } } return fallbackAvatar; } private static ModelImporterClipAnimation[] CreateClipAnimationsFromImportedAssets(string assetPath) { UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath); var clips = new List(); foreach (UnityEngine.Object asset in assets) { if (asset is AnimationClip animationClip) { clips.Add(HumanoidAnimationFbxImportSettings.CreateClipAnimation(animationClip)); } } return clips.ToArray(); } }