├── Editor ├── AtlasTexturePreview.cs.meta ├── TextureAtlasCreator.cs.meta ├── AtlasTexturePreview.cs └── TextureAtlasCreator.cs ├── Screenshots ├── AtlasPreview.PNG ├── SelectObjects.PNG ├── CreateAtlasWindow.PNG ├── AtlasPreview.PNG.meta ├── SelectObjects.PNG.meta └── CreateAtlasWindow.PNG.meta ├── Editor.meta ├── Screenshots.meta ├── .gitignore ├── LICENSE └── README.md /Editor/AtlasTexturePreview.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 70e4da0824b9424ea0d3d9888dda89c3 3 | timeCreated: 1532890202 -------------------------------------------------------------------------------- /Screenshots/AtlasPreview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioteixeira/UnityTextureAtlasCreator/HEAD/Screenshots/AtlasPreview.PNG -------------------------------------------------------------------------------- /Screenshots/SelectObjects.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioteixeira/UnityTextureAtlasCreator/HEAD/Screenshots/SelectObjects.PNG -------------------------------------------------------------------------------- /Screenshots/CreateAtlasWindow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caioteixeira/UnityTextureAtlasCreator/HEAD/Screenshots/CreateAtlasWindow.PNG -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0a8896254441f874180b3de2beb2e9c6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Screenshots.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 75b93214bac38a8488f7f5bec59bb88c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/TextureAtlasCreator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0d4aadfff63bab34f92201bfabfa0016 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | Assets/AssetStoreTools* 7 | 8 | # Visual Studio cache directory 9 | .vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | *.opendb 26 | 27 | # Unity3D generated meta files 28 | *.pidb.meta 29 | *.pdb.meta 30 | 31 | # Unity3D Generated File On Crash Reports 32 | sysinfo.txt 33 | 34 | # Builds 35 | *.apk 36 | *.unitypackage 37 | -------------------------------------------------------------------------------- /Editor/AtlasTexturePreview.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace TextureAtlasCreator 5 | { 6 | public class AtlasTexturePreview : EditorWindow 7 | { 8 | private Texture2D texture; 9 | 10 | public static void Init(Texture2D texture) 11 | { 12 | var window = GetWindowWithRect(new Rect(0, 0, 512, 512)); 13 | window.texture = texture; 14 | window.Show(); 15 | } 16 | 17 | private void OnGUI() 18 | { 19 | EditorGUI.DrawPreviewTexture(new Rect(0, 0, 512, 512), texture); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Caio Vinicius Marques Teixeira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityTextureAtlasCreator 2 | 3 | A simple tool to easily combine materials on a single texture atlas and material, useful to improve batching and reduce draw calls on mobile games. 4 | 5 | ## How it works 6 | * All textures are combined on a texture atlas using [Unity's built-in PackTextures method](https://docs.unity3d.com/ScriptReference/Texture2D.PackTextures.html) 7 | * A new material that uses the texture atlas is created. 8 | * New UV's are calculated and applied to each selected mesh. Currently it is not saving the new meshes on assets, but it should be easy to do. :) 9 | 10 | ## How to use 11 | * Add the Editor folder to your project. 12 | * Select the GameObjects that use the materials that you want to combine. 13 | 14 | ![](Screenshots/SelectObjects.PNG?raw=true) 15 | * Click on Window/TextureAtlasCreator to open the tool window, you can see all textures that can be combined on an atlas and also change the max size of the generated atlas. 16 | 17 | ![](Screenshots/CreateAtlasWindow.PNG?raw=true) 18 | 19 | * Click on "Create Texture Atlas" and wait a few seconds. You will be asked for paths to save the atlas texture and material. After some time you will see a window showing the generated atlas and all the selected meshes will be updated with new UVs and a new material that uses it. 20 | 21 | ![](Screenshots/AtlasPreview.PNG?raw=true) 22 | 23 | ## Current issues 24 | * Only works with materials using Mobile/Diffuse shader (actually, with a minor change it should work with any material that uses a single texture shader) 25 | * Can't handle meshes with UV maps outside of 0-1 range 26 | * Defining the atlas size could be somehow automatic 27 | -------------------------------------------------------------------------------- /Screenshots/AtlasPreview.PNG.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 55a3aea7469737d4daf1b7f95268f951 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 5 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | grayScaleToAlpha: 0 25 | generateCubemap: 6 26 | cubemapConvolution: 0 27 | seamlessCubemap: 0 28 | textureFormat: 1 29 | maxTextureSize: 2048 30 | textureSettings: 31 | serializedVersion: 2 32 | filterMode: -1 33 | aniso: -1 34 | mipBias: -1 35 | wrapU: -1 36 | wrapV: -1 37 | wrapW: -1 38 | nPOTScale: 1 39 | lightmap: 0 40 | compressionQuality: 50 41 | spriteMode: 0 42 | spriteExtrude: 1 43 | spriteMeshType: 1 44 | alignment: 0 45 | spritePivot: {x: 0.5, y: 0.5} 46 | spritePixelsToUnits: 100 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spriteGenerateFallbackPhysicsShape: 1 49 | alphaUsage: 1 50 | alphaIsTransparency: 0 51 | spriteTessellationDetail: -1 52 | textureType: 0 53 | textureShape: 1 54 | singleChannelComponent: 0 55 | maxTextureSizeSet: 0 56 | compressionQualitySet: 0 57 | textureFormatSet: 0 58 | platformSettings: 59 | - serializedVersion: 2 60 | buildTarget: DefaultTexturePlatform 61 | maxTextureSize: 2048 62 | resizeAlgorithm: 0 63 | textureFormat: -1 64 | textureCompression: 1 65 | compressionQuality: 50 66 | crunchedCompression: 0 67 | allowsAlphaSplitting: 0 68 | overridden: 0 69 | androidETC2FallbackOverride: 0 70 | spriteSheet: 71 | serializedVersion: 2 72 | sprites: [] 73 | outline: [] 74 | physicsShape: [] 75 | bones: [] 76 | spriteID: 77 | vertices: [] 78 | indices: 79 | edges: [] 80 | weights: [] 81 | spritePackingTag: 82 | userData: 83 | assetBundleName: 84 | assetBundleVariant: 85 | -------------------------------------------------------------------------------- /Screenshots/SelectObjects.PNG.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f7c892182eb717040885991fc7a185e0 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 5 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | grayScaleToAlpha: 0 25 | generateCubemap: 6 26 | cubemapConvolution: 0 27 | seamlessCubemap: 0 28 | textureFormat: 1 29 | maxTextureSize: 2048 30 | textureSettings: 31 | serializedVersion: 2 32 | filterMode: -1 33 | aniso: -1 34 | mipBias: -1 35 | wrapU: -1 36 | wrapV: -1 37 | wrapW: -1 38 | nPOTScale: 1 39 | lightmap: 0 40 | compressionQuality: 50 41 | spriteMode: 0 42 | spriteExtrude: 1 43 | spriteMeshType: 1 44 | alignment: 0 45 | spritePivot: {x: 0.5, y: 0.5} 46 | spritePixelsToUnits: 100 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spriteGenerateFallbackPhysicsShape: 1 49 | alphaUsage: 1 50 | alphaIsTransparency: 0 51 | spriteTessellationDetail: -1 52 | textureType: 0 53 | textureShape: 1 54 | singleChannelComponent: 0 55 | maxTextureSizeSet: 0 56 | compressionQualitySet: 0 57 | textureFormatSet: 0 58 | platformSettings: 59 | - serializedVersion: 2 60 | buildTarget: DefaultTexturePlatform 61 | maxTextureSize: 2048 62 | resizeAlgorithm: 0 63 | textureFormat: -1 64 | textureCompression: 1 65 | compressionQuality: 50 66 | crunchedCompression: 0 67 | allowsAlphaSplitting: 0 68 | overridden: 0 69 | androidETC2FallbackOverride: 0 70 | spriteSheet: 71 | serializedVersion: 2 72 | sprites: [] 73 | outline: [] 74 | physicsShape: [] 75 | bones: [] 76 | spriteID: 77 | vertices: [] 78 | indices: 79 | edges: [] 80 | weights: [] 81 | spritePackingTag: 82 | userData: 83 | assetBundleName: 84 | assetBundleVariant: 85 | -------------------------------------------------------------------------------- /Screenshots/CreateAtlasWindow.PNG.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ae021e90d5a102b4e97cf84a77f3b68c 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 5 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | grayScaleToAlpha: 0 25 | generateCubemap: 6 26 | cubemapConvolution: 0 27 | seamlessCubemap: 0 28 | textureFormat: 1 29 | maxTextureSize: 2048 30 | textureSettings: 31 | serializedVersion: 2 32 | filterMode: -1 33 | aniso: -1 34 | mipBias: -1 35 | wrapU: -1 36 | wrapV: -1 37 | wrapW: -1 38 | nPOTScale: 1 39 | lightmap: 0 40 | compressionQuality: 50 41 | spriteMode: 0 42 | spriteExtrude: 1 43 | spriteMeshType: 1 44 | alignment: 0 45 | spritePivot: {x: 0.5, y: 0.5} 46 | spritePixelsToUnits: 100 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spriteGenerateFallbackPhysicsShape: 1 49 | alphaUsage: 1 50 | alphaIsTransparency: 0 51 | spriteTessellationDetail: -1 52 | textureType: 0 53 | textureShape: 1 54 | singleChannelComponent: 0 55 | maxTextureSizeSet: 0 56 | compressionQualitySet: 0 57 | textureFormatSet: 0 58 | platformSettings: 59 | - serializedVersion: 2 60 | buildTarget: DefaultTexturePlatform 61 | maxTextureSize: 2048 62 | resizeAlgorithm: 0 63 | textureFormat: -1 64 | textureCompression: 1 65 | compressionQuality: 50 66 | crunchedCompression: 0 67 | allowsAlphaSplitting: 0 68 | overridden: 0 69 | androidETC2FallbackOverride: 0 70 | spriteSheet: 71 | serializedVersion: 2 72 | sprites: [] 73 | outline: [] 74 | physicsShape: [] 75 | bones: [] 76 | spriteID: 77 | vertices: [] 78 | indices: 79 | edges: [] 80 | weights: [] 81 | spritePackingTag: 82 | userData: 83 | assetBundleName: 84 | assetBundleVariant: 85 | -------------------------------------------------------------------------------- /Editor/TextureAtlasCreator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace TextureAtlasCreator 7 | { 8 | public class TextureAtlasCreator : EditorWindow 9 | { 10 | private List textures; 11 | private Texture2D atlasAsset; 12 | private MeshRenderer[] meshRenderers; 13 | private List meshFilters; 14 | private Dictionary rendererToTextureIndex; 15 | 16 | private string shaderName = "Mobile/Diffuse"; 17 | private int maxAtlasSize = 2048; 18 | 19 | [MenuItem("Window/TextureAtlasCreator")] 20 | private static void Init() 21 | { 22 | var window = (TextureAtlasCreator) GetWindow(typeof(TextureAtlasCreator)); 23 | window.Show(); 24 | } 25 | 26 | private void OnEnable() 27 | { 28 | meshFilters = new List(); 29 | textures = new List(); 30 | UpdateSelection(); 31 | } 32 | 33 | private void OnSelectionChange() 34 | { 35 | UpdateSelection(); 36 | } 37 | 38 | private void OnGUI() 39 | { 40 | AtlasSizeDropdown(); 41 | 42 | if (textures.Count == 0) 43 | { 44 | EditorGUILayout.HelpBox(string.Format("Can't find any material using {0} shader on selected objects", 45 | shaderName), MessageType.Error); 46 | return; 47 | } 48 | 49 | GUILayout.Label("Textures in selected meshes: ", EditorStyles.boldLabel); 50 | foreach (var texture in textures) 51 | { 52 | GUILayout.Label(texture.name); 53 | } 54 | 55 | if (GUILayout.Button("CreateTextureAtlas")) 56 | { 57 | Rect[] newUvs; 58 | atlasAsset = PackTextures(out newUvs); 59 | var atlasMaterial = CreateNewMaterial(); 60 | UpdateMeshes(newUvs, atlasMaterial); 61 | 62 | AtlasTexturePreview.Init(atlasAsset); 63 | } 64 | } 65 | 66 | private void AtlasSizeDropdown() 67 | { 68 | maxAtlasSize = EditorGUILayout.IntPopup("Max Atlas Size", 69 | maxAtlasSize, new[] {"256", "512", "1024", "2048", "4096", "8192"}, 70 | new[] {256, 512, 1024, 2048, 4096, 8192}); 71 | } 72 | 73 | private void UpdateSelection() 74 | { 75 | textures.Clear(); 76 | meshFilters.Clear(); 77 | foreach (var selectedObject in Selection.gameObjects) 78 | { 79 | meshRenderers = selectedObject.GetComponentsInChildren(); 80 | foreach (var meshFilter in meshRenderers) 81 | { 82 | meshFilters.Add(meshFilter.GetComponent()); 83 | } 84 | 85 | UpdateSelectedTextures(); 86 | } 87 | } 88 | 89 | private void UpdateMeshes(Rect[] newUvs, Material material) 90 | { 91 | for (int i = 0; i < meshFilters.Count; i++) 92 | { 93 | var renderer = meshRenderers[i]; 94 | var texture = renderer.sharedMaterial.mainTexture; 95 | var textIndex = textures.FindIndex(tex => tex == texture); 96 | 97 | var mesh = meshFilters[i].mesh; 98 | var uvRectOnAtlas = newUvs[textIndex]; 99 | var transformedUv = ComputeUvsOnAtlas(mesh, uvRectOnAtlas); 100 | 101 | mesh.uv = transformedUv; 102 | renderer.material = material; 103 | } 104 | } 105 | 106 | private static Vector2[] ComputeUvsOnAtlas(Mesh mesh, Rect uvRectOnAtlas) 107 | { 108 | var transformedUv = new Vector2[mesh.uv.Length]; 109 | for (var uvIndex = 0; uvIndex < mesh.uv.Length; uvIndex++) 110 | { 111 | var oldUv = mesh.uv[uvIndex]; 112 | var newUv = new Vector2 113 | { 114 | x = Mathf.Lerp(uvRectOnAtlas.xMin, uvRectOnAtlas.xMax, oldUv.x), 115 | y = Mathf.Lerp(uvRectOnAtlas.yMin, uvRectOnAtlas.yMax, oldUv.y) 116 | }; 117 | transformedUv[uvIndex] = newUv; 118 | } 119 | return transformedUv; 120 | } 121 | 122 | private void UpdateSelectedTextures() 123 | { 124 | foreach (var meshRenderer in meshRenderers) 125 | { 126 | var material = meshRenderer.sharedMaterial; 127 | if (material.shader.name != shaderName) 128 | { 129 | continue; 130 | } 131 | 132 | var texture = material.mainTexture as Texture2D; 133 | if (!textures.Contains(texture)) 134 | { 135 | textures.Add(texture); 136 | } 137 | } 138 | } 139 | 140 | private Texture2D PackTextures(out Rect[] uvs) 141 | { 142 | var atlas = new Texture2D(maxAtlasSize, maxAtlasSize); 143 | 144 | foreach (var texture in textures) 145 | { 146 | SetTextureAsReadable(texture, true); 147 | } 148 | 149 | AssetDatabase.Refresh(); 150 | 151 | uvs = atlas.PackTextures(textures.ToArray(), 2, maxAtlasSize); 152 | 153 | var uncompressedAtlas = new Texture2D(atlas.width, atlas.height); 154 | uncompressedAtlas.SetPixels(atlas.GetPixels()); 155 | 156 | var path = EditorUtility.SaveFilePanelInProject("Atlas Name", "", "png", ""); 157 | File.WriteAllBytes(path, uncompressedAtlas.EncodeToPNG()); 158 | 159 | foreach (var texture in textures) 160 | { 161 | SetTextureAsReadable(texture, false); 162 | } 163 | 164 | AssetDatabase.Refresh(); 165 | 166 | return AssetDatabase.LoadAssetAtPath(path); 167 | } 168 | 169 | private static void SetTextureAsReadable(Texture2D texture, bool importerIsReadable) 170 | { 171 | var texturePath = AssetDatabase.GetAssetPath(texture); 172 | var importer = (TextureImporter) AssetImporter.GetAtPath(texturePath); 173 | importer.isReadable = importerIsReadable; 174 | AssetDatabase.ImportAsset(texturePath); 175 | } 176 | 177 | private Material CreateNewMaterial() 178 | { 179 | var material = new Material(Shader.Find(shaderName)); 180 | var path = EditorUtility.SaveFilePanelInProject("Material Name", "", "mat", ""); 181 | material.mainTexture = atlasAsset; 182 | 183 | AssetDatabase.CreateAsset(material, path); 184 | AssetDatabase.Refresh(); 185 | return material; 186 | } 187 | } 188 | } 189 | --------------------------------------------------------------------------------