From 1ec8708da7df4cd04929f54efb56e26b01b7f8cf Mon Sep 17 00:00:00 2001 From: Mathias Solheim Date: Wed, 22 Oct 2025 14:41:02 +0200 Subject: [PATCH] Material/texture separation, improvements, fixes, and more - Fixed texture browser not loading if the window is open when Unity boots up. - Fixed blank saved search that appears when there's no saved searches. - Fixed error that appears when a material doesn't have a _MainTex property. - Moved texture name label to be under the texture so it's unobstructed, and added ellipsis on text overflow. - Added an icon to texture label showing if a thumbnail is for a material or a texture without a material. The icon goes from blue to yellow if it's favourited. - Added text at the bottom of the window that shows the full path and resolution of the texture you're hovering over. Also made the thumbnail size slider much smaller to make room for this. - Made the buttons that appear when hovering over a texture be transparent until you specifically hover over them too. And adjusted their size and icon size. - If a thumbnail is for a raw texture, the hover button to "locate material" will instead create a material for that texture, and the text and icon is changed to reflect that. If pressed the thumbnail will be refreshed to reflect now being a material. - Added filter buttons to show textures or materials separately. - Searching now checks both the texture name and the material name for matches. - Sorting now ignores case, makes more sense to me at least. - Added checkerboard background to better show if textures are transparent, it gets hidden when hovering over the texture. If the texture is non-square the checkers will only render behind its bounding box to ensure the aspect of the texture is clear. - Added user setting to change the tint of the checkered background, which also includes opacity. - Non-square textures will be zoomed in to fill the square when hovering over so you can see details better, it also pans across when moving the mouse from one side to another, this can be disabled in user settings. - Changed menu position to be inside the "Window" menu instead of "LEG", feels like a better placement for the average person. - Changed default shader main tex keyword from "_BaseMap to "_MainTex" which is the default for Unity. - Took border width into account for overall preview element size (borders would shrink the texture to be displayed as 126 instead of 128 for example). - Changed how TextureInfo works to be slightly more convenient and fill in more of its info automatically. - Changed the icon of "show used in scenes" from a brush to something I felt was a bit better, though it's very subjective and there wasn't many choices that would really fit. - Made the X in the search field a bit bigger to be easier to click. --- Editor/TextureBrowser.uss | 98 +++++-- Editor/TextureBrowserWindow.cs | 497 +++++++++++++++++++++++++-------- 2 files changed, 454 insertions(+), 141 deletions(-) diff --git a/Editor/TextureBrowser.uss b/Editor/TextureBrowser.uss index c72c24d..e77ecb5 100644 --- a/Editor/TextureBrowser.uss +++ b/Editor/TextureBrowser.uss @@ -2,6 +2,12 @@ height: 30px; } +.toolbar-button { + min-width: 30px; + width: 30px; + padding: 4px; +} + .texture-list-container { flex-grow: 1; } @@ -20,9 +26,8 @@ } .tex-parent { - background-size: contain; - background-repeat: no-repeat; margin: 2px; + margin-bottom: 19px; flex-direction: column-reverse; border-color: rgba(0,0,0,.7); border-width: 1px; @@ -30,32 +35,56 @@ .tex-parent:hover { border-color: white; - border-width: 1px; } .tex-mat-button { - width: 30px; - height: 30px; + width: 24px; + height: 24px; position: absolute; top: 2px; - right: 2px; + right: 0px; + padding-top: 1px; + padding-left: 3px; + padding-right: 1px; + opacity: 0.2; display: none; } +.tex-mat-button:hover { + opacity: 1; +} + .tex-parent:hover .tex-mat-button { display: initial; } +.tex-image { + position: absolute; + width: 100%; + height: 100%; + background-size: contain; + background-repeat: repeat; + background-size: 32px; + --unity-image-size: scale-and-crop; +} + .tex-fav-button { - width: 30px; - height: 30px; + width: 24px; + height: 24px; position: absolute; top: 2px; - left: 2px; + left: 0px; + padding-left: 1px; + padding-right: 1px; + opacity: 0.2; display: none; } -.tex-parent:hover .tex-fav-button{ +.tex-fav-button:hover { + opacity: 1; +} + +.tex-parent:hover .tex-fav-button { display: initial; } @@ -65,26 +94,56 @@ } .tex-label { - background-color: rgba(0,0,0,.7); + height: 16px; + background-color: rgba(0,0,0,.4); + bottom: -17px; + margin-left: -1px; + margin-right: -1px; + padding-left: 17px; + padding-right: 1px; + overflow: hidden; + text-overflow: ellipsis; +} + +.tex-label-icon { + width: 16px; + left: -16px; + --unity-image-tint-color: #6BB6D3; +} + +.tex-favourite .tex-label-icon { + --unity-image-tint-color: #BF8600; } .info-label { padding: 3px; } -.folder-selection{ +.path-label { + padding: 3px; + width: 10px; flex-grow: 1; - min-width: 500px; + overflow: hidden; + text-overflow: ellipsis; + -unity-text-overflow-position: start; } -.size-slider{ - flex-grow: 1; - min-width: 300px; +.preferences-button { + min-width: 26px; +} + +.size-slider { + width: 50px; } -.search-field{ +.search-field { flex-grow: 1; - min-width: 300px; + width: 100px; +} + +.unity-search-field-base__cancel-button { + width: 20px; + height: 19px; } /* saved search */ @@ -100,7 +159,7 @@ padding: 4px; } -.saved-search-button{ +.saved-search-button { height: 24px; flex-direction: row; padding-left: 10px; @@ -145,4 +204,3 @@ -unity-font-style: bold; margin-bottom: 10px; } - diff --git a/Editor/TextureBrowserWindow.cs b/Editor/TextureBrowserWindow.cs index 58a3338..6ec5371 100644 --- a/Editor/TextureBrowserWindow.cs +++ b/Editor/TextureBrowserWindow.cs @@ -16,7 +16,7 @@ public class TextureBrowserWindow : EditorWindow { [SerializeField] private StyleSheet m_styleSheet; - [MenuItem("LEG/Texture Browser")] + [MenuItem("Window/Texture Browser")] private static void Open() { var window = GetWindow(); @@ -27,19 +27,24 @@ private static void Open() } private static StyleSheet _styleSheet; // static copy for settings provider + private const int TexParentBorderWidth = 1; // can't fetch this at the start unfortunately so need to keep it here // window state - private Vector2 m_scroll; private TextureInfo m_draggedTexture; private Vector2 m_dragStartPosition; private string m_searchString = ""; private Texture2D m_materialIcon; + private Texture2D m_textureIcon; + private Texture2D m_generateMaterialIcon; private Texture2D m_favouriteIcon; private Texture2D m_usedIcon; + private Texture2D m_plusIcon; + private Texture2D m_refreshIcon; + private Texture2D m_settingsIcon; private VisualElement m_textureListView; private VisualElement m_quickSearchView; - private FilterFlags m_filter = FilterFlags.None; + private FilterFlags m_filter = FilterFlags.Materials | FilterFlags.Textures; // data @@ -49,7 +54,8 @@ private static void Open() private readonly List m_savedSearches = new(); private ToolbarSearchField m_searchField; private Label m_infoLabel; - private EditorCoroutine m_loadRoutine; + private Label m_resolutionLabel; + private Label m_pathLabel; private ToolbarButton m_refreshButton; private int m_loadTaskId; @@ -59,46 +65,73 @@ private enum FilterFlags None = 1 << 0, Favourites = 1 << 1, Used = 1 << 2, + Textures = 1 << 3, + Materials = 1 << 4, } - + private class TextureInfo : IComparable { - public string Name { get; } + public string Name { get; private set; } public Texture2D Texture { get; } - public Material Material { get; set; } + public string Path { get; private set; } + private Material _material; + public Material Material + { + get => _material; + set + { + if (_material == value) return; + _material = value; + + if (_material == null) + { + Name = Texture.name; + Path = AssetDatabase.GetAssetPath(Texture); + return; + } + + Name = _material.name; + Path = AssetDatabase.GetAssetPath(_material); + } + } + public bool IsMaterial { get => Material != null; } public bool IsFavourite { get; set; } - public VisualElement Element { get; set; } + public VisualElement UIElement { get; set; } - public TextureInfo(string name, Texture2D tex) + public TextureInfo(Texture2D tex) { - if (name.StartsWith("mat.")) - name = name.Remove(0, "mat.".Length); - Name = name; + Name = tex.name; Texture = tex; + Path = AssetDatabase.GetAssetPath(tex); } public int CompareTo(TextureInfo other) { if (ReferenceEquals(this, other)) return 0; if (other is null) return 1; - return string.Compare(Name, other.Name, StringComparison.Ordinal); + return string.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase); } } private void OnEnable() { _styleSheet = m_styleSheet; // for settings provider, hacky but whatever - - m_materialIcon = EditorGUIUtility.IconContent("Material Icon").image as Texture2D; - m_favouriteIcon = EditorGUIUtility.IconContent("Favorite").image as Texture2D; - m_usedIcon = EditorGUIUtility.IconContent("d_TerrainInspector.TerrainToolSplat On").image as Texture2D; + + m_materialIcon = EditorGUIUtility.IconContent("d_Material On Icon").image as Texture2D; + m_textureIcon = EditorGUIUtility.IconContent("d_RawImage Icon").image as Texture2D; + m_generateMaterialIcon = EditorGUIUtility.IconContent("d_ProceduralMaterial Icon").image as Texture2D; + m_favouriteIcon = EditorGUIUtility.IconContent("d_Favorite On Icon").image as Texture2D; + m_usedIcon = EditorGUIUtility.IconContent("LightProbeGroup Gizmo").image as Texture2D; + m_plusIcon = EditorGUIUtility.IconContent("Toolbar Plus").image as Texture2D; + m_refreshIcon = EditorGUIUtility.IconContent("d_Refresh").image as Texture2D; + m_settingsIcon = EditorGUIUtility.IconContent("Settings").image as Texture2D; } private void OnDisable() { // cleanup statics _styleSheet = null; - + m_data.Clear(); m_materialCache.Clear(); m_favourites.Clear(); @@ -123,8 +156,8 @@ private void CreateGUI() var saveSearchButton = new ToolbarButton(SaveCurrentSearchTerm) { tooltip = "Save Search", - iconImage = EditorGUIUtility.IconContent("Toolbar Plus").image as Texture2D - }.WithClasses("save-search-button").AddTo(toolbar); + iconImage = m_plusIcon + }.WithClasses("save-search-button", "toolbar-button").AddTo(toolbar); saveSearchButton.SetEnabled(false); @@ -137,11 +170,58 @@ private void CreateGUI() new ToolbarSpacer().AddTo(toolbar); + var texToggle = new ToolbarToggle() + { + value = m_filter.HasFlag(FilterFlags.Textures), + tooltip = "Show raw Textures without associated Materials" + } + .WithClasses("toolbar-button") + .AddTo(toolbar); + + texToggle.Add(new Image() { image = m_textureIcon }); + texToggle.RegisterValueChangedCallback(e => + { + if (e.newValue) + { + m_filter |= FilterFlags.Textures; + } + else + { + m_filter &= ~FilterFlags.Textures; + } + + PopulateList(); + }); + + var matToggle = new ToolbarToggle() + { + value = m_filter.HasFlag(FilterFlags.Materials), + tooltip = "Show Textures with Materials" + } + .WithClasses("toolbar-button") + .AddTo(toolbar); + + matToggle.Add(new Image() { image = m_materialIcon }); + matToggle.RegisterValueChangedCallback(e => + { + if (e.newValue) + { + m_filter |= FilterFlags.Materials; + } + else + { + m_filter &= ~FilterFlags.Materials; + } + + PopulateList(); + }); + var faveToggle = new ToolbarToggle() { value = m_filter.HasFlag(FilterFlags.Favourites), tooltip = "Show only Favourites" } + .WithClasses("toolbar-button") .AddTo(toolbar); faveToggle.Add(new Image() { image = m_favouriteIcon }); @@ -164,6 +244,7 @@ private void CreateGUI() value = m_filter.HasFlag(FilterFlags.Favourites), tooltip = "Show only textures used in open scenes" } + .WithClasses("toolbar-button") .AddTo(toolbar); usedToggle.Add(new Image() { image = m_usedIcon }); @@ -183,29 +264,30 @@ private void CreateGUI() m_refreshButton = new ToolbarButton(TryRefresh) { - iconImage = EditorGUIUtility.IconContent("Refresh").image as Texture2D, + iconImage = m_refreshIcon, tooltip = "Refresh content" } + .WithClasses("toolbar-button") .AddTo(toolbar); - + m_refreshButton.SetEnabled(false); // saved search list - + m_quickSearchView = new VisualElement().WithClasses("saved-search-list").AddTo(root); - + // texture grid m_textureListView = new ScrollView(ScrollViewMode.Vertical).AddTo(root); m_textureListView.AddToClassList("texture-list-container"); m_textureListView.contentContainer.AddToClassList("texture-list"); - + // footer var footer = new Toolbar().AddTo(root).WithClasses("footer"); - + m_infoLabel = new Label().AddTo(footer).WithClasses("info-label"); - + new SliderInt { value = ThumbnailSize, lowValue = 1, highValue = 4 } .WithClasses("size-slider") .AddTo(footer) @@ -215,31 +297,35 @@ private void CreateGUI() foreach (var e in m_data) { - if (e.Element != null) + if (e.UIElement != null) { - e.Element.style.width = ThumbnailSize * 64; - e.Element.style.height = ThumbnailSize * 64; + e.UIElement.style.minWidth = ThumbnailSize * 64 + TexParentBorderWidth * 2; + e.UIElement.style.minHeight = ThumbnailSize * 64 + TexParentBorderWidth * 2; } } }); new ToolbarButton(() => { SettingsService.OpenUserPreferences("Preferences/Texture Browser"); }) { - iconImage = EditorGUIUtility.IconContent("Settings").image as Texture2D, + iconImage = m_settingsIcon, tooltip = "Open preferences" } + .WithClasses("preferences-button") .AddTo(footer); + m_resolutionLabel = new Label().AddTo(footer).WithClasses("info-label"); + m_pathLabel = new Label().AddTo(footer).WithClasses("path-label"); + TryRefresh(); return; void TryRefresh() { - if (Progress.Exists(m_loadTaskId)) + if (m_loadTaskId != 0 && Progress.Exists(m_loadTaskId)) { return; } - + m_refreshButton.SetEnabled(false); EditorCoroutineUtility.StartCoroutine(FullRefresh(), this); } @@ -248,16 +334,16 @@ void TryRefresh() private IEnumerator FullRefresh() { m_loadTaskId = Progress.Start("Refreshing Textures"); - + LoadQuickSearch(); LoadFavourites(); LoadContent(); PopulateList(); yield return null; - + Progress.Remove(m_loadTaskId); - + m_refreshButton.SetEnabled(true); } @@ -277,11 +363,15 @@ private void RefreshQuickSearchContent() foreach (var search in m_savedSearches) { + if (string.IsNullOrEmpty(search)) + { + continue; + } + var button = new Button().WithClasses("saved-search-button"); button.RegisterCallback(evt => { LoadSearchTerm(search, evt.modifiers == EventModifiers.Shift); - }); button.Add(new Label { name = "label", text = search }); button.Add(new Button(() => DeleteSearchTerm(search)) { name = "delete", tooltip = "Remove", text = "X"}); @@ -295,11 +385,11 @@ private void SaveCurrentSearchTerm() { return; } - + m_savedSearches.Add(m_searchString); - + EditorPrefs.SetString(SavedSearchesKey, string.Join("|", m_savedSearches)); - + RefreshQuickSearchContent(); } @@ -314,59 +404,116 @@ private void LoadSearchTerm(string searchTerm, bool additive) m_searchField.value = searchTerm; } } - + private void DeleteSearchTerm(string searchTerm) { m_savedSearches.Remove(searchTerm); - + EditorPrefs.SetString(SavedSearchesKey, string.Join("|", m_savedSearches)); - + RefreshQuickSearchContent(); } - + #endregion - + private VisualElement CreateElement(TextureInfo texInfo) { - var texElement = new VisualElement().WithClasses("tex-parent"); - - texElement.name = texInfo.Texture.name; // texture name helps us ID this later - + var texParent = new VisualElement().WithClasses("tex-parent"); + + texParent.name = texInfo.Texture.name; // texture name helps us ID this later + + bool hoveringTexture = false; + float widthAspect = (float)texInfo.Texture.width / texInfo.Texture.height; + float heightAspect = (float)texInfo.Texture.height / texInfo.Texture.width; + bool textureAspectEven = widthAspect == heightAspect; + if (texInfo.IsFavourite) { - texElement.WithClasses("tex-favourite"); + texParent.WithClasses("tex-favourite"); } - - texElement.style.backgroundImage = texInfo.Texture; - texElement.style.width = ThumbnailSize * 64; - texElement.style.height = ThumbnailSize * 64; - - new Label { text = texInfo.Name }.WithClasses("tex-label").AddTo(texElement); - + + texParent.style.minWidth = ThumbnailSize * 64 + TexParentBorderWidth * 2; + texParent.style.minHeight = ThumbnailSize * 64 + TexParentBorderWidth * 2; + + var texImage = new Image { image = texInfo.Texture }.WithClasses("tex-image").AddTo(texParent); + texImage.style.backgroundImage = AssetDatabase.GetBuiltinExtraResource("Default-Checker-Gray.png"); + + if (ColorUtility.TryParseHtmlString(CheckerBGTint, out Color checkerBGTintColor)) + { + texImage.style.unityBackgroundImageTintColor = checkerBGTintColor; + } + else + { + texImage.style.unityBackgroundImageTintColor = new Color(1, 1, 1, 0.25f); + } + + if (!textureAspectEven) MatchImageAspectToTexture(texImage); + + var texNameLabel = new Label { text = texInfo.Name }.WithClasses("tex-label").AddTo(texParent); + var texLabelIcon = new Image { image = m_materialIcon }.WithClasses("tex-label-icon").AddTo(texNameLabel); + // button to ping material - - new Button(() => + var pingMaterialButton = new Button(() => { var material = FindOrCreateMaterial(texInfo); EditorGUIUtility.PingObject(material); - - }){ iconImage = m_materialIcon, tooltip = "Locate Material In Project"} + + }){ iconImage = m_materialIcon, tooltip = "Locate Material in Project" } .WithClasses("tex-mat-button") - .AddTo(texElement); - + .AddTo(texParent); + + if (!texInfo.IsMaterial) + { + pingMaterialButton.visible = false; + texLabelIcon.image = m_textureIcon; + + // button to generate a new material from the texture + new Button(() => + { + var material = FindOrCreateMaterial(texInfo); + EditorGUIUtility.PingObject(material); + ChangeTextureEntryToMaterial(material); + + }){ name = "generate-mat-button", iconImage = m_generateMaterialIcon, tooltip = "Generate new Material for Texture" } + .WithClasses("tex-mat-button") + .AddTo(texParent); + } + // button to toggle favourite - + new Button(() => { ToggleFavourite(texInfo); - }){ iconImage = m_favouriteIcon, tooltip = "Favourite"} + }){ iconImage = m_favouriteIcon, tooltip = "Favourite" } .WithClasses("tex-fav-button") - .AddTo(texElement); - + .AddTo(texParent); + + // enter and leave events for hovering + + texParent.RegisterCallback(evt => + { + hoveringTexture = true; + m_resolutionLabel.text = $"[{texInfo.Texture.width} x {texInfo.Texture.height}]"; + m_pathLabel.text = texInfo.Path; + texImage.style.backgroundSize = new BackgroundSize(new Length(0, LengthUnit.Pixel), new Length(0, LengthUnit.Pixel)); + + if (ZoomNonSquare) ResetImageAspect(texImage); + }); + + texParent.RegisterCallback(evt => + { + hoveringTexture = false; + m_resolutionLabel.text = string.Empty; + m_pathLabel.text = string.Empty; + texImage.style.backgroundSize = new BackgroundSize(new Length(32, LengthUnit.Pixel), new Length(32, LengthUnit.Pixel)); + + if (ZoomNonSquare) MatchImageAspectToTexture(texImage); + }); + // start drag on mouse down - texElement.RegisterCallback(evt => + texParent.RegisterCallback(evt => { if (evt.button == 0) // Left mouse button { @@ -374,59 +521,138 @@ private VisualElement CreateElement(TextureInfo texInfo) m_dragStartPosition = evt.localMousePosition; } }); - + // rmb context - texElement.RegisterCallback(_ => + texParent.RegisterCallback(_ => { var menu = new GenericMenu(); - menu.AddItem(new GUIContent("Find in Project"), false, () => + menu.AddItem(new GUIContent("Locate Texture in Project"), false, () => { EditorGUIUtility.PingObject(texInfo.Texture); }); menu.ShowAsContext(); }); - texElement.RegisterCallback(evt => + texParent.RegisterCallback(evt => { // start drag after a small movement - + if (m_draggedTexture != null && Vector2.Distance(evt.localMousePosition, m_dragStartPosition) > 5) { var material = FindOrCreateMaterial(m_draggedTexture); + if (!texInfo.IsMaterial) ChangeTextureEntryToMaterial(material); + DragAndDrop.PrepareStartDrag(); DragAndDrop.objectReferences = new Object[] { material }; DragAndDrop.StartDrag(material.name); m_draggedTexture = null; // Reset dragged texture } + + // pan a zoomed texture relative to the mouse position + + if (!textureAspectEven && ZoomNonSquare && hoveringTexture) + { + float borderSafeZone = 0.2f; // percentage in 0 to 1, adds an invisible border around the texture where it doesn't pan + float thumbnailSafeZoneSize = ThumbnailSize * 64 * borderSafeZone; + float thumbnailSizeMinusSafeZone = (ThumbnailSize * 64) - (thumbnailSafeZoneSize * 2); + float localMousePosX = Mathf.Clamp01((evt.mousePosition.x - (texImage.worldBound.x + thumbnailSafeZoneSize)) / thumbnailSizeMinusSafeZone) * -1; + float localMousePosY = Mathf.Clamp01((evt.mousePosition.y - (texImage.worldBound.y + thumbnailSafeZoneSize)) / thumbnailSizeMinusSafeZone); + float posU = 0; + float posV = 0; + + if (heightAspect < 1) + { + posU = (localMousePosX + 0.5f) * (1 - heightAspect); + } + if (widthAspect < 1) + { + posV = (localMousePosY - 0.5f) * (1 - widthAspect); + } + + texImage.uv = new Rect(posU, posV, 1, 1); + texImage.MarkDirtyRepaint(); + } }); - + // reset dragged texture on mouse up - texElement.RegisterCallback(_ => + texParent.RegisterCallback(_ => { - m_draggedTexture = null; + m_draggedTexture = null; }); - return texElement; + return texParent; + + void ChangeTextureEntryToMaterial(Material material) + { + texInfo.Material = material; + texNameLabel.text = texInfo.Name; + texLabelIcon.image = m_materialIcon; + pingMaterialButton.visible = true; + texParent.Q