diff --git a/Editor/SolidColorTextures.cs b/Editor/SolidColorTextures.cs new file mode 100644 index 0000000..c1e08e0 --- /dev/null +++ b/Editor/SolidColorTextures.cs @@ -0,0 +1,564 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using UnityEditor; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace Unity.Labs.SuperScience +{ + /// + /// Scans the project for textures comprised of a single solid color. + /// Use this utility to identify redundant textures, and textures which are larger than they need to be. + /// + public class SolidColorTextures : EditorWindow + { + /// + /// Tree structure for folder scan results. + /// This is the root object for the project scan, and represents the results in a hierarchy that matches the + /// project's folder structure for an easy to read presentation of solid color textures. + /// When the Scan method encounters a texture, we initialize one of these using the asset path to determine where it belongs. + /// + class Folder + { + // TODO: Share code between this window and others that display a folder structure + const int k_ShrunkTextureSize = 32; + const int k_IndentAmount = 15; + const int k_SeparatorLineHeight = 1; + static readonly GUIContent k_ShrinkGUIContent = new GUIContent("Shrink", "Apply import settings to reduce this texture to the shrink size (32x32)"); + static readonly GUIContent k_ShrinkAllGUIContent = new GUIContent("Shrink All", "Apply import settings to all solid color textures in this directory and its subdirectories shrink them to the minimum size (32x32)"); + static readonly GUILayoutOption k_ShrinkAllWidth = GUILayout.Width(100); + + readonly SortedDictionary m_Subfolders = new SortedDictionary(); + readonly List<(string, Texture2D)> m_Textures = new List<(string, Texture2D)>(); + bool m_Visible; + + /// + /// The number of solid color textures in this folder. + /// + public int Count { get; private set; } + + /// + /// Clear the contents of this container. + /// + public void Clear() + { + m_Subfolders.Clear(); + m_Textures.Clear(); + Count = 0; + } + + /// + /// Add a texture to this folder at a given path. + /// + /// The path of the texture. + /// The texture to add. + public void AddTextureAtPath(string path, Texture2D texture) + { + var folder = GetOrCreateFolderForAssetPath(path); + folder.m_Textures.Add((path, texture)); + } + + /// + /// Get the Folder object which corresponds to the given path. + /// If this is the first asset encountered for a given folder, create a chain of folder objects + /// rooted with this one and return the folder at the end of that chain. + /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one + /// more solid color texture. + /// + /// Path to a solid color texture relative to this folder. + /// The folder object corresponding to the folder containing the texture at the given path. + Folder GetOrCreateFolderForAssetPath(string path) + { + var directories = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var folder = this; + folder.Count++; + var length = directories.Length - 1; + for (var i = 0; i < length; i++) + { + var directory = directories[i]; + var subfolders = folder.m_Subfolders; + if (!subfolders.TryGetValue(directory, out var subfolder)) + { + subfolder = new Folder(); + subfolders[directory] = subfolder; + } + + folder = subfolder; + folder.Count++; + } + + return folder; + } + + /// + /// Draw GUI for this Folder. + /// + /// The name of the folder. + public void Draw(string name) + { + var wasVisible = m_Visible; + using (new GUILayout.HorizontalScope()) + { + m_Visible = EditorGUILayout.Foldout(m_Visible, $"{name}: {Count}", true); + if (GUILayout.Button(k_ShrinkAllGUIContent, k_ShrinkAllWidth)) + ShrinkAndFinalize(); + } + + DrawLineSeparator(); + + // Hold alt to apply visibility state to all children (recursively) + if (m_Visible != wasVisible && Event.current.alt) + SetVisibleRecursively(m_Visible); + + if (!m_Visible) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var kvp in m_Subfolders) + { + kvp.Value.Draw(kvp.Key); + } + + foreach (var (_, texture) in m_Textures) + { + using (new GUILayout.HorizontalScope()) + { + EditorGUILayout.ObjectField(texture.name, texture, typeof(Texture2D), false); + if (GUILayout.Button(k_ShrinkGUIContent)) + ShrinkAndFinalize(); + } + } + + if (m_Textures.Count > 0) + DrawLineSeparator(); + } + } + + /// + /// Draw a separator line. + /// + static void DrawLineSeparator() + { + EditorGUILayout.Separator(); + using (new GUILayout.HorizontalScope()) + { + GUILayout.Space(EditorGUI.indentLevel * k_IndentAmount); + GUILayout.Box(GUIContent.none, Styles.LineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true)); + } + + EditorGUILayout.Separator(); + } + + /// + /// Shrink all textures in this folder and subfolders, and refresh the AssetDatabase on completion. + /// + void ShrinkAndFinalize() + { + try + { + AssetDatabase.StartAssetEditing(); + ShrinkAllTexturesRecursively(); + } + finally + { + AssetDatabase.StopAssetEditing(); + AssetDatabase.Refresh(); + } + } + + /// + /// Shrink all textures in this folder and subfolders. + /// + void ShrinkAllTexturesRecursively() + { + foreach (var (path, _) in m_Textures) + { + var importer = AssetImporter.GetAtPath(path); + if (importer == null) + { + Debug.LogWarning($"Could not get asset importer for {path}"); + continue; + } + + if (!(importer is TextureImporter textureImporter)) + continue; + + textureImporter.maxTextureSize = k_ShrunkTextureSize; + textureImporter.SaveAndReimport(); + } + + foreach (var kvp in m_Subfolders) + { + kvp.Value.ShrinkAllTexturesRecursively(); + } + } + + /// + /// Set the visibility state of this folder, its contents and their children and all of its subfolders and their contents and children. + /// + /// Whether this object and its children should be visible in the GUI. + void SetVisibleRecursively(bool visible) + { + m_Visible = visible; + foreach (var kvp in m_Subfolders) + { + kvp.Value.SetVisibleRecursively(visible); + } + } + + /// + /// Sort the contents of this folder and all subfolders by name. + /// + public void SortContentsRecursively() + { + m_Textures.Sort((a, b) => a.Item1.CompareTo(b.Item1)); + foreach (var kvp in m_Subfolders) + { + kvp.Value.SortContentsRecursively(); + } + } + } + + /// + /// Container for unique color rows. + /// + class ColorRow + { + public bool expanded; + public readonly List textures = new List(); + } + + static class Styles + { + internal static readonly GUIStyle LineStyle = new GUIStyle + { + normal = new GUIStyleState + { +#if UNITY_2019_4_OR_NEWER + background = Texture2D.grayTexture +#else + background = Texture2D.whiteTexture +#endif + } + }; + } + + const string k_MenuItemName = "Window/SuperScience/Solid Color Textures"; + const string k_WindowTitle = "Solid Color Textures"; + const string k_NoMissingReferences = "No solid color textures"; + const string k_ProjectFolderName = "Project"; + const int k_TextureColumnWidth = 150; + const int k_ColorPanelWidth = 150; + const string k_Instructions = "Click the Scan button to scan your project for solid color textures. WARNING: " + + "This will load every texture in your project. For large projects, this may take a long time and/or crash the Editor."; + const string k_ScanFilter = "t:Texture2D"; + const int k_ProgressBarHeight = 15; + const int k_MaxScanUpdateTimeMilliseconds = 50; + + static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for solid color textures"); + static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); + + static readonly GUILayoutOption k_ColorPanelWidthOption = GUILayout.Width(k_ColorPanelWidth); + static readonly GUILayoutOption k_ColorSwatchWidthOption = GUILayout.Width(30); + + static readonly Vector2 k_MinSize = new Vector2(400, 200); + + static readonly Stopwatch k_StopWatch = new Stopwatch(); + + Vector2 m_ColorListScrollPosition; + Vector2 m_FolderTreeScrollPosition; + readonly Folder m_ParentFolder = new Folder(); + readonly SortedDictionary m_TexturesByColor = new SortedDictionary(); + static readonly string[] k_ScanFolders = {"Assets", "Packages"}; + int m_ScanCount; + int m_ScanProgress; + IEnumerator m_ScanEnumerator; + + /// + /// Initialize the window + /// + [MenuItem(k_MenuItemName)] + static void Init() { GetWindow(k_WindowTitle).Show(); } + + void OnEnable() + { + minSize = k_MinSize; + m_ScanCount = 0; + m_ScanProgress = 0; + } + + void OnDisable() { m_ScanEnumerator = null; } + + void OnGUI() + { + EditorGUIUtility.labelWidth = position.width - k_TextureColumnWidth - k_ColorPanelWidth; + + if (m_ScanEnumerator == null) + { + if (GUILayout.Button(k_ScanGUIContent)) + Scan(); + } + else + { + if (GUILayout.Button(k_CancelGUIContent)) + m_ScanEnumerator = null; + } + + if (m_ParentFolder.Count == 0) + { + EditorGUILayout.HelpBox(k_Instructions, MessageType.Info); + GUIUtility.ExitGUI(); + return; + } + + if (m_ParentFolder.Count == 0) + { + GUILayout.Label(k_NoMissingReferences); + } + else + { + using (new GUILayout.HorizontalScope()) + { + using (new GUILayout.VerticalScope(k_ColorPanelWidthOption)) + { + DrawColors(); + } + + using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) + { + m_FolderTreeScrollPosition = scrollView.scrollPosition; + m_ParentFolder.Draw(k_ProjectFolderName); + } + } + } + + if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) + { + var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); + EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + } + } + + /// + /// Draw a list of unique colors. + /// + void DrawColors() + { + GUILayout.Label($"{m_TexturesByColor.Count} Unique Colors"); + using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) + { + m_ColorListScrollPosition = scrollView.scrollPosition; + foreach (var kvp in m_TexturesByColor) + { + var color = Color32ToInt.Convert(kvp.Key); + var row = kvp.Value; + var textures = row.textures; + + using (new GUILayout.HorizontalScope()) + { + row.expanded = EditorGUILayout.Foldout(row.expanded, $"{textures.Count} Texture(s)", true); + EditorGUILayout.ColorField(new GUIContent(), color, false, true, false, k_ColorSwatchWidthOption); + } + + if (row.expanded) + { + foreach (var texture in row.textures) + { + EditorGUILayout.ObjectField(texture, typeof(Texture2D), true); + } + } + } + } + } + + /// + /// Update the current scan coroutine. + /// + void UpdateScan() + { + if (m_ScanEnumerator == null) + return; + + k_StopWatch.Reset(); + k_StopWatch.Start(); + + // Process as many steps as possible within a given time frame + while (m_ScanEnumerator.MoveNext()) + { + // Process for a maximum amount of time and early-out to keep the UI responsive + if (k_StopWatch.ElapsedMilliseconds > k_MaxScanUpdateTimeMilliseconds) + break; + } + + m_ParentFolder.SortContentsRecursively(); + Repaint(); + } + + /// + /// Coroutine for processing scan results. + /// + /// Texture assets to scan. + /// IEnumerator used to run the coroutine. + IEnumerator ProcessScan(Dictionary textureAssets) + { + m_ScanCount = textureAssets.Count; + m_ScanProgress = 0; + + // We will have to repeat the scan multiple times because the preview utility works asynchronously + while (m_ScanProgress < m_ScanCount) + { + var remainingTextureAssets = new Dictionary(textureAssets); + foreach (var kvp in remainingTextureAssets) + { + var path = kvp.Key; + var textureAsset = kvp.Value; + var texture = textureAsset; + + // For non-readable textures, get a preview texture which is readable + // TODO: get a full-size preview; AssetPreview.GetAssetPreview returns a 128x128 texture + if (!textureAsset.isReadable) + { + texture = AssetPreview.GetAssetPreview(textureAsset); + if (texture == null) + continue; + } + + CheckForSolidColorTexture(path, texture, textureAsset); + m_ScanProgress++; + textureAssets.Remove(path); + yield return null; + } + } + + m_ScanEnumerator = null; + EditorApplication.update -= UpdateScan; + } + + /// + /// Scan the project for solid color textures and populate the data structures for UI. + /// + void Scan() + { + var guids = AssetDatabase.FindAssets(k_ScanFilter, k_ScanFolders); + if (guids == null || guids.Length == 0) + return; + + m_TexturesByColor.Clear(); + m_ParentFolder.Clear(); + + var textureAssets = new Dictionary(); + foreach (var guid in guids) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrEmpty(path)) + { + Debug.Log($"Could not convert {guid} to path"); + continue; + } + + var texture = AssetDatabase.LoadAssetAtPath(path); + if (texture == null) + { + Debug.LogWarning($"Could not load texture at {path}"); + continue; + } + + // Skip non-2D textures (which don't support GetPixels) + if (!(texture is Texture2D texture2D)) + continue; + + // Skip textures which are child assets (fonts, embedded textures, etc.) + if (!AssetDatabase.IsMainAsset(texture)) + continue; + + textureAssets.Add(path, texture2D); + } + + m_ScanEnumerator = ProcessScan(textureAssets); + EditorApplication.update += UpdateScan; + } + + /// + /// Add a texture to UI data structures if is comprised of a single solid color. + /// + /// The path to the texture asset. + /// The texture to test. This is different from because the original asset may not be readable. + /// The texture asset loaded from the AssetDatabase. + void CheckForSolidColorTexture(string path, Texture2D texture, Texture2D textureAsset) + { + if (IsSolidColorTexture(texture, out var colorValue)) + { + m_ParentFolder.AddTextureAtPath(path, textureAsset); + GetOrCreateRowForColor(colorValue).textures.Add(textureAsset); + } + } + + /// + /// Check if a texture is comprised of a single solid color. + /// + /// The texture to check. + /// The color of the texture, converted to an int. + /// True if the texture is a single solid color. + static bool IsSolidColorTexture(Texture2D texture, out int colorValue) + { + // Skip "degenerate" textures like font atlases + if (texture.width == 0 || texture.height == 0) + { + colorValue = default; + return false; + } + + var pixels = texture.GetPixels(); + + // It is unlikely to get a null pixels array, but we should check just in case + if (pixels == null) + { + Debug.LogWarning($"Could not read {texture}"); + colorValue = default; + return false; + } + + // It is unlikely, but possible that we got this far and there are no pixels. + var pixelCount = pixels.Length; + if (pixelCount == 0) + { + Debug.LogWarning($"No pixels in {texture}"); + colorValue = default; + return false; + } + + // Convert to int for faster comparison + colorValue = Color32ToInt.Convert(pixels[0]); + var isSolidColor = true; + for (var i = 1; i < pixelCount; i++) + { + var pixel = Color32ToInt.Convert(pixels[i]); + if (pixel != colorValue) + { + isSolidColor = false; + break; + } + } + + return isSolidColor; + } + + /// + /// Get or create a for a given color value. + /// + /// The color value to use for this row. + /// The color row for the color value. + ColorRow GetOrCreateRowForColor(int colorValue) + { + if (m_TexturesByColor.TryGetValue(colorValue, out var row)) + return row; + + row = new ColorRow(); + m_TexturesByColor[colorValue] = row; + return row; + } + } +} diff --git a/Editor/SolidColorTextures.cs.meta b/Editor/SolidColorTextures.cs.meta new file mode 100644 index 0000000..b688e30 --- /dev/null +++ b/Editor/SolidColorTextures.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b23aff7567ab0b4189a24e5dfc70573 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Unity.Labs.SuperScience.Editor.api b/Editor/Unity.Labs.SuperScience.Editor.api index c49d157..800e665 100644 --- a/Editor/Unity.Labs.SuperScience.Editor.api +++ b/Editor/Unity.Labs.SuperScience.Editor.api @@ -38,4 +38,9 @@ namespace Unity.Labs.SuperScience { public RunInEditHelper() {} } + + public class SolidColorTextures : UnityEditor.EditorWindow + { + public SolidColorTextures() {} + } } diff --git a/README.md b/README.md index e4d58de..690283c 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,35 @@ The goal of the MissingReferences windows is to identify assets in your project Note that the Missing Project References window will load all of the assets in your project, synchronously, when you hit Refresh. In large projects, this can crash Unity, so use this window at your own risk! If you want to use this with large projects, replace the call to `AssetDatabase.GetAllAssetPaths()` with a call to `AssetDatabase.FindAssets()` and some narrower search, or refactor the script to work on the current selection. +## Solid Color Textures: Optimize texture memory by shrinking solid color textures +Sometimes materials which are generated by digital content creation software contain textures which are just a solid color. Often these are generated for normal or smoothness channels, where details can be subtle, so it is difficult to be sure whether or not the texture is indeed a solid color. Sometimes the detail is "hidden" in the alpha channel or too subtle to see in the texture preview inspector. + +Thankfully, this is what computers are for! This utility will scan all of the textures in your project and report back on textures where every single pixel is the same color. It even gives you a handy summary in the left column of _what color_ these textures are, and groups them by color. The panel to the right shows a collapsible tree view of each texture, grouped by location, as well as a button for each texture or location to shrink the texture(s) down to the smallest possible size (32x32). This is the quick-and-dirty way to optimize the memory and cache efficiency of these textures, without risking any missing references. Of course, the most optimal way to handle these textures is with a custom shader that uses a color value instead of a texture. Short of that, you should try to cut back to just a _single_ solid color texture per-color. The summary in the left panel should only show one texture for each unique color. + +The scan process can take a long time, especially for large projects. Also, since most textures in your project will not have the `isReadable` flag set, we check a 128x128 preview (generated by `AssetPreview.GetAssetPreview`) of the texture instead. This turns out to be the best way to get access to an unreadable texture, and proves to be a handy performance optimization as well. It is _possible_ that there are textures with very subtle detail which _perfectly_ filters out to a solid color texture at this scale, but this corner case is pretty unlikely. Still, you should look out for this in case shrinking these textures ends up making a noticeable effect. + +You may be wondering, "why is it so bad to have solid color textures?" +- Textures occupy space in video memory, which can be in short supply on some platforms, especially mobile. +- Even though the asset in the project may be small (solid color PNGs are small regardless of dimensions), the texture that is included in your final build can be much larger. GPU texture compression doesn't work the same way as PNG or JPEG compression, and it is the GPU-compatible texture data which is included in Player builds. This means that your 4096x4096 solid-black PNG texture may occupy only 5KB in the Assets folder, but will be a whopping 5.3MB (>1000x larger!) in the build. +- Sampling from a texture in a shader takes significantly more time than reading a color value from a shader property. +- Looking up colors in a _large_ texture can lead to cache misses. Even with mipmaps enabled, the renderer isn't smart enough to know that it can use the smallest mip level for these textures. If you have a solid color texture at 4096x4096 that occupies the whole screen, the GPU is going to spend a lot of wasted time sampling pixels that all return the same value. + +## `Color32` To Int: Convert colors to and from a single integer value as fast as possible +This one simple trick will save your CPU millions of cycles! Read on to learn more. + +The `Color32` struct in Unity is designed to be easily converted to and from an `int`. It does this by storing each color value in a `byte`. You can concatenate 4 `bytes` to form an `int`, and then you can do operations like add, subtract, and compare on these values _one time_ instead of repeating the same operation _four times_. Thus, for an application like the Solid Color Textures window, this reduces the time to process each pixel by a factor of 4. The conversion is _basically free_. The only CPU work needed is to set a field on a struct. + +This works by taking advantage of the `[FieldOffset]` attribute in C# which can be applied to value type fields. This allows us to manually specify how many bytes from the beginning of the struct a field should start. Note that your struct also needs the `[StructLayout(LayoutKind.Explicit)]` attribute in order to use `[FieldOffset]`. + +In this case, we define a struct (`Color32ToInt`) with both an `int` field and a `Color32` field to both have a field offset of `0`. This means that they both occupy the same space in memory, and because they are both of equal size (4 bytes) they will fully overwrite each other when either one is set. If we set a value of `32` into the `int` field, we will read a color with `32` in the `alpha` channel, and `0` in all other channels from the `Color32` field. If we set a value of `new Color32(0, 0, 32, 0)` to the `Color32` field, we will read a `8,192` (`0x00002000`) from the `int` field. Pretty neat, huh? I bet you thought you could only pull off this kind of hack in C++. We don't even need unsafe code! In fact, if you look at the [source](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Math/Color32.cs) for `Color32`, you can see that we also take advantage of this trick internally, though we don't expose the int value. + +Note that you can't perform any operation on the `int` version of a color and expect it to work the same as doing that operation on each individual channel. For example, multiplying two colors that were converted to `ints` will not have the same result as multiplying the values of each channel individually. + +One final tip, left as an exercise for the reader: this trick also works on arrays (of equal length), and any other value types where you can align their fields with equivalent primitives. It works for floating point values as well, but you can't concatenate or decompose them them like integer types. + ## Global Namespace Watcher: Clean up types in the global namespace It's easy to forget to add your types to a namespace. This won't prevent your code from compiling, but it will lead to headaches down the road. If you have a `Utils` class, and I have a `Utils` class, we're going to run into problems, sometimes even if we _do_ use a namespace, but if we don't, we'll have way fewer options for how to fix it. You can read more about C# namespaces with a quick web search, or by heading over to [the official documentation](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/namespaces) The Global Namespace Watcher window in this repository provides a simple list of assemblies with types in the global namespace. The window shows a collapsible list of assemblies in a simple scroll view, along with property fields containing references to assembly definitions and MonoScript objects corresponding to the assemblies and types which have been found. Each assembly row indicates how many types are in the global namespace. Users can single-click one of these properties to ping the corresponding asset in the Project View, or double-click it to open the corresponding file in the default editor. -Users can filter the list based on whether the assemblies are defined within the project (Assets or Packages), and whether the types identified have corresponding MonoScript objects. The goal is to reach a state where no types in the project or its packages define types in the global namespace. When you reach that goal, you get a happy little message that pats you on the back. :) \ No newline at end of file +Users can filter the list based on whether the assemblies are defined within the project (Assets or Packages), and whether the types identified have corresponding MonoScript objects. The goal is to reach a state where no types in the project or its packages define types in the global namespace. When you reach that goal, you get a happy little message that pats you on the back. :) diff --git a/Runtime/Unity.Labs.SuperScience.api b/Runtime/Unity.Labs.SuperScience.api index 03345ec..be64dfa 100644 --- a/Runtime/Unity.Labs.SuperScience.api +++ b/Runtime/Unity.Labs.SuperScience.api @@ -3,6 +3,14 @@ // make sure the XML doc file is present and located next to the scraped dll namespace Unity.Labs.SuperScience { + public struct Color32ToInt + { + public UnityEngine.Color32 Color { get; } + public int Int { get; } + public static int Convert(UnityEngine.Color32 color); + public static UnityEngine.Color32 Convert(int value); + } + public class ColorContributor : UnityEngine.MonoBehaviour { public UnityEngine.Color color { get; } diff --git a/Runtime/Utilities.meta b/Runtime/Utilities.meta new file mode 100644 index 0000000..f2c5429 --- /dev/null +++ b/Runtime/Utilities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f7c140d9e7f45a641afd799b8f0f6844 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utilities/Color32ToInt.cs b/Runtime/Utilities/Color32ToInt.cs new file mode 100644 index 0000000..edc17db --- /dev/null +++ b/Runtime/Utilities/Color32ToInt.cs @@ -0,0 +1,79 @@ +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Unity.Labs.SuperScience +{ + /// + /// Conversion struct which takes advantage of Color32 struct layout for fast conversion to and from Int32. + /// + [StructLayout(LayoutKind.Explicit)] + public struct Color32ToInt + { + /// + /// Int field which shares an offset with the color field. + /// Set m_Color to read a converted value from this field. + /// + [FieldOffset(0)] + int m_Int; + + /// + /// Color32 field which shares an offset with the int field. + /// Set m_Int to read a converted value from this field. + /// + [FieldOffset(0)] + Color32 m_Color; + + /// + /// The int value. + /// + public int Int => m_Int; + + /// + /// The color value. + /// + public Color32 Color => m_Color; + + /// + /// Constructor for Color32 to Int32 conversion. + /// + /// The color which will be converted to an int. + Color32ToInt(Color32 color) + { + m_Int = 0; + m_Color = color; + } + + /// + /// Constructor for Int32 to Color32 conversion. + /// + /// The int which will be converted to an Color32. + Color32ToInt(int value) + { + m_Color = default; + m_Int = value; + } + + /// + /// Convert a Color32 to an Int32. + /// + /// The Color32 which will be converted to an int. + /// The int value for the given color. + + public static int Convert(Color32 color) + { + var convert = new Color32ToInt(color); + return convert.m_Int; + } + + /// + /// Convert a Color32 to an Int32. + /// + /// The int which will be converted to an Color32. + /// The Color32 value for the given int. + public static Color32 Convert(int value) + { + var convert = new Color32ToInt(value); + return convert.m_Color; + } + } +} diff --git a/Runtime/Utilities/Color32ToInt.cs.meta b/Runtime/Utilities/Color32ToInt.cs.meta new file mode 100644 index 0000000..107df45 --- /dev/null +++ b/Runtime/Utilities/Color32ToInt.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f90fa99ae98b2745bb230e14ff44051 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: