Skip to content

Commit

Permalink
Add README.md section for SolidColorTextures;
Browse files Browse the repository at this point in the history
Misc cleanup and polish
  • Loading branch information
mtschoen-unity committed May 28, 2022
1 parent 0356324 commit ba92239
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 6 deletions.
17 changes: 11 additions & 6 deletions Editor/SolidColorTextures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ class Folder
// TODO: Share code between this window and MissingProjectReferences
static class Styles
{
internal static readonly GUIStyle ProSkinLineStyle = new GUIStyle
internal static readonly GUIStyle LineStyle = new GUIStyle
{
normal = new GUIStyleState
{
#if UNITY_2019_4_OR_NEWER
background = Texture2D.grayTexture
#else
background = Texture2D.whiteTexture
#endif
}
};
}
Expand Down Expand Up @@ -99,9 +103,8 @@ Folder GetOrCreateFolderForAssetPath(string path)
for (var i = 0; i < length; i++)
{
var directory = directories[i];
Folder subfolder;
var subfolders = folder.m_Subfolders;
if (!subfolders.TryGetValue(directory, out subfolder))
if (!subfolders.TryGetValue(directory, out var subfolder))
{
subfolder = new Folder();
subfolders[directory] = subfolder;
Expand Down Expand Up @@ -168,7 +171,7 @@ static void DrawLineSeparator()
using (new GUILayout.HorizontalScope())
{
GUILayout.Space(EditorGUI.indentLevel * k_IndentAmount);
GUILayout.Box(GUIContent.none, Styles.ProSkinLineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true));
GUILayout.Box(GUIContent.none, Styles.LineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true));
}

EditorGUILayout.Separator();
Expand Down Expand Up @@ -379,6 +382,7 @@ void UpdateScan()
}

m_ParentFolder.SortContentsRecursively();
Repaint();
}

/// <summary>
Expand All @@ -390,6 +394,8 @@ IEnumerator ProcessScan(Dictionary<string, Texture2D> 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<string, Texture2D>(textureAssets);
Expand Down Expand Up @@ -471,8 +477,7 @@ void Scan()
/// <param name="textureAsset">The texture asset loaded from the AssetDatabase.</param>
void CheckForSolidColorTexture(string path, Texture2D texture, Texture2D textureAsset)
{
int colorValue;
if (IsSolidColorTexture(texture, out colorValue))
if (IsSolidColorTexture(texture, out var colorValue))
{
m_ParentFolder.AddTextureAtPath(path, textureAsset);
GetOrCreateRowForColor(colorValue).textures.Add(textureAsset);
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,29 @@ The goal of the MissingReferences windows is to identify assets in your project
- Serialized UnityEvent properties are missing their target object or method, or references a method which doesn't exist

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.

0 comments on commit ba92239

Please sign in to comment.