diff --git a/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneChevronVisuals.cs b/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneChevronVisuals.cs new file mode 100644 index 000000000..7316f07fa --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneChevronVisuals.cs @@ -0,0 +1,43 @@ +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Tests.Graphics +{ + [TestFixture] + public partial class TestSceneChevronVisual : OsuGridTestScene + { + protected override Ruleset CreateRuleset() => new SentakkiRuleset(); + public TestSceneChevronVisual() : base(1, 2) + { + Cell(0).Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White + }); + Cell(1).Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White + }); + Cell(0).Add(new SlideChevron()); + Cell(1).Add(new DrawableChevron() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(80, 62.5f), + Thickness = 7 + }); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneFanChevronVisuals.cs b/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneFanChevronVisuals.cs new file mode 100644 index 000000000..3e2a40e8c --- /dev/null +++ b/osu.Game.Rulesets.Sentakki.Tests/Graphics/TestSceneFanChevronVisuals.cs @@ -0,0 +1,117 @@ +using System; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; +using osu.Game.Rulesets.Sentakki.UI; +using osu.Game.Rulesets.Sentakki.UI.Components; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Tests.Graphics +{ + [TestFixture] + public partial class TestSceneFanChevronVisual : OsuGridTestScene + { + protected override Ruleset CreateRuleset() => new SentakkiRuleset(); + private readonly SlideVisual slide; + + private readonly Container fanChevContainer; + + [Cached] + private readonly DrawablePool chevronPool; + public TestSceneFanChevronVisual() : base(1, 2) + { + Cell(0).Add(chevronPool = new DrawablePool(62)); + Cell(0).Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White + }); + Cell(0).Add(new SentakkiRing + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(SentakkiPlayfield.RINGSIZE) + }); + Cell(0).Add(slide = new SlideVisual()); + Cell(0).Add(new SentakkiRing + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(SentakkiPlayfield.RINGSIZE) + }); + Cell(1).Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White + }); + Cell(1).Add(new SentakkiRing + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(SentakkiPlayfield.RINGSIZE) + }); + float rot = SentakkiExtensions.GetRotationForLane(4); + Cell(1).Add(fanChevContainer = new Container() + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(600), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Rotation = rot, + }); + + Vector2 lineStart = SentakkiExtensions.GetPositionAlongLane(SentakkiPlayfield.INTERSECTDISTANCE, 0); + Vector2 middleLineEnd = SentakkiExtensions.GetPositionAlongLane(SentakkiPlayfield.INTERSECTDISTANCE, 4); + Vector2 middleLineDelta = middleLineEnd - lineStart; + + for (int i = 0; i < 11; ++i) + { + float progress = (i + 2f) / 12f; + + float scale = progress - (2f / 12); + var middlePosition = lineStart + (middleLineDelta * progress); + + float t = 6.5f + (2.5f * scale); + + float chevWidth = MathF.Abs(lineStart.X - middlePosition.X); + + (float sin, float cos) = MathF.SinCos((-135 + 90f) / 180f * MathF.PI); + + Vector2 secondPoint = new Vector2(sin, -cos) * chevWidth; + Vector2 one = new Vector2(chevWidth, 0); + + var middle = (one + secondPoint) * 0.5f; + float h = (middle - Vector2.Zero).Length + (t * 3); + + float w = (secondPoint - one).Length; + + const float safe_space_ratio = (570 / 600f) * 600; + + float y = safe_space_ratio * scale; + + var fanChev = new DrawableChevron() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = w + 30, + Height = h + 30, + Thickness = t, + Y = -y + 300 - 50, + FanChevron = true, + }; + + fanChevContainer.Add(fanChev); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + slide.Path = SlidePaths.CreateSlidePath(new SlideBodyPart(SlidePaths.PathShapes.Fan, 4, false)); + } + } +} diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs index 5c20871d1..b83f3b162 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneAllSlides.cs @@ -28,13 +28,9 @@ public partial class TestSceneAllSlides : OsuTestScene [Cached] private readonly DrawablePool chevronPool; - [Cached] - private readonly SlideFanChevrons fanChevrons; - public TestSceneAllSlides() { Add(chevronPool = new DrawablePool(62)); - Add(fanChevrons = new SlideFanChevrons()); Add(new SentakkiRing { diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs index 644bc1a00..539e454cb 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneCircleChaining.cs @@ -28,13 +28,9 @@ public partial class TestSceneCircleChaining : OsuTestScene [Cached] private readonly DrawablePool chevronPool = null!; - [Cached] - private readonly SlideFanChevrons fanChevrons = null!; - public TestSceneCircleChaining() { Add(chevronPool = new DrawablePool(62)); - Add(fanChevrons = new SlideFanChevrons()); Add(new SentakkiRing { diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs index 48c583451..3b8a10fc6 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneFanSlide.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; using osu.Game.Rulesets.Sentakki.UI; @@ -19,13 +20,14 @@ public partial class TestSceneFanSlide : OsuTestScene protected int EndPath; [Cached] - private readonly SlideFanChevrons fanChevrons; + private readonly DrawablePool chevronPool = null!; + private readonly SlideVisual slide; public TestSceneFanSlide() { - Add(fanChevrons = new SlideFanChevrons()); + Add(chevronPool = new DrawablePool(62)); Add(new SentakkiRing { @@ -38,6 +40,7 @@ public TestSceneFanSlide() AddSliderStep("Progress", 0.0f, 1.0f, 0.0f, p => { slide.Progress = p; + slide.UpdateChevronVisibility(); }); AddSliderStep("Rotation", 0.0f, 360f, 22.5f, p => diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs index cf2226104..8315d9412 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/Slides/TestSceneSlide.cs @@ -33,13 +33,9 @@ public abstract partial class TestSceneSlide : OsuTestScene [Cached] private readonly DrawablePool chevronPool; - [Cached] - private readonly SlideFanChevrons fanChevrons; - protected TestSceneSlide() { Add(chevronPool = new DrawablePool(62)); - Add(fanChevrons = new SlideFanChevrons()); Add(new SentakkiRing { @@ -62,6 +58,7 @@ protected TestSceneSlide() AddSliderStep("Progress", 0.0f, 1.0f, 0.0f, p => { slide.Progress = p; + slide.UpdateChevronVisibility(); }); AddToggleStep("Mirrored", b => diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs index a48fd0c4f..2a2056ab6 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideFan.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Sentakki.Objects; @@ -27,7 +28,7 @@ public partial class TestSceneSlideFan : OsuTestScene private int depthIndex; [Cached] - private readonly SlideFanChevrons fanChevrons; + private readonly DrawablePool chevronPool = null!; public TestSceneSlideFan() { @@ -39,7 +40,7 @@ public TestSceneSlideFan() Rotation = -22.5f }); - Add(fanChevrons = new SlideFanChevrons()); + Add(chevronPool = new DrawablePool(62)); AddStep("Miss Single", () => testSingle(2000)); AddStep("Hit Single", () => testSingle(2000, true)); diff --git a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs index 8e0ed20fe..66b7a732b 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Objects/TestSceneSlideNote.cs @@ -30,9 +30,6 @@ public partial class TestSceneSlideNote : OsuTestScene [Cached] private readonly DrawablePool chevronPool; - [Cached] - private readonly SlideFanChevrons fanChevrons; - public TestSceneSlideNote() { base.Content.Add(content = new SentakkiInputManager(new SentakkiRuleset().RulesetInfo)); @@ -44,7 +41,6 @@ public TestSceneSlideNote() }); Add(chevronPool = new DrawablePool(62)); - Add(fanChevrons = new SlideFanChevrons()); AddStep("Miss Single", () => testSingle(2000)); AddStep("Hit Single", () => testSingle(2000, true)); diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/DrawableChevron.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/DrawableChevron.cs new file mode 100644 index 000000000..a2c5341cc --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/DrawableChevron.cs @@ -0,0 +1,167 @@ +using System.Runtime.InteropServices; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides; + +public partial class DrawableChevron : Sprite, ITexturedShaderDrawable +{ + public NoteShape Shape { get; init; } = NoteShape.Ring; + private float thickness = 7f; + public float Thickness + { + get => thickness; + set + { + if (thickness == value) + return; + thickness = value; + Invalidate(Invalidation.DrawNode); + } + } + + private float shadowRadius = 15f; + public float ShadowRadius + { + get => shadowRadius; + set + { + if (shadowRadius == value) + return; + shadowRadius = value; + Invalidate(Invalidation.DrawNode); + } + } + + private bool glow; + public bool Glow + { + get => glow; + set + { + if (glow == value) + return; + glow = value; + Invalidate(Invalidation.DrawNode); + } + } + + private bool fanChevron; + public bool FanChevron + { + get => fanChevron; + set + { + if (fanChevron == value) + return; + + fanChevron = value; + Invalidate(Invalidation.DrawNode); + } + } + + public new IShader TextureShader { get; private set; } = null!; + + protected override DrawNode CreateDrawNode() => new ChevronDrawNode(this); + + private BindableBool exBindable = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders, IRenderer renderer, DrawableHitObject? hitObject) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "chevron"); + Texture = renderer.WhitePixel; + + if (hitObject is null) + return; + + // Bind exnote + exBindable.BindTo(((DrawableSentakkiHitObject)hitObject).ExBindable); + exBindable.BindValueChanged(b => Glow = b.NewValue, true); + } + + private partial class ChevronDrawNode : SpriteDrawNode + { + protected new DrawableChevron Source => (DrawableChevron)base.Source; + protected override bool CanDrawOpaqueInterior => false; + private IUniformBuffer? shapeParameters; + private IUniformBuffer? chevParameters; + + private float thickness; + private Vector2 size; + private bool glow; + private float shadowRadius; + private bool fanChev; + + public ChevronDrawNode(DrawableChevron source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + thickness = Source.Thickness; + size = Source.DrawSize; + shadowRadius = Source.shadowRadius; + glow = Source.Glow; + fanChev = Source.FanChevron; + } + + protected override void BindUniformResources(IShader shader, IRenderer renderer) + { + base.BindUniformResources(shader, renderer); + + shapeParameters ??= renderer.CreateUniformBuffer(); + + shapeParameters.Data = shapeParameters.Data with + { + Thickness = thickness, + Size = size, + ShadowRadius = shadowRadius, + Glow = glow, + }; + + chevParameters ??= renderer.CreateUniformBuffer(); + chevParameters.Data = chevParameters.Data with + { + FanChevron = fanChev + }; + + shader.BindUniformBlock("m_shapeParameters", shapeParameters); + shader.BindUniformBlock("m_chevParameters", chevParameters); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + shapeParameters?.Dispose(); + chevParameters?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ShapeParameters + { + public UniformFloat Thickness; + public UniformPadding4 _; + public UniformVector2 Size; + public UniformFloat ShadowRadius; + public UniformBool Glow; + + public UniformPadding8 __; + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ChevParameters + { + public UniformBool FanChevron; + public UniformPadding12 _; + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/ISlideChevron.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/ISlideChevron.cs deleted file mode 100644 index 391560c0f..000000000 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/ISlideChevron.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides -{ - public interface ISlideChevron - { - // The SlideBody completion threshold that causes this chevron to disappear - public double DisappearThreshold { get; set; } - - public float Alpha { get; set; } - - public SlideVisual? SlideVisual { get; set; } - - public static void UpdateProgress(ISlideChevron chevron) - { - chevron.Alpha = chevron.SlideVisual?.Progress >= chevron.DisappearThreshold ? 0 : 1; - } - } -} diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideChevron.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideChevron.cs index fab4b15c6..6db796edd 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideChevron.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideChevron.cs @@ -1,30 +1,44 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; +using osuTK; namespace osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides { - public partial class SlideChevron : PoolableDrawable, ISlideChevron + public partial class SlideChevron : PoolableDrawable { public double DisappearThreshold { get; set; } - public SlideVisual? SlideVisual { get; set; } + private DrawableChevron chevron = null!; + + public bool FanChevron + { + get => chevron.FanChevron; + set => chevron.FanChevron = value; + } + + public float Thickness + { + get => chevron.Thickness; + set => chevron.Thickness = value; + } public SlideChevron() { Anchor = Anchor.Centre; Origin = Anchor.Centre; + Size = new Vector2(80, 60); } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load() { - AddInternal(new Sprite + AddInternal(chevron = new DrawableChevron() { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = textures.Get("slide"), + RelativeSizeAxes = Axes.Both, + + Thickness = 6.5f }); } } diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideFanChevron.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideFanChevron.cs deleted file mode 100644 index 8a73a9880..000000000 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideFanChevron.cs +++ /dev/null @@ -1,26 +0,0 @@ -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osuTK; - -namespace osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides -{ - public partial class SlideFanChevron : CompositeDrawable, ISlideChevron - { - public double DisappearThreshold { get; set; } - public SlideVisual? SlideVisual { get; set; } - - private readonly IBindable textureSize = new Bindable(); - - public SlideFanChevron((BufferedContainerView view, IBindable sizeBindable) chevron) - { - Anchor = Origin = Anchor.Centre; - - textureSize.BindValueChanged(v => Size = v.NewValue); - textureSize.BindTo(chevron.sizeBindable); - - AddInternal(chevron.view); - } - } -} diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideFanChevrons.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideFanChevrons.cs deleted file mode 100644 index d9e45ac5e..000000000 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideFanChevrons.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Layout; -using osu.Framework.Utils; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides -{ - /// - /// This drawable holds a set of all chevron buffered drawables, and is used to preload all/draw of them outside of playfield. (To avoid Playfield transforms re-rendering the chevrons) - ///
- /// A view of each chevron, along with their size, would be used by SlideFanVisual. - ///
- public partial class SlideFanChevrons : CompositeDrawable - { - private Container chevrons = null!; - - public SlideFanChevrons() - { - Alpha = 0; - AlwaysPresent = true; - - // we are doing this in ctor to guarantee that this object is properly initialized before BDL - loadChevronsTextures(); - } - - public (BufferedContainerView, IBindable) Get(int index) - { - var chevron = chevrons[index]; - - var view = chevron.CreateView(); - view.RelativeSizeAxes = Axes.Both; - - return (view, chevron.SizeBindable); - } - - private void loadChevronsTextures() - { - AddInternal(chevrons = new Container()); - - for (int i = 0; i < 11; ++i) - { - float progress = (i + 2) / (float)12; - float scale = progress; - - chevrons.Add(new ChevronBackingTexture(scale, scale)); - } - } - - private partial class ChevronBackingTexture : BufferedContainer - { - public Bindable SizeBindable { get; } = new Bindable(); - - // This is to ensure that drawables using this texture is sized correctly (since autosize only happens during the first update) - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) - { - if (invalidation == Invalidation.DrawSize) - SizeBindable.Value = DrawSize; - - return base.OnInvalidate(invalidation, source); - } - - private float chevHeight, chevWidth; - - public ChevronBackingTexture(float lengthScale, float heightScale) - : base(cachedFrameBuffer: true) - { - chevHeight = 16 + (10 * heightScale); - chevWidth = 6 + (210 * lengthScale); - AutoSizeAxes = Axes.Both; - - BufferedContainer content; - - // Effect container - // We are doing this ourelves to increase the padding' - AddInternal(content = new Container - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - // Outlines - new Container - { - X = 2.5f, - Masking = true, - CornerRadius = chevHeight / 4, - CornerExponent = 2.5f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomRight, - Rotation = 22.5f, - Width = chevWidth, - Height = chevHeight, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Gray - }, - }, - new Container - { - X = -2.5f, - Masking = true, - CornerRadius = chevHeight / 4, - CornerExponent = 2.5f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomLeft, - Rotation = -22.5f, - Width = chevWidth, - Height = chevHeight, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Gray - }, - }, - // Inners - new Container - { - X = 2.5f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomRight, - Size = new Vector2(chevWidth, chevHeight), - Rotation = 22.5f, - Padding = new MarginPadding(2), - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - - CornerRadius = (chevHeight - 4) / 4, - CornerExponent = 2.5f, - Colour = Color4.White, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White - } - }, - }, - new Container - { - X = -2.5f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomLeft, - Size = new Vector2(chevWidth, chevHeight), - Rotation = -22.5f, - Padding = new MarginPadding(2), - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = (chevHeight - 4) / 4, - CornerExponent = 2.5f, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White - } - }, - }, - } - }.WithEffect(new GlowEffect - { - BlurSigma = new Vector2(20), - Placement = EffectPlacement.Behind, - Colour = Color4.Black - })); - - // PadExtent doesn't fully avoid the clipped shadows, we pad with a more conservative estimate - content.Padding = new MarginPadding(Blur.KernelSize(25)); - } - } - } -} diff --git a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideVisual.cs b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideVisual.cs index 42a9ca24e..bbafc3e08 100644 --- a/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideVisual.cs +++ b/osu.Game.Rulesets.Sentakki/Objects/Drawables/Pieces/Slides/SlideVisual.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Sentakki.Configuration; +using osu.Game.Rulesets.Sentakki.UI; +using osuTK; namespace osu.Game.Rulesets.Sentakki.Objects.Drawables.Pieces.Slides { @@ -31,10 +33,15 @@ public SentakkiSlidePath Path } } + public void UpdateProgress(SlideChevron chevron) + { + chevron.Alpha = Progress >= chevron.DisappearThreshold ? 0 : 1; + } + public void UpdateChevronVisibility() { for (int i = 0; i < chevrons.Count; i++) - ISlideChevron.UpdateProgress((ISlideChevron)chevrons[i]); + UpdateProgress(chevrons[i]); } public SlideVisual() @@ -46,27 +53,19 @@ public SlideVisual() [Resolved] private DrawablePool? chevronPool { get; set; } - private Container chevrons = null!; + private Container chevrons = null!; private readonly BindableBool snakingIn = new BindableBool(true); - private readonly List fanChevrons = new List(); - [BackgroundDependencyLoader] - private void load(SentakkiRulesetConfigManager? sentakkiConfig, SlideFanChevrons? fanChevrons) + private void load(SentakkiRulesetConfigManager? sentakkiConfig) { sentakkiConfig?.BindWith(SentakkiRulesetSettings.SnakingSlideBody, snakingIn); AddRangeInternal(new Drawable[] { - chevrons = new Container() + chevrons = new Container() }); - - if (fanChevrons != null) - { - for (int i = 0; i < 11; ++i) - this.fanChevrons.Add(new SlideFanChevron(fanChevrons.Get(i))); - } } private void updateVisuals() @@ -80,7 +79,7 @@ private void updateVisuals() tryCreateFanChevrons(); } - private const int chevrons_per_eith = 8; + private const int chevrons_per_eith = 9; private const double ring_radius = 297; private const double chevrons_per_distance = chevrons_per_eith * 8 / (2 * Math.PI * ring_radius); private const double endpoint_distance = 30; // margin for each end @@ -118,7 +117,11 @@ private void tryCreateRegularChevrons() chevron.DisappearThreshold = (runningDistance + distance) / this.path.TotalDistance; chevron.Rotation = angle; chevron.Depth = chevrons.Count; - chevron.SlideVisual = this; + + chevron.Thickness = 6.5f; + chevron.Height = 60; + chevron.FanChevron = false; + chevron.Width = 80; chevrons.Add(chevron); previousPosition = position; @@ -130,29 +133,56 @@ private void tryCreateRegularChevrons() private void tryCreateFanChevrons() { + if (chevronPool is null) + return; + if (!path.EndsWithSlideFan) return; var delta = path.PositionAt(1) - path.FanOrigin; + Vector2 lineStart = SentakkiExtensions.GetPositionAlongLane(SentakkiPlayfield.INTERSECTDISTANCE, 0); + Vector2 middleLineEnd = SentakkiExtensions.GetPositionAlongLane(SentakkiPlayfield.INTERSECTDISTANCE, 4); + Vector2 middleLineDelta = middleLineEnd - lineStart; for (int i = 0; i < 11; ++i) { - float progress = (i + 1) / (float)12; - float scale = progress; - SlideFanChevron fanChev = fanChevrons[i]; + float progress = (i + 2f) / 12f; + + float scale = progress - ((1f) / 12f); + var middlePosition = lineStart + (middleLineDelta * progress); + + float t = 6.5f + (2.5f * scale); + + float chevWidth = MathF.Abs(lineStart.X - middlePosition.X) - t; + + (float sin, float cos) = MathF.SinCos((-135 + 90f) / 180f * MathF.PI); - const float safe_space_ratio = 570 / 600f; + Vector2 secondPoint = new Vector2(sin, -cos) * chevWidth; + Vector2 one = new Vector2(chevWidth, 0); + + var middle = (one + secondPoint) * 0.5f; + float h = (middle - Vector2.Zero).Length + (t * 3); + + float w = (secondPoint - one).Length; + + const float safe_space_ratio = (570 / 600f); float y = safe_space_ratio * scale; - fanChev.Position = path.FanOrigin + (delta * y); - fanChev.Rotation = fanChev.Position.GetDegreesFromPosition(path.FanOrigin); + var chevron = chevronPool.Get(); + + chevron.Position = path.FanOrigin + (delta * y); + chevron.Rotation = chevron.Position.GetDegreesFromPosition(path.PositionAt(1)); + + chevron.DisappearThreshold = path.FanStartProgress + ((i + 1) / 11f * (1 - path.FanStartProgress)); + chevron.Depth = chevrons.Count; - fanChev.DisappearThreshold = path.FanStartProgress + ((i + 1) / 11f * (1 - path.FanStartProgress)); - fanChev.Depth = chevrons.Count; - fanChev.SlideVisual = this; + chevron.Width = w + 30; + chevron.Height = h + 30; + chevron.Thickness = t; + chevron.FanChevron = true; - chevrons.Add(fanChev); + chevrons.Add(chevron); } } @@ -171,7 +201,7 @@ public void PerformEntryAnimation(double duration) .Delay(currentOffset) .FadeIn(fadeDuration) // This finally clause ensures the chevron maintains the correct visibility state after a rewind - .Finally(static c => ISlideChevron.UpdateProgress((ISlideChevron)c)); + .Finally(c => UpdateProgress(c)); currentOffset += offsetIncrement; } @@ -192,7 +222,7 @@ public void PerformExitAnimation(double duration) { var chevron = chevrons[i]; - if (((ISlideChevron)chevron).DisappearThreshold <= Progress) + if (chevron.DisappearThreshold <= Progress) { chevron.Alpha = 0; continue; diff --git a/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_chevron.fs b/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_chevron.fs new file mode 100644 index 000000000..52a15d245 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_chevron.fs @@ -0,0 +1,128 @@ +#ifndef SENTAKKI_CHEVRON_FS +#define SENTAKKI_CHEVRON_FS + +#include "sh_noteBase.fs" + +layout(std140, set = 1, binding = 0) uniform m_chevParameters +{ + bool fanChevron; +}; + +const int N = 6; + +float sdPolygon( in vec2 p, in vec2 origin, in vec2[N] v ) +{ + vec2 P = p-origin; + + const int num = v.length(); + float d = dot(P-v[0],P-v[0]); + float s = 1.0; + for( int i=0, j=num-1; i=v[i].y, + P.y e.y*w.x ); + if( all(cond) || all(not(cond)) ) s=-s; + } + + return s*sqrt(d); +} + +vec4 sdfFill(in float dist, in float borderThickness, in float shadowThickness){ + vec3 shadowColor = glow ? vec3(1) : vec3(0); + float shadowAlpha = glow ? 0.75: 0.6; + + float base = smoothstep(borderThickness - 2.0, borderThickness - 3.0, dist); + float outline = smoothstep(borderThickness, borderThickness - 1.0, dist); + + if(shadowThickness < 1) + return vec4(vec3(max(outline * 0.5, base)), outline) * v_Colour; + + float shadowDist = dist - borderThickness; + + float shadow = pow((1 - clamp(((1 / shadowThickness) * shadowDist), 0.0 , 1.0)) * shadowAlpha, 2.0); + float exclusion = smoothstep(borderThickness, borderThickness - 1.0, dist); // Inner cutout for shadow + + float innerShading = smoothstep(borderThickness -2.0, 0.0 , dist); + + vec4 shadowPart = vec4(shadowColor,shadow) * (1 - exclusion) * v_Colour; + vec4 fillPart = vec4(vec3(max(outline * 0.5, base)), outline) * v_Colour; + + //vec4 stylizedFill = mix(fillPart, v_Colour * 0.85, innerShading); + + return shadowPart + fillPart; +} + +float chev(in vec2 p, in vec2 centre, in vec2 hexSize, in float thickness){ + float h = hexSize.y*0.5; + float w = hexSize.x*0.5; + + vec2 v0 = vec2(0, -h); + vec2 v1 = vec2(w, h - thickness); + vec2 v2 = vec2(w, h ); + vec2 v3 = vec2(0.0, -h + thickness); + vec2 v4 = vec2(-w, h); + vec2 v5 = vec2(-w, h - thickness); + + // add more points + vec2[] polygon = vec2[](v0,v1,v2, v3, v4, v5); + + float dist = sdPolygon(p, centre, polygon); + + return dist; +} + +#define PI 3.1415926538 + +float fanChev(in vec2 p, in vec2 centre, in vec2 hexSize, in float thickness){ + float h = hexSize.y*0.5; + float w = hexSize.x*0.5; + + vec2 v0 = vec2(0, -h); + vec2 v1 = vec2(w, h - thickness); + + float rad1 = ((200+2.5)/180.0) * PI; + float rad2 = ((160-2.5)/180.0) * PI; + + vec2 v2 = vec2(sin(rad1) * thickness, -cos(rad1) * thickness) + v1; + vec2 v3 = vec2(0.0, -h + thickness); + + vec2 v5 = vec2(-w, h - thickness); + vec2 v4 = vec2(sin(rad2) * thickness, -cos(rad2) * thickness) + v5; + + // add more points + vec2[] polygon = vec2[](v0,v1,v2, v3, v4, v5); + + float dist = sdPolygon(p, centre, polygon); + + return dist; +} + + +void main(void) { + vec2 resolution = v_TexRect.zw - v_TexRect.xy; + vec2 pixelPos = (v_TexCoord - v_TexRect.xy) / resolution; + + vec2 p = pixelPos * size; + vec2 c = 0.5 * size; + + float shadeRadius = shadowRadius; + float borderThickness = thickness; + float paddingAmount = - borderThickness - shadeRadius; + + float ringSDF = 0; + if(fanChevron) + ringSDF = fanChev(p, c,size - vec2(shadeRadius + borderThickness) * 2, borderThickness); + else ringSDF = chev(p, c,size - vec2(shadeRadius + borderThickness) * 2, borderThickness); + + vec4 r = sdfFill(ringSDF, borderThickness, shadeRadius); + o_Colour = r; +} +#endif \ No newline at end of file diff --git a/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_hexNote.fs b/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_hexNote.fs index 99b674ac8..d49f854a0 100644 --- a/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_hexNote.fs +++ b/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_hexNote.fs @@ -40,28 +40,6 @@ float roundedHexSDF(in vec2 p, in vec2 origin, in float h, in float r) return length(P) * sign(P.x); } -// A simple hex SDF, adapted from https://andrewhungblog.wordpress.com/2018/07/28/shader-art-tutorial-hexagonal-grids/ -// Supports arbitrary heights, while maintaining identical radius -// This one doesn't support rounded corners -float hexSDF(in vec2 p, in vec2 origin, in float h, in float r) -{ - vec2 P = p - origin; - - if(abs(P.y) < h/2.0){ - return abs(P.x) - r; - } - - float hexSize = r; - const vec2 s = vec2(1, 1.7320508); - - float newY = (abs(P.y) - h/2.0); - p = vec2(P.x, newY); - - p = abs(p); - - return max(dot(p, s*.5), p.x) - hexSize; -} - void main(void) { vec2 resolution = v_TexRect.zw - v_TexRect.xy; vec2 pixelPos = (v_TexCoord - v_TexRect.xy) / resolution; diff --git a/osu.Game.Rulesets.Sentakki/Resources/Textures/slide.png b/osu.Game.Rulesets.Sentakki/Resources/Textures/slide.png deleted file mode 100644 index cd10c7422..000000000 Binary files a/osu.Game.Rulesets.Sentakki/Resources/Textures/slide.png and /dev/null differ diff --git a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs index 8e83bef2e..e739594fe 100644 --- a/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs +++ b/osu.Game.Rulesets.Sentakki/UI/DrawableSentakkiRuleset.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Sentakki.UI [Cached] public partial class DrawableSentakkiRuleset : DrawableRuleset, IKeyBindingHandler { - private SlideFanChevrons slideFanChevronsTextures = null!; - public DrawableSentakkiRuleset(SentakkiRuleset ruleset, IBeatmap beatmap, IReadOnlyList? mods) : base(ruleset, beatmap, mods) { @@ -35,23 +33,9 @@ public DrawableSentakkiRuleset(SentakkiRuleset ruleset, IBeatmap beatmap, IReadO mod.ApplyToTrack(speedAdjustmentTrack); } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - // We create and render the FanChevron outside of the playfield - // This is to ensure that the fan chevrons doesn't get affected by Playfield transforms (avoiding excessive buffer allocs/deallocs) - // FanSlides will use BufferedContainerView to show the chevrons - dependencies.CacheAs(slideFanChevronsTextures = new SlideFanChevrons()); - - return dependencies; - } - [BackgroundDependencyLoader] private void load() { - FrameStableComponents.Add(slideFanChevronsTextures); - Config.BindWith(SentakkiRulesetSettings.AnimationSpeed, configEntrySpeed); Config.BindWith(SentakkiRulesetSettings.TouchAnimationSpeed, configTouchEntrySpeed);