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

Add BackdropBlurContainer #6393

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
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
102 changes: 102 additions & 0 deletions osu.Framework.Tests/Visual/Containers/TestSceneBackdropBlur.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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.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) };
}
}
}
}
220 changes: 220 additions & 0 deletions osu.Framework/Graphics/BackdropBlurDrawNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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<BlurParameters> blurParametersBuffer;

private void drawBlurredFrameBuffer(IRenderer renderer, int kernelRadius, float sigma, float blurRotation)
{
blurParametersBuffer ??= renderer.CreateUniformBuffer<BlurParameters>();

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<BlendParameters> blendParametersBuffer;

protected override void DrawContents(IRenderer renderer)
{
renderer.SetBlend(DrawColourInfo.Blending);

if ((blurRadius.X > 0 || blurRadius.Y > 0) && backdropOpacity > 0)
{
blendParametersBuffer ??= renderer.CreateUniformBuffer<BlendParameters>();

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;
}
}
}
36 changes: 23 additions & 13 deletions osu.Framework/Graphics/BufferedDrawNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,28 +85,32 @@ protected sealed override void Draw(IRenderer renderer)
if (!SharedData.IsInitialised)
SharedData.Initialise(renderer);

if (RequiresRedraw)
if (RequiresMainBufferRedraw || RequiresEffectBufferRedraw)
{
FrameStatistics.Increment(StatisticsCounterType.FBORedraw);

SharedData.ResetCurrentEffectBuffer();

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();
Expand All @@ -120,6 +124,10 @@ protected sealed override void Draw(IRenderer renderer)
UnbindTextureShader(renderer);
}

protected virtual bool RequiresMainBufferRedraw => RequiresRedraw;

protected virtual bool RequiresEffectBufferRedraw => RequiresRedraw;

/// <summary>
/// Populates the contents of the effect buffers of <see cref="SharedData"/>.
/// This is invoked after <see cref="Child"/> has been rendered to the main buffer.
Expand All @@ -146,13 +154,15 @@ protected virtual void DrawContents(IRenderer renderer)
protected ValueInvokeOnDisposal<IFrameBuffer> 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<IFrameBuffer>(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
Expand Down
Loading
Loading