]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Feature/shader radial menu (#35152)
authorFildrance <fildrance@gmail.com>
Thu, 10 Apr 2025 10:42:53 +0000 (13:42 +0300)
committerGitHub <noreply@github.com>
Thu, 10 Apr 2025 10:42:53 +0000 (20:42 +1000)
* 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 <pa.pecherskij@interfax.ru>
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
Content.Client/UserInterface/Controls/RadialContainer.cs
Content.Client/UserInterface/Controls/RadialMenu.cs
Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
Resources/Prototypes/Shaders/shaders.yml
Resources/Textures/Shaders/radial-menu.swsl [new file with mode: 0644]

index 72555aab5f3083014fe18c82b441153d04276a02..0efa51f63d5778984c5a6c6317d183ce3a7dbea0 100644 (file)
@@ -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];
+
     /// <summary>
     /// Increment of radius per child element to be rendered.
     /// </summary>
@@ -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
     /// </summary>
     public RadialContainer()
     {
-
+        IoCManager.InjectDependencies(this);
+        _shader = _prototypeManager.Index<ShaderPrototype>("RadialMenu")
+                                   .InstanceUnique();
     }
 
     /// <inheritdoc />
@@ -161,6 +170,67 @@ public class RadialContainer : LayoutContainer
         return base.ArrangeOverride(finalSize);
     }
 
+    /// <inheritdoc />
+    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);
+    }
+
     /// <summary>
     /// Specifies the different radial alignment modes
     /// </summary>
index 9734cf2960119a1e03aa7560173e570d7d947efb..35aa655f3e9c70dd34c3e4474b910eb4dee6f331 100644 (file)
@@ -362,12 +362,12 @@ public interface IRadialMenuItemWithSector
     /// <summary>
     /// Angle in radian where button sector should start.
     /// </summary>
-    public float AngleSectorFrom { set; }
+    public float AngleSectorFrom { set; get; }
 
     /// <summary>
     /// Angle in radian where button sector should end.
     /// </summary>
-    public float AngleSectorTo { set; }
+    public float AngleSectorTo { set; get; }
 
     /// <summary>
     /// Outer radius for drawing segment and pointer detection.
@@ -388,6 +388,41 @@ public interface IRadialMenuItemWithSector
     /// Coordinates of center in parent component - button container.
     /// </summary>
     public Vector2 ParentCenter { set; }
+
+    /// <summary>
+    /// Marker, is menu item hovered currently.
+    /// </summary>
+    public bool IsHovered { get; }
+
+    /// <summary>
+    /// Color for menu item background when it is hovered over.
+    /// </summary>
+    Color HoverBackgroundColor { get; }
+
+    /// <summary>
+    /// Color for menu item default state.
+    /// </summary>
+    Color BackgroundColor { get; }
+
+    /// <summary>
+    /// Color for menu item border when item is hovered over.
+    /// </summary>
+    Color HoverBorderColor { get; }
+
+    /// <summary>
+    /// Color for menu item border default state.
+    /// </summary>
+    Color BorderColor { get; }
+
+    /// <summary>
+    /// Marker, if menu item background should be drawn.
+    /// </summary>
+    public bool DrawBackground { get; }
+
+    /// <summary>
+    /// Marker, if menu item borders should be drawn.
+    /// </summary>
+    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.
     /// </summary>
     /// <remarks>
-    /// By default color of border is same as color of background. Use <see cref="BorderColor"/>
+    /// Default color of border is same as color of background. Use <see cref="BorderColor"/>
     /// and <see cref="HoverBorderColor"/> to change it.
     /// </remarks>
     public bool DrawBorder { get; set; } = false;
@@ -459,12 +494,6 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
         set => _hoverBorderColorSrgb = Color.ToSrgb(value);
     }
 
-    /// <summary>
-    /// Color of separator lines.
-    /// Separator lines are used to visually separate sector of radial menu items.
-    /// </summary>
-    public Color SeparatorColor { get; set; } = new Color(128, 128, 128, 128);
-
     /// <inheritdoc />
     float IRadialMenuItemWithSector.AngleSectorFrom
     {
@@ -473,6 +502,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
             _angleSectorFrom = value;
             _isWholeCircle = IsWholeCircle(value, _angleSectorTo);
         }
+        get => _angleSectorFrom;
     }
 
     /// <inheritdoc />
@@ -483,6 +513,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
             _angleSectorTo = value;
             _isWholeCircle = IsWholeCircle(_angleSectorFrom, value);
         }
+        get => _angleSectorTo;
     }
 
     /// <inheritdoc />
@@ -504,44 +535,6 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
     {
     }
 
-    /// <inheritdoc />
-    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);
-        }
-    }
-
     /// <inheritdoc />
     protected override bool HasPoint(Vector2 point)
     {
index 15c8065a44084461c3fef4747c4351485fa793d8..589a97629d07fd767e92b492b4e1fe2611b5bb95 100644 (file)
@@ -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<Control>(ChildCount);
+        var toRemove = new ValueList<Control>(ChildCount);
         foreach (var child in Children)
         {
             if (child != ContextualButton && child != MenuOuterAreaButton)
index 6e0bbd55b43348291c4fcefc714fcc1d8ebb364d..631b8ee920ee35f72fb220105b9dbadda5c707cf 100644 (file)
 - 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 (file)
index 0000000..30fa1a4
--- /dev/null
@@ -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;
+}