From 56710697a97c83b5f9b1d881f8503b81b7678633 Mon Sep 17 00:00:00 2001 From: Fildrance Date: Thu, 10 Apr 2025 13:42:53 +0300 Subject: [PATCH] Feature/shader radial menu (#35152) * it works! kinda * so it works now * minor cleanup * central button now is useful too * more cleanup * minor cleanup * more cleanup * refactor: migrated code from toolbox (as it was rejected as too specific) * feat: moved border drawing for radial menu into RadialMenuTextureButton. Radial menu position setting into was moved to OverrideArrange to not being called on every frame * refactor: major reworks! * renamed DrawBagleSector to DrawAnnulusSector * Remove strange indexing * Regularize math * refactor: re-orienting segment elements to be Y-mirrored * refactor: extracted radial menu radius multiplier property, changed color pallet for radial menu button * refactor: removed icon backgrounds on textures used in current radial menu buttons with sectors, RadialContainer Radius renamed and now actually changed control radius. * refactor: in RadialMenuTextureButtonWithSector all sector colors are converted to and from sRGB in property getter-setters * refactor: renamed srgb to include Srgb suffix so devs gonna see that its srgb clearly * fix: enabled any functional keys pressed when pushing radial menu buttons * fix: radial menu sector now scales with UIScale * fix: accept only one event when clicking on radial menu ContextualButton * fix: now radial menu buttons accepts only click/alt-click, now clicks outside menu closes menu always * feat: simple radial menu prototype for easier creation * refactor: cleanup, restored emote filtering, button models now have class hierarchy * refactor: remove usage of closure from 'outside code' * refactor: remove non existing type from UiControlTest * refactor: remove unused using * refactor: revert ability to declare radial menu layers in xaml, scale 32px sprites using scale in radial menu * refactor: whitespaces * refactor: subscribe for dispose on existing radial menus * feat: now simple radial menu button models can have custom color for each sector background (and hover background color). Also added OpenOverMouseScreenPosition inside SimpleRadialMenu * fix: AI door menu now can be closed by verb if it gets unpowered * overlay and its registration * radial menu shader but it requires wierd offset * remove unused file * smol cleanup * remove unused code * neat internal subsctors in radial menu shaders * refactor finalize visual style * comments, simplify, extract variable and other minor refactors on radial-menu shader * refactor: extract more data from radial menu with sector to radial container for shader drawing * replaced DrawSeparators for RadialMenuTextureButtonWithSector with DrawBorder (no reason to make them separate), also now colors are properly applied * refactor: simplify hiding border, extended xml-doc for simple radial menu settings * refactor: remove duplication of radial menu shaders, use ValueList to collect ClearExistingChildrenRadialButtons buttons to remove * refactor: remove linq * fix: fix AI radial action serialization using invalid type * refactor: fix duplicate ShowDeviceNotRespondingPopup for AI by properly checking if it can interact * refactor: removed *if* blocks from shader, replaced with branchless logic * refactor: whitespaces, changed list to array in simple radial button preparing methods * fix: merge duplicated code --------- Co-authored-by: pa.pecherskij Co-authored-by: Eoin Mcloughlin --- .../UserInterface/Controls/RadialContainer.cs | 82 +++++++++- .../UserInterface/Controls/RadialMenu.cs | 87 +++++----- .../Controls/SimpleRadialMenu.xaml.cs | 3 +- Resources/Prototypes/Shaders/shaders.yml | 7 +- Resources/Textures/Shaders/radial-menu.swsl | 148 ++++++++++++++++++ 5 files changed, 272 insertions(+), 55 deletions(-) create mode 100644 Resources/Textures/Shaders/radial-menu.swsl diff --git a/Content.Client/UserInterface/Controls/RadialContainer.cs b/Content.Client/UserInterface/Controls/RadialContainer.cs index 72555aab5f..0efa51f63d 100644 --- a/Content.Client/UserInterface/Controls/RadialContainer.cs +++ b/Content.Client/UserInterface/Controls/RadialContainer.cs @@ -1,12 +1,23 @@ using Robust.Client.UserInterface.Controls; using System.Linq; using System.Numerics; +using Robust.Client.Graphics; +using Robust.Shared.Prototypes; namespace Content.Client.UserInterface.Controls; [Virtual] public class RadialContainer : LayoutContainer { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IClyde _clyde= default!; + private readonly ShaderInstance _shader; + + private readonly float[] _angles = new float[64]; + private readonly float[] _sectorMedians = new float[64]; + private readonly Color[] _sectorColors = new Color[64]; + private readonly Color[] _borderColors = new Color[64]; + /// /// Increment of radius per child element to be rendered. /// @@ -24,11 +35,7 @@ public class RadialContainer : LayoutContainer [ViewVariables(VVAccess.ReadWrite)] public Vector2 AngularRange { - get - { - return _angularRange; - } - + get => _angularRange; set { var x = value.X; @@ -89,7 +96,9 @@ public class RadialContainer : LayoutContainer /// public RadialContainer() { - + IoCManager.InjectDependencies(this); + _shader = _prototypeManager.Index("RadialMenu") + .InstanceUnique(); } /// @@ -161,6 +170,67 @@ public class RadialContainer : LayoutContainer return base.ArrangeOverride(finalSize); } + /// + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + float selectedFrom = 0; + float selectedTo = 0; + + var i = 0; + foreach (var child in Children) + { + if (child is not IRadialMenuItemWithSector menuWithSector) + { + continue; + } + + _angles[i] = menuWithSector.AngleSectorTo; + _sectorMedians[i] = (menuWithSector.AngleSectorTo + menuWithSector.AngleSectorFrom) / 2; + + if (menuWithSector.IsHovered) + { + // menuWithSector.DrawBackground; + // menuWithSector.DrawBorder; + _sectorColors[i] = menuWithSector.HoverBackgroundColor; + _borderColors[i] = menuWithSector.HoverBorderColor; + selectedFrom = menuWithSector.AngleSectorFrom; + selectedTo = menuWithSector.AngleSectorTo; + } + else + { + _sectorColors[i] = menuWithSector.BackgroundColor; + _borderColors[i] = menuWithSector.BorderColor; + } + + i++; + } + + var screenSize = _clyde.ScreenSize; + + var menuCenter = new Vector2( + ScreenCoordinates.X + (Size.X / 2) * UIScale, + screenSize.Y - ScreenCoordinates.Y - (Size.Y / 2) * UIScale + ); + + _shader.SetParameter("separatorAngles", _angles); + _shader.SetParameter("sectorMedianAngles", _sectorMedians); + _shader.SetParameter("selectedFrom", selectedFrom); + _shader.SetParameter("selectedTo", selectedTo); + _shader.SetParameter("childCount", i); + _shader.SetParameter("sectorColors", _sectorColors); + _shader.SetParameter("borderColors", _borderColors); + _shader.SetParameter("centerPos", menuCenter); + _shader.SetParameter("screenSize", screenSize); + _shader.SetParameter("innerRadius", CalculatedRadius * InnerRadiusMultiplier * UIScale); + _shader.SetParameter("outerRadius", CalculatedRadius * OuterRadiusMultiplier * UIScale); + + handle.UseShader(_shader); + handle.DrawRect(new UIBox2(0, 0, screenSize.X, screenSize.Y), Color.White); + handle.UseShader(null); + } + /// /// Specifies the different radial alignment modes /// diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 9734cf2960..35aa655f3e 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -362,12 +362,12 @@ public interface IRadialMenuItemWithSector /// /// Angle in radian where button sector should start. /// - public float AngleSectorFrom { set; } + public float AngleSectorFrom { set; get; } /// /// Angle in radian where button sector should end. /// - public float AngleSectorTo { set; } + public float AngleSectorTo { set; get; } /// /// Outer radius for drawing segment and pointer detection. @@ -388,6 +388,41 @@ public interface IRadialMenuItemWithSector /// Coordinates of center in parent component - button container. /// public Vector2 ParentCenter { set; } + + /// + /// Marker, is menu item hovered currently. + /// + public bool IsHovered { get; } + + /// + /// Color for menu item background when it is hovered over. + /// + Color HoverBackgroundColor { get; } + + /// + /// Color for menu item default state. + /// + Color BackgroundColor { get; } + + /// + /// Color for menu item border when item is hovered over. + /// + Color HoverBorderColor { get; } + + /// + /// Color for menu item border default state. + /// + Color BorderColor { get; } + + /// + /// Marker, if menu item background should be drawn. + /// + public bool DrawBackground { get; } + + /// + /// Marker, if menu item borders should be drawn. + /// + public bool DrawBorder { get; } } [Virtual] @@ -413,7 +448,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia /// Marker, that controls if border of segment should be rendered. Is false by default. /// /// - /// By default color of border is same as color of background. Use + /// Default color of border is same as color of background. Use /// and to change it. /// public bool DrawBorder { get; set; } = false; @@ -459,12 +494,6 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia set => _hoverBorderColorSrgb = Color.ToSrgb(value); } - /// - /// Color of separator lines. - /// Separator lines are used to visually separate sector of radial menu items. - /// - public Color SeparatorColor { get; set; } = new Color(128, 128, 128, 128); - /// float IRadialMenuItemWithSector.AngleSectorFrom { @@ -473,6 +502,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia _angleSectorFrom = value; _isWholeCircle = IsWholeCircle(value, _angleSectorTo); } + get => _angleSectorFrom; } /// @@ -483,6 +513,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia _angleSectorTo = value; _isWholeCircle = IsWholeCircle(_angleSectorFrom, value); } + get => _angleSectorTo; } /// @@ -504,44 +535,6 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia { } - /// - protected override void Draw(DrawingHandleScreen handle) - { - base.Draw(handle); - - if (_parentCenter == null) - { - return; - } - - // draw sector where space that button occupies actually is - var containerCenter = (_parentCenter.Value - Position) * UIScale; - - var angleFrom = _angleSectorFrom + _angleOffset; - var angleTo = _angleSectorTo + _angleOffset; - if (DrawBackground) - { - var segmentColor = DrawMode == DrawModeEnum.Hover - ? _hoverBackgroundColorSrgb - : _backgroundColorSrgb; - - DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, segmentColor); - } - - if (DrawBorder) - { - var borderColor = DrawMode == DrawModeEnum.Hover - ? _hoverBorderColorSrgb - : _borderColorSrgb; - DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false); - } - - if (!_isWholeCircle && DrawBorder) - { - DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor); - } - } - /// protected override bool HasPoint(Vector2 point) { diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs index 15c8065a44..589a97629d 100644 --- a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs +++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs @@ -7,6 +7,7 @@ using Robust.Client.GameObjects; using Robust.Shared.Timing; using Robust.Client.UserInterface.XAML; using Robust.Client.Input; +using Robust.Shared.Collections; namespace Content.Client.UserInterface.Controls; @@ -173,7 +174,7 @@ public partial class SimpleRadialMenu : RadialMenu private void ClearExistingChildrenRadialButtons() { - var toRemove = new List(ChildCount); + var toRemove = new ValueList(ChildCount); foreach (var child in Children) { if (child != ContextualButton && child != MenuOuterAreaButton) diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index 6e0bbd55b4..631b8ee920 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -108,4 +108,9 @@ - type: shader id: Hologram kind: source - path: "/Textures/Shaders/hologram.swsl" \ No newline at end of file + path: "/Textures/Shaders/hologram.swsl" + +- type: shader + id: RadialMenu + kind: source + path: "/Textures/Shaders/radial-menu.swsl" diff --git a/Resources/Textures/Shaders/radial-menu.swsl b/Resources/Textures/Shaders/radial-menu.swsl new file mode 100644 index 0000000000..30fa1a4cba --- /dev/null +++ b/Resources/Textures/Shaders/radial-menu.swsl @@ -0,0 +1,148 @@ +const highp float Thickness = 0.002; + +const highp float pi = 3.14159265; +const highp float twopi = 2.0 * pi; +const highp float halfpi = 0.5 * pi; +const highp float invpi = 1.0 / pi; + +uniform highp float innerRadius; +uniform highp float outerRadius; + +uniform highp vec4[64] sectorColors; +uniform highp vec4[64] borderColors; + +uniform highp float[64] separatorAngles; +uniform highp float[64] sectorMedianAngles; +uniform highp int childCount; +uniform highp vec2 centerPos; + +uniform highp float selectedFrom; +uniform highp float selectedTo; +uniform highp vec2 screenSize; + +highp float SMOOTH(highp float r, highp float R) +{ + return 1.0 - smoothstep(R - 1.0, R + 1.0, r); +} + +// line from center of circle to radius (outer arg) on theta0 angle (radian,) +highp float separator(highp vec2 d, highp float r, highp float outer, highp float theta0, highp float thickness) +{ + // rotate due to difference in coordinate spaces between shaders and ui + highp float theta1 = theta0 - halfpi; + highp vec2 p = outer * vec2(cos(theta1), -sin(theta1)); + highp float l = length(d - p * clamp(dot(d, p) / dot(p, p), 0.0, 1.0)); + return SMOOTH(l, thickness); +} + +highp float circle(highp float r, highp float radius, highp float width) +{ + return SMOOTH(r - width / 2.0, radius) - SMOOTH(r + width / 2.0, radius); +} + +// get angle between current point and circle center +highp float getAngle(highp vec2 d) +{ + highp vec2 n = normalize(d); + highp float angle = acos(n.x); + int isNegativeY = int(n.y < 0.0); + angle = angle + (twopi - angle * 2) * isNegativeY; + // rotate + angle = mod(angle - halfpi, twopi); + return angle; +} + +highp float pcurve(highp float x, highp float a, highp float b ) +{ + highp float k = pow(a + b,a + b) / (pow(a, a) * pow(b, b)); + return k * pow(x, a) * pow(1.0 - x, b); +} + +// gets alpha for radial gradients based on pcurve +highp float fillGradient(highp float r, highp float inner, highp float outer) +{ + highp float nInner = inner / outer; + highp float nR = r / outer; + return pcurve(nR, nInner, 1.0); +} + +void fragment() +{ + highp vec4 col = vec4(0.0); + + //angle of the line + highp vec2 d = FRAGCOORD.xy - centerPos; + highp float angle = getAngle(d); + + highp float r = length(FRAGCOORD.xy - centerPos); + // fill sectors + int isInsideRange = int(r > innerRadius && r < outerRadius); + highp float g = fillGradient(r, innerRadius, outerRadius); + + // trying to mix in color per button + + highp float from = 0; + for (int i = 0; i < childCount; i++) + { + highp float to = separatorAngles[i]; + int isInSector = int(angle > from && angle < to); + col += isInsideRange + * vec4(sectorColors[i].xyz , g)* isInSector; + + from = to; + } + + // get step of radial menu buttons in radian + highp float halfSectorAngleSize = (separatorAngles[1] - separatorAngles[0]) * 0.5; + + for (int i = 0; i < childCount; i++) + { + highp float sectorMedian = sectorMedianAngles[i]; + highp float sectorMedianToAngleDiff = abs(sectorMedian - angle); + highp vec4 borderColor = borderColors[i]; + highp vec4 borderColorLight = borderColor * 0.6; + int isInInnerRadius = int(r > innerRadius); + highp float iAngle = twopi - separatorAngles[i]; + // button separators + highp float sectorFromAngle; + int isCurrentZero = int(i > 0); + sectorFromAngle = separatorAngles[i - 1] * isCurrentZero; + + col += isInInnerRadius + * separator(d, r, outerRadius, sectorFromAngle, 0.5) * borderColor; + col += isInInnerRadius + * separator(d, r, outerRadius, iAngle, 0.5) * borderColor; + + // set up decorations + // inner button 'square' decoration + int isInInnerBorderRange = int(r > innerRadius + 15); + col += isInInnerRadius * isInInnerBorderRange + * separator(d, r, outerRadius - 15, twopi - sectorMedian - halfSectorAngleSize * 0.8, 1.0) * 0.2 * borderColorLight; + col += isInInnerRadius * isInInnerBorderRange + * separator(d, r, outerRadius - 15, twopi - sectorMedian + halfSectorAngleSize * 0.8, 1.0) * 0.2 * borderColorLight; + + int isInInnerBorderSector = int(sectorMedianToAngleDiff < halfSectorAngleSize * 0.8); + col += isInInnerRadius * isInInnerBorderRange * isInInnerBorderSector + * circle(r, innerRadius + 15, 2.0) * 0.2 * borderColorLight; + col += isInInnerRadius * isInInnerBorderRange * isInInnerBorderSector + * circle(r, outerRadius - 15, 2.0) * 0.2 * borderColorLight; + + // outer button decorative elements + int isInOuterBorderOuterSector = int(sectorMedianToAngleDiff < halfSectorAngleSize * 0.2); + col += isInOuterBorderOuterSector + * circle(r, innerRadius - 5, 4.0) * 0.4 * borderColor; + + int isInOuterBorderInnerSector = int(sectorMedianToAngleDiff < halfSectorAngleSize * 0.6); + col += isInOuterBorderInnerSector + * circle(r, outerRadius + 10, 4.0) * 0.4 * borderColor; + + // outer and inner circle of sectors + int isOnSectorBorder = int(sectorMedianToAngleDiff < halfSectorAngleSize); + col += isOnSectorBorder + * circle(r, innerRadius, 2.0) * borderColor; + col += isOnSectorBorder + * circle(r, outerRadius, 2.0) * borderColor; + } + + COLOR = col; +} -- 2.51.2