using System; using System.Collections.Generic; using System.Linq; using Unity.Collections; using Unity.HLODSystem.Utils; using UnityEngine; namespace Unity.HLODSystem { public class TexturePacker : IDisposable { private NativeArray m_detector = new NativeArray(1, Allocator.Persistent); public class MaterialTexture : IDisposable { private DisposableList m_textures = new DisposableList(); private NativeArray m_detector = new NativeArray(1, Allocator.Persistent); public void Add(WorkingTexture texture) { m_textures.Add(texture.Clone()); } public MaterialTexture Clone() { MaterialTexture nmt = new MaterialTexture(); for (int i = 0; i < m_textures.Count; ++i) { nmt.Add(m_textures[i]); } return nmt; } public int Count => m_textures.Count; public WorkingTexture this[int index] { get => m_textures[index]; set => m_textures[index] = value; } public void Dispose() { m_textures.Dispose(); m_textures = null; m_detector.Dispose(); } } public class TextureAtlas : IDisposable { private NativeArray m_detector = new NativeArray(1, Allocator.Persistent); private List m_objects; private List m_uvs; private List m_guids; private DisposableList m_textures; public List Objects => m_objects; public List UVs => m_uvs; public DisposableList Textures => m_textures; protected TextureAtlas(List objs, List uvs, List guids) { m_objects = objs; m_uvs = uvs; m_guids = guids; m_textures = new DisposableList(); } public bool Contains(object obj) { return m_objects.Contains(obj); } public Rect GetUV(Guid textureGuid) { int index = m_guids.IndexOf(textureGuid); return m_uvs[index]; } public void Dispose() { m_textures.Dispose(); m_detector.Dispose(); } } class TextureAtlasCreator : TextureAtlas { public TextureAtlasCreator(List objs, List uvs, List guids, DisposableList combiners) : base(objs, uvs, guids) { for (int i = 0; i < combiners.Count; ++i) { Textures.Add(combiners[i].GetTexture().Clone()); } } } class Score { public Source Lhs; public Source Rhs; public int MatchCount; } class TextureCombiner : IDisposable { private WorkingTexture m_texture; private NativeArray m_detector = new NativeArray(1, Allocator.Persistent); public TextureCombiner(Allocator allocator, TextureFormat format, int width, int height, bool linear) { m_texture = new WorkingTexture(allocator, format, width, height, linear); } public void Dispose() { m_texture?.Dispose(); m_detector.Dispose(); } public void SetTexture(WorkingTexture source, int x, int y) { m_texture.Blit(source, x, y); } public WorkingTexture GetTexture() { return m_texture; } } class Source : IDisposable { private NativeArray m_detector = new NativeArray(1, Allocator.Persistent); public static Score GetScore(Source lhs, Source rhs) { int match = lhs.m_textureGuids.Intersect(rhs.m_textureGuids).Count(); return new Score() { Lhs = lhs, Rhs = rhs, MatchCount = match }; } public static Source Combine(Source lhs, Source rhs) { Source newSource = new Source(); newSource.m_obj = new List(); newSource.m_obj.AddRange(lhs.m_obj); newSource.m_obj.AddRange(rhs.m_obj); newSource.m_textures = new DisposableList(); newSource.m_textureGuids = new List(); for (int i = 0; i < lhs.m_textures.Count; ++i) { newSource.m_textures.Add(lhs.m_textures[i].Clone()); newSource.m_textureGuids.Add(lhs.m_textureGuids[i]); } for (int i = 0; i < rhs.m_textures.Count; ++i) { if (newSource.m_textureGuids.Contains(rhs.m_textureGuids[i]) == false) { newSource.m_textures.Add(rhs.m_textures[i].Clone()); newSource.m_textureGuids.Add(rhs.m_textureGuids[i]); } } return newSource; } public static int CombineTextureCount(Source lhs, Source rhs) { int count = lhs.m_textureGuids.Count; for (int i = 0; i < rhs.m_textureGuids.Count; ++i) { if (lhs.m_textureGuids.Contains(rhs.m_textureGuids[i]) == false) { count += 1; } } return count; } private List m_obj; private List m_textureGuids; private DisposableList m_textures; //use for combine. //this constructor should not call from out private Source() { } public Source(object obj, DisposableList textures) { m_obj = new List(); m_obj.Add(obj); m_textures = textures; m_textureGuids = new List(textures.Count); for (int i = 0; i < textures.Count; ++i) { m_textureGuids.Add(textures[i][0].GetGUID()); } } public void Dispose() { m_textures?.Dispose(); m_detector.Dispose(); } public Source Clone() { Source ns = new Source(); ns.m_obj = new List(); ns.m_obj.AddRange(m_obj); ns.m_textureGuids = new List(); ns.m_textureGuids.AddRange(m_textureGuids); ns.m_textures = new DisposableList(); for (int i = 0; i < m_textures.Count; ++i) { ns.m_textures.Add(m_textures[i].Clone()); } return ns; } public int GetMaxTextureCount(int packTextureSize, int maxSourceSize) { int minTextureCount = packTextureSize / maxSourceSize; //width * height minTextureCount = minTextureCount * minTextureCount; //we can't pack one texture. //so, we should use half size texture. while (minTextureCount < m_textures.Count) minTextureCount = minTextureCount * 4; return minTextureCount; } public TextureAtlas CreateAtlas(TextureFormat format, int packTextureSize, bool linear) { if (m_textures.Count == 0) { return null; } int itemCount = Mathf.CeilToInt(Mathf.Sqrt(m_textures.Count)); int itemSize = packTextureSize / itemCount; TextureAtlas atlas; using (DisposableList resizedTextures = CreateResizedTextures(itemSize, itemSize)) using (DisposableList combiners = new DisposableList()) { List uvs = new List(resizedTextures.Count); List guids = new List(resizedTextures.Count); for (int i = 0; i < resizedTextures.Count; ++i) { int x = i % itemCount; int y = i / itemCount; for (int k = combiners.Count; k < resizedTextures[i].Count; ++k) combiners.Add(new TextureCombiner(Allocator.Persistent, format, packTextureSize, packTextureSize, linear)); uvs.Add(new Rect( (float) (x * itemSize)/ (float) packTextureSize, (float) (y * itemSize)/ (float) packTextureSize, (float) resizedTextures[i][0].Width / (float) packTextureSize, (float) resizedTextures[i][0].Height / (float) packTextureSize)); guids.Add(m_textures[i][0].GetGUID()); for (int k = 0; k < resizedTextures[i].Count; ++k) { combiners[k].SetTexture(resizedTextures[i][k], x * itemSize, y * itemSize); } } atlas = new TextureAtlasCreator(m_obj, uvs, guids, combiners); } return atlas; } private DisposableList CreateResizedTextures(int newWidth, int newHeight) { DisposableList resized = new DisposableList(); for (int i = 0; i < m_textures.Count; ++i) { MaterialTexture newMT = new MaterialTexture(); for (int k = 0; k < m_textures[i].Count; ++k) { int targetWidth = Mathf.Min(newWidth, m_textures[i][k].Width); int targetHeight = Mathf.Min(newHeight, m_textures[i][k].Height); WorkingTexture resizedTexture = m_textures[i][k].Resize(Allocator.Persistent, targetWidth, targetHeight); newMT.Add(resizedTexture); resizedTexture.Dispose(); } resized.Add(newMT); } return resized; } } class TaskGroup : IDisposable { private TextureFormat m_format; private int m_packTextureSize; private int m_maxCount; private bool m_linear; private DisposableList m_sources = new DisposableList(); public TaskGroup(TextureFormat format, int packTextureSize, bool linear, int maxCount) { m_format = format; m_packTextureSize = packTextureSize; m_maxCount = maxCount; m_linear = linear; } public void Dispose() { m_sources.Dispose(); } public void AddSource(Source source) { m_sources.Add(source.Clone()); } public void CombineSources() { List scoreList = new List(); for (int i = 0; i < m_sources.Count; ++i) { for (int k = i + 1; k < m_sources.Count; ++k) { scoreList.Add(Source.GetScore(m_sources[i], m_sources[k])); } } scoreList.Sort((lhs, rhs) => rhs.MatchCount - lhs.MatchCount); for (int i = 0; i < scoreList.Count; ++i) { Score score = scoreList[i]; if (CanCombine(score.Lhs, score.Rhs) == false) continue; Source combinedSource = Source.Combine(score.Lhs, score.Rhs); //Do not dispose lhs, rhs sources //these just moved so must not be released. m_sources.Remove(score.Lhs); m_sources.Remove(score.Rhs); m_sources.Add(combinedSource); //Remove merged Score and make Score by combinedScore. scoreList.RemoveAll(s => s.Lhs == score.Lhs || s.Lhs == score.Rhs || s.Rhs == score.Lhs || s.Rhs == score.Rhs); //last is combinedSource. for (int k = 0; k < m_sources.Count - 1; ++k) { scoreList.Add(Source.GetScore(m_sources[k], combinedSource)); } scoreList.Sort((lhs, rhs) => rhs.MatchCount - lhs.MatchCount); i = -1; //for back to the first loop.. } } public DisposableList CreateTextureAtlases() { DisposableList atlases = new DisposableList(); for (int i = 0; i < m_sources.Count; ++i) { TextureAtlas item = m_sources[i].CreateAtlas(m_format, m_packTextureSize, m_linear); if ( item != null ) atlases.Add(item); } return atlases; } private bool CanCombine(Source lhs, Source rhs) { return Source.CombineTextureCount(lhs, rhs) <= m_maxCount; } } DisposableList m_sources = new DisposableList(); DisposableList m_atlas = new DisposableList(); public TexturePacker() { } public void Dispose() { m_sources.Dispose(); m_atlas.Dispose(); m_detector.Dispose(); } //TODO: must clear what the ownership of texture. public void AddTextureGroup(object obj, List textures) { DisposableList copyTextures = new DisposableList(); for (int i = 0; i < textures.Count; ++i) { copyTextures.Add(textures[i].Clone()); } Source source = new Source(obj, copyTextures); m_sources.Add(source); } public void Pack(TextureFormat format, int packTextureSize, int maxSourceSize, bool linear) { //First, we should separate each group by count. using (DisposableDictionary taskGroups = new DisposableDictionary()) { for (int i = 0; i < m_sources.Count; ++i) { int maxCount = m_sources[i].GetMaxTextureCount(packTextureSize, maxSourceSize); if (taskGroups.ContainsKey(maxCount) == false) taskGroups.Add(maxCount, new TaskGroup(format, packTextureSize, linear, maxCount)); taskGroups[maxCount].AddSource(m_sources[i]); } //Second, we should figure out which group should be combined from each taskGroup. foreach (var taskGroup in taskGroups.Values) { taskGroup.CombineSources(); m_atlas.AddRange(taskGroup.CreateTextureAtlases()); } } } public TextureAtlas GetAtlas(object obj) { for (int i = 0; i < m_atlas.Count; ++i) { if (m_atlas[i].Contains(obj)) return m_atlas[i]; } return null; } public TextureAtlas[] GetAllAtlases() { return m_atlas.ToArray(); } } }