A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System.Collections.Generic;
5using osu.Framework.Graphics.OpenGL;
6using osu.Framework.Graphics.Primitives;
7using osu.Framework.Graphics.Shaders;
8using osu.Framework.Graphics.Batches;
9using osuTK;
10using osu.Framework.Graphics.Textures;
11using osu.Framework.Graphics.Colour;
12using System;
13using System.Runtime.CompilerServices;
14using osu.Framework.Graphics.Effects;
15using osu.Framework.Graphics.OpenGL.Vertices;
16
17namespace osu.Framework.Graphics.Containers
18{
19 public partial class CompositeDrawable
20 {
21 /// <summary>
22 /// A draw node responsible for rendering a <see cref="CompositeDrawable"/> and the <see cref="DrawNode"/>s of its children.
23 /// </summary>
24 protected class CompositeDrawableDrawNode : DrawNode, ICompositeDrawNode
25 {
26 private static readonly float cos_45 = MathF.Cos(MathF.PI / 4);
27
28 protected new CompositeDrawable Source => (CompositeDrawable)base.Source;
29
30 /// <summary>
31 /// The <see cref="IShader"/> to use for rendering.
32 /// </summary>
33 protected IShader Shader { get; private set; }
34
35 /// <summary>
36 /// The <see cref="DrawNode"/>s of the children of our <see cref="CompositeDrawable"/>.
37 /// </summary>
38 public List<DrawNode> Children { get; set; }
39
40 /// <summary>
41 /// Information about how masking of children should be carried out.
42 /// </summary>
43 private MaskingInfo? maskingInfo;
44
45 /// <summary>
46 /// The screen-space version of <see cref="OpenGL.MaskingInfo.MaskingRect"/>.
47 /// Used as cache of screen-space masking quads computed in previous frames.
48 /// Assign null to reset.
49 /// </summary>
50 private Quad? screenSpaceMaskingQuad;
51
52 /// <summary>
53 /// Information about how the edge effect should be rendered.
54 /// </summary>
55 private EdgeEffectParameters edgeEffect;
56
57 /// <summary>
58 /// Whether to use a local vertex batch for rendering. If false, a parenting vertex batch will be used.
59 /// </summary>
60 private bool forceLocalVertexBatch;
61
62 /// <summary>
63 /// The vertex batch used for child quads during the back-to-front pass.
64 /// </summary>
65 private QuadBatch<TexturedVertex2D> quadBatch;
66
67 private int sourceChildrenCount;
68
69 public CompositeDrawableDrawNode(CompositeDrawable source)
70 : base(source)
71 {
72 }
73
74 public override void ApplyState()
75 {
76 base.ApplyState();
77
78 if (!Source.Masking && (Source.BorderThickness != 0.0f || Source.EdgeEffect.Type != EdgeEffectType.None))
79 throw new InvalidOperationException("Can not have border effects/edge effects if masking is disabled.");
80
81 Vector3 scale = DrawInfo.MatrixInverse.ExtractScale();
82 float blendRange = Source.MaskingSmoothness * (scale.X + scale.Y) / 2;
83
84 // Calculate a shrunk rectangle which is free from corner radius/smoothing/border effects
85 float shrinkage = Source.CornerRadius - Source.CornerRadius * cos_45 + blendRange + Source.borderThickness;
86
87 // Normalise to handle negative sizes, and clamp the shrinkage to prevent size from going negative.
88 RectangleF shrunkDrawRectangle = Source.DrawRectangle.Normalize();
89 shrunkDrawRectangle = shrunkDrawRectangle.Shrink(new Vector2(Math.Min(shrunkDrawRectangle.Width / 2, shrinkage), Math.Min(shrunkDrawRectangle.Height / 2, shrinkage)));
90
91 maskingInfo = !Source.Masking
92 ? (MaskingInfo?)null
93 : new MaskingInfo
94 {
95 ScreenSpaceAABB = Source.ScreenSpaceDrawQuad.AABB,
96 MaskingRect = Source.DrawRectangle.Normalize(),
97 ConservativeScreenSpaceQuad = Quad.FromRectangle(shrunkDrawRectangle) * DrawInfo.Matrix,
98 ToMaskingSpace = DrawInfo.MatrixInverse,
99 CornerRadius = Source.effectiveCornerRadius,
100 CornerExponent = Source.CornerExponent,
101 BorderThickness = Source.BorderThickness,
102 BorderColour = Source.BorderColour,
103 // We are setting the linear blend range to the approximate size of a _pixel_ here.
104 // This results in the optimal trade-off between crispness and smoothness of the
105 // edges of the masked region according to sampling theory.
106 BlendRange = blendRange,
107 AlphaExponent = 1,
108 };
109
110 edgeEffect = Source.EdgeEffect;
111 screenSpaceMaskingQuad = null;
112 Shader = Source.Shader;
113 forceLocalVertexBatch = Source.ForceLocalVertexBatch;
114 sourceChildrenCount = Source.internalChildren.Count;
115 }
116
117 public virtual bool AddChildDrawNodes => true;
118
119 private void drawEdgeEffect()
120 {
121 if (maskingInfo == null || edgeEffect.Type == EdgeEffectType.None || edgeEffect.Radius <= 0.0f || edgeEffect.Colour.Linear.A <= 0)
122 return;
123
124 RectangleF effectRect = maskingInfo.Value.MaskingRect.Inflate(edgeEffect.Radius).Offset(edgeEffect.Offset);
125
126 screenSpaceMaskingQuad ??= Quad.FromRectangle(effectRect) * DrawInfo.Matrix;
127
128 MaskingInfo edgeEffectMaskingInfo = maskingInfo.Value;
129 edgeEffectMaskingInfo.MaskingRect = effectRect;
130 edgeEffectMaskingInfo.ScreenSpaceAABB = screenSpaceMaskingQuad.Value.AABB;
131 edgeEffectMaskingInfo.CornerRadius = maskingInfo.Value.CornerRadius + edgeEffect.Radius + edgeEffect.Roundness;
132 edgeEffectMaskingInfo.BorderThickness = 0;
133 // HACK HACK HACK. We abuse blend range to give us the linear alpha gradient of
134 // the edge effect along its radius using the same rounded-corners shader.
135 edgeEffectMaskingInfo.BlendRange = edgeEffect.Radius;
136 edgeEffectMaskingInfo.AlphaExponent = 2;
137 edgeEffectMaskingInfo.EdgeOffset = edgeEffect.Offset;
138 edgeEffectMaskingInfo.Hollow = edgeEffect.Hollow;
139 edgeEffectMaskingInfo.HollowCornerRadius = maskingInfo.Value.CornerRadius + edgeEffect.Radius;
140
141 GLWrapper.PushMaskingInfo(edgeEffectMaskingInfo);
142
143 GLWrapper.SetBlend(edgeEffect.Type == EdgeEffectType.Glow ? BlendingParameters.Additive : BlendingParameters.Mixture);
144
145 Shader.Bind();
146
147 ColourInfo colour = ColourInfo.SingleColour(edgeEffect.Colour);
148 colour.TopLeft.MultiplyAlpha(DrawColourInfo.Colour.TopLeft.Linear.A);
149 colour.BottomLeft.MultiplyAlpha(DrawColourInfo.Colour.BottomLeft.Linear.A);
150 colour.TopRight.MultiplyAlpha(DrawColourInfo.Colour.TopRight.Linear.A);
151 colour.BottomRight.MultiplyAlpha(DrawColourInfo.Colour.BottomRight.Linear.A);
152
153 DrawQuad(
154 Texture.WhitePixel,
155 screenSpaceMaskingQuad.Value,
156 colour, null, null, null,
157 // HACK HACK HACK. We re-use the unused vertex blend range to store the original
158 // masking blend range when rendering edge effects. This is needed for smooth inner edges
159 // with a hollow edge effect.
160 new Vector2(maskingInfo.Value.BlendRange));
161
162 Shader.Unbind();
163
164 GLWrapper.PopMaskingInfo();
165 }
166
167 private const int min_amount_children_to_warrant_batch = 8;
168
169 private bool mayHaveOwnVertexBatch(int amountChildren) => forceLocalVertexBatch || amountChildren >= min_amount_children_to_warrant_batch;
170
171 private void updateQuadBatch()
172 {
173 if (Children == null)
174 return;
175
176 if (quadBatch == null && mayHaveOwnVertexBatch(sourceChildrenCount))
177 quadBatch = new QuadBatch<TexturedVertex2D>(100, 1000);
178 }
179
180 public override void Draw(Action<TexturedVertex2D> vertexAction)
181 {
182 updateQuadBatch();
183
184 // Prefer to use own vertex batch instead of the parent-owned one.
185 if (quadBatch != null)
186 vertexAction = quadBatch.AddAction;
187
188 base.Draw(vertexAction);
189
190 drawEdgeEffect();
191
192 if (maskingInfo != null)
193 {
194 MaskingInfo info = maskingInfo.Value;
195 if (info.BorderThickness > 0)
196 info.BorderColour *= DrawColourInfo.Colour.AverageColour;
197
198 GLWrapper.PushMaskingInfo(info);
199 }
200
201 if (Children != null)
202 {
203 for (int i = 0; i < Children.Count; i++)
204 Children[i].Draw(vertexAction);
205 }
206
207 if (maskingInfo != null)
208 GLWrapper.PopMaskingInfo();
209 }
210
211 internal override void DrawOpaqueInteriorSubTree(DepthValue depthValue, Action<TexturedVertex2D> vertexAction)
212 {
213 DrawChildrenOpaqueInteriors(depthValue, vertexAction);
214 base.DrawOpaqueInteriorSubTree(depthValue, vertexAction);
215 }
216
217 /// <summary>
218 /// Performs <see cref="DrawOpaqueInteriorSubTree"/> on all children of this <see cref="CompositeDrawableDrawNode"/>.
219 /// </summary>
220 /// <param name="depthValue">The previous depth value.</param>
221 /// <param name="vertexAction">The action to be performed on each vertex of the draw node in order to draw it if required. This is primarily used by textured sprites.</param>
222 [MethodImpl(MethodImplOptions.AggressiveInlining)]
223 protected virtual void DrawChildrenOpaqueInteriors(DepthValue depthValue, Action<TexturedVertex2D> vertexAction)
224 {
225 bool canIncrement = depthValue.CanIncrement;
226
227 // Assume that if we can't increment the depth value, no child can, thus nothing will be drawn.
228 if (canIncrement)
229 {
230 updateQuadBatch();
231
232 // Prefer to use own vertex batch instead of the parent-owned one.
233 if (quadBatch != null)
234 vertexAction = quadBatch.AddAction;
235
236 if (maskingInfo != null)
237 GLWrapper.PushMaskingInfo(maskingInfo.Value);
238 }
239
240 // We still need to invoke this method recursively for all children so their depth value is updated
241 if (Children != null)
242 {
243 for (int i = Children.Count - 1; i >= 0; i--)
244 Children[i].DrawOpaqueInteriorSubTree(depthValue, vertexAction);
245 }
246
247 // Assume that if we can't increment the depth value, no child can, thus nothing will be drawn.
248 if (canIncrement)
249 {
250 if (maskingInfo != null)
251 GLWrapper.PopMaskingInfo();
252 }
253 }
254
255 protected override void Dispose(bool isDisposing)
256 {
257 base.Dispose(isDisposing);
258
259 // Children disposed via their source drawables
260 Children = null;
261
262 quadBatch?.Dispose();
263 }
264 }
265 }
266}