Skip to content
Merged
564 changes: 564 additions & 0 deletions Editor/SolidColorTextures.cs

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Editor/SolidColorTextures.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Editor/Unity.Labs.SuperScience.Editor.api
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ namespace Unity.Labs.SuperScience
{
public RunInEditHelper() {}
}

public class SolidColorTextures : UnityEditor.EditorWindow
{
public SolidColorTextures() {}
}
}
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. :)
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. :)
8 changes: 8 additions & 0 deletions Runtime/Unity.Labs.SuperScience.api
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
8 changes: 8 additions & 0 deletions Runtime/Utilities.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions Runtime/Utilities/Color32ToInt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Runtime.InteropServices;
using UnityEngine;

namespace Unity.Labs.SuperScience
{
/// <summary>
/// Conversion struct which takes advantage of Color32 struct layout for fast conversion to and from Int32.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct Color32ToInt
{
/// <summary>
/// Int field which shares an offset with the color field.
/// Set m_Color to read a converted value from this field.
/// </summary>
[FieldOffset(0)]
int m_Int;

/// <summary>
/// Color32 field which shares an offset with the int field.
/// Set m_Int to read a converted value from this field.
/// </summary>
[FieldOffset(0)]
Color32 m_Color;

/// <summary>
/// The int value.
/// </summary>
public int Int => m_Int;

/// <summary>
/// The color value.
/// </summary>
public Color32 Color => m_Color;

/// <summary>
/// Constructor for Color32 to Int32 conversion.
/// </summary>
/// <param name="color">The color which will be converted to an int.</param>
Color32ToInt(Color32 color)
{
m_Int = 0;
m_Color = color;
}

/// <summary>
/// Constructor for Int32 to Color32 conversion.
/// </summary>
/// <param name="value">The int which will be converted to an Color32.</param>
Color32ToInt(int value)
{
m_Color = default;
m_Int = value;
}

/// <summary>
/// Convert a Color32 to an Int32.
/// </summary>
/// <param name="color">The Color32 which will be converted to an int.</param>
/// <returns>The int value for the given color.</returns>

public static int Convert(Color32 color)
{
var convert = new Color32ToInt(color);
return convert.m_Int;
}

/// <summary>
/// Convert a Color32 to an Int32.
/// </summary>
/// <param name="value">The int which will be converted to an Color32.</param>
/// <returns>The Color32 value for the given int.</returns>
public static Color32 Convert(int value)
{
var convert = new Color32ToInt(value);
return convert.m_Color;
}
}
}
11 changes: 11 additions & 0 deletions Runtime/Utilities/Color32ToInt.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.