From 92ad47a1d0b7b9a992f1af239e86afa7328d8cfb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 31 Jul 2026 22:40:28 +0300 Subject: [PATCH 1/2] Implement TextureInspector component --- .../Visualisation/TextureInspector.cs | 339 ++++++++++++++++++ .../Visualisation/TextureVisualiser.cs | 24 +- .../Resources/Shaders/sh_TextureChannels.fs | 38 ++ 3 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 osu.Framework/Graphics/Visualisation/TextureInspector.cs create mode 100644 osu.Framework/Resources/Shaders/sh_TextureChannels.fs diff --git a/osu.Framework/Graphics/Visualisation/TextureInspector.cs b/osu.Framework/Graphics/Visualisation/TextureInspector.cs new file mode 100644 index 0000000000..06293f0f5b --- /dev/null +++ b/osu.Framework/Graphics/Visualisation/TextureInspector.cs @@ -0,0 +1,339 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osuTK; + +namespace osu.Framework.Graphics.Visualisation +{ + internal partial class TextureInspector : VisibilityContainer + { + private const float width = 600; + + private readonly BasicCheckbox rCheckbox; + private readonly BasicCheckbox gCheckbox; + private readonly BasicCheckbox bCheckbox; + private readonly BasicCheckbox aCheckbox; + + private readonly TexturePreview preview; + private readonly Checkerboard checkerboard; + private readonly Container previewContainer; + private readonly InteractiveContainer interactiveContainer; + + public TextureInspector() + { + RelativeSizeAxes = Axes.Y; + Child = new GridContainer + { + RelativeSizeAxes = Axes.Y, + Width = width, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, size: 1), + }, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = interactiveContainer = new InteractiveContainer + { + RelativeSizeAxes = Axes.Both, + Child = previewContainer = new Container + { + Children = new Drawable[] + { + checkerboard = new Checkerboard(), + preview = new TexturePreview() + } + } + } + } + }, + new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Margin = new MarginPadding { Vertical = 10 }, + Children = new Drawable[] + { + rCheckbox = new BasicCheckbox + { + LabelText = "R", + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { Value = true } + }, + gCheckbox = new BasicCheckbox + { + LabelText = "G", + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { Value = true } + }, + bCheckbox = new BasicCheckbox + { + LabelText = "B", + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { Value = true } + }, + aCheckbox = new BasicCheckbox + { + LabelText = "A", + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { Value = true } + } + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + rCheckbox.Current.BindValueChanged(_ => updatePreview()); + gCheckbox.Current.BindValueChanged(_ => updatePreview()); + bCheckbox.Current.BindValueChanged(_ => updatePreview()); + aCheckbox.Current.BindValueChanged(_ => updatePreview(), true); + } + + public void Inspect(Texture texture) + { + previewContainer.Size = texture.Size; + checkerboard.Size = texture.Size; + preview.Texture = texture; + preview.Size = texture.Size; + interactiveContainer.Fit(); + } + + private void updatePreview() => preview.UpdateComponents(rCheckbox.Current.Value, gCheckbox.Current.Value, bCheckbox.Current.Value, aCheckbox.Current.Value); + + protected override void PopIn() => this.ResizeWidthTo(width, 500, Easing.OutQuint); + + protected override void PopOut() => this.ResizeWidthTo(0, 500, Easing.OutQuint); + + private partial class TexturePreview : Sprite + { + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TextureChannels"); + } + + private bool r; + private bool g; + private bool b; + private bool a; + + public void UpdateComponents(bool r, bool g, bool b, bool a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new TexturePreviewDrawNode(this); + + protected class TexturePreviewDrawNode : SpriteDrawNode + { + public new TexturePreview Source => (TexturePreview)base.Source; + + public TexturePreviewDrawNode(TexturePreview source) + : base(source) + { + } + + private bool r; + private bool g; + private bool b; + private bool a; + + public override void ApplyState() + { + base.ApplyState(); + + r = Source.r; + g = Source.g; + b = Source.b; + a = Source.a; + } + + private IUniformBuffer? parametersBuffer; + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + parametersBuffer ??= renderer.CreateUniformBuffer(); + parametersBuffer.Data = new TextureChannelParameters + { + R = r, + G = g, + B = b, + A = a + }; + + shader.BindUniformBlock("m_TextureChannelParameters", parametersBuffer); + } + + protected internal override bool CanDrawOpaqueInterior => false; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + parametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct TextureChannelParameters + { + public UniformBool R; + public UniformBool G; + public UniformBool B; + public UniformBool A; + } + } + } + + private partial class Checkerboard : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get("Checkerboard", WrapMode.Repeat, WrapMode.Repeat); + } + } + + private partial class InteractiveContainer : Container + { + protected override Container Content => ScalableContent; + + protected readonly Container ScalableContent; + private readonly bool smooth; + + public InteractiveContainer(bool smooth = true) + { + this.smooth = smooth; + + RelativeSizeAxes = Axes.Both; + AddInternal(ScalableContent = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both + }); + } + + private float zoom = 1f; + + public void ResetPosition() + { + ScalableContent.Anchor = Anchor.Centre; + ScalableContent.Origin = Anchor.Centre; + ScalableContent.Position = Vector2.Zero; + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override void OnDrag(DragEvent e) + { + base.OnDrag(e); + ScalableContent.Position += e.Delta; + } + + protected override bool OnScroll(ScrollEvent e) + { + base.OnScroll(e); + + ScalableContent.OriginPosition = ToSpaceOfOtherDrawable(e.MousePosition, ScalableContent); + ScalableContent.Anchor = Anchor.TopLeft; + ScalableContent.Position = e.MousePosition; + + zoom += (e.ScrollDelta.Y > 0 ? 1 : -1) * zoom * 0.1f; + ScalableContent.ScaleTo(zoom, smooth ? 150 : 0, Easing.OutQuint); + isFit = false; + + return true; + } + + protected void SetZoom(float newZoom, Vector2? mousePosition = null, double duration = 0) + { + ScalableContent.ClearTransforms(); + + zoom = newZoom; + + if (mousePosition.HasValue) + { + ScalableContent.OriginPosition = ToSpaceOfOtherDrawable(mousePosition.Value, ScalableContent); + ScalableContent.Anchor = Anchor.TopLeft; + ScalableContent.Position = mousePosition.Value; + } + + ScalableContent.ScaleTo(zoom, duration, Easing.OutQuint); + isFit = false; + } + + public void Fit(double duration = 0) + { + ResetPosition(); + SetZoom(Math.Min(DrawSize.X / ScalableContent.Child.DrawWidth, DrawSize.Y / ScalableContent.Child.DrawHeight), null, duration); + isFit = true; + } + + protected override bool OnClick(ClickEvent e) + { + base.OnClick(e); + return true; + } + + private bool isFit = true; + + protected override bool OnDoubleClick(DoubleClickEvent e) + { + base.OnDoubleClick(e); + + if (isFit) + SetZoom(1f, e.MousePosition, 250); + else + Fit(); + + return true; + } + } + } +} diff --git a/osu.Framework/Graphics/Visualisation/TextureVisualiser.cs b/osu.Framework/Graphics/Visualisation/TextureVisualiser.cs index ae14335a09..132ed89059 100644 --- a/osu.Framework/Graphics/Visualisation/TextureVisualiser.cs +++ b/osu.Framework/Graphics/Visualisation/TextureVisualiser.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; using osuTK; @@ -29,12 +30,15 @@ internal partial class TextureVisualiser : ToolWindow private readonly BindableInt visualisedMipLevel = new BindableInt(-1) { MinValue = -1, MaxValue = IRenderer.MAX_MIPMAP_LEVELS }; + private readonly TextureInspector textureInspector; + [Resolved] private IRenderer renderer { get; set; } public TextureVisualiser() : base("Textures", "(Ctrl+F3 to toggle)") { + MainHorizontalContent.Add(textureInspector = new TextureInspector()); ScrollContent.Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -119,11 +123,22 @@ private void addTexture(Texture texture) => Schedule(() => if (target.Any(p => p.Texture == texture)) return; - target.Add(new TexturePanel(texture, visualisedMipLevel)); + target.Add(new TexturePanel(texture, visualisedMipLevel) + { + Clicked = () => inspectTexture(texture) + }); }); + private void inspectTexture(Texture texture) + { + textureInspector.Inspect(texture); + textureInspector.Show(); + } + private partial class TexturePanel : CompositeDrawable { + public Action Clicked; + private readonly WeakReference textureReference; public Texture Texture => textureReference.TryGetTarget(out var tex) ? tex : null; @@ -176,6 +191,13 @@ public TexturePanel(Texture texture, BindableInt visualisedMipLevel) }; } + protected override bool OnClick(ClickEvent e) + { + base.OnClick(e); + Clicked?.Invoke(); + return true; + } + protected override void Update() { base.Update(); diff --git a/osu.Framework/Resources/Shaders/sh_TextureChannels.fs b/osu.Framework/Resources/Shaders/sh_TextureChannels.fs new file mode 100644 index 0000000000..cfb4668d22 --- /dev/null +++ b/osu.Framework/Resources/Shaders/sh_TextureChannels.fs @@ -0,0 +1,38 @@ +#ifndef TEXTURE_CHANNELS_FS +#define TEXTURE_CHANNELS_FS + +#include "sh_Utils.h" +#include "sh_Masking.h" +#include "sh_TextureWrapping.h" + +layout(location = 2) in mediump vec2 v_TexCoord; + +layout(std140, set = 0, binding = 0) uniform m_TextureChannelParameters +{ + bool R; + bool G; + bool B; + bool A; +}; + +layout(set = 1, binding = 0) uniform lowp texture2D m_Texture; +layout(set = 1, binding = 1) uniform lowp sampler m_Sampler; + +layout(location = 0) out vec4 o_Colour; + +void main(void) +{ + vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); + lowp vec4 col = wrappedSampler(wrappedCoord, v_TexRect, m_Texture, m_Sampler, -0.9); + if (A && !R && !G && !B) + { + col = vec4(col.a, col.a, col.a, 1.0); + } + else + { + col *= vec4(float(R), float(G), float(B), 1.0); + } + o_Colour = getRoundedColor(col, wrappedCoord); +} + +#endif From 31ddcfc02d414413793ecb299677beadc91723d1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 31 Jul 2026 23:24:26 +0300 Subject: [PATCH 2/2] Improve TextureInspector UI --- .../Visualisation/TextureInspector.cs | 239 +++++++++++++----- 1 file changed, 174 insertions(+), 65 deletions(-) diff --git a/osu.Framework/Graphics/Visualisation/TextureInspector.cs b/osu.Framework/Graphics/Visualisation/TextureInspector.cs index 06293f0f5b..ed9fb38348 100644 --- a/osu.Framework/Graphics/Visualisation/TextureInspector.cs +++ b/osu.Framework/Graphics/Visualisation/TextureInspector.cs @@ -2,17 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Runtime.InteropServices; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osuTK; +using osuTK.Graphics; namespace osu.Framework.Graphics.Visualisation { @@ -20,11 +24,7 @@ internal partial class TextureInspector : VisibilityContainer { private const float width = 600; - private readonly BasicCheckbox rCheckbox; - private readonly BasicCheckbox gCheckbox; - private readonly BasicCheckbox bCheckbox; - private readonly BasicCheckbox aCheckbox; - + private readonly ChannelTabControl channelSelector; private readonly TexturePreview preview; private readonly Checkerboard checkerboard; private readonly Container previewContainer; @@ -33,14 +33,15 @@ internal partial class TextureInspector : VisibilityContainer public TextureInspector() { RelativeSizeAxes = Axes.Y; + Padding = new MarginPadding(10); Child = new GridContainer { RelativeSizeAxes = Axes.Y, Width = width, RowDimensions = new[] { - new Dimension(), new Dimension(GridSizeMode.AutoSize), + new Dimension(), }, ColumnDimensions = new[] { @@ -50,19 +51,37 @@ public TextureInspector() { new Drawable[] { - new Container + new GridContainer { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = interactiveContainer = new InteractiveContainer + RelativeSizeAxes = Axes.X, + Height = 25, + Margin = new MarginPadding { Bottom = 10 }, + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Child = previewContainer = new Container + new Dimension(GridSizeMode.Relative, size: 1), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] { - Children = new Drawable[] + new SpriteText { - checkerboard = new Checkerboard(), - preview = new TexturePreview() + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "Channels: ", + Font = FrameworkFont.Regular, + Colour = FrameworkColour.Yellow + }, + channelSelector = new ChannelTabControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both } } } @@ -70,47 +89,20 @@ public TextureInspector() }, new Drawable[] { - new FillFlowContainer + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Margin = new MarginPadding { Vertical = 10 }, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = interactiveContainer = new InteractiveContainer { - rCheckbox = new BasicCheckbox - { - LabelText = "R", - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = { Value = true } - }, - gCheckbox = new BasicCheckbox - { - LabelText = "G", - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = { Value = true } - }, - bCheckbox = new BasicCheckbox - { - LabelText = "B", - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = { Value = true } - }, - aCheckbox = new BasicCheckbox + RelativeSizeAxes = Axes.Both, + Child = previewContainer = new Container { - LabelText = "A", - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = { Value = true } + Children = new Drawable[] + { + checkerboard = new Checkerboard(), + preview = new TexturePreview() + } } } } @@ -119,14 +111,19 @@ public TextureInspector() }; } + private enum Channel + { + All, + R, + G, + B, + A + } + protected override void LoadComplete() { base.LoadComplete(); - - rCheckbox.Current.BindValueChanged(_ => updatePreview()); - gCheckbox.Current.BindValueChanged(_ => updatePreview()); - bCheckbox.Current.BindValueChanged(_ => updatePreview()); - aCheckbox.Current.BindValueChanged(_ => updatePreview(), true); + channelSelector.Current.BindValueChanged(c => preview.UpdateChannels(c.NewValue), true); } public void Inspect(Texture texture) @@ -138,12 +135,91 @@ public void Inspect(Texture texture) interactiveContainer.Fit(); } - private void updatePreview() => preview.UpdateComponents(rCheckbox.Current.Value, gCheckbox.Current.Value, bCheckbox.Current.Value, aCheckbox.Current.Value); - protected override void PopIn() => this.ResizeWidthTo(width, 500, Easing.OutQuint); protected override void PopOut() => this.ResizeWidthTo(0, 500, Easing.OutQuint); + private partial class ChannelTabControl : BasicTabControl + { + public ChannelTabControl() + { + Items = Enum.GetValues().ToList(); + } + + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Depth = -1, + Masking = true + }; + + protected override TabItem CreateTabItem(Channel value) + => new ChannelTabItem(value); + + public partial class ChannelTabItem : TabItem + { + private readonly Box highlight; + + public ChannelTabItem(Channel value) + : base(value) + { + AutoSizeAxes = Axes.None; + Width = 100; + RelativeSizeAxes = Axes.Y; + Padding = new MarginPadding { Horizontal = 5 }; + + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = FrameworkColour.BlueGreen, + }, + highlight = new Box + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White.Opacity(.2f), + Blending = BlendingParameters.Additive + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = value.ToString(), + Font = FrameworkFont.Regular, + Colour = FrameworkColour.Yellow + } + }); + } + + protected override void OnActivated() => updateState(); + + protected override void OnDeactivated() => updateState(); + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() => highlight.FadeTo(IsHovered ? 1 : Active.Value ? 0.5f : 0f, 200); + } + } + private partial class TexturePreview : Sprite { [BackgroundDependencyLoader] @@ -157,12 +233,45 @@ private void load(ShaderManager shaders) private bool b; private bool a; - public void UpdateComponents(bool r, bool g, bool b, bool a) + public void UpdateChannels(Channel channel) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; + switch (channel) + { + case Channel.All: + r = true; + g = true; + b = true; + a = true; + break; + + case Channel.R: + r = true; + g = false; + b = false; + a = false; + break; + + case Channel.G: + r = false; + g = true; + b = false; + a = false; + break; + + case Channel.B: + r = false; + g = false; + b = true; + a = false; + break; + + case Channel.A: + r = false; + g = false; + b = false; + a = true; + break; + } Invalidate(Invalidation.DrawNode); }