using System; using System.Collections.Generic; using System.Text; using UnityEditor.Rendering.Converter; using UnityEngine; using UnityEngine.Categorization; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using static UnityEditor.Rendering.AnimationClipUpgrader; using ClipPath = UnityEditor.Rendering.AnimationClipUpgrader.ClipPath; namespace UnityEditor.Rendering.Universal { /// /// Represents an animation clip asset that needs to be converted from Built-in RP to URP. /// Tracks the animation clip and all its dependent assets (materials, prefabs, etc.). /// [Serializable] internal class AnimationClipConverterItem : RenderPipelineConverterAssetItem { public AnimationClipConverterItem(GlobalObjectId gid, string assetPath) : base(gid, assetPath) { info = assetPath; } /// /// List of assets that reference this animation clip (e.g., prefabs, scene GameObjects). /// Used to determine if the clip can be safely upgraded based on its usage context. /// [SerializeReference] public List dependencies = new(); } /// /// Converts animation clips that animate material properties from Built-in RP to URP. /// Updates property paths in animation clips to match the new URP shader properties, /// ensuring animations continue to work after material conversion. /// [Serializable] [URPHelpURL("urp/features/rp-converter")] [PipelineConverter("Built-in", "Universal Render Pipeline (Universal Renderer)")] [BatchModeConverterClassInfo("BuiltInToURP", "AnimationClip")] [ElementInfo(Name = "Animation Clip", Order = 110, Description = "Updates animation clips that reference material properties to work with URP shaders.\nEnsures material animations continue working after converting Materials from Built-in RP to URP.")] internal sealed class AnimationClipConverter : IRenderPipelineConverter { public bool isEnabled { get { if (GraphicsSettings.currentRenderPipeline is not UniversalRenderPipelineAsset urpAsset) return false; return urpAsset.scriptableRenderer is UniversalRenderer; } } public string isDisabledMessage => "Converter requires URP with an Universal Renderer. Convert your project to URP to use this converter."; [SerializeField] internal List assets = new(); /// /// Builds a list of animation clips that animate material properties and their search queries. /// Each entry contains a search query to find assets referencing the clip, and the clip's path. /// /// List of tuples containing search queries and animation clip paths. public static List<(string animationClip, string searchQuery)> GetAnimationClipsSearchList() { List<(string materialName, string searchQuery)> list = new(); // Find all animation clips in the project foreach (var animationClip in AssetDatabaseHelper.FindAssets()) { // Only include clips that animate material properties (e.g., color, float values) if (!AnimationClipUpgrader.IsAnimatingMaterialProperties(animationClip)) continue; // Create a search query using the GlobalObjectId to find all assets referencing this clip string formattedId = $"<$object:{GlobalObjectId.GetGlobalObjectIdSlow(animationClip)},UnityEngine.Object$>"; list.Add(($"p: ref={formattedId}", AssetDatabase.GetAssetPath(animationClip))); } return list; } /// /// Scans the project for animation clips that need conversion and identifies their dependencies. /// /// Callback invoked with the list of items to convert when scanning completes. public void Scan(Action> onScanFinish) { assets.Clear(); AnimationClipConverterItem currentAnimationClipItem = null; void OnSearchFinish() { // Create a defensive copy to avoid exposing the internal mutable collection var results = new List(assets.Count); foreach (var asset in assets) { if (asset.dependencies.Count == 0) continue; results.Add(asset); } onScanFinish?.Invoke(results); } using (UnityEngine.Pool.DictionaryPool>.Get(out var animatorUsingClip)) using (UnityEngine.Pool.HashSetPool<(string,string)>.Get(out var animatorReferences)) { void OnAnimationClipDependenciesSearchFinish() { var query = new List<(string, string)>(animatorReferences); SearchServiceUtils.RunQueuedSearch ( SearchServiceUtils.IndexingOptions.DeepSearch, query, (item, path) => { var unityObject = item.ToObject(); if (unityObject == null) return; var gid = GlobalObjectId.GetGlobalObjectIdSlow(unityObject); int type = gid.identifierType; // 1=Asset, 2=SceneObject var assetItem = new RenderPipelineConverterAssetItem(gid.ToString()) { name = unityObject.name, info = path, }; if (animatorUsingClip.TryGetValue(path, out var list)) { foreach (var i in list) { i.dependencies.Add(assetItem); } } }, OnSearchFinish // Invoked when all queued searches complete ); } SearchServiceUtils.RunQueuedSearch ( SearchServiceUtils.IndexingOptions.DeepSearch, GetAnimationClipsSearchList(), (searchItem, animationClipPath) => { // Search callback invoked for each asset that references an animation clip. // The queued search processes results grouped by animation clip. When we encounter // a different animationClipPath than the current one, it indicates we've started // processing dependencies for a new animation clip. // // This batching approach efficiently groups all dependencies (prefabs, GameObjects, etc.) // under their respective animation clips without requiring separate searches per clip. // Create a new converter item when we encounter a different animation clip if (currentAnimationClipItem == null || !currentAnimationClipItem.assetPath.Equals(animationClipPath)) { currentAnimationClipItem = new AnimationClipConverterItem( GlobalObjectId.GetGlobalObjectIdSlow(AssetDatabase.LoadAssetAtPath(animationClipPath)), animationClipPath); assets.Add(currentAnimationClipItem); } // Add the dependent asset (prefab, GameObject, etc.) to the current clip's dependency list var assetItem = new RenderPipelineConverterAssetItem(searchItem.id); // If the animation clip is being referenced by a controller, we need to enqueue where this controller is being used // Handle everything at the end and fill clip dependencies, with controller dependencies. if (assetItem.assetPath.EndsWith(".controller")) { var controller = assetItem.LoadObject(); string formattedId = $"<$object:{GlobalObjectId.GetGlobalObjectIdSlow(controller)},UnityEngine.Object$>"; animatorReferences.Add(($"ref={formattedId}", AssetDatabase.GetAssetPath(controller))); if (!animatorUsingClip.TryGetValue(assetItem.assetPath, out var list)) { list = new (); animatorUsingClip[assetItem.assetPath] = list; } list.Add(currentAnimationClipItem); } else { // A game object with an Animation component directly referencing the animation clip currentAnimationClipItem.dependencies.Add(assetItem); } }, OnAnimationClipDependenciesSearchFinish // Queue animators ); } } /// /// Cache of material upgrade paths from Built-in shaders to URP shaders. /// Used to determine how animated material properties should be remapped. /// internal AnimationClipUpgradePathsCache m_UpgradePathsToNewShaders; /// /// Prepares the converter by loading all material upgrade paths. /// Called once before converting any animation clips. /// public void BeforeConvert() { m_UpgradePathsToNewShaders = new (BuiltInToURP3DMaterialUpgrader.FetchMaterialUpgraders()); } /// /// Cleans up resources after conversion completes. /// public void AfterConvert() { m_UpgradePathsToNewShaders.Dispose(); m_UpgradePathsToNewShaders = null; } /// /// Converts a single animation clip item by upgrading its material property paths. /// /// The animation clip item to convert. /// Output message describing the conversion result or any errors. /// Status indicating success or failure of the conversion. public Status Convert(IRenderPipelineConverterItem item, out string message) { var assetItem = item as AnimationClipConverterItem; var animationClip = assetItem.LoadObject() as AnimationClip; if (animationClip == null) { message = $"Failed to load {assetItem.name} - Asset Path {assetItem.assetPath}"; return Status.Error; } var errorString = new StringBuilder(); var status = ConvertObject(animationClip, assetItem.dependencies, errorString); message = errorString.ToString(); return status; } /// /// Performs the actual conversion of an animation clip by analyzing its usage context /// and upgrading material property paths where safe to do so. /// /// The animation clip to convert. /// List of assets that reference this clip (prefabs, GameObjects, etc.). /// Output message builder for logging results and errors. /// Status indicating success or failure of the conversion. Status ConvertObject(AnimationClip clip, List dependencies, StringBuilder message) { var status = Status.Success; try { var clipPaths = new ClipPath[] { clip }; // Extract material property animation data from the clip var clipData = AnimationClipUpgrader.GetAssetDataForClipsFiltered(clipPaths); if (clipData.Count == 0) { message.AppendLine($"The given animation clip ({clip}) does not have material bindings"); return Status.Error; } using (UnityEngine.Pool.ListPool.Get(out var tmpGameObjects)) { // Analyze how the clip is used across different GameObjects/prefabs to determine // if it can be safely upgraded. A clip used with both upgraded and non-upgraded // materials cannot be converted without breaking one of those use cases. // // Dependencies include: // - Prefabs that use this animation clip // - GameObjects in scenes that use this clip // - Scene assets (skipped as GameObjects provide sufficient context) foreach (var d in dependencies) { if (d.LoadObject() is GameObject go) GatherClipsUsageForGameObject(go, clipData, m_UpgradePathsToNewShaders, default); } // Track which clips were successfully upgraded and which could not be converted var upgraded = new HashSet<(IAnimationClip Clip, ClipPath Path, ShaderPropertyUsage Usage)>(); var notUpgraded = new HashSet<(IAnimationClip Clip, ClipPath Path, ShaderPropertyUsage Usage)>(); // Perform the upgrade, filtering out clips with ambiguous usage UpgradeClips(clipData, upgraded, notUpgraded); // Generate result message based on upgrade outcome if (upgraded.Count == 1) { status = Status.Success; bool setDirty = true; foreach (var item in upgraded) { var (Clip, Path, Usage) = item; if (Usage.HasFlag(ShaderPropertyUsage.InvalidShader)) { message.Append("Unable to migrate Animation Clip"); message.AppendLine("- There is not converter defined for the target shader, or the target shader must be upgraded before."); status = Status.Warning; setDirty = false; } } if (setDirty) EditorUtility.SetDirty(clip); } else if (notUpgraded.Count == 1) { status = Status.Warning; // Provide detailed information about why the clip couldn't be upgraded foreach (var item in notUpgraded) { var (Clip, Path, Usage) = item; message.AppendLine("Unable to migrate Animation Clip:"); if (Usage.HasFlag(ShaderPropertyUsage.InvalidShader)) { message.AppendLine("- There is not converter defined for the target shader, or the target shader must be upgraded before."); status = Status.Warning; } if (Usage.HasFlag(ShaderPropertyUsage.MultipleUpgradePaths)) { message.Append("- The upgraders define multiple upgrade paths for one of the properties"); status = Status.Error; } } } } } catch (Exception ex) { message.AppendLine($"Error converting {clip}: {ex.Message}"); } return status; } } }