Last active
November 16, 2023 11:39
-
-
Save OptoCloud/27b21da6b0510efed63bfc159fc1fcec to your computer and use it in GitHub Desktop.
Finds duplicate and linked assets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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