diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneBackdropBlur.cs b/osu.Framework.Tests/Visual/Containers/TestSceneBackdropBlur.cs new file mode 100644 index 0000000000..57382140dc --- /dev/null +++ b/osu.Framework.Tests/Visual/Containers/TestSceneBackdropBlur.cs @@ -0,0 +1,102 @@ +// 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.Containers; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Containers +{ + public partial class TestSceneBackdropBlur : TestSceneMasking + { + public TestSceneBackdropBlur() + { + Remove(TestContainer, false); + + BackdropBlurContainer buffer; + Path path; + + AddRange( + new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = FrameworkColour.YellowGreenDark, + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + TestContainer, + buffer = new BackdropBlurContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Red, + Padding = new MarginPadding(100), + Children = new Drawable[] + { + path = new GradientPath + { + PathRadius = 50, + Vertices = new[] + { + new Vector2(0, 0), + new Vector2(150, 50), + new Vector2(250, -25), + new Vector2(400, 25) + } + } + } + } + } + } + } + ); + + AddSliderStep("blur", 0f, 20f, 5f, blur => + { + buffer.BlurTo(new Vector2(blur)); + }); + + AddSliderStep("container alpha", 0f, 1f, 1f, alpha => + { + buffer.Alpha = alpha; + }); + + AddSliderStep("child alpha", 0f, 1f, 0.5f, alpha => + { + path.Alpha = alpha; + }); + + AddSliderStep("mask cutoff", 0f, 1f, 0.0f, cutoff => + { + buffer.MaskCutoff = cutoff; + }); + + AddSliderStep("fbo scale (x)", 0.01f, 4f, 1f, scale => + { + buffer.EffectBufferScale = buffer.EffectBufferScale with { X = scale }; + }); + + AddSliderStep("fbo scale (y)", 0.01f, 4f, 1f, scale => + { + buffer.EffectBufferScale = buffer.EffectBufferScale with { Y = scale }; + }); + } + + private partial class GradientPath : SmoothPath + { + protected override Color4 ColourAt(float position) + { + return base.ColourAt(position) with { A = 0.5f + (position * 0.5f) }; + } + } + } +} diff --git a/osu.Framework/Graphics/BackdropBlurDrawNode.cs b/osu.Framework/Graphics/BackdropBlurDrawNode.cs new file mode 100644 index 0000000000..dba60eb208 --- /dev/null +++ b/osu.Framework/Graphics/BackdropBlurDrawNode.cs @@ -0,0 +1,220 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Graphics +{ + public class BackdropBlurDrawNode : BufferedDrawNode + { + public BackdropBlurDrawNode(IBufferedDrawable source, DrawNode child, BackdropBlurDrawNodeSharedData sharedData) + : base(source, child, sharedData) + { + } + + protected new IBackdropBlurDrawable Source => (IBackdropBlurDrawable)base.Source; + + protected new BackdropBlurDrawNodeSharedData SharedData => (BackdropBlurDrawNodeSharedData)base.SharedData; + + private Vector2 blurSigma; + private Vector2I blurRadius; + private float blurRotation; + + private float maskCutoff; + + private IShader blurShader; + private IShader blendShader; + + private RectangleF backBufferDrawRect; + + private Vector2 effectBufferScale; + private Vector2 effectBufferSize; + + private float backdropOpacity; + private float backdropTintStrength; + + public override void ApplyState() + { + base.ApplyState(); + + backBufferDrawRect = Source.LastBackBufferDrawRect; + + effectBufferScale = Source.EffectBufferScale; + effectBufferSize = new Vector2(MathF.Ceiling(DrawRectangle.Width * effectBufferScale.X), MathF.Ceiling(DrawRectangle.Height * effectBufferScale.Y)); + + blurSigma = Source.BlurSigma * effectBufferScale; + blurRadius = new Vector2I(Blur.KernelSize(blurSigma.X), Blur.KernelSize(blurSigma.Y)); + blurRotation = Source.BlurRotation; + + maskCutoff = Source.MaskCutoff; + backdropOpacity = Source.BackdropOpacity; + backdropTintStrength = Source.BackdropTintStrength; + + blurShader = Source.BlurShader; + blendShader = Source.BlendShader; + } + + protected override void PopulateContents(IRenderer renderer) + { + base.PopulateContents(renderer); + + // we need the intermediate blur pass in order to draw the final blending pass, so we always have to draw both passes. + if ((blurRadius.X > 0 || blurRadius.Y > 0) && backdropOpacity > 0) + { + renderer.PushScissorState(false); + + renderer.PushDepthInfo(new DepthInfo(false)); + + if (blurRadius.X > 0) drawBlurredFrameBuffer(renderer, blurRadius.X, blurSigma.X, blurRotation); + if (blurRadius.Y > 0) drawBlurredFrameBuffer(renderer, blurRadius.Y, blurSigma.Y, blurRotation + 90); + + renderer.PopDepthInfo(); + + renderer.PopScissorState(); + } + } + + private IUniformBuffer blurParametersBuffer; + + private void drawBlurredFrameBuffer(IRenderer renderer, int kernelRadius, float sigma, float blurRotation) + { + blurParametersBuffer ??= renderer.CreateUniformBuffer(); + + if (renderer.FrameBuffer == null) + throw new InvalidOperationException("No frame buffer available to blur with."); + + IFrameBuffer current = SharedData.GetCurrentSourceBuffer(out bool isBackBuffer); + IFrameBuffer target = SharedData.GetNextEffectBuffer(); + + renderer.SetBlend(BlendingParameters.None); + + renderer.PushScissorState(false); + + renderer.PushDepthInfo(new DepthInfo(false)); + + var rect = isBackBuffer + ? backBufferDrawRect.RelativeIn(DrawRectangle) * target.Size + : new RectangleF(0, 0, current.Texture.Width, current.Texture.Height); + + using (BindFrameBuffer(target)) + { + float radians = float.DegreesToRadians(blurRotation); + + blurParametersBuffer.Data = blurParametersBuffer.Data with + { + Radius = kernelRadius, + Sigma = sigma, + TexSize = current.Size, + Direction = new Vector2(MathF.Cos(radians), MathF.Sin(radians)) + }; + + blurShader.BindUniformBlock("m_BlurParameters", blurParametersBuffer); + blurShader.Bind(); + renderer.DrawFrameBuffer(current, rect, ColourInfo.SingleColour(Color4.White)); + blurShader.Unbind(); + } + + renderer.PopDepthInfo(); + + renderer.PopScissorState(); + } + + protected override bool RequiresEffectBufferRedraw => true; + + private IUniformBuffer blendParametersBuffer; + + protected override void DrawContents(IRenderer renderer) + { + renderer.SetBlend(DrawColourInfo.Blending); + + if ((blurRadius.X > 0 || blurRadius.Y > 0) && backdropOpacity > 0) + { + blendParametersBuffer ??= renderer.CreateUniformBuffer(); + + blendParametersBuffer.Data = blendParametersBuffer.Data with + { + MaskCutoff = maskCutoff, + BackdropOpacity = backdropOpacity, + BackdropTintStrength = backdropTintStrength, + }; + + renderer.BindTexture(SharedData.MainBuffer.Texture, 1); + + blendShader.BindUniformBlock("m_BlendParameters", blendParametersBuffer); + blendShader.Bind(); + renderer.DrawFrameBuffer(SharedData.CurrentEffectBuffer, DrawRectangle, DrawColourInfo.Colour); + blendShader.Unbind(); + } + else + { + base.DrawContents(renderer); + } + } + + protected override Vector2 GetFrameBufferSize(IFrameBuffer frameBuffer) + { + if (frameBuffer != SharedData.MainBuffer) + return effectBufferSize; + + return base.GetFrameBufferSize(frameBuffer); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + blurParametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct BlurParameters + { + public UniformVector2 TexSize; + public UniformInt Radius; + public UniformFloat Sigma; + public UniformVector2 Direction; + private readonly UniformPadding8 pad1; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct BlendParameters + { + public UniformFloat MaskCutoff; + public UniformFloat BackdropOpacity; + public UniformFloat BackdropTintStrength; + private readonly UniformPadding4 pad1; + } + } + + public class BackdropBlurDrawNodeSharedData : BufferedDrawNodeSharedData + { + public BackdropBlurDrawNodeSharedData(RenderBufferFormat[] mainBufferFormats) + : base(2, mainBufferFormats, clipToRootNode: true) + { + } + + public IFrameBuffer GetCurrentSourceBuffer(out bool isBackBuffer) + { + var buffer = CurrentEffectBuffer; + + if (buffer == MainBuffer && Renderer.FrameBuffer != null) + { + isBackBuffer = true; + return Renderer.FrameBuffer; + } + + isBackBuffer = false; + return buffer; + } + } +} diff --git a/osu.Framework/Graphics/BufferedDrawNode.cs b/osu.Framework/Graphics/BufferedDrawNode.cs index f250b96411..8e2007d445 100644 --- a/osu.Framework/Graphics/BufferedDrawNode.cs +++ b/osu.Framework/Graphics/BufferedDrawNode.cs @@ -85,7 +85,7 @@ protected sealed override void Draw(IRenderer renderer) if (!SharedData.IsInitialised) SharedData.Initialise(renderer); - if (RequiresRedraw) + if (RequiresMainBufferRedraw || RequiresEffectBufferRedraw) { FrameStatistics.Increment(StatisticsCounterType.FBORedraw); @@ -93,20 +93,24 @@ protected sealed override void Draw(IRenderer renderer) using (establishFrameBufferViewport(renderer)) { - // Fill the frame buffer with drawn children - using (BindFrameBuffer(SharedData.MainBuffer)) + if (RequiresMainBufferRedraw) { - // We need to draw children as if they were zero-based to the top-left of the texture. - // We can do this by adding a translation component to our (orthogonal) projection matrix. - renderer.PushOrtho(screenSpaceDrawRectangle); - renderer.Clear(new ClearInfo(backgroundColour)); - - DrawOther(Child, renderer); - - renderer.PopOrtho(); + // Fill the frame buffer with drawn children + using (BindFrameBuffer(SharedData.MainBuffer)) + { + // We need to draw children as if they were zero-based to the top-left of the texture. + // We can do this by adding a translation component to our (orthogonal) projection matrix. + renderer.PushOrtho(screenSpaceDrawRectangle); + renderer.Clear(new ClearInfo(backgroundColour)); + + DrawOther(Child, renderer); + + renderer.PopOrtho(); + } } - PopulateContents(renderer); + if (RequiresEffectBufferRedraw) + PopulateContents(renderer); } SharedData.DrawVersion = GetDrawVersion(); @@ -120,6 +124,10 @@ protected sealed override void Draw(IRenderer renderer) UnbindTextureShader(renderer); } + protected virtual bool RequiresMainBufferRedraw => RequiresRedraw; + + protected virtual bool RequiresEffectBufferRedraw => RequiresRedraw; + /// /// Populates the contents of the effect buffers of . /// This is invoked after has been rendered to the main buffer. @@ -146,13 +154,15 @@ protected virtual void DrawContents(IRenderer renderer) protected ValueInvokeOnDisposal BindFrameBuffer(IFrameBuffer frameBuffer) { // This setter will also take care of allocating a texture of appropriate size within the frame buffer. - frameBuffer.Size = frameBufferSize; + frameBuffer.Size = GetFrameBufferSize(frameBuffer); frameBuffer.Bind(); return new ValueInvokeOnDisposal(frameBuffer, static b => b.Unbind()); } + protected virtual Vector2 GetFrameBufferSize(IFrameBuffer frameBuffer) => frameBufferSize; + private ValueInvokeOnDisposal<(BufferedDrawNode node, IRenderer renderer)> establishFrameBufferViewport(IRenderer renderer) { // Disable masking for generating the frame buffer since masking will be re-applied diff --git a/osu.Framework/Graphics/BufferedDrawNodeSharedData.cs b/osu.Framework/Graphics/BufferedDrawNodeSharedData.cs index 60afcef1f6..ecd191361e 100644 --- a/osu.Framework/Graphics/BufferedDrawNodeSharedData.cs +++ b/osu.Framework/Graphics/BufferedDrawNodeSharedData.cs @@ -45,7 +45,7 @@ public class BufferedDrawNodeSharedData : IDisposable private readonly RenderBufferFormat[] mainBufferFormats; private readonly TextureFilteringMode filterMode; - private IRenderer renderer; + protected IRenderer Renderer; private IFrameBuffer mainBuffer; /// @@ -80,11 +80,11 @@ public BufferedDrawNodeSharedData(int effectBufferCount, RenderBufferFormat[] ma /// /// The which contains the original version of the rendered . /// - public IFrameBuffer MainBuffer => mainBuffer ??= renderer.CreateFrameBuffer(mainBufferFormats, filterMode); + public IFrameBuffer MainBuffer => mainBuffer ??= Renderer.CreateFrameBuffer(mainBufferFormats, filterMode); public void Initialise(IRenderer renderer) { - this.renderer = renderer; + Renderer = renderer; IsInitialised = true; } @@ -110,7 +110,7 @@ public IFrameBuffer GetNextEffectBuffer() return getEffectBufferAtIndex(currentEffectBuffer); } - private IFrameBuffer getEffectBufferAtIndex(int index) => effectBuffers[index] ??= renderer.CreateFrameBuffer(filteringMode: filterMode); + private IFrameBuffer getEffectBufferAtIndex(int index) => effectBuffers[index] ??= Renderer.CreateFrameBuffer(filteringMode: filterMode); /// /// Resets . @@ -120,7 +120,7 @@ public IFrameBuffer GetNextEffectBuffer() public void Dispose() { - renderer?.ScheduleDisposal(d => d.Dispose(true), this); + Renderer?.ScheduleDisposal(d => d.Dispose(true), this); GC.SuppressFinalize(this); } diff --git a/osu.Framework/Graphics/Colour/ColourInfo.cs b/osu.Framework/Graphics/Colour/ColourInfo.cs index 6b1ad0ed33..b6a3bbf597 100644 --- a/osu.Framework/Graphics/Colour/ColourInfo.cs +++ b/osu.Framework/Graphics/Colour/ColourInfo.cs @@ -189,6 +189,26 @@ public readonly ColourInfo MultiplyAlpha(float alpha) return result; } + public readonly ColourInfo MultiplyAlpha(ColourInfo other) + { + if (other.HasSingleColour && other.singleColour.Alpha == 1.0) + return this; + + if (TryExtractSingleColour(out SRGBColour single) && other.TryExtractSingleColour(out SRGBColour alphaSingle)) + { + single.MultiplyAlpha(alphaSingle.Alpha); + return single; + } + + ColourInfo result = this; + result.TopLeft.MultiplyAlpha(other.TopLeft.Alpha); + result.BottomLeft.MultiplyAlpha(other.BottomLeft.Alpha); + result.TopRight.MultiplyAlpha(other.TopRight.Alpha); + result.BottomRight.MultiplyAlpha(other.BottomRight.Alpha); + + return result; + } + public readonly bool Equals(ColourInfo other) { if (!HasSingleColour) diff --git a/osu.Framework/Graphics/Containers/BackdropBlurContainer.cs b/osu.Framework/Graphics/Containers/BackdropBlurContainer.cs new file mode 100644 index 0000000000..b19e42656e --- /dev/null +++ b/osu.Framework/Graphics/Containers/BackdropBlurContainer.cs @@ -0,0 +1,175 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Layout; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container that blurs the content of its nearest parent behind its children. + /// If all children are of a specific non- type, use the + /// generic version . + /// + public partial class BackdropBlurContainer : BackdropBlurContainer + { + } + + /// + /// A container that blurs the content of its nearest parent behind its children. + /// + public partial class BackdropBlurContainer : Container, IBufferedContainer, IBackdropBlurDrawable where T : Drawable + { + public Vector2 BlurSigma + { + get => blurSigma; + set + { + if (value == blurSigma) + return; + + blurSigma = value; + updateRefCount(); + } + } + + private Vector2 blurSigma; + + public float BlurRotation { get; set; } + + public virtual float BackdropOpacity => 1 - MathF.Pow(1 - base.DrawColourInfo.Colour.MaxAlpha, 2); + + public float MaskCutoff { get; set; } + + public float BackdropTintStrength { get; set; } + + public Vector2 EffectBufferScale { get; set; } = Vector2.One; + + [Resolved] + private IBackbufferProvider backbufferProvider { get; set; } = null!; + + public IShader TextureShader { get; private set; } = null!; + + private readonly BackdropBlurDrawNodeSharedData sharedData; + + public BackdropBlurContainer(RenderBufferFormat[] formats = null) + { + sharedData = new BackdropBlurDrawNodeSharedData(formats); + } + + IShader IBackdropBlurDrawable.BlurShader => blurShader; + + IShader IBackdropBlurDrawable.BlendShader => backdropBlurShader; + + private IShader blurShader = null!; + + private IShader backdropBlurShader = null!; + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); + blurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR); + backdropBlurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BACKDROP_BLUR_BLEND); + } + + protected override DrawNode CreateDrawNode() => new BackdropBlurContainerDrawNode(this, new CompositeDrawableDrawNode(this), sharedData); + + private RectangleF lastBackBufferDrawRect; + + RectangleF IBackdropBlurDrawable.LastBackBufferDrawRect => lastBackBufferDrawRect; + + protected override void Update() + { + base.Update(); + + Invalidate(Invalidation.DrawNode); + + lastBackBufferDrawRect = backbufferProvider.ScreenSpaceDrawQuad.AABBFloat; + } + + private bool isRefCounted; + + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if ((invalidation & Invalidation.Parent) > 0) + updateRefCount(); + + return base.OnInvalidate(invalidation, source); + } + + private void updateRefCount() + { + if (backbufferProvider is RefCountedBackbufferProvider refCount) + { + bool shouldBeRefCounted = Parent != null && (BlurSigma.X > 0 || BlurSigma.Y > 0); + + if (shouldBeRefCounted != isRefCounted) + { + if (shouldBeRefCounted) + refCount.Increment(); + else + refCount.Decrement(); + + isRefCounted = shouldBeRefCounted; + } + } + } + + public Color4 BackgroundColour => Color4.Transparent; + public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo; + + // Children should not receive the true colour to avoid colour doubling when the frame-buffers are rendered to the back-buffer. + public override DrawColourInfo DrawColourInfo + { + get + { + // Todo: This is incorrect. + var blending = Blending; + blending.ApplyDefaultToInherited(); + + return new DrawColourInfo(Color4.White, blending); + } + } + + public Vector2 FrameBufferScale { get; set; } = Vector2.One; + + protected override void Dispose(bool isDisposing) + { + if (isRefCounted && backbufferProvider is RefCountedBackbufferProvider refCount) + { + refCount.Decrement(); + isRefCounted = false; + } + + base.Dispose(isDisposing); + } + + private class BackdropBlurContainerDrawNode : BackdropBlurDrawNode, ICompositeDrawNode + { + public BackdropBlurContainerDrawNode(IBufferedDrawable source, CompositeDrawableDrawNode child, BackdropBlurDrawNodeSharedData sharedData) + : base(source, child, sharedData) + { + } + + protected new CompositeDrawableDrawNode Child => (CompositeDrawableDrawNode)base.Child; + + public List Children + { + get => Child.Children; + set => Child.Children = value; + } + + public bool AddChildDrawNodes => RequiresRedraw; + } + } +} diff --git a/osu.Framework/Graphics/Containers/BufferedContainer.cs b/osu.Framework/Graphics/Containers/BufferedContainer.cs index aa9aa210a5..a1fa3c7980 100644 --- a/osu.Framework/Graphics/Containers/BufferedContainer.cs +++ b/osu.Framework/Graphics/Containers/BufferedContainer.cs @@ -41,7 +41,7 @@ public BufferedContainer(RenderBufferFormat[] formats = null, bool pixelSnapping /// appearance of the container at the cost of performance. Such effects include /// uniform fading of children, blur, and other post-processing effects. /// - public partial class BufferedContainer : Container, IBufferedContainer, IBufferedDrawable + public partial class BufferedContainer : Container, IBufferedContainer, IBufferedDrawable, IBackbufferProvider where T : Drawable { private bool drawOriginal; diff --git a/osu.Framework/Graphics/Containers/IBackbufferProvider.cs b/osu.Framework/Graphics/Containers/IBackbufferProvider.cs new file mode 100644 index 0000000000..ed8f2fb995 --- /dev/null +++ b/osu.Framework/Graphics/Containers/IBackbufferProvider.cs @@ -0,0 +1,15 @@ +// 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.Allocation; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container which ensures that its children are drawn to a framebuffer. + /// + [Cached] + public interface IBackbufferProvider : IContainer + { + } +} diff --git a/osu.Framework/Graphics/Containers/IBufferedContainer.cs b/osu.Framework/Graphics/Containers/IBufferedContainer.cs index 8fba37ff0a..af910218d8 100644 --- a/osu.Framework/Graphics/Containers/IBufferedContainer.cs +++ b/osu.Framework/Graphics/Containers/IBufferedContainer.cs @@ -1,10 +1,12 @@ // 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.Allocation; using osuTK; namespace osu.Framework.Graphics.Containers { + [Cached] public interface IBufferedContainer : IContainer { Vector2 BlurSigma { get; set; } diff --git a/osu.Framework/Graphics/Containers/RefCountedBackbufferProvider.cs b/osu.Framework/Graphics/Containers/RefCountedBackbufferProvider.cs new file mode 100644 index 0000000000..181d0b53f3 --- /dev/null +++ b/osu.Framework/Graphics/Containers/RefCountedBackbufferProvider.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Logging; + +namespace osu.Framework.Graphics.Containers +{ + /// + /// A container which, when a child requests it, will wrap its content in a . + /// + [Cached] + public partial class RefCountedBackbufferProvider : Container, IBackbufferProvider + { + private int refCount; + + private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; + + private BufferedContainer? bufferedContainer; + + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(content); + } + + internal void Increment() => refCount++; + + internal void Decrement() => refCount--; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + Debug.Assert(refCount >= 0); + + if (refCount > 0 && bufferedContainer == null) + { + Logger.Log($@"{nameof(RefCountedBackbufferProvider)} became active."); + + ClearInternal(false); + AddInternal(bufferedContainer = new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Child = content + }); + } + else if (refCount == 0 && bufferedContainer != null) + { + Logger.Log($@"{nameof(RefCountedBackbufferProvider)} became inactive."); + + bufferedContainer?.Clear(false); + bufferedContainer = null; + + ClearInternal(); + AddInternal(content); + } + } + } +} diff --git a/osu.Framework/Graphics/IBackdropBlurDrawable.cs b/osu.Framework/Graphics/IBackdropBlurDrawable.cs new file mode 100644 index 0000000000..4857f25df1 --- /dev/null +++ b/osu.Framework/Graphics/IBackdropBlurDrawable.cs @@ -0,0 +1,52 @@ +// 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.Primitives; +using osu.Framework.Graphics.Shaders; +using osuTK; + +namespace osu.Framework.Graphics +{ + /// + /// A drawable that can blur the background behind itself. + /// + public interface IBackdropBlurDrawable : IBufferedDrawable + { + /// + /// Controls the amount of blurring in two orthogonal directions (X and Y if + /// is zero). + /// Blur is parametrized by a gaussian image filter. This property controls + /// the standard deviation (sigma) of the gaussian kernel. + /// + public Vector2 BlurSigma { get; } + + /// + /// Rotates the blur kernel clockwise. In degrees. Has no effect if + /// has the same magnitude in both directions. + /// + public float BlurRotation { get; } + + /// + /// The opacity at which the blurred backbuffer is drawn. + /// + public float BackdropOpacity { get; } + + /// + /// The alpha at which the content is no longer considered opaque and the background will not be blurred behind it. + /// + public float MaskCutoff { get; } + + /// + /// Controls how much the blurred backbuffer is tinted by the content. + /// + public float BackdropTintStrength { get; } + + public Vector2 EffectBufferScale { get; } + + public IShader BlurShader { get; } + + public IShader BlendShader { get; } + + public RectangleF LastBackBufferDrawRect { get; } + } +} diff --git a/osu.Framework/Graphics/Lines/BackdropBlurPath.cs b/osu.Framework/Graphics/Lines/BackdropBlurPath.cs new file mode 100644 index 0000000000..ef76845d5a --- /dev/null +++ b/osu.Framework/Graphics/Lines/BackdropBlurPath.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Layout; +using osuTK; + +namespace osu.Framework.Graphics.Lines +{ + /// + /// A which can blur the content underneath it. + /// + public partial class BackdropBlurPath : SmoothPath, IBackdropBlurDrawable + { + public Vector2 BlurSigma + { + get => blurSigma; + set + { + if (value == blurSigma) + return; + + blurSigma = value; + updateRefCount(); + } + } + + private Vector2 blurSigma; + + public float BlurRotation { get; set; } + + public virtual float BackdropOpacity => MathF.Min(1, (FrameBufferDrawColour?.Colour.MaxAlpha ?? 1) * 2.5f); + + public float MaskCutoff { get; set; } + + public float BackdropTintStrength { get; set; } + + public Vector2 EffectBufferScale { get; set; } = Vector2.One; + + [Resolved] + private IBackbufferProvider backbufferProvider { get; set; } + + protected override BufferedDrawNodeSharedData CreateSharedData() => new BackdropBlurDrawNodeSharedData(new[] { RenderBufferFormat.D16 }); + + protected override DrawNode CreateDrawNode() => new BackdropBlurDrawNode(this, new PathDrawNode(this), (BackdropBlurDrawNodeSharedData)SharedData); + + IShader IBackdropBlurDrawable.BlurShader => blurShader; + + IShader IBackdropBlurDrawable.BlendShader => blendShader; + + private IShader blurShader = null!; + + private IShader blendShader = null!; + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + blurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR); + blendShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BACKDROP_BLUR_BLEND); + } + + private RectangleF lastBackBufferDrawRect; + + RectangleF IBackdropBlurDrawable.LastBackBufferDrawRect => lastBackBufferDrawRect; + + protected override void Update() + { + base.Update(); + + lastBackBufferDrawRect = backbufferProvider.ScreenSpaceDrawQuad.AABBFloat; + } + + private bool isRefCounted; + + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if ((invalidation & Invalidation.Parent) > 0) + updateRefCount(); + + return base.OnInvalidate(invalidation, source); + } + + private void updateRefCount() + { + if (backbufferProvider is RefCountedBackbufferProvider refCount) + { + bool shouldBeRefCounted = Parent != null && (BlurSigma.X > 0 || BlurSigma.Y > 0); + + if (shouldBeRefCounted != isRefCounted) + { + if (shouldBeRefCounted) + refCount.Increment(); + else + refCount.Decrement(); + + isRefCounted = shouldBeRefCounted; + } + } + } + + protected override void Dispose(bool isDisposing) + { + if (isRefCounted && backbufferProvider is RefCountedBackbufferProvider refCount) + { + refCount.Decrement(); + isRefCounted = false; + } + + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Framework/Graphics/Lines/Path.cs b/osu.Framework/Graphics/Lines/Path.cs index 85c69475d9..e8060dda69 100644 --- a/osu.Framework/Graphics/Lines/Path.cs +++ b/osu.Framework/Graphics/Lines/Path.cs @@ -321,9 +321,13 @@ protected override bool OnInvalidate(Invalidation invalidation, InvalidationSour return result; } - private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(new[] { RenderBufferFormat.D16 }, clipToRootNode: true); + protected virtual BufferedDrawNodeSharedData CreateSharedData() => new BufferedDrawNodeSharedData(new[] { RenderBufferFormat.D16 }, clipToRootNode: true); - protected override DrawNode CreateDrawNode() => new PathBufferedDrawNode(this, new PathDrawNode(this), sharedData); + private BufferedDrawNodeSharedData sharedData; + + protected BufferedDrawNodeSharedData SharedData => sharedData ??= CreateSharedData(); + + protected override DrawNode CreateDrawNode() => new PathBufferedDrawNode(this, new PathDrawNode(this), SharedData); private class PathBufferedDrawNode : BufferedDrawNode { @@ -352,7 +356,7 @@ protected override void Dispose(bool isDisposing) texture?.Dispose(); texture = null; - sharedData.Dispose(); + sharedData?.Dispose(); } } } diff --git a/osu.Framework/Graphics/Lines/Path_DrawNode.cs b/osu.Framework/Graphics/Lines/Path_DrawNode.cs index 3ff49ec497..d8aa32a952 100644 --- a/osu.Framework/Graphics/Lines/Path_DrawNode.cs +++ b/osu.Framework/Graphics/Lines/Path_DrawNode.cs @@ -17,7 +17,7 @@ namespace osu.Framework.Graphics.Lines { public partial class Path { - private class PathDrawNode : DrawNode + protected class PathDrawNode : DrawNode { private const int max_res = 24; diff --git a/osu.Framework/Graphics/Rendering/IRenderer.cs b/osu.Framework/Graphics/Rendering/IRenderer.cs index 9ccba5c66b..339f0ea031 100644 --- a/osu.Framework/Graphics/Rendering/IRenderer.cs +++ b/osu.Framework/Graphics/Rendering/IRenderer.cs @@ -148,6 +148,11 @@ public interface IRenderer /// bool UsingBackbuffer { get; } + /// + /// The current framebuffer, or null if the backbuffer is used. + /// + public IFrameBuffer? FrameBuffer { get; } + /// /// The texture for a white pixel. /// diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index 8066d379ed..d46a0d3ceb 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -88,7 +88,7 @@ protected internal Storage? CacheStorage /// /// The current framebuffer, or null if the backbuffer is used. /// - protected IFrameBuffer? FrameBuffer { get; private set; } + public IFrameBuffer? FrameBuffer { get; private set; } /// /// The current shader, or null if no shader is currently bound. diff --git a/osu.Framework/Graphics/Shaders/ShaderManager.cs b/osu.Framework/Graphics/Shaders/ShaderManager.cs index 27fa27653e..e7425e8a29 100644 --- a/osu.Framework/Graphics/Shaders/ShaderManager.cs +++ b/osu.Framework/Graphics/Shaders/ShaderManager.cs @@ -159,5 +159,6 @@ public static class FragmentShaderDescriptor public const string GLOW = "Glow"; public const string BLUR = "Blur"; public const string VIDEO = "Video"; + public const string BACKDROP_BLUR_BLEND = "BackdropBlurBlend"; } } diff --git a/osu.Framework/Resources/Shaders/sh_BackdropBlurBlend.fs b/osu.Framework/Resources/Shaders/sh_BackdropBlurBlend.fs new file mode 100644 index 0000000000..628cf7c2f8 --- /dev/null +++ b/osu.Framework/Resources/Shaders/sh_BackdropBlurBlend.fs @@ -0,0 +1,55 @@ +#ifndef BLUR_FS +#define BLUR_FS + +#include "sh_Utils.h" +#include "sh_Masking.h" +#include "sh_TextureWrapping.h" + +#undef INV_SQRT_2PI +#define INV_SQRT_2PI 0.39894 + +layout(location = 2) in mediump vec2 v_TexCoord; + +layout(std140, set = 0, binding = 0) uniform m_BlendParameters +{ + lowp float g_MaskCutoff; + lowp float g_BackdropOpacity; + lowp float g_BackdropTintStrength; +}; + +layout(set = 2, binding = 0) uniform lowp texture2D m_Texture; +layout(set = 2, binding = 1) uniform lowp sampler m_Sampler; + +layout(set = 3, binding = 0) uniform lowp texture2D m_Mask; +layout(set = 3, binding = 1) uniform lowp sampler m_MaskSampler; + +layout(location = 0) out vec4 o_Colour; + +void main(void) +{ + vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); + + vec4 foreground = wrappedSampler(wrappedCoord, v_TexRect, m_Mask, m_MaskSampler, -0.9); + + if (foreground.a > g_MaskCutoff) { + foreground *= v_Colour; + + vec4 background = wrappedSampler(wrappedCoord, v_TexRect, m_Texture, m_Sampler, -0.9) * g_BackdropOpacity; + + if (background.a > 0) { + + background.rgb = mix(background.rgb / background.a, background.rgb / background.a * foreground.rgb / foreground.a, g_BackdropTintStrength * foreground.a) * background.a; + + float alpha = background.a + (1.0 - background.a) * foreground.a; + + o_Colour = vec4(mix(background.rgb, foreground.rgb, foreground.a) / alpha, alpha); + } else { + o_Colour = foreground * v_Colour; + } + + } else { + o_Colour = foreground * v_Colour; + } +} + +#endif \ No newline at end of file