// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shaders; using osu.Framework.Utils; using osu.Framework.Graphics.Sprites; using osu.Framework.Layout; namespace osu.Framework.Graphics.Containers { /// /// A container that renders its children to an internal framebuffer, and then /// blits the framebuffer to the screen, instead of directly rendering the children /// to the screen. This allows otherwise impossible effects to be applied to the /// appearance of the container at the cost of performance. Such effects include /// uniform fading of children, blur, and other post-processing effects. /// If all children are of a specific non- type, use the /// generic version . /// public class BufferedContainer : BufferedContainer { /// public BufferedContainer(RenderbufferInternalFormat[] formats = null, bool pixelSnapping = false) : base(formats, pixelSnapping) { } } /// /// A container that renders its children to an internal framebuffer, and then /// blits the framebuffer to the screen, instead of directly rendering the children /// to the screen. This allows otherwise impossible effects to be applied to the /// 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 where T : Drawable { private bool drawOriginal; /// /// If true the original buffered children will be drawn a second time on top of any effect (e.g. blur). /// public bool DrawOriginal { get => drawOriginal; set { if (drawOriginal == value) return; drawOriginal = value; ForceRedraw(); } } private Vector2 blurSigma = Vector2.Zero; /// /// 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 => blurSigma; set { if (blurSigma == value) return; blurSigma = value; ForceRedraw(); } } private float blurRotation; /// /// Rotates the blur kernel clockwise. In degrees. Has no effect if /// has the same magnitude in both directions. /// public float BlurRotation { get => blurRotation; set { if (blurRotation == value) return; blurRotation = value; ForceRedraw(); } } private ColourInfo effectColour = Color4.White; /// /// The multiplicative colour of drawn buffered object after applying all effects (e.g. blur). Default is . /// Does not affect the original which is drawn when is true. /// public ColourInfo EffectColour { get => effectColour; set { if (effectColour.Equals(value)) return; effectColour = value; Invalidate(Invalidation.DrawNode); } } private BlendingParameters effectBlending = BlendingParameters.Inherit; /// /// The to use after applying all effects. Default is . /// inherits the blending mode of the original, i.e. is used. /// Does not affect the original which is drawn when is true. /// public BlendingParameters EffectBlending { get => effectBlending; set { if (effectBlending == value) return; effectBlending = value; Invalidate(Invalidation.DrawNode); } } private EffectPlacement effectPlacement; /// /// Whether the buffered effect should be drawn behind or in front of the original. /// Behind by default. Does not have any effect if is false. /// public EffectPlacement EffectPlacement { get => effectPlacement; set { if (effectPlacement == value) return; effectPlacement = value; Invalidate(Invalidation.DrawNode); } } private Color4 backgroundColour = new Color4(0, 0, 0, 0); /// /// The background colour of the framebuffer. Transparent black by default. /// public Color4 BackgroundColour { get => backgroundColour; set { if (backgroundColour == value) return; backgroundColour = value; ForceRedraw(); } } private Vector2 frameBufferScale = Vector2.One; public Vector2 FrameBufferScale { get => frameBufferScale; set { if (frameBufferScale == value) return; frameBufferScale = value; ForceRedraw(); } } /// /// Whether the rendered framebuffer shall be cached until is called /// or the size of the container (i.e. framebuffer) changes. /// If false, then the framebuffer is re-rendered before it is blitted to the screen; equivalent /// to calling every frame. /// public bool CacheDrawnFrameBuffer; private bool redrawOnScale = true; /// /// Whether to redraw this when the draw scale changes. /// public bool RedrawOnScale { get => redrawOnScale; set { if (redrawOnScale == value) return; redrawOnScale = value; screenSpaceSizeBacking?.Invalidate(); } } /// /// Forces a redraw of the framebuffer before it is blitted the next time. /// Only relevant if is true. /// public void ForceRedraw() => Invalidate(Invalidation.DrawNode); /// /// In order to signal the draw thread to re-draw the buffered container we version it. /// Our own version (update) keeps track of which version we are on, whereas the /// drawVersion keeps track of the version the draw thread is on. /// When forcing a redraw we increment updateVersion, pass it into each new drawnode /// and the draw thread will realize its drawVersion is lagging behind, thus redrawing. /// private long updateVersion; public IShader TextureShader { get; private set; } public IShader RoundedTextureShader { get; private set; } private IShader blurShader; private readonly BufferedContainerDrawNodeSharedData sharedData; /// /// Constructs an empty buffered container. /// /// The render buffer formats attached to the frame buffers of this . /// Whether the frame buffer position should be snapped to the nearest pixel when blitting. /// This amounts to setting the texture filtering mode to "nearest". public BufferedContainer(RenderbufferInternalFormat[] formats = null, bool pixelSnapping = false) { sharedData = new BufferedContainerDrawNodeSharedData(formats, pixelSnapping); AddLayout(screenSpaceSizeBacking); } [BackgroundDependencyLoader] private void load(ShaderManager shaders) { TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); blurShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR); } protected override DrawNode CreateDrawNode() => new BufferedContainerDrawNode(this, sharedData); public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) { var result = base.UpdateSubTreeMasking(source, maskingBounds); childrenUpdateVersion = updateVersion; return result; } protected override RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => ScreenSpaceDrawQuad.AABBFloat; // Make sure children never get masked away private Vector2 lastScreenSpaceSize; // We actually only care about Invalidation.MiscGeometry | Invalidation.DrawInfo private readonly LayoutValue screenSpaceSizeBacking = new LayoutValue(Invalidation.Presence | Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { var result = base.OnInvalidate(invalidation, source); if ((invalidation & Invalidation.DrawNode) > 0) { ++updateVersion; result = true; } return result; } private long childrenUpdateVersion = -1; protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && childrenUpdateVersion != updateVersion; protected override void Update() { base.Update(); // Invalidate drawn frame buffer every frame. if (!CacheDrawnFrameBuffer) ForceRedraw(); else if (!screenSpaceSizeBacking.IsValid) { Vector2 drawSize = ScreenSpaceDrawQuad.AABBFloat.Size; if (!RedrawOnScale) { Matrix3 scaleMatrix = Matrix3.CreateScale(DrawInfo.MatrixInverse.ExtractScale()); Vector2Extensions.Transform(ref drawSize, ref scaleMatrix, out drawSize); } if (!Precision.AlmostEquals(lastScreenSpaceSize, drawSize)) { ++updateVersion; lastScreenSpaceSize = drawSize; } screenSpaceSizeBacking.Validate(); } } /// /// The blending which uses for the effect. /// public BlendingParameters DrawEffectBlending { get { BlendingParameters blending = EffectBlending; blending.CopyFromParent(Blending); blending.ApplyDefaultToInherited(); return blending; } } /// /// Creates a view which can be added to a container to display the content of this . /// /// The view. public BufferedContainerView CreateView() => new BufferedContainerView(this, sharedData); 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); } } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); sharedData.Dispose(); } } public enum EffectPlacement { Behind, InFront, } }