diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneFastCircle.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneFastCircle.cs new file mode 100644 index 0000000000..b0e464e289 --- /dev/null +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneFastCircle.cs @@ -0,0 +1,188 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Testing; +using osuTK; +using osuTK.Input; + +namespace osu.Framework.Tests.Visual.Drawables +{ + public partial class TestSceneFastCircle : ManualInputManagerTestScene + { + private TestCircle fastCircle = null!; + private Circle circle = null!; + private CircularContainer fastCircleMask = null!; + private CircularContainer circleMask = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 100), + new Dimension(), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.5f), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "FastCircle" + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Circle" + } + }, + new Drawable[] + { + fastCircleMask = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.TopRight, + Size = new Vector2(200), + Child = fastCircle = new TestCircle + { + Anchor = Anchor.TopRight, + Origin = Anchor.Centre, + Size = new Vector2(200), + Clicked = onClick + } + }, + circleMask = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.TopRight, + Size = new Vector2(200), + Child = circle = new Circle + { + Anchor = Anchor.TopRight, + Origin = Anchor.Centre, + Size = new Vector2(200) + } + }, + } + } + }; + }); + + [Test] + public void TestInput() + { + testInput(new Vector2(200, 100)); + testInput(new Vector2(100, 200)); + testInput(new Vector2(200, 200)); + } + + [Test] + public void TestSmoothness() + { + AddStep("Change smoothness to 0", () => fastCircle.EdgeSmoothness = circle.MaskingSmoothness = 0); + AddStep("Change smoothness to 1", () => fastCircle.EdgeSmoothness = circle.MaskingSmoothness = 1); + AddStep("Change smoothness to 5", () => fastCircle.EdgeSmoothness = circle.MaskingSmoothness = 5); + } + + [Test] + public void TestNestedMasking() + { + AddToggleStep("Toggle parent masking", m => fastCircleMask.Masking = circleMask.Masking = m); + } + + [Test] + public void TestRotation() + { + resize(new Vector2(200, 100)); + AddToggleStep("Toggle rotation", rotate => + { + fastCircle.ClearTransforms(); + circle.ClearTransforms(); + + if (rotate) + { + fastCircle.Spin(2000, RotationDirection.Clockwise); + circle.Spin(2000, RotationDirection.Clockwise); + } + }); + } + + [Test] + public void TestShear() + { + resize(new Vector2(200, 100)); + AddToggleStep("Toggle shear", shear => + { + fastCircle.Shear = circle.Shear = shear ? new Vector2(0.5f, 0) : Vector2.Zero; + }); + } + + [Test] + public void TestScale() + { + resize(new Vector2(200, 100)); + AddToggleStep("Toggle scale", scale => + { + fastCircle.Scale = circle.Scale = scale ? new Vector2(2f, 1f) : Vector2.One; + }); + } + + private void testInput(Vector2 size) + { + resize(size); + AddStep("Click outside the corner", () => clickNearCorner(-Vector2.One)); + AddAssert("input not received", () => clicked == false); + AddStep("Click inside the corner", () => clickNearCorner(Vector2.One)); + AddAssert("input received", () => clicked); + } + + private void resize(Vector2 size) + { + AddStep($"Resize to {size}", () => + { + fastCircle.Size = circle.Size = size; + }); + } + + private void clickNearCorner(Vector2 offset) + { + clicked = false; + InputManager.MoveMouseTo(fastCircle.ToScreenSpace(new Vector2(fastCircle.Radius * (1f - MathF.Sqrt(0.5f))) + offset)); + InputManager.Click(MouseButton.Left); + } + + private bool clicked; + + private void onClick() => clicked = true; + + private partial class TestCircle : FastCircle + { + public Action? Clicked; + + protected override bool OnClick(ClickEvent e) + { + base.OnClick(e); + Clicked?.Invoke(); + return true; + } + } + } +} diff --git a/osu.Framework.Tests/Visual/Performance/TestSceneCircleBoxAlternatePerformance.cs b/osu.Framework.Tests/Visual/Performance/TestSceneCircleBoxAlternatePerformance.cs new file mode 100644 index 0000000000..5f06e8c154 --- /dev/null +++ b/osu.Framework.Tests/Visual/Performance/TestSceneCircleBoxAlternatePerformance.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; + +namespace osu.Framework.Tests.Visual.Performance +{ + public sealed partial class TestSceneCircleBoxAlternatePerformance : RepeatedDrawablePerformanceTestScene + { + private int index; + + protected override Drawable CreateDrawable() + { + index++; + if (index % 2 == 0) + return new Circle(); + + return new Box(); + } + } +} diff --git a/osu.Framework.Tests/Visual/Performance/TestSceneFastCircleBoxAlternatePerformance.cs b/osu.Framework.Tests/Visual/Performance/TestSceneFastCircleBoxAlternatePerformance.cs new file mode 100644 index 0000000000..137cb6ca1d --- /dev/null +++ b/osu.Framework.Tests/Visual/Performance/TestSceneFastCircleBoxAlternatePerformance.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; + +namespace osu.Framework.Tests.Visual.Performance +{ + public sealed partial class TestSceneFastCircleBoxAlternatePerformance : RepeatedDrawablePerformanceTestScene + { + private int index; + + protected override Drawable CreateDrawable() + { + index++; + if (index % 2 == 0) + return new FastCircle(); + + return new Box(); + } + } +} diff --git a/osu.Framework.Tests/Visual/Performance/TestSceneFastCirclePerformance.cs b/osu.Framework.Tests/Visual/Performance/TestSceneFastCirclePerformance.cs new file mode 100644 index 0000000000..3e4bd26c0d --- /dev/null +++ b/osu.Framework.Tests/Visual/Performance/TestSceneFastCirclePerformance.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; + +namespace osu.Framework.Tests.Visual.Performance +{ + public sealed partial class TestSceneFastCirclePerformance : RepeatedDrawablePerformanceTestScene + { + protected override Drawable CreateDrawable() => new FastCircle(); + } +} diff --git a/osu.Framework/Graphics/Shapes/FastCircle.cs b/osu.Framework/Graphics/Shapes/FastCircle.cs new file mode 100644 index 0000000000..660239ad45 --- /dev/null +++ b/osu.Framework/Graphics/Shapes/FastCircle.cs @@ -0,0 +1,131 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; +using osu.Framework.Graphics.Shaders; +using osuTK; + +namespace osu.Framework.Graphics.Shapes +{ + /// + /// A circle that is rendered directly to the screen using a specialised shader. + /// This behaves slightly differently from but offers + /// higher performance in scenarios where many circles are drawn at once. + /// + public partial class FastCircle : Drawable + { + private float edgeSmoothness = 1f; + + public float EdgeSmoothness + { + get => edgeSmoothness; + set + { + if (edgeSmoothness == value) + return; + + edgeSmoothness = value; + + if (IsLoaded) + Invalidate(Invalidation.DrawNode); + } + } + + private IShader shader = null!; + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle"); + } + + public float Radius => MathF.Min(DrawSize.X, DrawSize.Y) * 0.5f; + + public override bool Contains(Vector2 screenSpacePos) + { + if (!base.Contains(screenSpacePos)) + return false; + + float cRadius = Radius; + return DrawRectangle.Shrink(cRadius).DistanceExponentiated(ToLocalSpace(screenSpacePos), 2f) <= cRadius * cRadius; + } + + protected override DrawNode CreateDrawNode() => new FastCircleDrawNode(this); + + private class FastCircleDrawNode : DrawNode + { + protected new FastCircle Source => (FastCircle)base.Source; + + public FastCircleDrawNode(FastCircle source) + : base(source) + { + } + + private Quad screenSpaceDrawQuad; + private Vector4 drawRectangle; + private Vector2 blend; + private IShader shader = null!; + + public override void ApplyState() + { + base.ApplyState(); + + screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad; + drawRectangle = new Vector4(0, 0, Source.DrawWidth, Source.DrawHeight); + shader = Source.shader; + blend = new Vector2(Source.edgeSmoothness * Math.Min(Source.DrawWidth, Source.DrawHeight) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height)); + } + + protected override void Draw(IRenderer renderer) + { + base.Draw(renderer); + + if (!renderer.BindTexture(renderer.WhitePixel)) + return; + + shader.Bind(); + + var vertexAction = renderer.DefaultQuadBatch.AddAction; + + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomLeft, + TexturePosition = new Vector2(0, drawRectangle.W), + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomLeft.SRGB, + }); + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomRight, + TexturePosition = new Vector2(drawRectangle.Z, drawRectangle.W), + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomRight.SRGB, + }); + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopRight, + TexturePosition = new Vector2(drawRectangle.Z, 0), + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopRight.SRGB, + }); + vertexAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopLeft, + TexturePosition = Vector2.Zero, + TextureRect = drawRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopLeft.SRGB, + }); + + shader.Unbind(); + } + } + } +} diff --git a/osu.Framework/Resources/Shaders/sh_FastCircle.fs b/osu.Framework/Resources/Shaders/sh_FastCircle.fs new file mode 100644 index 0000000000..307f00a74c --- /dev/null +++ b/osu.Framework/Resources/Shaders/sh_FastCircle.fs @@ -0,0 +1,26 @@ +#ifndef FAST_CIRCLE_FS +#define FAST_CIRCLE_FS + +#undef HIGH_PRECISION_VERTEX +#define HIGH_PRECISION_VERTEX + +#include "sh_Utils.h" +#include "sh_Masking.h" + +layout(location = 2) in highp vec2 v_TexCoord; + +layout(location = 0) out vec4 o_Colour; + +void main(void) +{ + highp vec2 pixelPos = v_TexRect.zw * 0.5 - abs(v_TexCoord - v_TexRect.zw * 0.5); + highp float radius = min(v_TexRect.z, v_TexRect.w) * 0.5; + + highp float dst = max(pixelPos.x, pixelPos.y) > radius ? radius - min(pixelPos.x, pixelPos.y) : distance(pixelPos, vec2(radius)); + + highp float alpha = v_BlendRange.x == 0.0 ? float(dst < radius) : (clamp(radius - dst, 0.0, v_BlendRange.x) / v_BlendRange.x); + + o_Colour = getRoundedColor(vec4(vec3(1.0), alpha), vec2(0.0)); +} + +#endif