A game framework written with osu! in mind.
at master 266 lines 12 kB view raw
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}