Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement FastCircle component #6290

Merged
merged 21 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions osu.Framework.Tests/Visual/Drawables/TestSceneFastCircle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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();
}
}
131 changes: 131 additions & 0 deletions osu.Framework/Graphics/Shapes/FastCircle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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
{
/// <summary>
/// A circle that is rendered directly to the screen using a specialised shader.
/// This behaves slightly differently from <see cref="Circle"/> but offers
/// higher performance in scenarios where many circles are drawn at once.
/// </summary>
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();
}
}
}
}
Loading
Loading