using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEditor; using UnityEngine; namespace Poi { static class PoiHelpers { static readonly string suffixSeparator = "_"; /// /// Changes a path in Assets to an absolute windows path /// /// /// public static string LocalAssetsPathToAbsolutePath(string localPath) { localPath = NormalizePathSlashes(localPath); const string assets = "Assets/"; if(localPath.StartsWith(assets)) { localPath = localPath.Remove(0, assets.Length); localPath = $"{Application.dataPath}/{localPath}"; } return localPath; } /// /// Replaces all forward slashes \ with back slashes / /// /// /// public static string NormalizePathSlashes(string path) { if(!string.IsNullOrEmpty(path)) path = path.Replace('\\', '/'); return path; } /// /// Ensures directory exists inside the assets folder /// /// public static void EnsurePathExistsInAssets(string assetPath) { Directory.CreateDirectory(LocalAssetsPathToAbsolutePath(assetPath)); } /// /// Adds a suffix to the end of the string then returns it /// /// /// /// public static string AddSuffix(string str, params string[] suffixes) { bool ignoreSeparatorOnce = string.IsNullOrWhiteSpace(str); StringBuilder sb = new StringBuilder(str); foreach(var suff in suffixes) { if(ignoreSeparatorOnce) { sb.Append(suff); ignoreSeparatorOnce = false; continue; } sb.Append(suffixSeparator + suff); } return sb.ToString(); } /// /// Removes suffix from the end of string then returns it /// /// /// Each to be removed in order /// public static string RemoveSuffix(string str, string[] suffixes) { var suffixList = suffixes.ToList(); suffixList.Remove(str); while(suffixList.Any(str.EndsWith)) foreach(string sfx in suffixList) { string s = suffixSeparator + sfx; if(!str.EndsWith(sfx)) continue; int idx = str.LastIndexOf(s, StringComparison.Ordinal); if(idx != -1) str = str.Remove(idx, s.Length); } return str; } /// /// Draws a GUI ilne /// /// /// internal static void DrawLine(bool spaceBefore = true, bool spaceAfter = true) { float spaceHeight = 3f; if(spaceBefore) GUILayout.Space(spaceHeight); Rect rect = EditorGUILayout.GetControlRect(false, 1); rect.height = 1; EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 1)); if(spaceAfter) GUILayout.Space(spaceHeight); } /// /// Destroys an object with DestroyImmediate in object mode and Destroy in play mode /// /// internal static void DestroyAppropriate(UnityEngine.Object obj) { if(EditorApplication.isPlaying) UnityEngine.Object.Destroy(obj); else UnityEngine.Object.DestroyImmediate(obj); } /// /// Changes path from full windows path to a local path in the Assets folder /// /// /// Path starting with Assets internal static string AbsolutePathToLocalAssetsPath(string path) { if(path.StartsWith(Application.dataPath)) path = "Assets" + path.Substring(Application.dataPath.Length); return path; } /// /// Selects and highlights the asset in your unity Project tab /// /// internal static void PingAssetAtPath(string path) { var inst = AssetDatabase.LoadAssetAtPath(path).GetInstanceID(); EditorGUIUtility.PingObject(inst); } internal static Vector2Int DrawResolutionPicker(Vector2Int size, ref bool linked, ref bool autoDetect, int[] presets = null, string[] presetNames = null) { EditorGUI.BeginDisabledGroup(autoDetect); EditorGUILayout.BeginHorizontal(); { EditorGUILayout.PrefixLabel("Size"); EditorGUI.BeginChangeCheck(); size.x = EditorGUILayout.IntField(size.x); if(linked && EditorGUI.EndChangeCheck()) size.y = size.x; EditorGUILayout.LabelField("x", GUILayout.MaxWidth(12)); EditorGUI.BeginChangeCheck(); size.y = EditorGUILayout.IntField(size.y); if(linked && EditorGUI.EndChangeCheck()) size.x = size.y; if(presets != null && presetNames != null) { EditorGUI.BeginChangeCheck(); int selectedPresetIndex = EditorGUILayout.Popup(GUIContent.none, -1, presetNames, GUILayout.MaxWidth(16)); if(EditorGUI.EndChangeCheck() && selectedPresetIndex != -1) size = new Vector2Int(presets[selectedPresetIndex], presets[selectedPresetIndex]); } linked = GUILayout.Toggle(linked, "L", EditorStyles.miniButton, GUILayout.MaxWidth(16)); } EditorGUILayout.EndHorizontal(); EditorGUI.EndDisabledGroup(); autoDetect = EditorGUILayout.Toggle("Auto detect", autoDetect); return size; } /// /// Gets the combined maximum width and height of the passed in textures /// /// /// internal static Vector2Int GetMaxSizeFromTextures(params Texture2D[] textures) { var sizes = textures.Where(tex => tex).Select(tex => new Vector2Int(tex.width, tex.height)).ToArray(); if(sizes.Length == 0) return default; int maxW = sizes.Max(wh => wh.x); int maxH = sizes.Max(wh => wh.y); return new Vector2Int(maxW, maxH); } internal static Texture2D PackTextures(Vector2Int resolution, Texture2D red, Texture2D green, Texture2D blue, Texture2D alpha, bool invertRed, bool invertGreen, bool invertBlue, bool invertAlpha) { // Setup Material var mat = new Material(PoiExtensions.PackerShader); mat.SetTexture("_Red", red); mat.SetTexture("_Green", green); mat.SetTexture("_Blue", blue); mat.SetTexture("_Alpha", alpha); mat.SetInt("_Invert_Red", Convert.ToInt32(invertRed)); mat.SetInt("_Invert_Green", Convert.ToInt32(invertGreen)); mat.SetInt("_Invert_Blue", Convert.ToInt32(invertBlue)); mat.SetInt("_Invert_Alpha", Convert.ToInt32(invertAlpha)); // Create texture and render to it var tex = new Texture2D(resolution.x, resolution.y); tex.BakeMaterialToTexture(mat); // Cleanup DestroyAppropriate(mat); return tex; } internal static Dictionary UnpackTextureToChannels(Texture2D packedTexture, bool invert, Vector2Int resolution) { var channels = new Dictionary { {PoiExtensions.PoiTextureChannel.Red.ToString().ToLower(), new Texture2D(resolution.x, resolution.y, TextureFormat.RGB24, true)}, {PoiExtensions.PoiTextureChannel.Green.ToString().ToLower(), new Texture2D(resolution.x, resolution.y, TextureFormat.RGB24, true)}, {PoiExtensions.PoiTextureChannel.Blue.ToString().ToLower(), new Texture2D(resolution.x, resolution.y, TextureFormat.RGB24, true)}, {PoiExtensions.PoiTextureChannel.Alpha.ToString().ToLower(), new Texture2D(resolution.x, resolution.y, TextureFormat.RGB24, true)} }; var mat = new Material(PoiExtensions.UnpackerShader); mat.SetTexture("_MainTex", packedTexture); mat.SetInt("_Invert", Convert.ToInt32(invert)); for(int i = 0; i < 4; i++) { mat.SetFloat("_Mode", i); channels.ElementAt(i).Value.BakeMaterialToTexture(mat); } return channels; } internal static void DrawWithLabelWidth(float width, Action action) { if(action == null) return; float old = EditorGUIUtility.labelWidth; action.Invoke(); EditorGUIUtility.labelWidth = old; } internal static PoiExtensions.PoiTextureChannel DrawChannelSelector(PoiExtensions.PoiTextureChannel currentSelection, params string[] labels) { if(labels == null) return PoiExtensions.PoiTextureChannel.RGBA; return (PoiExtensions.PoiTextureChannel)GUILayout.SelectionGrid((int)currentSelection, labels, labels.Length); } } internal static class PoiExtensions { public enum PoiTextureChannel { RGBA, Red, Green, Blue, Alpha } public static Shader PackerShader { get { return Shader.Find("Hidden/Poi/TexturePacker"); } } public static Shader UnpackerShader { get { return Shader.Find("Hidden/Poi/TextureUnpacker"); } } internal static Texture2D GetChannelAsTexture(this Texture2D tex, PoiTextureChannel chan, bool invert = false, Vector2Int sizeOverride = default) { if(chan == PoiTextureChannel.RGBA) return tex; if(sizeOverride == default) sizeOverride = new Vector2Int(tex.width, tex.height); Material mat = new Material(UnpackerShader); mat.SetFloat("_Mode", (int)chan - 1); mat.SetInt("_Invert", Convert.ToInt32(invert)); mat.SetTexture("_MainTex", tex); var newTex = new Texture2D(sizeOverride.x, sizeOverride.y, TextureFormat.RGB24, true); newTex.name = chan.ToString(); newTex.BakeMaterialToTexture(mat); newTex.Apply(false, false); return newTex; } /// /// Extension method that bakes a material to /// /// Texture to bake to /// Material to bake to internal static void BakeMaterialToTexture(this Texture2D tex, Material materialToBake) { var res = new Vector2Int(tex.width, tex.height); RenderTexture renderTexture = RenderTexture.GetTemporary(res.x, res.y); Graphics.Blit(null, renderTexture, materialToBake); //transfer image from rendertexture to texture RenderTexture.active = renderTexture; tex.ReadPixels(new Rect(Vector2.zero, res), 0, 0); tex.Apply(false, false); //clean up variables RenderTexture.active = null; RenderTexture.ReleaseTemporary(renderTexture); } internal static void SaveTextureAsset(this Texture2D tex, string assetPath, bool overwrite) { var bytes = tex.EncodeToPNG(); // Ensure directory exists then convert path to local asset path if(!assetPath.StartsWith("Assets", StringComparison.OrdinalIgnoreCase)) { Directory.CreateDirectory(Path.GetDirectoryName(assetPath)); assetPath = PoiHelpers.AbsolutePathToLocalAssetsPath(assetPath); } else { string absolutePath = PoiHelpers.LocalAssetsPathToAbsolutePath(assetPath); Directory.CreateDirectory(Path.GetDirectoryName(absolutePath)); } if(AssetDatabase.LoadAssetAtPath(assetPath) && !overwrite) assetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath); File.WriteAllBytes(assetPath, bytes); AssetDatabase.Refresh(); } internal static Texture2D GetReadableTextureCopy(this Texture2D tex) { byte[] pix = tex.GetRawTextureData(); Texture2D finalTex = new Texture2D(tex.width, tex.height, tex.format, false); finalTex.LoadRawTextureData(pix); finalTex.Apply(); return finalTex; } /// /// Rounds vector to closest power of two. Optionally, if above ceiling, square root down by one power of two /// /// /// Power of two ceiling. Will be rounded to power of two if not power of two already /// internal static Vector2Int ClosestPowerOfTwo(this Vector2Int vec, int? ceiling = null) { int x = Mathf.ClosestPowerOfTwo(vec.x); int y = Mathf.ClosestPowerOfTwo(vec.y); if(ceiling != null) { int ceil = Mathf.ClosestPowerOfTwo((int) ceiling); x = Mathf.Clamp(x, x, ceil); y = Mathf.Clamp(y, y, ceil); } return new Vector2Int(x, y); } } }