Skip to content

Instantly share code, notes, and snippets.

@OptoCloud
Last active November 16, 2023 11:39
Show Gist options
  • Save OptoCloud/27b21da6b0510efed63bfc159fc1fcec to your computer and use it in GitHub Desktop.
Save OptoCloud/27b21da6b0510efed63bfc159fc1fcec to your computer and use it in GitHub Desktop.
Finds duplicate and linked assets
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
public class LinkedMaterialSelector : EditorWindow
{
static bool FileExistsScuffed(string path)
{
return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(path));
}
static string GenerateNonConflictingPath(string path)
{
if (string.IsNullOrEmpty(path))
return GUID.Generate().ToString();
if (!FileExistsScuffed(path))
return path;
var parts = path.Split('_').ToList();
if (!int.TryParse(parts[parts.Count-1], out int i))
{
i = 0;
parts.Add("0");
}
while (FileExistsScuffed(path)) ;
{
parts[parts.Count - 1] = (++i).ToString();
path = string.Join("_", parts);
}
return path;
}
static string GetFileDirectoryPath(string filePath)
{
int lastSlash = filePath.LastIndexOf('/');
if (lastSlash <= 0)
return string.Empty;
return filePath.Substring(0, lastSlash);
}
static bool EnsureFolder(string path)
{
if (string.IsNullOrEmpty(path))
return false;
if (AssetDatabase.IsValidFolder(path))
return true;
string[] parts = path.Split('/');
if (parts.Length < 2)
return false;
string currentPath = parts[0];
if (currentPath != "Assets")
return false;
for (int i = 1; i < parts.Length; i++)
{
string newPath = currentPath + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(newPath))
{
if (string.IsNullOrEmpty(AssetDatabase.CreateFolder(currentPath, parts[i])))
{
return false;
}
}
currentPath = newPath;
}
return AssetDatabase.IsValidFolder(path);
}
static bool TryMoveFile(string src, string dst)
{
if (!EnsureFolder(GetFileDirectoryPath(dst)))
return false;
if (!string.IsNullOrEmpty(AssetDatabase.ValidateMoveAsset(src, dst)))
return false;
return string.IsNullOrEmpty(AssetDatabase.MoveAsset(src, dst));
}
static bool IsSame(Keyframe a, Keyframe b)
{
return
Mathf.Approximately(a.time, b.time) &&
Mathf.Approximately(a.value, b.value) &&
Mathf.Approximately(a.inTangent, b.inTangent) &&
Mathf.Approximately(a.inWeight, b.inWeight) &&
Mathf.Approximately(a.outTangent, b.outTangent) &&
Mathf.Approximately(a.outWeight, b.outWeight);
}
static bool IsSame(AnimationCurve a, AnimationCurve b)
{
if (a == b || a.length != b.length)
return false;
for (int j = 0; j < a.length; j++)
{
if (
!IsSame(a[j], b[j]) ||
AnimationUtility.GetKeyLeftTangentMode(a, j) != AnimationUtility.GetKeyLeftTangentMode(b, j) ||
AnimationUtility.GetKeyRightTangentMode(a, j) != AnimationUtility.GetKeyRightTangentMode(b, j)
)
{
return false;
}
}
return true;
}
static bool IsSame(AnimationClip a, AnimationClip b)
{
if (a == b || a.empty && b.empty)
return true;
if (!Mathf.Approximately(a.length, b.length) || !Mathf.Approximately(a.frameRate, b.frameRate))
return false;
string path = AssetDatabase.GetAssetPath(a);
if (!path.StartsWith("Assets"))
return false;
EditorCurveBinding[] aBindings = AnimationUtility.GetCurveBindings(a);
EditorCurveBinding[] bBindings = AnimationUtility.GetCurveBindings(b);
if (aBindings.Length != bBindings.Length)
return false;
for (int i = 0; i < aBindings.Length; i++)
{
EditorCurveBinding aBinding = aBindings[i];
EditorCurveBinding bBinding = bBindings[i];
if (aBinding.path != bBinding.path || aBinding.propertyName != bBinding.propertyName)
return false;
AnimationCurve aCurve = AnimationUtility.GetEditorCurve(a, aBinding);
AnimationCurve bCurve = AnimationUtility.GetEditorCurve(b, bBinding);
if (!IsSame(aCurve, bCurve))
return false;
}
return true;
}
static IEnumerable<T> FindAssetsByType<T>() where T : UnityEngine.Object
{
string filter = "t:" + typeof(T).ToString().Replace("UnityEngine.", "");
foreach (string guid in AssetDatabase.FindAssets(filter))
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
if (asset != null)
{
yield return asset;
}
}
}
static IEnumerable<Material> FindLinkedMaterials(Texture2D selectedTexture)
{
foreach (Material material in FindAssetsByType<Material>())
{
foreach (string texName in material.GetTexturePropertyNames())
{
if (material.GetTexture(texName) is Texture2D texture && texture == selectedTexture)
{
yield return material;
}
}
}
}
static IEnumerable<Texture2D> FindLinkedTextures(Material selectedMaterial)
{
foreach (string texName in selectedMaterial.GetTexturePropertyNames())
{
if (selectedMaterial.GetTexture(texName) is Texture2D texture)
{
yield return texture;
}
}
}
static IEnumerable<Texture2D> FindDuplicateTextures(Texture2D selectedTexture)
{
foreach (Texture2D texture in FindAssetsByType<Texture2D>())
{
if (texture.imageContentsHash == selectedTexture.imageContentsHash)
{
yield return texture;
}
}
}
static IEnumerable<AnimationClip> FindDuplicateAnimationClips(AnimationClip selectedAnimationClip)
{
foreach (AnimationClip animationClip in FindAssetsByType<AnimationClip>())
if (IsSame(animationClip, selectedAnimationClip))
yield return animationClip;
}
private static void Crawl(BlendTree blendTree)
{
foreach (ChildMotion childMotion in blendTree.children)
Crawl(childMotion.motion);
}
private const string ANIM_PATH = "Assets/Project/Components/Animations/";
private static HashSet<AnimationClip> AllClips = new HashSet<AnimationClip>();
private static HashSet<AnimationClip> DuplicateClips = new HashSet<AnimationClip>();
private static Motion Crawl(Motion motion)
{
if (motion is AnimationClip clip)
{
var matchingClips = AllClips.Where(c => IsSame(c, clip));
string clipPath = AssetDatabase.GetAssetPath(clip);
if (!clipPath.StartsWith(ANIM_PATH))
{
AnimationClip betterClip = matchingClips.Where(c => c != clip && AssetDatabase.GetAssetPath(c).StartsWith(ANIM_PATH)).FirstOrDefault();
if (betterClip != null)
{
clip = betterClip;
}
else
{
string dstPath = GenerateNonConflictingPath(clipPath.Substring(clipPath.LastIndexOf('/') + 1));
Debug.Log(dstPath);
if (TryMoveFile(clipPath, ANIM_PATH + $"anim_{GUID.Generate()}.anim"))
{
AssetDatabase.Refresh();
}
else
{
Debug.Log("Failed to move!");
}
}
}
foreach (var badClip in matchingClips.ToArray().Where(c => c != clip))
{
AllClips.Remove(badClip);
DuplicateClips.Add(badClip);
}
motion = clip;
}
else if (motion is BlendTree blendTree)
{
Crawl(blendTree);
}
return motion;
}
private static void Crawl(AnimatorState animatorState)
{
animatorState.motion = Crawl(animatorState.motion);
}
private static void Crawl(AnimatorStateMachine stateMachine)
{
ChildAnimatorState[] states = stateMachine.states;
for (int i = 0; i < states.Length; i++)
{
Crawl(states[i].state);
}
ChildAnimatorStateMachine[] machines = stateMachine.stateMachines;
for (int i = 0; i < machines.Length; i++)
{
Crawl(machines[i].stateMachine);
}
}
private static void Crawl(AnimatorController controller)
{
foreach (AnimatorControllerLayer layer in controller.layers)
{
Crawl(layer.stateMachine);
}
}
[MenuItem("Assets/Refloc/Linked Materials", true)]
private static bool SelectLinkedMaterialsValidator()
{
return Selection.activeObject is Texture2D;
}
[MenuItem("Assets/Refloc/Linked Materials")]
private static void SelectLinkedMaterials()
{
Selection.objects = FindLinkedMaterials(Selection.activeObject as Texture2D).Cast<Object>().ToArray();
}
[MenuItem("Assets/Refloc/Linked Textures", true)]
private static bool SelectLinkedTexture2DsValidator()
{
return Selection.activeObject is Material;
}
[MenuItem("Assets/Refloc/Linked Textures")]
private static void SelectLinkedTexture2Ds()
{
Selection.objects = FindLinkedTextures(Selection.activeObject as Material).Cast<Object>().ToArray();
}
[MenuItem("Assets/Refloc/Duplicates", true)]
private static bool SelectDuplicatesValidator()
{
return Selection.activeObject is Texture2D || Selection.activeObject is AnimationClip;
}
[MenuItem("Assets/Refloc/Duplicates")]
private static void SelectDuplicates()
{
if (Selection.activeObject is Texture2D texture)
{
Selection.objects = FindDuplicateTextures(texture).Cast<Object>().ToArray();
}
else if (Selection.activeObject is AnimationClip animationClip)
{
Selection.objects = FindDuplicateAnimationClips(animationClip).Cast<Object>().Where(a => a != animationClip).ToArray();
}
}
[MenuItem("Assets/Refloc/Deduplicate", true)]
private static bool DeDuplicateValidator()
{
return Selection.activeObject is Texture2D || Selection.activeObject is AnimationClip;
}
[MenuItem("Assets/Refloc/Deduplicate")]
private static void DeDuplicate()
{
if (Selection.activeObject is Texture2D texture) // Materials, and ???
{
}
else if (Selection.activeObject is AnimationClip animationClip) // AnimatorOverrideControllers, Assets, Prefabs, Scripts
{
AllClips = new HashSet<AnimationClip>(FindAssetsByType<AnimationClip>());
foreach (AnimatorController controller in FindAssetsByType<RuntimeAnimatorController>().Cast<AnimatorController>())
{
Crawl(controller);
}
Debug.Log(DuplicateClips.Count);
foreach (var badClip in DuplicateClips)
{
try
{
AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(badClip));
}
catch (System.Exception)
{
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment