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 13 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
146 changes: 146 additions & 0 deletions osu.Framework.Tests/Visual/Drawables/TestSceneFastCircle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}

private void testInput(Vector2 size)
{
AddStep($"Resize to {size}", () =>
{
fastCircle.Size = circle.Size = 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 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,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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();
}
}
126 changes: 126 additions & 0 deletions osu.Framework/Graphics/Shapes/FastCircle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
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 edgeSmoothness;
private IShader shader = null!;

public override void ApplyState()
{
base.ApplyState();

screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad;
drawRectangle = new Vector4(0, 0, screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height);
shader = Source.shader;
edgeSmoothness = new Vector2(Source.edgeSmoothness);
}

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, screenSpaceDrawQuad.Height),
TextureRect = drawRectangle,
BlendRange = edgeSmoothness,
Colour = DrawColourInfo.Colour.BottomLeft.SRGB,
});
vertexAction(new TexturedVertex2D(renderer)
{
Position = screenSpaceDrawQuad.BottomRight,
TexturePosition = new Vector2(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height),
TextureRect = drawRectangle,
BlendRange = edgeSmoothness,
Colour = DrawColourInfo.Colour.BottomRight.SRGB,
});
vertexAction(new TexturedVertex2D(renderer)
{
Position = screenSpaceDrawQuad.TopRight,
TexturePosition = new Vector2(screenSpaceDrawQuad.Width, 0),
TextureRect = drawRectangle,
BlendRange = edgeSmoothness,
Colour = DrawColourInfo.Colour.TopRight.SRGB,
});
vertexAction(new TexturedVertex2D(renderer)
{
Position = screenSpaceDrawQuad.TopLeft,
TexturePosition = new Vector2(0, 0),
TextureRect = drawRectangle,
BlendRange = edgeSmoothness,
Colour = DrawColourInfo.Colour.TopLeft.SRGB,
});

shader.Unbind();
}
}
}
}
26 changes: 26 additions & 0 deletions osu.Framework/Resources/Shaders/sh_FastCircle.fs
Original file line number Diff line number Diff line change
@@ -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));
Comment on lines +16 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the use of v_TexRect here.

  • Have you tested textures at all? It looks like you're not actually sampling the texture.
  • What about cropped textures? (Texture.Crop())
  • Does this work for rotated ellipses (textured and non-textured)? I assume so but it should be checked.

Copy link
Contributor Author

@EVAST9919 EVAST9919 May 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the thing: I'm abusing texture coordinates to pass screenspace size to the shader without uniforms (to compute antialiasing). This new circle isn't a sprite, so textures can't be set. (in my defence current circle isn't a sprite either)
Regarding rotation: I'll add test showing it's working.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify: I'm not trying replicate CircularContainer, I'm simplifying one particular usage of it, which is CircularContainer with a plane Box with no texture inside (Circle class) to avoid masking overhead in cases where multiple circes are being drawn consecutively (GPU side) and reducing total drawable count (CPU side)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the thing: I'm abusing texture coordinates to pass screenspace size to the shader without uniforms (to compute antialiasing).

As a side note I can apply hacks explored in this pr to the Triangles shader since it's being widely used in the game to eliminate uniform buffer. Will pr after checking whether performance gain is worth doing so.

}

#endif
Loading