Skip to content

Instantly share code, notes, and snippets.

@AliAlbarrak
Last active April 23, 2025 10:35
Show Gist options
  • Save AliAlbarrak/e3f66632d3b4f7c3fccbcd750dab1e4b to your computer and use it in GitHub Desktop.
Save AliAlbarrak/e3f66632d3b4f7c3fccbcd750dab1e4b to your computer and use it in GitHub Desktop.
Auto generate Unity's SpriteAtlas per scene and per asset bundle [WIP]
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.U2D;
public static class SpriteAtlasGenerator
{
private const string AtlasFolder = "Assets/Sprites/Sprite Atlas";
private const int GroupingThreshold = 20;
private const string TemplateAtlasName = "Template";
private const string AtlasAddressableGroupName = "Sprite Atlases";
[MenuItem("Tools/Kammelna/Generate Sprite Atlases", priority = 1)]
private static void GenerateSpriteAtlas()
{
EnsureAtlasFolderExist();
var bundleGroups = GetBundlesTextureGroups();
var scenesGroups = GetScenesTextureGroups();
var commonDependencies = ExtractCommonDependencies(bundleGroups, scenesGroups);
CreateAtlases(scenesGroups, "Scenes");
CreateAtlases(bundleGroups, "Bundles", true);
CreateCommonAtlases(commonDependencies);
AssetDatabase.Refresh();
AddressableAssetSettingsDefaultObject.Settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget);
}
private static void CreateAtlases(Dictionary<List<string>, List<string>> scenesGroups, string groupName, bool markAddressable = false)
{
var alphaGroup = new List<Object>();
var template = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(GetAtlasPath(TemplateAtlasName));
foreach (var pair in scenesGroups)
{
var textures = new List<Object>();
foreach (var texturePath in pair.Value)
{
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
if (texture.isReadable)
{
alphaGroup.Add(texture);
continue;
}
textures.Add(texture);
}
if (!textures.Any())
continue;
var name = pair.Key.Count > 3 ? $"Multiple {groupName} ({pair.Key.Count})" : string.Join("_", pair.Key);
CreateAtlas(template, textures, name, markAddressable);
}
CreateAtlas(template, alphaGroup, $"Read Write Enabled Sprites ({groupName})", markAddressable, true);
}
private static void CreateCommonAtlases(List<string> commonDependencies)
{
var template = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(GetAtlasPath(TemplateAtlasName));
var textures = commonDependencies.Select(AssetDatabase.LoadAssetAtPath<Texture>).ToList();
var normalTextures = textures.Where(texture => !texture.isReadable).Cast<Object>().ToList();
var alphaTextures = textures.Where(texture => texture.isReadable).Cast<Object>().ToList();
CreateAtlas(template, normalTextures, "Scenes Bundle Commons", true);
CreateAtlas(template, alphaTextures, "Scenes Bundle Commons Read Write Enabled", true, true);
}
private static void CreateAtlas(SpriteAtlas template, List<Object> textures, string atlasName, bool markAddressable = false, bool readable = false)
{
var alphaAtlas = Object.Instantiate(template);
if(readable)
{
var settings = alphaAtlas.GetTextureSettings();
settings.readable = true;
alphaAtlas.SetTextureSettings(settings);
}
alphaAtlas.Add(textures.ToArray());
var atlasPath = AssetDatabase.GenerateUniqueAssetPath(GetAtlasPath(atlasName));
AssetDatabase.CreateAsset(alphaAtlas, atlasPath);
if (markAddressable)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
var assetGroup = settings.groups.FirstOrDefault(group => group.Name == AtlasAddressableGroupName);
if(assetGroup == null)
assetGroup = settings.CreateGroup(AtlasAddressableGroupName, false, false, false, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema));
assetGroup.GetSchema<ContentUpdateGroupSchema>().StaticContent = true;
settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(atlasPath), assetGroup, false, false);
}
}
private static List<string> ExtractCommonDependencies(Dictionary<List<string>,List<string>> bundleGroups, Dictionary<List<string>,List<string>> scenesGroups)
{
var bundleDependencies = bundleGroups.SelectMany(pair => pair.Value);
var scenesDependencies = scenesGroups.SelectMany(pair => pair.Value);
var commonDependencies = bundleDependencies.Intersect(scenesDependencies).ToList();
foreach (var pair in bundleGroups)
pair.Value.RemoveList(commonDependencies);
foreach (var pair in scenesGroups)
pair.Value.RemoveList(commonDependencies);
return commonDependencies;
}
private static string GetAtlasPath(string name)
{
return $"{AtlasFolder}/{name}.spriteatlas";
}
private static void EnsureAtlasFolderExist()
{
if (!AssetDatabase.IsValidFolder(AtlasFolder))
{
var split = AtlasFolder.LastIndexOf('/');
var path = AtlasFolder.Substring(0, split);
var name = AtlasFolder.Substring(split + 1);
AssetDatabase.CreateFolder(path, name);
AssetDatabase.Refresh();
}
}
private static Dictionary<List<string>, List<string>> GetBundlesTextureGroups()
{
Dictionary<string, List<string>> assetsByDependent = new Dictionary<string, List<string>>();
var groupsList = new List<string>();
foreach (var assetGroup in AddressableAssetSettingsDefaultObject.Settings.groups.Where(group => group.GetSchema<ContentUpdateGroupSchema>() != null))
{
foreach (var assetEntry in assetGroup.entries)
{
if (AssetDatabase.GetMainAssetTypeAtPath(assetEntry.AssetPath) != typeof(GameObject))
continue;
groupsList.Add(assetGroup.Name);
AddToDependencies(assetsByDependent, assetEntry.AssetPath, assetGroup.Name);
}
}
groupsList = groupsList.Distinct().ToList();
return ComputeTextureGroups(assetsByDependent, groupsList);
}
private static Dictionary<List<string>, List<string>> GetScenesTextureGroups()
{
Dictionary<string, List<string>> assetsByDependent = new Dictionary<string, List<string>>();
var scenesList = new List<string>();
foreach (var scene in EditorBuildSettings.scenes.Where(scene => scene.enabled))
{
var sceneName = Path.GetFileNameWithoutExtension(scene.path);
scenesList.Add(sceneName);
AddToDependencies(assetsByDependent, scene.path, sceneName);
}
return ComputeTextureGroups(assetsByDependent, scenesList);
}
private static Dictionary<List<string>, List<string>> ComputeTextureGroups(Dictionary<string, List<string>> assetsByDependent, List<string> groupsList)
{
var textureGroups = assetsByDependent.GroupBy(pair => pair.Value, new StringListComparer())
.OrderBy(group => group.Select(pair => pair.Key).Count())
.ToDictionary(group => group.Key, group => group.Select(pair => pair.Key).ToList());
if (!textureGroups.ContainsKey(groupsList))
textureGroups[groupsList] = new List<string>();
while (textureGroups.Min(pair => pair.Value.Count) < GroupingThreshold)
{
var minGroup = textureGroups.Where(pair => pair.Key.Count > 2 && pair.Value.Count < GroupingThreshold).MinBy(pair => pair.Key.Count);
if (minGroup.Key.Count == groupsList.Count)
break;
var newGroup = textureGroups.Where(pair => pair.Key.Count > minGroup.Key.Count && !minGroup.Key.Except(pair.Key).Any()).MinBy(pair => pair.Key.Count);
textureGroups.Remove(minGroup.Key);
newGroup.Value.AddRange(minGroup.Value);
textureGroups[newGroup.Key] = newGroup.Value;
}
return textureGroups;
}
private static void AddToDependencies(IDictionary<string, List<string>> assetsByDependent, string path, string dependentName)
{
string[] dependencies = AssetDatabase.GetDependencies(path);
foreach (var dependencyPath in dependencies)
{
if (!dependencyPath.StartsWith("Assets/Sprites") || dependencyPath.Contains("/Resources/"))
continue;
if (AssetDatabase.GetMainAssetTypeAtPath(dependencyPath) != typeof(Texture2D))
continue;
if (!assetsByDependent.ContainsKey(dependencyPath))
assetsByDependent[dependencyPath] = new List<string>();
if(!assetsByDependent[dependencyPath].Contains(dependentName))
assetsByDependent[dependencyPath].Add(dependentName);
}
}
private class StringListComparer : IEqualityComparer<List<string>>
{
public bool Equals(List<string> x, List<string> y)
{
if (x is null || y is null)
return x is null && y is null;
return x.All(y.Contains);
}
public int GetHashCode(List<string> obj)
{
return 0;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment